【UOJ #351】新年的叶子(树的直径,期望)
这的确是一道好题,我们不妨依循思路一步步推导,看问题是如何被解决的。
做一些约定,设$m$为树的叶子节点个数,设$len$为该树的直径(经过的点数)。
毫无疑问,直径可能有多条,我们需要把所有直径都破坏掉才能终止,但这些直径并不是毫无联系的。
引理:若$len$为奇数,则所有直径有同一个中点;若$len$为偶数,则所有直径有同一条最中间的边。
- 至于证明,用反证法即可,大致就是如果存在两条直径的中点不是同一个,那就会产生更长的一条直径。
题目要求的终止状态就是不存在两个可以构成直径的点,可以构成直径的点指的是原先处在某一条直径的两端的点,我们可以先按$len$的奇偶讨论:
- 若$len$为奇数,即存在一个中点$x$,我们不妨将$x$设成根,不难发现,如果两个点$a,b$的$lca$是$x$,那$a,b$就能构成直径,否则就不能。这启发我们把所有可以构成直径的点,按照他们在$x$的哪个亲儿子下分组,那样同一个组内的点互相不能构成直径,不在一个组内的两点能构成直径。
- 若$len$为偶数,即所有直径都会经过中间两个点,我们模仿之前的思路,从这两个点之间分开,把这些可以构成直径的点分成的两个组,道理同上。
这样我们得到了若干个点的集合,假设我们得到了$t$个集合,其集合大小用$k$表示,所有集合大小之和用$sum$表示,并且我们只关心集合的大小,集合中具体有那些点并不重要。然后我们发现,如果存在两个集合都没有被完全染黑,原先的直径还是存在的,也就是说我们第一次使得只剩下一个集合没有被完全染黑的时间就是我们所希望的。
我们按照这个精简后的题目做,这里有两种做法,分别从两个不同角度求解。
法一:
在此之前,我们必须先明白一件事,期望实际上是若干个随机事件的概率乘上该事件的价值的和。
我们知道经过无限次染色后所有叶子终究都会被染黑,而每一种染黑的方案中这些叶子被染黑是有顺序的,由于每次染色都是随机的,所以产生每一种顺序的概率是均等的,但是不同的顺序会导致它们终止的时间不同。所以这个做法总体上是把所有事件按照点染黑的顺序分组分别算,最后把贡献加起来。
有诸多顺序它们的期望步数是相等的,我们会把它们合起来算。我们枚举一个集合,将它作为最后那个没有被完全染黑的集合,并枚举这个集合在终止时有多少个点已经被染黑了,显然所有满足以此为终止状态的顺序的期望步数都是一样的。这里要注意的是,染黑的最后一个点一定是其它集合中的点,这是需要特殊考虑的。
于是我们可以写出贡献和的表达式:
$$ans = \frac{1}{sum!} \sum_{i = 1}^{t} \sum_{j = 0}^{k_i - 1} \tbinom{k_i}{j} (m - k_i) (m - k_i + j - 1)! (k - j)! \sum_{z = k_i - j + 1}^{sum} \frac{m}{z}$$
这里可能有些需要解释的地方,就是为什么这么算是对的,这可能一开始看会觉得难以理解。你会发现对于每一个顺序而言,我们让$\sum_{z} \frac{m}{z}$成为它的贡献的行为令人费解,因为这个式子在做的东西是无序的,并没有按照我们所想的顺序染色。但是每个顺序的概率是均等的,我们仅仅只需要这个期望式子中的一部分,只要把$sum!$除掉就可以了。
法二:
既然我们要算的是所有合法事件的概率乘贡献之和(这里所说的合法事件指第一次使得只剩下一个集合没有被完全染黑,因为只有这些是被算进答案里的),用容斥也是可以得到的。
如果我们枚举一个集合$i$,想要让$i$成为最后那个没有被完全染黑的集合,我们算出只关心其余集合的点把它们染黑的期望时间。在这个期望中,每一种方案的最后一个染色的点一定不在$i$中。这里有两种情况,一种是在染色结束时$i$中仍存在没染黑的点,这属于合法情况;另一种是$i$中的点已经全部被染黑了,也就是所有点都没染黑了,这是需要去掉的。我们考虑每一种全部染黑的方案会在除了最后染色的集合外的其他$t-1$个集合中都被算到,所以最终的答案就是枚举每个集合算其余点被染黑的期望时间减去$t-1$倍的把所有点都染黑的期望时间。
我们发现这么容斥后,剩下的事件的概率和恰好是$1$,单纯从概率分支树上来讲肯定是对的。具体来讲就是一种全部染黑的方案事实上是一种合法方案的一个分支,本来就不应该算进去,而合法事件显然是不重复不遗漏的。
这是法二的实现:
#include <cstdio> #include <algorithm> using namespace std; typedef long long LL; const int N = 500005; const int MOD = 998244353; int n, m, bt, len, su, ans, tmp; int bl[N], inv[N], sui[N], dep[N], fa[N], deg[N]; int yu, la[N], to[N << 1], pr[N << 1]; inline void Ade(int a, int b) { to[++yu] = b, pr[yu] = la[a], la[a] = yu; } void Dfs(int x, int fat, int &c) { dep[x] = dep[fa[x] = fat] + 1; if (dep[x] == len / 2) ++c; for (int i = la[x]; i; i = pr[i]) if (to[i] != fat) Dfs(to[i], x, c); } inline int Add(int a, int b) { return (a += b) >= MOD? a - MOD : a; } int main() { scanf("%d", &n); for (int i = 1, x, y; i < n; ++i) { scanf("%d%d", &x, &y); Ade(x, y), Ade(y, x); ++deg[x], ++deg[y]; } Dfs(1, 0, tmp); int rt = 1, tl = 1; for (int i = 1; i <= n; ++i) { if (dep[i] > dep[rt]) rt = i; if (deg[i] == 1) ++m; } Dfs(rt, 0, tmp); for (int i = 1; i <= n; ++i) if (dep[i] > dep[tl]) tl = i; len = dep[tl]; inv[1] = 1, sui[1] = m; for (int i = 2; i <= n; ++i) { inv[i] = MOD - (LL)MOD / i * inv[MOD % i] % MOD; sui[i] = Add(sui[i - 1], (LL)m * inv[i] % MOD); } if (len & 1) { int md = -1; for (int i = tl; i; i = fa[i]) if (dep[i] == (len + 1) / 2) md = i; dep[md] = 0; for (int i = la[md]; i; i = pr[i]) { Dfs(to[i], md, tmp = 0); if (tmp) bl[++bt] = tmp, su += bl[bt]; } } else { int m1 = -1, m2 = -1; for (int i = tl; i; i = fa[i]) { if (dep[i] == len / 2) m1 = i; if (dep[i] == len / 2 + 1) m2 = i; } dep[m2] = 0, Dfs(m1, m2, bl[++bt]); dep[m1] = 0, Dfs(m2, m1, bl[++bt]); su = bl[1] + bl[2]; } for (int i = 1; i <= bt; ++i) ans = Add(ans, sui[su - bl[i]]); ans = Add(ans, MOD - (bt - 1LL) * sui[su] % MOD); printf("%d\n", ans); return 0; }