树形DP

树形DP就是在树上DP。

P2515 [HAOI2010] 软件安装(口胡)

发现原图是一个森林与一堆环所构成的不知道什么东西。先将每个连通块染色,对于每个连通块判环将环以及连在环身上的东西判掉。然后直接建一个没有权值的虚空节点将森林连成一棵树即可直接DP。

P3262 [JLOI2015] 战争调度

  • 题意:给一颗 \(n\) 层的满二叉树,给每个点染黑白色,对于每一条从根节点到叶子节点的路径,这条路径的贡献是这条路径上所有与叶子结点同色的节点所给出的特定的权值。染成黑色的点不超过 \(m\) 个。问总的最大贡献。\(n\le10\)
  • 先考虑暴力。如果枚举每个叶子节点的状态然后去统计,对于每种叶子节点,还需要枚举除了叶子节点外的所有节点的颜色,因此复杂度为 \(O(2^{2^{n-1}})\)
  • 考虑如何优化一下。发现对于每个点,如果其到根节点的状态确定,则对于其子树,其最优解是确定了的,且不对树其他部分有影响。
  • 因此考虑类似于状压DP的枚举某个点到根节点的状态,再dfs下去,直到叶子节点。设 \(f[x][i]\) 表示对于节点 \(x\),子树内有 \(i\) 个黑点。
  • 叶子节点可以根据所枚举的路径直接统计其 \(f\) 值。由于上面说的对于每个节点只要上面到根的路径确定了这种状态的最大值也就确定了,因此对于每个点,直接对于每种情况取 \(max\) 就可以了。因此对于每种到根节点的状态,转移方程为:\(f[x][i+j]_{i+j\le min(m,siz_x)}=max(f[ls_x][i]+f[rs_x][j])\)
  • 注意对于每次枚举要清空 \(f\) 值。因为每个点一定会被枚举 \(2^{dep-1}\) 次,\(dep\) 是深度,而每次枚举的到根节点的路径都不同,但由于DP状态与到根节点的路径没有关系,因此必须每次清空
#include<bits/stdc++.h>
using namespace std;
const int N=2e3+5,M=15;
int war[N][M],farmer[N][M],n,m,num,f[N][N];
void dfs(int x,int fa,int siz)
{
	for(int i= 0;i<=siz;++i)//清空
    	f[x][i]=0;
	if(siz==1)
	{
		for(int i=0;i<n-1;i++)
			if((fa>>i)&1)
				f[x][1]+=war[x][i+1];
			else f[x][0]+=farmer[x][i+1];
		return;
	}
	for(int k=0;k<=1;k++)
	{
		dfs(x<<1,(fa<<1)|k,siz>>1);
		dfs((x<<1)|1,(fa<<1)|k,siz>>1);
		for(int i=0;i<=min(m,siz);i++)
		for(int j=0;j+i<=min(m,siz);j++)
			f[x][i+j]=max(f[x][i+j],f[x<<1][i]+f[(x<<1)|1][j]);
	}
		
}
signed main()
{
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	cin>>n>>m;
	num=1<<(n-1);
	for(int i=num;i<num*2;i++)for(int j=1;j<n;j++)cin>>war[i][j];
	for(int i=num;i<num*2;i++)for(int j=1;j<n;j++)cin>>farmer[i][j];
	dfs(1,0,(1<<n)-1);
	int res=0;
	for(int i=0;i<=m;i++) res=max(res,f[1][i]);
	cout<<res<<'\n';
	return 0;
}

P1272 重建道路

  • 题意:给一颗有 \(n\) 个节点的有根树,给定 \(P\),要求最少去掉多少条边可以分割出来一个大小为 \(P\) 的子树。注意,由于是有根树,所以除去割去的树剩下的部分不算做其子树。\(n\)
  • 还是先考虑暴力。枚举每条边是否被分割,再写个check。时间复杂度 \(O(n·2^{n-1})\)
  • 发现对于某个点,其内部分割了什么、怎么分割的其实与最终答案无关。最终答案无非关乎两样东西:分割出来的剩下的子树大小、分割的最小边数。由于分割的边数就是我们要求的,同时其值也与剩下的子树大小强相关,因此设 \(f[u][i]\) 表示对于节点 \(u\)剩下的子树大小为 \(i\) 需要割的最小边数。
  • 再考虑转移。发现情况事实上相当复杂。对于每一颗子树,都需要单独讨论情况,比如这颗子树删与不删等等。由于 \(n\) 不大,便考虑给状态新加一维,子树维。设 \(f[u][k][i]\)\(k\) 表示前 \(k\) 个儿子的信息。这时发现如果要维护当前儿子的信息,对于每一个 \(i\) 有以下两种情况:
  1. 不选当前子树,则最小的割边数加一(要把当前子树割掉)
  2. 选当前子树,则枚举一个 \(j\) 选择最小的组合方案,即在当前子树中留 \(j\) 个点,已经处理过的子树中留\(i-j\)个点(具体看转移方程)
  • 因此,转移方程就为\(f[u][k][i]=min_{j<i}(f[u][k-1][i]+1,f[u][k-1][k-j]+f[u][siz_v][j])\)
    这里的 \(siz_v\) 是当前子树的大小。\(j<i\) 是因为对于 \(u\) 所在的子树而言,\(u\) 本身至少要选,不然就没有意义了
  • 事实上,由于 \(n\) 足够小,因此对于 \(k\) 这一维,压不压缩都可以,不压缩更便于理解。不过一般DP题都是需要压缩的。如果要压缩的话,\(i\) 就必须倒着枚举,因为更新 \(f\) 值必须用之前的值来更新,如果先更新前面的话,就会导致后面的值不是由上一个子树中的 \(f\) 值来更新的。这里要理解透彻,如果不理解直接看代码的话可能比较难懂
  • 最后统计答案时由于需要统计分出去刚好大小为 \(P\) 的子树,因此除了根节点外,其他所有点还必须与其父亲节点分离,因此需要加1。
#include<bits/stdc++.h>
using namespace std;
const int N=155;
int n,p,tot[N],f[N][N],siz[N]; 
//f->the min number to cut 
vector <int> q[N];
void dfs(int u,int fa)
{
	siz[u]=1,f[u][1]=0;
	for(int k=0;k<tot[u];k++)
	{
		int v=q[u][k];
		if(v==fa) continue;
		dfs(v,u);
		siz[u]+=siz[v];
		for(int i=siz[u];i>=0;i--) //注意倒着 
		{
			f[u][i]++;   //如果不选就++,如果要选也就被覆盖掉了 
			for(int j=0;j<=min(i-1,siz[v]);j++)
			{
				f[u][i]=min(f[u][i],f[u][i-j]+f[v][j]);//注意理解 
			}
		}
	}
}
int main()
{
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	memset(f,0x3f3f,sizeof(f));   //记得初始化 
	cin>>n>>p;
	for(int i=1,u,v;i<=n-1;i++)
	{
		cin>>u>>v;
		q[u].push_back(v),tot[u]++;
		q[v].push_back(u),tot[v]++;
	}
	dfs(1,0);
	int ans=f[1][p];
	for(int i=2;i<=n;i++) ans=min(ans,f[i][p]+1);//与父亲分离 
	cout<<ans<<'\n';
	return 0;
}
posted @ 2024-10-12 16:34  all_for_god  阅读(24)  评论(0)    收藏  举报