树状DP

说一下近日所学的主要两种题型,一个是分叉情况问题,一种是树上背包问题。
分叉情况问题
具体的题可以参考小胖守皇宫和三色二叉树。

点击查看代码
小胖守皇宫
#include<bits/stdc++.h>
using namespace std;
const int N=2000; 
vector<int>son[N];
int fa[N],h[N],f[N][3];
//f[0]守自己,f[1]父亲守,f[2]儿子守 
int n;
void dp(int x)
{
	int tot=0x3f3f3f3f;
	if(f[x][0])
	{
		return ;
	}
	if(son[x].size()==0)
	{
		f[x][0]=h[x];
		f[x][1]=0;
		f[x][2]=0x7ffffff;	
		return ;
	}
	f[x][0]=h[x];
	for(int i=0;i<son[x].size();i++)
	{
		int y=son[x][i];
		dp(y);
	}
	for(int i=0;i<son[x].size();i++)
	{
		int y=son[x][i];
		f[x][0]+=min(min(f[y][0],f[y][1]),f[y][2]);
		f[x][1]+=min(f[y][0],f[y][2]);
		f[x][2]+=min(f[y][0],f[y][2]);
		tot=min(tot,f[y][0]-f[y][2]);//求自己守和让儿子守的差值
		
	}
	if(tot>0)//无儿子守,强迫差值最小的儿子守
	{
		f[x][2]+=tot;
	}
}
int main()
{
//	memset(f,0x3f,sizeof(f));
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		int id,cost,num;
		cin>>id>>cost>>num;
		h[id]=cost;
		for(int j=1;j<=num;j++)
		{
			int x;
			cin>>x;
			son[id].push_back(x);
			fa[x]=id;
		}
	}
	int root;
	for(int i=1;i<=n;i++)
	{
		if(fa[i]==0)
		{
			root=i;
			break;
		}
	}
	dp(root);
//	cout<<f[root][0]<<endl;
	cout<<min(f[root][0],f[root][2]);
}

点击查看代码
三色二叉树
#include<bits/stdc++.h>
using namespace std;
const int N=20000;
struct lmy
{
	int lc,rc;
}a[N];
char s[N];
int f[N][3];//最多 
//f[0]该节点为绿色,绿色总数 
//f[1]该节点为红色,绿色总数 
//f[2]该节点为蓝色,绿色总数
int g[N][3];//最少 
int cnt;
int build()
{
	int u=++cnt;
	if(s[cnt]=='2')
	{
		a[u].lc=build();
		a[u].rc=build();
	}
	else if(s[cnt]=='1')
	{
		a[u].lc=build();
	}
	return u;
}
void big(int u)
{
	if(a[u].lc==0&&a[u].rc==0)//reach the bottom 
	{
		f[u][0]=1;
		return ;
	}
	else if(a[u].lc!=0&&a[u].rc!=0)//has two child
	{
		big(a[u].lc);
		big(a[u].rc);
	}
	else//only has one child
	{
		big(a[u].lc);
	}
	int x=a[u].lc;
	int y=a[u].rc;
	f[u][0]=max(f[x][1]+f[y][2],f[x][2]+f[y][1])+1;
	f[u][1]=max(f[x][0]+f[y][2],f[x][2]+f[y][0]);
	f[u][2]=max(f[x][0]+f[y][1],f[x][1]+f[y][0]);
}
void small(int u)
{
	if(a[u].lc==0&&a[u].rc==0)//reach the bottom 
	{
		g[u][0]=1;
		return ;
	}
	else if(a[u].lc!=0&&a[u].rc!=0)//has two child
	{
		small(a[u].lc);
		small(a[u].rc);
	}
	else//only has one child
	{
		small(a[u].lc);
	}
	int x=a[u].lc;
	int y=a[u].rc;
	g[u][0]=min(g[x][1]+g[y][2],g[x][2]+g[y][1])+1;
	g[u][1]=min(g[x][0]+g[y][2],g[x][2]+g[y][0]);
	g[u][2]=min(g[x][0]+g[y][1],g[x][1]+g[y][0]);
}
int main()
{
	cin>>s+1;
	build();//建图 
	big(1);
	cout<<max(max(f[1][0],f[1][1]),f[1][2])<<" ";
	small(1);
	cout<<min(min(g[1][0],g[1][1]),g[1][2]);
}

