「FJWC2020Day4-IJN」夕张的改造 题解

题意简述

给定一棵 \(n\) 个节点的树。一次操作为断边再连边,需保证仍为树。求 \(k\) 次操作后树的形态个数。

\(n \leq 5000\)\(0 \leq k \leq n\)

被绕晕了,对我来说是一道超好的题,写一篇题解理清思路,也希望帮助到你。

题目分析

由于我们总能浪费步数,即断开连上同一条边,或连上断开同一条边,所以我们求的答案为 \(0 \sim k\) 次不浪费的操作后,树的形态个数。

考虑操作了 \(k\) 次,我们就断开了 \(k\) 条边,并且需要这些边不会被连上。如果不考虑后来连上的 \(k\) 条边,此时树被划分为了 \(k + 1\) 个联通块。答案就是用 \(k\) 条边,不连上断开的边,把 \(k + 1\) 个联通块连成一棵树的方案数。

对于不会浪费步数的证明:

假设断开了一条连上的边,那么剩余边数不足以将其连成一棵树,故每次连边都不会浪费。

对于 \(k\) 个操作一起考虑的正确性证明:

即需要证明,存在一种方案,过程中始终合法,断边之后再连边,仍为一棵树。

由于不能断开一条新连上的边,所以所有新连上的边会被保存到最后,即每次连上的边只能答案中选取。

考虑某一次断边,在这次断边之前的操作均合法。断边后会得到两个联通块,需要在最终答案中的 \(k\) 条新增的边中,使用一条未被使用的边。最终答案里肯定有联通这两个联通块的边,否则不是一棵树,矛盾。而在断边之前我们保证这是一棵树,所以这些边都是没有被使用的。所以我们一定能够选择一条边来连上。

我们只需要考虑的浪费就是连上一条断开的边。如果没有这个限制,答案怎么统计呢?

根据 Prufer 序列经典结论,用 \(k\) 条边把大小分别为 \(a_1, \ldots, a_{k + 1}\) 的联通块连成树的方案数为 \(n ^ {(k + 1) - 2} \prod \limits _ {i = 1} ^ {k + 1} a_i\)。前者对于每一种方案都是相同的,也很好求。重点在于求出每一种方案下,后者之和,即求 \(\sum \prod \limits _ {i = 1} ^ {k + 1} a_i\)

考虑 \(\prod a_i\) 的意义,即为从每一个联通块中,选出一个关键点,求方案数。同时为了 \(\sum\),我们还要统计所有情况下的方案数之和。不妨记 \(F[u][x][0 / 1]\) 表示以 \(u\) 为根的子树中,得到 \(x\) 个联通块,并且根节点所在联通块有没有选出关键点,所有情况下的 \(\prod a_i\) 之和。边界 \(F[i][1][0 / 1] = 1\)

考虑树形 DP 孩子 \(u\) 合并到 \(v\)。决策是否断边,分类讨论一下。

  1. 断开 \(u\)\(v\) 之间的边。
    即此时 \(u\)\(v\) 不在同一联通块中,那么 \(u\) 中关键点必须选出。

    \[F[v][i + j][0 / 1] \gets F[v][i + j][0 / 1] + F[v][i][0 / 1] \times F[u][j][1] \]

    之所以两个 \(\sum\prod\) 直接相乘,是因为 \(u\) 中每一个 \(\prod\) 需要分别和 \(v\) 中的 \(\prod\) 相乘产生一种方案,最后再求和,两个 \(\sum \prod\) 直接相乘就满足了。

  2. 不断开 \(u\)\(v\) 之间的边。
    类似地得到如下转移方程。

    \[F[v][i + j][0] \gets F[v][i + j][0] + F[v][i][0] \times F[u][j][0] \]

    \[F[v][i + j][1] \gets F[v][i + j][1] + F[v][i][0] \times F[u][j][1] + F[v][i][1] \times F[u][j][0] \]

我们所求的 \(\sum \prod a_i\) 便是 \(F[1][k + 1][1]\)。也就是用 \(F[1][k + 1][1] \times n ^ {(k + 1) - 2}\) 表示操作了 \(k\) 次,可能连上一条断开的边,树的形态个数。注意到,这样统计是会重复的。不妨这样考虑,我们做的是钦定 \(n - 1 - k\) 条边不会改变,其他边可变可不变的方案数。这是二项式反演经典「至少」类问题,即「至少」\(n - 1 - k\) 条边不改变的方案数。我们要求的是不能连回去,恰好改变 \(k\) 条边,也就是恰好 \(n - 1 - k\) 条边不变的方案。

\(f(n - 1 - x) = dp[1][x + 1][1] \times n ^ {(x + 1) - 2}\),即 \(f(x) = dp[1][n - x][1] \times n ^ {(n - x) - 2}\) 表示「至少」\(x\) 条边不变。

考虑怎么表示出 \(g(x)\) 表示「恰好」\(x\) 条边不变。\(g(x)\)\(f(x)\) 的基础上,需要去掉大于 \(x\) 条边不变的情况。对于一个 \(t > x\)\(g(t)\) 反复贡献了 \(\dbinom{t}{x}\) 次。

