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

P9669 [ICPC2022 Jinan R] DFS Order 2

upd 2024.11.15

树形 dp+回退背包

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

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

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

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

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

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

dfs 的过程时走到 u,如果走进一个子树后要回到 u,那么这个子树一定全部遍历了一遍。所以方案数会跟子树遍历的方案数有关,可以预处理。设 hu 表示 u 子树的遍历方案,假如 um 个儿子,那么有 hu=m!×vsonuhv

考虑树形 dp。首先 u 点的 dfs 序与其子树无关,子树对其的影响只是 ×hu,所以根据乘法交换律,容易设 dpu,i 表示 暂时不考虑 u 子树对方案数的影响u 的 dfs 序最终为 i 的方案数。显然在这题里转移不同,为 dpu,idpv,j。枚举 v,如果要转移,需要知道此时 v 排在 u 后面 k 个位置的方案数 gk(这里指 dfs 序)。

实际上就是在 u 的子树中选若干个排在 v 前遍历,然后遍历 v。这就是经典的 01 背包问题,我们需要知道选的子树数,还有遍历总点数,所以设 fi,j 表示当前选了 i 个子树,总点数为 j 的方案数。转移易得。

然后考虑 fi,j 的贡献,首先 i 个子树有 i! 种遍历顺序,除此之外还要考虑剩下的子树有 (mi1)! 种遍历顺序,最后 hv 表示除 v 子树外的 hvgk+1=gk+1+fi,j×i!×(mi1)!×hv

最后 dpv,i+k=dpv,i+k+dpu,i×gk

这里的背包显然要排除当前的 v,若每个儿子都跑一次,复杂度为 O(n4),无法通过。这里背包的转移简单,可以用到回退背包,即我们可以对所有子树做一次背包,每次减去 vf 的贡献即可。复杂度 O(n3)

回退背包转移顺序与正常转移相反,原因是循环的顺序改变是因为本身我们的 (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 @   Fire_Raku  阅读(53)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
more_horiz
keyboard_arrow_up dark_mode palette
选择主题
点击右上角即可分享
微信分享提示