树形dp 题解

SAO

题意 让你求有向图的拓扑序个数,保证这个有向图忽略掉边的方向,可以构成一颗树。
题解
你直接在有向图上面怎么怎么dp一下显然是不行的,然后,你发现,它是在树形dp作业里面,所以考虑树形dp。
我们考虑设 dp[i][j] 表示在最终的拓扑序中,\(i\) 前面有 \(j\) 个。
然后你考虑如何转移。
我们设 \(v\)\(u\) 的儿子,当前要从 \(u\) 转移到 \(v\)
我们对着这两个点之间的关系进行分类讨论。
首先考虑 \(u\) 要在 \(v\) 前面的情况。
假设 \(u\) 前面有 \(i\) 个点,\(v\) 前面有 \(j\) 个点。
那么,首先 \(u\)\(v\) 的前面的点一定是没有互相影响的对吧,这个你直接对着这些点分类讨论一下,一共只有几种情况,然后你就能证出来了。
所以你可以直接暴力枚举 \(v\) 前面的点里面有 \(k\) 个点是在 \(u\) 前面的,那么选出来这 \(k\) 个点往里面塞的方案即是 \(C_{i+k}^{k}\),然后 \(u\) 后面的方案数即是 \(C_{siz_u+siz_v-i-j-1}^{siz_v-k}\)
那么转移的方程式就很显然了。

\[dp_{u,i+j}=\sum \limits _{k>j} ^ {siz[v]} dp_{u,i}\times dp_{v,k} \times C_{i+j}^{j}\times C_{siz_u+siz_v-i-k-1}^{siz_v-j} \]

然后另一种情况同理,于是你得到了一个暴力 \(O(n^3)\) 的代码。
于是前缀和优化之,然后做完了呀。

#include<bits/stdc++.h>
#define int long long 
using namespace std;
int n;
const int N=1010,mod=1e9+7;
int a[N],siz[N],dp[N][N],lx[N],inv[N],g[N],sum[N];
vector<pair<int,int> >e[N];
int read() {int x;cin>>x;return x;}
int ksm(int x,int y) {int t=1;while(y){if(y&1)t=t*x%mod;x=x*x%mod;y>>=1;}return t;}
int C(int x,int y) {if(x<y) return 0;return lx[x]*inv[y]%mod*inv[x-y]%mod;}
void dfs(int x,int fa) {
	siz[x]=1;
	// memset(dp[x],1,sizeof(dp[x]));
	// cerr<<x<<" "<<fa<<endl;
	for(auto y:e[x]) {
		if(y.first==fa) continue;
		dfs(y.first,x);
		memcpy(g,dp[x],sizeof(g));
		memset(dp[x],0,sizeof(dp[x]));
		for(int i=1;i<=siz[y.first];i++) {
			sum[i]=sum[i-1]+dp[y.first][i];
			sum[i]%=mod;
		}
		if(y.second==1) {
			for(int i=1;i<=siz[x];i++) {
				for(int j=0;j<siz[y.first];j++) {
						if(!g[i]) continue;
						dp[x][i+j]+=(g[i]*(sum[siz[y.first]]-sum[j]+mod)%mod*C(i+j-1,j)%mod*C(siz[x]+siz[y.first]-i-j,siz[x]-i)%mod)%mod;
						dp[x][i+j]%=mod;
				}
			}	
		}
		else {
			for(int i=1;i<=siz[y.first];i++) {
					for(int k=1;k<=siz[x];k++) {
						dp[x][i+k]+=(g[k]*(sum[i])%mod*C(i+k-1,k-1)%mod*C(siz[x]+siz[y.first]-i-k,siz[y.first]-i))%mod;
						dp[x][i+k]%=mod;
					}
				// }
			}
		}
		siz[x]+=siz[y.first];
	}
}
void work() {
	n=read();
	lx[0]=1;
	for(int i=1;i<n;i++) {
		int u,v,f=0;
		char c;
		u=read();cin>>c;v=read();
		u++;v++;
		if(c=='<') f=1;
		e[u].push_back(make_pair(v,f));
		e[v].push_back(make_pair(u,f^1));
	}
	for(int i=1;i<=n;i++) {
		for(int j=1;j<=n;j++) {
			dp[i][j]=1;
		}
	}
	dfs(1,0);
	int ans=0;
	for(int i=1;i<=n;i++) {
		ans=(ans+dp[1][i])%mod;
		e[i].clear();siz[i]=0;
	}
	cout<<ans<<endl;
}
signed main() {
	lx[1]=1;
	for(int i=2;i<N;i++) {
		lx[i]=lx[i-1]*i%mod;
	}
	inv[N-1]=ksm(lx[N-1],mod-2);
	for(int i=N-2;i>=0;i--) {
		inv[i]=(inv[i+1]*(i+1))%mod;
	}
	int t=read();
	while(t--) work();
	return 0;
}

ARC179D

题意

给你一棵树,走过每条边都要花费 \(1\) 的代价,然后你还有一个传送门,你可以把传送门放在你当前遍历到的节点上,然后继续走,每次从当前节点回到传送门所在节点是不需要代价的,求遍历整棵树的最小代价。

解法