\[g(x) = f(x) - \sum _ {i = x + 1} ^ {n - 1} \binom{i}{x} g(i) \]

移项得:

\[\begin{aligned} f(x) &= g(x) + \sum _ {i = x + 1} ^ {n - 1} \binom{i}{x} g(i) \\ &= \sum _ {i = x} ^ {n - 1} \binom{i}{x} g(i) \end{aligned} \]

反演得到:

\[g(x) = \sum _ {i = x} ^ {n - 1} (-1) ^ {i - x} \binom{i}{x} f(i) \]

对于操作了 \(k\) 次,即为保证「恰好」\(n - 1 - k\) 条边不变,答案为 \(g(n - 1 - k)\)。总答案即为 \(\sum \limits _ {i = 0} ^ k g(n - 1 - i)\)

当然,你也可以直接使用我在《学习笔记》提到的模型求解。

时间复杂度:\(\Theta(n ^ 2)\),瓶颈在于树形 DP 和反演。

树形 DP 时间复杂度 \(\Theta(n ^ 2)\) 说明:

考虑一对 \((u, v)\) 只会在 \(\operatorname{lca}\) 处合并,共有 \(\Theta(n^ 2)\) 个点对。

注意 \(k\) 需要和 \(n - 1\)\(\min\)

代码

点击查看直接套用模型的代码
F[0] = 1;
for (int i = 1, pw = 1; i <= n - 1; ++i)
    F[i] = mul(f[1][i + 1][1], pw), pw = mul(pw, n);
int ans = 0;
for (int i = 0; i <= k; ++i) {
    for (int j = 0; j <= i; ++j) {
        if ((i - j) & 1)
            G[i] = sub(G[i], mul(F[j], C(n - 1 - j, n - 1 - i)));
        else
            G[i] = add(G[i], mul(F[j], C(n - 1 - j, n - 1 - i)));
    }
    ans = add(ans, G[i]);
}
#include <cstdio>
#include <iostream>
#include <cstring>
using namespace std;

const int N = 5010;
const int mod = 998244353;

inline int add(int a, int b) { return a + b >= mod ? a + b - mod : a + b; }
inline int sub(int a, int b) { return a - b < 0 ? a - b + mod : a - b; }
inline int mul(int a, int b) { return 1ll * a * b % mod; }
inline void toadd(int& a, int b) { a = add(a, b); }

int n, k, fa[N];
int frac[N], ifrac[N], Inv[N];

inline int C(int n, int m) { return mul(frac[n], mul(ifrac[m], ifrac[n - m])); }

int f[N][N][2];  // j 个联通块,根所在的联通块有没有被选中
int siz[N];

int F[N], G[N];

signed main() {
#ifndef XuYueming
    freopen("kaisou.in", "r", stdin);
    freopen("kaisou.out", "w", stdout);
#endif
    scanf("%d%d", &n, &k), k = min(k, n - 1);
    for (int i = 2; i <= n; ++i) scanf("%d", &fa[i]), ++fa[i];
    frac[0] = ifrac[0] = 1;
    for (int i = 1; i <= n; ++i) {
        frac[i] = mul(frac[i - 1], i);
        Inv[i] = i == 1 ? 1 : mul(mod - mod / i, Inv[mod % i]);
        ifrac[i] = mul(ifrac[i - 1], Inv[i]);
        f[i][1][0] = f[i][1][1] = 1;
        siz[i] = 1;
    }
    for (int u = n; u >= 1; --u) {
        int v = fa[u];
        static int g[N][2];
        memcpy(g, f[v], sizeof(g));
        memset(f[v], 0x00, sizeof(f[v]));
        for (int i = 1; i <= siz[v]; ++i)
            for (int j = 1; j <= siz[u]; ++j) {
                toadd(f[v][i + j][0], mul(f[u][j][1], g[i][0]));
                toadd(f[v][i + j][1], mul(f[u][j][1], g[i][1]));
                toadd(f[v][i + j - 1][0], mul(f[u][j][0], g[i][0]));
                toadd(f[v][i + j - 1][1], add(mul(f[u][j][1], g[i][0]), mul(f[u][j][0], g[i][1])));
            }
        siz[v] += siz[u];
    }
    F[n - 1] = 1;
    for (int i = n - 2, p = 1; i >= 0; --i) F[i] = mul(p, f[1][n - i][1]), p = mul(p, n);
                                            // p 即为 n^{(n-x)-2}
    int ans = 0;
    for (int i = n - 1; i >= n - 1 - k; --i) {
        for (int j = i; j <= n - 1; ++j) {
            if ((j - i) & 1)
                G[i] = sub(G[i], mul(C(j, i), F[j]));
            else
                G[i] = add(G[i], mul(C(j, i), F[j]));
        }
        ans = add(ans, G[i]);
    }
    printf("%d", ans);
    return 0;
}
posted @ 2024-09-06 07:35  XuYueming  阅读(112)  评论(0)    收藏  举报