Loading

P9669 [ICPC2022 Jinan R] DFS Order 2 (树形 dp+回退背包)

P9669 [ICPC2022 Jinan R] DFS Order 2

upd 2024.11.15

树形 dp+回退背包

什么决定一个点的出现的位置?子树以内的点一定不会影响到,那么就先不考虑它。那我们可以描述这样一个过程,如果确定要去某个点,那么从根到目标位置,其实就是一个选子树进入再出来反复的过程。

一个子树完全遍历的方案数是可以预处理的,那么选子树问题就是一个背包问题了。

考虑答案的求法,树形 dp 可做。自然设 \(f_{i,j}\) 表示第 \(i\) 个点在第 \(j\) 个遍历的方案数。那么转移考虑枚举进入目标儿子 \(v\)。但在这之前,还可以选择一些子树先走,然后再进入 \(v\),最后遍历剩下子树。

怎么求到 \(v\) 的时候是第几个位置?只需要知道先走的子树的总点数就可以,写个背包。

然后转移带一些组合数,最后答案要乘上自身子树遍历的方案数。

背包复杂度太大,注意到每次只需要撤销自身子树对背包的影响就可以了,不需要重做一遍背包。

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;
}
posted @ 2024-04-10 20:39  Fire_Raku  阅读(46)  评论(0编辑  收藏  举报