P9669 [ICPC2022 Jinan R] DFS Order 2
树形 dp+回退背包
dfs 的过程时走到 \(u\),如果走进一个子树后要回到 \(u\),那么这个子树一定全部遍历了一遍。所以方案数会跟子树遍历的方案数有关,可以预处理。设 \(h_u\) 表示 \(u\) 子树的遍历方案,假如 \(u\) 有 \(m\) 个儿子,那么有 \(h_u=m!\times \prod\limits_{v\in son_u} h_v\)。
考虑树形 dp。首先 \(u\) 点的 dfs 序与其子树无关,子树对其的影响只是 \(\times h_u\),所以根据乘法交换律,容易设 \(dp_{u,i}\) 表示 暂时不考虑 \(u\) 子树对方案数的影响,\(u\) 的 dfs 序最终为 \(i\) 的方案数。显然在这题里转移不同,为 \(dp_{u,i}\rightarrow dp_{v,j}\)。枚举 \(v\),如果要转移,需要知道此时 \(v\) 排在 \(u\) 后面 \(k\) 个位置的方案数 \(g_k\)(这里指 dfs 序)。
实际上就是在 \(u\) 的子树中选若干个排在 \(v\) 前遍历,然后遍历 \(v\)。这就是经典的 01 背包问题,我们需要知道选的子树数,还有遍历总点数,所以设 \(f_{i,j}\) 表示当前选了 \(i\) 个子树,总点数为 \(j\) 的方案数。转移易得。
然后考虑 \(f_{i,j}\) 的贡献,首先 \(i\) 个子树有 \(i!\) 种遍历顺序,除此之外还要考虑剩下的子树有 \((m-i-1)!\) 种遍历顺序,最后 \(hv\) 表示除 \(v\) 子树外的 \(\prod h_v\),\(g_{k+1}=g_{k+1}+f_{i,j}\times i!\times (m-i-1)\times hv\)。
最后 \(dp_{v,i+k}=dp_{v,i+k}+dp_{u,i}\times g_k\)。
这里的背包显然要排除当前的 \(v\),若每个儿子都跑一次,复杂度为 \(O(n^4)\),无法通过。这里背包的转移简单,可以用到回退背包,即我们可以对所有子树做一次背包,每次减去 \(v\) 对 \(f\) 的贡献即可。复杂度 \(O(n^3)\)。
回退背包转移顺序与正常转移相反,原因是循环的顺序改变是因为本身我们的 (01) 背包是要用 没更新的数 去更新数,所以现在要用 已经还原的数 去更新待还原的数。
#include <bits/stdc++.h>
#define pii std::pair<int, int>
#define fi first
#define se second
#define pb push_back
typedef long long i64;
const i64 iinf = 0x3f3f3f3f, linf = 0x3f3f3f3f3f3f3f3f;
const i64 N = 510, mod = 998244353;
int n;
i64 fac[N], inv[N];
i64 sz[N], cnt[N];
i64 g[N], h[N], f[N][N], dp[N][N];
std::vector<int> e[N];
i64 qpow(i64 a, i64 b) {
i64 ret = 1;
while(b) {
if(b & 1) ret = ret * a % mod;
a = a * a % mod;
b >>= 1;
}
return ret;
}
void dfs(int u, int fa) {
sz[u] = h[u] = 1;
for(auto v : e[u]) {
if(v == fa) continue;
dfs(v, u);
h[u] = h[u] * h[v] % mod;
sz[u] += sz[v];
cnt[u]++;
}
h[u] = h[u] * fac[cnt[u]] % mod;
}
void DP(int u, int fa) {
memset(f, 0, sizeof(f));
f[0][0] = 1;
i64 m = cnt[u];
for(auto v : e[u]) {
if(v == fa) continue;
for(int j = m; j >= 1; j--) {
for(int k = sz[u]; k >= sz[v]; k--) {
f[j][k] = (f[j][k] + f[j - 1][k - sz[v]]) % mod;
}
}
}
for(auto v : e[u]) {
if(v == fa) continue;
i64 hv = h[u] * inv[m] % mod * qpow(h[v], mod - 2) % mod;
for(int j = 1; j <= m; j++) {
for(int k = sz[v]; k <= sz[u]; k++) {
f[j][k] = ((f[j][k] - f[j - 1][k - sz[v]]) % mod + mod) % mod;
}
} //注意循环为从小到大
memset(g, 0, sizeof(g));
for(int j = 0; j < m; j++) {
for(int k = 0; k < sz[u]; k++) {
g[k + 1] = (g[k + 1] + f[j][k] * fac[j] % mod * fac[m - j - 1] % mod * hv % mod) % mod;
}
}
for(int j = 1; j <= n; j++) {
for(int k = 1; k <= sz[u]; k++) {
dp[v][j + k] = (dp[v][j + k] + dp[u][j] * g[k] % mod) % mod;
}
}
for(int j = m; j >= 1; j--) {
for(int k = sz[u]; k >= sz[v]; k--) {
f[j][k] = (f[j][k] + f[j - 1][k - sz[v]]) % mod;
}
}
}
for(auto v : e[u]) {
if(v == fa) continue;
DP(v, u);
}
}
void Solve() {
std::cin >> n;
fac[0] = 1;
for(int i = 1; i <= n + 1; i++) fac[i] = fac[i - 1] * i % mod;
inv[n + 1] = qpow(fac[n + 1], mod - 2);
for(int i = n; i >= 0; i--) inv[i] = inv[i + 1] * (i + 1) % mod;
for(int i = 1; i < n; i++) {
int u, v;
std::cin >> u >> v;
e[u].pb(v), e[v].pb(u);
}
dfs(1, 0);
dp[1][1] = 1;
DP(1, 0);
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= n; j++) {
std::cout << (dp[i][j] * h[i]) % mod << " \n"[j == n];
}
}
}
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
Solve();
return 0;
}
标签:ICPC2022,遍历,sz,int,Jinan,i64,Order,dp,mod
From: https://www.cnblogs.com/FireRaku/p/18127371