不难发现这两个代码的共同点,作为dp,f[i][j]的j总是表示其中可能对结果造成影响的情况分支。因此这种问题的关键除了状态转移方程外,就是去确定可能并可行的分支。其中的关键点就是要找到会对结果造成影响的分支并使分支尽可能的少,例如三色二叉树关于红色和蓝色的情况对这道题没有影响,因此我们可以忽略它。
还有一个确定状态转移方程的小技巧,先抓住其中一个节点分析(不是树根或树叶),以这个节点单独分析,不去考虑其他节点的变化,这样就不容易被头疼的递归搞晕。然后是看叶子节点,按照推出的动态转移方程看看有什么差异(主要是看需不需要赋初始值),最后看一看边界情况和特殊处理就可以了。
接着说一下这两个题的特殊知识点

vector容器 推荐学习链接:传送门
这个内容十分详细

build()建树 直接看三色二叉树代码就可以。

树上背包问题
例题可以看the more the better,选课和苹果二叉树。
the more the better和选课类似,代码只展示选课。

选课

点击查看代码
#include <iostream>
#include <cstring>
#include<cmath>
#include<algorithm>
using namespace std;
const int N=2500;
int cnt,to[N],nxt[N],h[N];
int n,m,s[N],f[N][N];
void add(int a,int b)
{
	cnt++;
	to[cnt]=b;
	nxt[cnt]=h[a];
	h[a]=cnt;
}
void dp(int z)
{
	for(int i=h[z];i;i=nxt[i])
	{
		int y=to[i];
		dp(y);
		for(int j=m;j>=0;j--)
		{
			for(int k=1;k<=j;k++)
			{
				f[z][j]=max(f[z][j],f[y][j-k]+f[z][k]);
			}
		}
	}
}
int main()
{
	cnt=0;
	memset(f,0,sizeof(f));
	memset(to,0,sizeof(to));
	memset(nxt,0,sizeof(nxt));
	memset(h,0,sizeof(h));
	cin>>n>>m;
	for(int i=1;i<=n;i++)
	{
		int a,b;
		cin>>a>>b;
		add(a,i);
		f[i][1]=b;
	}
	m++;
	dp(0);
	cout<<f[0][m]<<endl;
} 

苹果二叉树

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=2500;
int cnt,to[N],w[N],nxt[N],h[N];
int n,m,s[N],f[N][N];
void add(int a,int b,int c)
{

	to[cnt]=b;
	w[cnt]=c;
	nxt[cnt]=h[a];
	h[a]=cnt++;

}
void dp(int z,int fa)
{
	for(int i=h[z];~i;i=nxt[i])
	{
		
		int y=to[i];
		if(y==fa)
		{
			continue;
		}
		dp(y,z);
		for(int j=m;j>=0;j--)
		{
			for(int k=0;k<j;k++)
			{
				f[z][j]=max(f[z][j],f[z][j-k-1]+f[y][k]+w[i]);
			}
		}
	}
}
int main()
{
	memset(h,-1,sizeof(h));
	cin>>n>>m;
	for(int i=1;i<=n-1;i++)
	{
		int a,b,c;
		cin>>a>>b>>c;
		add(a,b,c);
		add(b,a,c);
	}
	dp(1,-1);
	cout<<f[1][m];
} 

奉上模板,注有解释
点击查看代码
void dp(int u)
{
	for(int i=h[u];i;i=nxt[i])//通过链式结构遍历儿子节点 
	{
		int son=to[i];
		dp(son);
		for(int j=m-v[u];j>=0;j--)
		//枚举背包容积,同背包问题,但要保留该节点
		//该节点是其他节点的前提,必选 
		{
			for(int k=0;k<=j;k++)
			//决策点,k不断更新状态
			//已遍历的儿子节点+正在遍历的儿子节点
			//f[u][j-k]为已遍历的儿子节点背包容积为j-k的最优解
 
			{
				f[u][j]=max(f[u][j],f[u][j-k]+f[son][k]);
			}
		}
	}
	for(int i=m;i>=v[u];i--)//添上该节点 
	{
		f[u][i]=f[u][i-v[u]]+w[u];
	}
	for(int i=0;i<v[u];i++)
	//如果该节点选不了,它的儿子节点也无法选 
	//无法选的置0 
	{
		f[u][i]=0;
	}
}

总结
树状DP与递归的关系密不可分,但我们在做题的时候可以先将其忽略,以免其对我们的思维造成影响。树状DP的这两个题型就到这里,拜拜。

posted @ 2024-02-17 12:09  zhengchenxi  阅读(73)  评论(0)    收藏  举报