题目传送门

算法:min-max 容斥、树上背包、NTT。

题意简述

有一棵 \(n\) 个点的树。一开始所有点都是白色,每次操作会随机选择 \(\frac{n \times (n + 1)}{2}\) 条路径中的一条,将路径上所有点染黑。求所有点都被染黑的期望操作数。

\(n \le 50\)。多组数据。对 \(998, 244, 353\) 取模。

题解

套路性地,我们使用 min-max 容斥。

\[\begin{aligned}E(\max(U)) &= \sum_{\phi \neq S \subset U} (-1) ^ {\lvert S \rvert - 1} E(\min(S)) \\ &= \sum_{\phi \neq S \subset U} (-1) ^ {\lvert S \rvert - 1} \frac{1}{P(路径经过\ S\ 内的点)} \\ &= \sum_{\phi \neq S \subset U} (-1) ^ {\lvert S \rvert - 1} \frac{1}{1 - P(路径不经过\ S\ 内的点)}\end{aligned}
\]

如果我们把树画出来,并将 \(S\) 内的点标记,可以发现,这些点把原树分成了若干个连通块,而每个连通块内部可以任意选取路径,能够保证该路径不经过 \(S\) 内的点。而一旦路径跨越连通块,那么一定经过 \(S\) 内的点。

记共分成了 \(x\) 个连通块 \(T_1, T_2, \cdots, T_x\),则不经过 \(S\) 的路径数为

\[\sum_{i = 1} ^ {x} \frac{\lvert T_i \rvert \times \left( \lvert T_i \rvert + 1 \right)}{2}
\]

那么概率为

\[\frac{\sum_{i = 1} ^ {x} \lvert T_i \rvert \times \left( \lvert T_i \rvert + 1 \right)}{n \times (n + 1)}
\]

继续化简原式,得

\[\begin{aligned}E(\max(U)) &= \sum_{\phi \neq S \subset U} (-1) ^ {\lvert S \rvert - 1} \frac{1}{1 - \dfrac{\sum_{i = 1} ^ {x} \lvert T_i \rvert \times \left( \lvert T_i \rvert + 1 \right)}{n \times (n + 1)}} \\ &= \sum_{\phi \neq S \subset U} (-1) ^ {\lvert S \rvert - 1} \frac{n \times (n + 1)}{n \times (n + 1) - \sum_{i = 1} ^ {x} \lvert T_i \rvert \times \left( \lvert T_i \rvert + 1 \right)} \\ &= \frac{n \times (n + 1)}{2} \sum_{\phi \neq S \subset U} (-1) ^ {\lvert S \rvert - 1} \frac{1}{\dfrac{n \times (n + 1)}{2} - \sum_{i = 1} ^ {x} \dfrac{\lvert T_i \rvert \times \left( \lvert T_i \rvert + 1 \right)}{2}}\end{aligned}
\]

对于最终情况,一个划分方案给答案带来的贡献仅仅是 \((-1) ^ {|S| - 1}\) 和 \(\sum_{i = 1} ^ x \dfrac{\lvert T_i \rvert \times \left( \lvert T_i \rvert + 1 \right)}{2}\) 两块。为了获得这些信息,我们只关心于选点个数为奇数、偶数的方案中,有多少个方案的 \(\sum_{i = 1} ^ x \dfrac{\lvert T_i \rvert \times \left( \lvert T_i \rvert + 1 \right)}{2}\) 为我枚举的常数 \(k\)。

考虑设计一个动态规划来解决这个问题。我们先选取 \(1\) 号点作为全树的根,把该树变成一棵有根树。

记 \(f[i][odd][j][k]\) 表示只考虑了以 \(i\) 为根的子树,选了奇数/偶数个点,包含 \(i\) 的连通块大小为 \(j\),\(\sum_{i = 1} ^ x \dfrac{\lvert T_i \rvert \times \left( \lvert T_i \rvert + 1 \right)}{2}\)(不含 \(i\) 所在连通块)的值为 \(k\) 的方案数。其中 \(j = 0\) 表示 \(i\) 号点不在连通块中(即在所选的点集中)。