首先啊,根据一贯的套路,是要先从根节点dp一遍,然后跑换根dp的。
所以考虑从根节点出发怎么做。
鉴于题目里面说了,不需要你再回到出发点,所以这启示我们状态里面要有一维表示是否要回到根节点。
然后你发现,放传送门和不放传送门区别很大,所以你给状态里面又加了一维,表示传送门放没放在根节点上。
所以最后,你设 \(dp_{i,0/1,0/1}\) 表示把 \(i\) 的子树都遍历一遍,是否要回到根节点,根节点上放不放传送门的最小代价。
转移是显然的。

\[dp_{u,1,1}=\sum min(dp_{v,1,1}+2,dp_{v,0,0}+1) \]

\[dp_{u,1,0}=2*siz_u \]

\[dp_{u,0,1}=dp_{u,1,1}-\max min(dp_{v,1,1}+2,dp_{v,0,0}+1)-dp_{v,1,0}-1 \]

\[dp_{u,0,0}=2*siz_u-len \]

然后,你只需要换根就好了。
换根的步骤很经典了对吧,我们只需要每次清楚当前子节点对父节点的贡献,然后给当前子节点加上父节点的贡献即可。
于是,恭喜你,你做完了。

#include<bits/stdc++.h>
using namespace std;
int n;
const int N=2e5+100;
// int dp[N][2][2];
int dp[N][2][2],siz[N];
vector<int>e[N];
int ans=1e9;
struct node {
	int fir,sec;
	int get(int x) {
		if(x!=fir) return fir;
		return sec;
	}
}maxx[N],len[N];
int read() {int x;cin>>x;return x;}
void dfs(int x,int fa) {
	siz[x]=1;
	for(auto y:e[x]) {
		if(y==fa) continue;
		dfs(y,x);
		siz[x]+=siz[y];
		dp[x][1][1]+=min(dp[y][1][1]+2,dp[y][0][0]+1);
		int xx=min(dp[y][1][1]+2,dp[y][0][0]+1);
		xx-=dp[y][0][1]+1;
		if(xx>maxx[x].fir) {
			maxx[x].sec=maxx[x].fir;
			maxx[x].fir=xx;
		}
		else {
			maxx[x].sec=max(maxx[x].sec,xx);
		}
		xx=len[y].fir+1;
		if(xx>len[x].fir) {
			len[x].sec=len[x].fir;
			len[x].fir=xx;
		}
		else {
			len[x].sec=max(len[x].sec,xx);
		}
	}
	dp[x][0][1]=dp[x][1][1]-maxx[x].fir;
	dp[x][1][0]=2*siz[x]-2;
	dp[x][0][0]=dp[x][1][0]-len[x].fir;
}
void DP(int x,int fa) {
	ans=min(ans,min(min(dp[x][0][1],dp[x][1][0]),min(dp[x][0][0],dp[x][1][1])));
	for(auto y:e[x]) {
		if(y==fa) continue;
		int len1=len[x].get(len[y].fir+1);
		int maxx1=maxx[x].get(min(dp[y][1][1]+2,dp[y][0][0]+1)-dp[y][0][1]-1);
		int tmp[2][2];
		tmp[0][0]=dp[x][0][0];
		tmp[0][1]=dp[x][0][1];
		tmp[1][0]=dp[x][1][0];
		tmp[1][1]=dp[x][1][1];
        dp[x][1][0]=2*(n-siz[y])-2;
        dp[x][0][0]=dp[x][1][0]-len1;
        dp[x][1][1]-=min(dp[y][1][1]+2,dp[y][0][0]+1);
        dp[x][0][1]=dp[x][1][1]-maxx1;
        dp[y][1][0]=2*n-2;
        if(len1+1>len[y].fir) {
        	len[y].sec=len[y].fir;
        	len[y].fir=len1+1;
        }
        else {
        	len[y].sec=max(len[y].sec,len1+1);
        }
        dp[y][0][0]=dp[y][1][0]-len[y].fir;
        dp[y][1][1]+=min(dp[x][1][1]+2,dp[x][0][0]+1);
        if(min(dp[x][1][1]+2,dp[x][0][0]+1)-dp[x][0][1]-1>maxx[y].fir) {
        	maxx[y].sec=maxx[y].fir;
        	maxx[y].fir=min(dp[x][1][1]+2,dp[x][0][0]+1)-dp[x][0][1]-1;
        } 
        else maxx[y].sec=max(maxx[y].sec,min(dp[x][1][1]+2,dp[x][0][0]+1)-dp[x][1][0]-1);
        // maxx[y].upd(min(f[x][1][1]+2,f[x][0][0]+1)-f[x][1][0]-1);
        dp[y][0][1]=dp[y][1][1]-maxx[y].fir;
		DP(y,x);
		dp[x][0][0]=tmp[0][0];
		dp[x][0][1]=tmp[0][1];
		dp[x][1][0]=tmp[1][0];
		dp[x][1][1]=tmp[1][1];
	}
}
signed main() {
	n=read();
	int u,v;
	for(int i=1;i<n;i++) {
		u=read();v=read();
		e[u].push_back(v);
		e[v].push_back(u);
	}
	dfs(1,0);
	dp[1][0][0]=dp[1][0][0];
	dp[1][1][1]=dp[1][1][1];
	dp[1][1][0]=dp[1][1][0];
	dp[1][0][1]=dp[1][0][1];
	DP(1,0);
	cout<<ans<<endl;
	return 0;
}
posted @ 2025-10-30 16:37  wjx_2010  阅读(5)  评论(0)    收藏  举报

欢迎来到wjxland