[ZJOI2022] 树
[ZJOI2022] 树
一些经典的 dp 手法。
考虑这个题目在讲什么,每个点都要朝左右两边连各一条有向边,限制是一个点要么左边没有入边要么右边没有入边,但不能两边同时没有入边。
发现没法转化,考虑硬做。设 \(f_{i, j, k, l}\) 表示考虑前 \(i\) 个点,有 \(j\) 条向右的有向边终点待定,有 \(k\) 个点必须接受以后的向左的入边,\(l\) 个点可以接受而不必须接受。有朴素的 \(O(n^5)\) 的 dp。
发现后面两维很冗余,考虑怎么干掉这两维。发现关键在于维护必须和非必须,启发我们考虑容斥,必须接 = 随便接 - 不接。这样设 \(f_{i, j, k}\) 其中 \(k\) 表示有 \(k\) 个点是随便接不接的。代码如下:
f[1][1][1] = 1;
for(int i = 2; i <= n; ++i) {
for(int j = 1; j < i; ++j) {
for(int k = 0; k < i; ++k) if(f[i - 1][j][k]) {
int w = 1ll * f[i - 1][j][k] * k % P;
add(f[i][j + 1][k + 1], w);
sub(f[i][j + 1][k], w);
for(int l = 1; l <= j; ++l)
add(f[i][j - l + 1][k], 1ll * binom[j][l] * w % P);
}
}
for(int j = 0; j <= i; ++j)
add(F[i], f[i][1][j]);
cout << F[i] << '\n';
}
写完后发现这时候第二维转移是 \(O(n)\) 的,导致最终复杂度变成了 \(O(n^4)\)。考虑把这部分也优化掉。有一个经典手法,就是在每个点就钦定好向右连边的终点,我们只需维护目前需要多少个终点,支持新增一个终点即可。总复杂度 \(O(n^3)\)。
int main() {
cin >> n >> P;
f[1][1][1] = 1;
for(int i = 2; i <= n; ++i) {
int ans = 0;
for(int j = 0; j <= n - i + 1; ++j)
for(int k = 0; k < i; ++k) if(f[i - 1][j][k]) {
int w = 1ll * f[i - 1][j][k] * k % P;
add(f[i][j - 1][k], 1ll * w * (j - 1) % P);
if(j == 1) add(ans, w);
add(f[i][j][k], 1ll * w * j % P);
add(f[i][j][k + 1], 1ll * w * j % P);
add(f[i][j + 1][k + 1], 1ll * w * (j + 1) % P);
sub(f[i][j][k], 1ll * w * j % P);
sub(f[i][j + 1][k], 1ll * w * (j + 1) % P);
}
cout << ans << '\n';
}
}