此外,我们记一个数组 \(g[i][odd][k]\) 表示以 \(i\) 为根的子树,选了奇数/偶数个点,\(\sum_{i = 1} ^ x \dfrac{\lvert T_i \rvert \times \left( \lvert T_i \rvert + 1 \right)}{2}\)(含 \(i\) 所在连通块)的值为 \(k\) 的方案数。显然有

\[g[i][odd][k'] = \sum_{j = 0} ^ n \sum_{k = 0}^{\frac{n \times (n + 1)}{2}} \left[ \frac{j \times (j + 1)}{2} + k == k' \right] f[i][odd][j][k]
\]

转移类似树上背包。合并 \(i\) 与一个儿子 \(v\) 时,有转移式:

\[f[i][odd][j][k] = \left\{\begin{aligned} &\sum_{odd' \in \{0, 1\}} \sum_{k' = 0} ^ {k} f[i][odd][j][k'] \cdot g[v][odd \oplus odd'][k - k'] \quad & j = 0 \\ &\sum_{odd' \in \{0, 1\}} \sum_{j' = 1} ^ {j} \sum_{k' = 0}^{k} f[i][odd][j][k] \cdot f[v][odd \oplus odd'][j - j'][k - k'] \quad & j \neq 0\end{aligned}\right.
\]

暴力转移复杂度是 \(\mathcal{O}(n^7)\) 的,并不可接受。

我们留意到 \(j\) 一维的大小是不超过 \(i\) 的子树大小的,这是树上背包的经典形式。因此复杂度就少了一个 \(n\),变成了 \(\mathcal{O}(n^6)\)。

2020.02.16 upt:出给模拟赛后发现 \(\mathcal{O}(n^6)\) 剪个枝可以跑得飞快,把 NTT 按在地上锤。

继续观察,发现 \(k\) 一维的转移是一个卷积形式,可以使用 NTT 进行优化。换句话说,除了把 \(f[i]\) 向 \(g[i]\) 算贡献的地方,如果我们把状态的最后一维看成一个 \(\frac{n \times (n + 1)}{2}\) 次的多项式,那么这里的所有运算都可以看成多项式加法和多项式乘法。因此我们可以在一开始就用点值表示法表示 \(f\) 和 \(g\) 的值,只有在 \(f\) 向 \(g\) 算贡献的时候,我们才进行一次 INTT,把点值表示法还原回系数表示法,得到 \(g\) 后,就又可以还原成点值表示法。

于是原本 \(\mathcal{O}(n^2)\) 的转移被优化到了 \(\mathcal{O}(\log n)\)。这样以后总复杂度变为 \(\mathcal{O}(n ^ 4 \log n)\)。

注意常数优化应该是可以通过的。

#include <algorithm>
#include <cstdio>
#include <cstring>
#include <vector> const int MaxN = 50 + 1;
const int MaxV = 2048 + 5;
const int Mod = 998244353, Prt = 3; struct Graph {
int cnte;
int Head[MaxN], To[MaxN * 2], Next[MaxN * 2]; inline void clear() {
cnte = 0;
memset(Head, 0, sizeof Head);
memset(To, 0, sizeof To);
memset(Next, 0, sizeof Next);
} inline void addEdge(int from, int to) {
cnte++; To[cnte] = to;
Next[cnte] = Head[from]; Head[from] = cnte;
}
}; int Te, N;
int Fa[MaxN], Siz[MaxN];
int F[MaxN][2][MaxN][MaxV], G[MaxN][2][MaxV];
int Rev[13][MaxV], W[2][MaxV], Inv[MaxV], Log[MaxV];
Graph Gr;
inline int add(int x, int y) { return (x += y) >= Mod ? x - Mod : x; }
inline int sub(int x, int y) { return (x -= y) < 0 ? x + Mod : x; }
inline int mul(int x, int y) { return 1LL * x * y % Mod; }
inline int pw(int x, int y) { int z = 1; for (; y; y >>= 1, x = mul(x, x)) if (y & 1) z = mul(z, x); return z; }
inline int inv(int x) { return pw(x, Mod - 2); }
inline int sep(int x, int y) { return mul(x, inv(y)); }
inline void inc(int &x, int y = 1) { x = add(x, y); }
inline void dec(int &x, int y = 1) { x = sub(x, y); } void init() {
scanf("%d", &N);
for (int i = 1; i < N; ++i) {
int u, v;
scanf("%d %d", &u, &v);
Gr.addEdge(u, v);
Gr.addEdge(v, u);
}
} void dfs1(int u) {
Siz[u] = 1;
for (int i = Gr.Head[u]; i; i = Gr.Next[i]) {
int v = Gr.To[i];
if (v == Fa[u]) continue;
Fa[v] = u;
dfs1(v);
Siz[u] += Siz[v];
}
} inline void ntt(int *a, int n, int f) {
for (int i = 1; i < n; ++i)
if (i < Rev[Log[n]][i]) std::swap(a[i], a[Rev[Log[n]][i]]);
for (int i = 1; i < n; i <<= 1) {
int w = W[f][i];
for (int j = 0; j < n; j += (i << 1)) {
int x = 1;
for (int k = 0; k < i; ++k, x = mul(x, w)) {
int lson = a[j + k], rson = a[i + j + k];
a[j + k] = add(lson, mul(rson, x));
a[i + j + k] = sub(lson, mul(rson, x));
}
}
}
if (f == 1)
for (int i = 0; i < n; ++i) a[i] = mul(a[i], Inv[n]);
} inline int getPow2(int n) {
int v = 1;
while (v < n) v <<= 1;
return v;
} inline void calcG(int u) {
for (int odd = 0; odd <= 1; ++odd) {
for (int j = 0; j <= Siz[u]; ++j)
for (int k = 0; k <= Siz[u] * (Siz[u] + 1) / 2; ++k) {
int newK = j * (j + 1) / 2 + k;
if (newK > N * (N + 1) / 2) break;
inc(G[u][odd][newK], F[u][odd][j][k]);
}
}
} void dfs2(int u) {
F[u][1][0][0] = F[u][0][1][0] = 1;
int sz = 1;
std::vector<int> vec;
for (int i = Gr.Head[u]; i; i = Gr.Next[i]) {
int v = Gr.To[i];
if (v == Fa[u]) continue;
dfs2(v);
vec.push_back(v);
}
std::sort(vec.begin(), vec.end(), [](int x, int y){ return Siz[x] < Siz[y]; });
for (int v : vec) {
sz += Siz[v];
static int f[2][MaxN][MaxV];
int len = getPow2((sz - 1) * sz / 2 + 1);
for (int j = 0; j <= sz; ++j)
for (int k = 0; k < len; ++k)
f[0][j][k] = f[1][j][k] = 0;
ntt(G[v][0], len, 0); ntt(G[v][1], len, 0);
for (int j = 0; j <= sz - Siz[v]; ++j) ntt(F[u][0][j], len, 0), ntt(F[u][1][j], len, 0);
for (int j = 0; j <= Siz[v]; ++j) ntt(F[v][0][j], len, 0), ntt(F[v][1][j], len, 0); for (int j = 0; j <= sz; ++j) {
for (int odd2 = 0; odd2 <= 1; ++odd2) {
if (j == 0) {
for (int k = 0; k < len; ++k)
inc(f[0][j][k], mul(F[u][odd2][j][k], G[v][odd2][k]));
} else {
for (int j2 = std::max(0, j - sz + Siz[v]); j2 <= Siz[v] && j2 < j; ++j2) {
for (int k = 0; k < len; ++k)
inc(f[0][j][k], mul(F[u][odd2][j - j2][k], F[v][odd2][j2][k]));
}
}
}
}
for (int j = 0; j <= sz; ++j) {
for (int odd2 = 0; odd2 <= 1; ++odd2) {
if (j == 0) {
for (int k = 0; k < len; ++k)
inc(f[1][j][k], mul(F[u][1 ^ odd2][j][k], G[v][odd2][k]));
} else {
for (int j2 = std::max(0, j - sz + Siz[v]); j2 <= Siz[v] && j2 < j; ++j2) {
for (int k = 0; k < len; ++k)
inc(f[1][j][k], mul(F[u][1 ^ odd2][j - j2][k], F[v][odd2][j2][k]));
}
}
}
} for (int j = 0; j <= sz; ++j) {
ntt(f[0][j], len, 1);
for (int k = 0; k < len; ++k)
F[u][0][j][k] = f[0][j][k];
}
for (int j = 0; j <= sz; ++j) {
ntt(f[1][j], len, 1);
for (int k = 0; k < len; ++k)
F[u][1][j][k] = f[1][j][k];
}
}
calcG(u);
} void solve() {
dfs1(1);
dfs2(1);
int ans = 0;
for (int odd = 0; odd <= 1; ++odd)
for (int k = 0; k < N * (N + 1) / 2; ++k) {
if (odd == 1) inc(ans, mul(G[1][odd][k], inv(N * (N + 1) / 2 - k)));
else dec(ans, mul(G[1][odd][k], inv(N * (N + 1) / 2 - k)));
}
ans = mul(ans, N * (N + 1) / 2);
printf("%d\n", ans);
} void clear() {
memset(Fa, 0, sizeof Fa);
memset(Siz, 0, sizeof Siz);
memset(F, 0, sizeof F);
memset(G, 0, sizeof G);
Gr.clear();
} int main() {
for (int i = 1, l = 0; i <= 2048; i <<= 1, ++l) {
Log[i] = l;
Rev[l][0] = 0;
for (int j = 1; j < i; ++j) {
Rev[l][j] = (Rev[l][j >> 1]) >> 1;
if (j & 1) Rev[l][j] |= (i >> 1);
}
Inv[i] = inv(i);
}
for (int f = 0; f <= 1; ++f)
for (int i = 1; i < 2048; i <<= 1)
W[f][i] = pw(pw(Prt, f == 0 ? 1 : Mod - 2), (Mod - 1) / (i << 1));
scanf("%d", &Te);
for (int t = 1; t <= Te; ++t) {
init();
printf("Case #%d: ", t);
solve();
clear();
}
return 0;
}

牛客 17439:Endless Pallet的更多相关文章

  1. 牛客网程序员面试金典:1.1确定字符互异(java实现)

    问题描述: 请实现一个算法,确定一个字符串的所有字符是否全都不同.这里我们要求不允许使用额外的存储结构. 给定一个string iniString,请返回一个bool值,True代表所有字符全都不同, ...

  2. 牛客网 --java问答题

    http://www.nowcoder.com/ 主要是自己什么都不怎么会.在这里可以学习很多的! 第一天看题自己回答,第二天看牛客网的答案! 1 什么是Java虚拟机?为什么Java被称作是“平台无 ...

  3. 【面试笔试算法】牛客网一站通Offer编程题2016.4.19

    牛客网一站通offer (一)字符串变形 1. 题目: 对于一个给定的字符串,我们需要在线性(也就是O(n))的时间里对它做一些变形.首先这个字符串中包含着一些空格,就像"Hello Wor ...

  4. 牛客网《BAT面试算法精品课》学习笔记

    目录 牛客网<BAT面试算法精品课>学习笔记 牛客网<BAT面试算法精品课>笔记一:排序 牛客网<BAT面试算法精品课>笔记二:字符串 牛客网<BAT面试算法 ...

  5. 牛客小白月赛13 小A买彩票 (记忆化搜索)

    链接:https://ac.nowcoder.com/acm/contest/549/C来源:牛客网 时间限制:C/C++ 1秒,其他语言2秒 空间限制:C/C++ 262144K,其他语言52428 ...

  6. 牛客小白月赛13-J小A的数学题 (莫比乌斯反演)

    链接:https://ac.nowcoder.com/acm/contest/549/J来源:牛客网 题目描述 小A最近开始研究数论题了,这一次他随手写出来一个式子,∑ni=1∑mj=1gcd(i,j ...

  7. C++版 - HDUoj 2010 3阶的水仙花数 - 牛客网

    版权声明: 本文为博主Bravo Yeung(知乎UserName同名)的原创文章,欲转载请先私信获博主允许,转载时请附上网址 http://blog.csdn.net/lzuacm. C++版 - ...

  8. 【并查集缩点+tarjan无向图求桥】Where are you @牛客练习赛32 D

    目录 [并查集缩点+tarjan无向图求桥]Where are you @牛客练习赛32 D PROBLEM SOLUTION CODE [并查集缩点+tarjan无向图求桥]Where are yo ...

  9. 牛客OI周赛7-提高组 A 小睿睿的等式

    链接:https://ac.nowcoder.com/acm/contest/371/A来源:牛客网 小睿睿在游戏开始时有n根火柴棒,他想知道能摆成形如“A+B=n”的等式且使用的火柴棒数也恰好等于n ...

随机推荐

  1. spring cloud微服务实践三

    上篇文章里我们实现了spring cloud中的服务提供者和使用者.接下来我们就来看看spring cloud中微服务的其他组件. 注:这一个系列的开发环境版本为 java1.8, spring bo ...

  2. Go语言学习笔记(7)——函数和方法

    Go语言中同时有函数和方法! 函数: go程序必须要包含一个main函数.main函数不能有任何参数和返回值! 1. 定义方法示例: func max(num1, num2 int) int { // ...

  3. ActiveMQ的静态网络配置

    static networkConnector是用于创建一个静态的配置对于网络中的多个Broker做集群,这种协议用于复合url,一个复合url包括多个url地址. <networkConnec ...

  4. Luogu5327 ZJOI2019语言(树上差分+线段树合并)

    暴力树剖做法显然,即使做到两个log也不那么优美. 考虑避免树剖做到一个log.那么容易想到树上差分,也即要对每个点统计所有经过他的路径产生的总贡献(显然就是所有这些路径端点所构成的斯坦纳树大小),并 ...

  5. Java非常好用的反射框架Reflections

    MAVEN 坐标 <dependency> <groupId>org.reflections</groupId> <artifactId>reflect ...

  6. java中针对 try和finally一些总结

    结论 1.不管有没有异常,finally中的代码都会执行 2.当try.catch中有return时,finally中的代码依然会继续执行 3.finally是在return后面的表达式运算之后执行的 ...

  7. php底层变量存储

    变量存储 php的变量使用一个结构体 zval来保存的,在Zend/zend.h中我们可以看到zval的定义 struct _zval_struct { /* Variable information ...

  8. C# LoadXml System.Xml.XmlException: Data at the root level is invalid. Line 1, position 1.

    去掉BOM头 writer = new XmlTextWriter(stream, new UnicodeEncoding(false,false)); 如果是UTF8 writer = new Xm ...

  9. access 数据库创建表SQL语法

    create table R_CAIFA_B13 ( ID AUTOINCREMENT PRIMARY KEY, XB varchar(255), C1 varchar(50), C2 varchar ...

  10. 跨站脚本攻击XSS(二)——session劫持

    转载自:http://www.cnblogs.com/dolphinX/p/3403027.html 在跨站脚本攻击XSS中简单介绍了XSS的原理及一个利用XSS盗取存在cookie中用户名和密码的小 ...