【ICPC2022济南站】【树形dp】【删物品背包dp】C.DFS Order 2

5 篇文章 0 订阅
5 篇文章 0 订阅

【题意】

题目链接:https://codeforces.com/gym/104076/problem/C
简要题意:给定一棵n个点的有根树,对于所有的二元组 ( i , j ) (i,j) (i,j)求这颗树所有可能的dfs序中有多少个dfs序满足第 i i i个点出现在dfs序第 j j j个位置。

【思路】

赛场上假了无数次以后,我终于才理清楚了这题的dp思路。
状态定义:
定义 d p [ u ] [ i ] dp[u][i] dp[u][i]表示只考虑 u u u子树外的点的情况下,dfs序中在 u u u前面有 i i i个点的方案数。注意,这个 d p dp dp值并不能直接作为答案,还要乘上 u u u子树内部的所有可能的dfs序方案数。显然这个 d p dp dp的取值与 u u u子树的情况无关,因此这道题 d p dp dp的转移与一般树形 d p dp dp不同,这道题应当自上而下用父亲的信息更新儿子的信息。上文提到过,为了得到答案,我们还需要 u u u子树内部的dfs序方案数量,因此定义 d p 2 [ u ] dp2[u] dp2[u]表示 u u u子树内的dfs序方案数。
状态转移
设我们当前在 u u u点,我们考虑更新 u u u的一个儿子 v v v d p dp dp信息,我们需要知道dfs序有多少个点在 v v v前面,我们把这些点分为在 u u u的子树内和 u u u的子树外两类,最后类似背包的思路合并即可。

  • 对于 u u u子树外的点的信息,我们通过 u u u d p dp dp值即可获得;
  • 对于 u u u子树内的点,在 v v v前面的点的数量取决于 u u u的儿子们的排列顺序,我们可以把u的所有儿子的子树大小 s i z siz siz拿来做一次背包,与一般背包不同的是,后续用背包的结果更新dp值时还需要考虑放置顺序,因此我们还需要加一维表示当前背包的大小,便于考虑物品顺序。设 c [ i ] [ j ] c[i][j] c[i][j]表示放置了 i i i个物品,总大小为 j j j的方案数,考虑放置子树 v v v,则转移显然: c [ i ] [ j ] = c ′ [ i ] [ j ] + c ′ [ i − 1 ] [ j − s i z [ v ] ] c[i][j]=c'[i][j]+c'[i-1][j-siz[v]] c[i][j]=c[i][j]+c[i1][jsiz[v]]
    c ′ c' c表示未考虑 v v v的dp值, c c c表示已考虑 v v v的dp值,因此 i i i这一维倒序枚举更新即可不需要额外的数组。若我们要求 v v v的dp值,则我们背包里需要删除 v v v这个物品。由于背包dp的本质是一种多项式的卷积(此处的dp等价于 c ( y , x ) = ∏ v ∈ s o n u ( 1 + y ∗ x s i z [ v ] ) c(y,x)=\prod_{v\in son_u}(1+y*x^{siz[v]}) c(y,x)=vsonu(1+yxsiz[v])),放置顺序无关紧要,我们不妨假设 v v v是最后一个加入背包的物品。此时相当于我们已知c数组而需要求 c ′ c' c数组(注意,这里的 c ′ c' c数组在参考代码里就是 d d d数组)。移项可得转移:
    c ′ [ i ] [ j ] = c [ i ] [ j ] − c ′ [ i − 1 ] [ j − s i z [ v ] ] c'[i][j]=c[i][j]-c'[i-1][j-siz[v]] c[i][j]=c[i][j]c[i1][jsiz[v]]
    在得到 c ′ c' c数组后,设在 v v v前面有 j j j u u u子树内的点的方案数为 b [ j ] b[j] b[j] u u u一共有 c n t cnt cnt个儿子,则有转移:
    b [ j ] = ∑ i = 0 c n t − 1 c ′ [ i ] [ j ] ∗ i ! ∗ ( c n t − 1 − i ) ! b[j]=\sum_{i=0}^{cnt-1} c'[i][j]*i!*(cnt-1-i)! b[j]=i=0cnt1c[i][j]i!(cnt1i)!
    而对于 u u u这个点,它在dfs序中一定位于其他 u u u子树内的点的前面,我们可以在设置 c c c数组初值时考虑它,即: c [ 0 ] [ 1 ] = 1 c[0][1]=1 c[0][1]=1
  • 现在,我们已知 b b b数组和 d p dp dp数组,考虑用背包dp的思路对它们进行合并。但是我们目前仅考虑了u的儿子之间的排列顺序,尚未考虑这些儿子子树内的排序方案。设 B = ∏ x ∈ s o n u d p 2 [ x ] B=\prod_{x\in son_u}dp2[x] B=xsonudp2[x]。当我们用这些信息去更新 v v v的dp值时,需要注意除去常数B中和 v v v子树有关的信息。诚然,不除去这部分信息的话,我们直接得到的就是 v v v的答案数组,但是这并不利于我们进一步dfs求v的子树的dp信息,因此我们在这里的处理是除去这部分信息。即定义 C = ∏ x ∈ s o n u , x ≠ v = B d p 2 [ v ] C=\prod_{x\in son_u,x\neq v}=\frac B {dp2[v]} C=xsonu,x=v=dp2[v]B,有以下转移:
    对 于 i ∈ [ 1 , n ] , d p [ v ] [ i ] = C ∗ ∑ j = 0 s i z [ u ] b [ j ] ∗ d p [ u ] [ i − j ] 对于i\in [1,n],dp[v][i]=C*\sum_{j=0}^{siz[u]}b[j]*dp[u][i-j] i[1,n],dp[v][i]=Cj=0siz[u]b[j]dp[u][ij]
    j j j的上确界是小于 s i z [ u ] siz[u] siz[u]的,比赛时为了求稳所以定枚举上界为 s i z [ u ] siz[u] siz[u]

而对于dp2数组,我们需要在处理dp数组前就提前dfs一次先得到它。设 u u u总共有 c n t cnt cnt个儿子,考虑 u u u子树内所有可能的dfs序,转移显然:
d p 2 [ u ] = c n t ! ∗ ∏ d p 2 [ v ] dp2[u]=cnt!*\prod dp2[v] dp2[u]=cnt!dp2[v]
此时,第 i i i个点出现在dfs序中第 j j j个位置的方案数就是 d p [ i ] [ j − 1 ] ∗ d p 2 [ i ] dp[i][j-1]*dp2[i] dp[i][j1]dp2[i]
参考代码:(比赛时写的代码)

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N=505,mod=998244353;
int n,dp[N][N],siz[N],c[N][N],d[N][N],b[N],fac[N],dp2[N];
vector<int>g[N];
void Add(int&x,int y){
	((x+=y)>=mod)&&(x-=mod);
}
inline int mul(int x,int y){
	return 1ll*x*y%mod;
}
inline int dec(int x,int y){
	return x<y?x-y+mod:x-y;
}
inline int ksm(int a,int b){
	int ret=1;
	while(b){
		if(b&1)ret=mul(ret,a);
		a=mul(a,a);
		b>>=1;
	}return ret;
}
void dfs1(int u,int fa){
	siz[u]=1;
	dp2[u]=1;
	int cnt=0;
	for(int v:g[u]){
		if(v==fa)continue;
		cnt++;
		dfs1(v,u);
		siz[u]+=siz[v];
		dp2[u]=mul(dp2[u],dp2[v]);
	}
	dp2[u]=mul(dp2[u],fac[cnt]);
}
void dfs2(int u,int fa){
	int cnt=0,sum=1;
	memset(c,0,sizeof c);
	c[0][1]=1;
	int lsr=1;
	for(int v:g[u]){
		if(v==fa)continue;
		cnt++;sum+=siz[v];
		lsr=mul(lsr,dp2[v]);
		for(int i=cnt;i>=1;i--)
			for(int j=sum;j>=siz[v];j--)
				Add(c[i][j],c[i-1][j-siz[v]]);
	}
	for(int v:g[u]){
		if(v==fa)continue;
		memset(d,0,sizeof d);
		for(int i=0;i<=cnt;i++)
			for(int j=0;j<=siz[u];j++)
				d[i][j]=dec(c[i][j],(j>=siz[v]&&i>0)?d[i-1][j-siz[v]]:0);
		memset(b,0,sizeof b);
		for(int i=0;i<cnt;i++)
			for(int j=0;j<=siz[u];j++)
				Add(b[j],mul(mul(fac[i],fac[cnt-i-1]),d[i][j]));
		int bas=mul(lsr,ksm(dp2[v],mod-2));
		for(int i=0;i<=n;i++)
			for(int j=0;j<=siz[u];j++)
				Add(dp[v][i+j],mul(bas,mul(dp[u][i],b[j])));
	}
	for(int v:g[u]){
		if(v!=fa)dfs2(v,u);
	}
}
int main()
{
	scanf("%d",&n);
	fac[0]=1;
	for(int i=1;i<=n;i++)fac[i]=mul(fac[i-1],i);
	for(int i=2;i<=n;i++){
		int u,v;
		scanf("%d%d",&u,&v);
		g[u].push_back(v);
		g[v].push_back(u);
	}
	dp[1][0]=1;
	dfs1(1,0);
	dfs2(1,0);
	for(int i=1;i<=n;i++){
		for(int j=0;j<n;j++)
			cout<<mul(dp[i][j],dp2[i])<<" ";
		cout<<"\n";
	}
		
}
  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值