简介
树形DP,字面意思,在树结构上的DP,通常根据比较子节点的最优得到父节点的状态或根据父节点预处理后再遍历得到子节点的解。
由于是入门篇,我们暂且先讨论前者。
原理
树一大特点就具有子结构,而DP也是要求最优子结构,并且两者都有相同的基本操作—“递归”,那么可见这两者就可以非常和谐的结合在一起了。
下面先看一个最简单的例子
一.NC15033 小G有一个大树
小G想要把自己家院子里的橘子树搬到家门口(QAQ。。就当小G是大力水手吧)
可是小G是个平衡性灰常灰常差的人,他想找到一个这个橘子树的平衡点。
怎么描述这棵树呢。。。就把它看成由一个个节点构成的树吧。结点数就
代表树重。
输入描述:
多组数据输入输出,
第一行包含一个整数n(3<=n<=1000)代表树的结点的个数
以下n-1行描述(1-n)节点间的连接关系。
输出描述:
输出两个个整数 x,num 分别代表树的平衡点,和删除平衡点后最大子树的结点数(如果结点数相同输出编号小的)。
题目意思就是给定一个任意分布没有给出根节点的树,问在那个点分开时,可以保证两端的节点数尽可能相等,
处理这个问题时,我们只需先用vector双向存边,在从任意一个边开始进行dfs,在dfs过程中找到以该节点断开时的重量分配情况,需要注意的是,为了避免重复访问父节点,我们需要在参数中加入fa,并且手动限制不会回到父节点,保证遍历能顺利结束,在dfs过程中,实际上做到了由浅入深,完成了dp的积累过程全部点的重量减去它的一个方向的重量就是另一个方向,取这两个值中更大的一个,使其最小即为所求。
#include
using namespace std;
const int N=1e5+1000;
vector<int> G[N];
int f[N];
int n,a,b;
int maxnode,maxsum=0x3f3f3f3f;
void dfs(int q,int fa)
{f[q]=1;int maxn=0;
for(int i=0;i<G[q].size();i++)
{int t=G[q][i];
if(t==fa)continue;
dfs(t,q);
f[q]+=f[t];
maxn=max(maxn,f[t]);
}
maxn=max(maxn,n-f[q]);
if(maxn<maxsum)
{
maxnode=q;
maxsum=maxn;
}
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n-1;i++)
{
scanf("%d %d",&a,&b);
G[a].push_back(b);
G[b].push_back(a);
}
dfs(1,0);
cout<<maxnode<<" "<<maxsum<<endl;
}
这就是最简单的树形dp的例子
下面在介绍2种变形
分别是最大独立集,和最小支配集
NC51178最大独立集 没有上司的舞会
题目描述
Ural大学有N名职员,编号为1~N。
他们的关系就像一棵以校长为根的树,父节点就是子节点的直接上司。
每个职员有一个快乐指数,用整数 Hi给出,其中 1≤i≤N。
现在要召开一场周年庆宴会,不过,没有职员愿意和直接上司一起参会。
在满足这个条件的前提下,主办方希望邀请一部分职员参会,使得所有参会职员的快乐指数总和最大,求这个最大值。
输入描述:
第一行一个整数N。
接下来N行,第 i 行表示 i 号职员的快乐指数Hi。
接下来N-1行,每行输入一对整数L, K,表示K是L的直接上司。
最后一行输入0。
输出描述:
输出最大的快乐指数。
这道题和上道题的最大的区别在于,这道题规定了谁是谁的父节点,所以不用双向存边,只需开一个父亲数组记录所有数的情况,保证可以找到唯一的根节点,从根节点开始dfs,
在dfs之前,我们先分析这个问题的本质, 也就是对基于第一层的选择,影响后续的决策过程,我们可以这样定义状态方程,用f[i][0]表示不选择i点时,i点及其子树能选出的最多人数,f[i][1]表示选择i点时,i点及其 子树的最多人数。
之后我们分析发现如果不选择i点本身,那么上一层的结果不受限制所以有• f[i][0] = Σ(max (f[j][0], f[j][1])) ,但是如果选择了i点,那么上一层的状态一定是没有选的状态,即• f[i][1] = Hi+ Σf[j][0] ,
关于边界和结果的处理就是这样
f[i][0] = 0, f[i][1] =Hi--------i是叶子节点
结果为max(f[root][0], f[root][1])
,分析完之后,我们首先要找到唯一的根节点,从头开始搜索,
while(fa[i]!=0)i++;//找父亲节点的位置
可以用简单的while循环寻找,需要注意的是,fa数组的值在读入数据时一并处理,只要当过儿子节点的,都使其值等于1,按序寻找即可找到父节点,
#include
using namespace std;
int h[10000],fa[10000];
int dp[10000][2];
vector<int> v[10000];
void dfs(int n)
{
dp[n][1]=h[n];
for(int i=0;i<v[n].size();i++)
{int w=v[n][i];
dfs(w);
dp[n][1]+=dp[w][0];
dp[n][0]+=max(dp[w][0],dp[w][1]);
}
}
int main()
{int n;
cin>>n;
for(int i=1;i<=n;i++)
scanf("%d",&h[i]);
for(int i=1;i<n;i++)
{
int x,y;
scanf("%d%d",&x,&y);
v[y].push_back(x);
fa[x]++;
}int i=1;
while(fa[i]!=0)i++;//找父亲节点的位置
dfs(i);
printf("%d\n",max(dp[i][0],dp[i][1]));
return 0;
return 0;
}
这样就处理了一个局部的最大独立集问题
NC24953CellPhoneNetwork最小支配集
题目描述
Farmer John has decided to give each of his cows a cell phone in hopes to encourage their social interaction. This, however, requires him to set up cell phone towers on his N (1 ≤ N ≤ 10,000) pastures (conveniently numbered 1…N) so they can all communicate.
Exactly N-1 pairs of pastures are adjacent, and for any two pastures A and B (1 ≤ A ≤ N; 1 ≤ B ≤ N; A ≠ B) there is a sequence of adjacent pastures such that A is the first pasture in the sequence and B is the last. Farmer John can only place cell phone towers in the pastures, and each tower has enough range to provide service to the pasture it is on and all pastures adjacent to the pasture with the cell tower.
Help him determine the minimum number of towers he must install to provide cell phone service to each pasture.
输入描述:
下面试试邻接矩阵的写法
#include
using namespace std;
const int INF=0x3f3f3f3f;
vector <int> v[10001];
int dp[10001][3];//0表示该点覆盖 1表示该点儿子覆盖 2表示该点父亲覆盖
void dfs(int q,int fa)
{int tmp=INF;
int flag=1;
int hp=0;
dp[q][0]=1;
for(int i=0;i<v[q].size();i++)
{
int t=v[q][i];
if(fa==t)continue;
hp=1;
dfs(t,q);
dp[q][0]+=min(dp[t][0],min(dp[t][1],dp[t][2]));
if(q==1)dp[q][2]=INF;
else dp[q][2]+=min(dp[t][1],dp[t][0]);
if(dp[t][0]<=dp[t][1])
{
flag=0;
dp[q][1]+=dp[t][0];
}
else {
dp[q][1]+=dp[t][1];
tmp=min(tmp,dp[t][0]-dp[t][1]);
}
}
if(flag)dp[q][1]+=tmp;
if(hp==0)dp[q][1]=INF;
}
int main()
{
int n;
cin>>n;
for(int i=1;i<n;i++)
{
int x,y;
cin>>x>>y;
v[x].push_back(y);
v[y].push_back(x);
}
dfs(1,0);
printf("%d\n",min(dp[1][0],min(dp[1][1],dp[1][2])));
}
如果换成链式前项星,那么代码细节会少一点,也就是对于父亲节点的判断手段发生变换,减少2个特判处理
void add(int u,int v)
{
edge[++tot].to=v;
edge[tot].nex=head[u];
head[u]=tot;
}
void dfs(int u)
{
vis[u]=1;
dp[u][0]=1;dp[u][1]=dp[u][2]=0;
int flag=0,res=inf;
//int len=G[u].size();
for(int i=head[u];~i;i=edge[i].nex)
{
int v=edge[i].to;
if(!vis[v])
{
dfs(v);
dp[u][2]+=min(dp[v][1],dp[v][0]);
dp[u][0]+=min(dp[v][1],min(dp[v][0],dp[v][2]));
if(dp[v][0]<=dp[v][1])
{
flag=1;dp[u][1]+=dp[v][0];
}
else
{
dp[u][1]+=dp[v][1];
res=min(res,dp[v][0]-dp[v][1]);
}
}
}
if(!flag) dp[u][1]+=res;
}
这就是最基础的树形dp的问题,及通过树的性质,一层层的进行状态转移,结合dfs用递归,从根出发扫到最后,实际上也就是递推中的从0出发。