树形dp总结
题型
1.树的直径(树形dp,dfs)
树的直径
1.树的最长路径
链接: link.
2.大臣的旅费
链接: link.
树的最长路径是用的树形dp做法,也就是记录每个点的最大和次大距离,维护一个答案变量ans,ans=max(ans,d1+d2)
// 代码模板
int dfs(int u,int father)//对于每个点需要返回一个从这个点出发的最大值
{
int dist=0;//表示从当前点往下走的最大长度
int d1=0,d2=0;
for(int i=h[u];i!=-1;i=ne[i])
{
int j=e[i];
if(j==father) continue;
int d=dfs(j,u)+w[i];
dist=max(dist,d);
//更新最大或次大值
if(d>=d1) d2=d1,d1=d;
else if(d>d2)d2=d;
}
ans=max(ans,d1+d2);
return dist;
}
大臣的旅费用的是dfs的方法:
从任意一个点开始dfs,求出最长路径,再从最长路径的终点开始dfs,求出最终的最长路径
//代码模板
dis用于记录从父节点往下的路径长度,dist[]存放每个点到根的距离
void dfs(int u,int father,int dis)
{
dist[u]=dis;
for(int i=h[u];i>=0;i=ne[i])
{
if(e[i]!=father)
{
dfs(e[i],u,dis+w[i]);
}
}
}
int main()
{
cin>>n;
memset(h,-1,sizeof h);
for(int i=0;i<n-1;i++)
{
int a,b,c;
cin>>a>>b>>c;
add(a,b,c),add(b,a,c);//无向图需要建立两个关系
}
dfs(1,-1,0);
int u=dist[1];
//找到距离根最远的点
for(int i=1;i<=n;i++)
{
if(dist[i]>dist[u]) u=i;
}
//从最远的点开始再dfs一遍
dfs(u,-1,0);
for(int i=1;i<=n;i++)
{
if(dist[i]>dist[u])
u=i;
}
int s=dist[u];
cout<<s<<endl;
}
3.数字转换
如果一个数 x 的约数之和 y(不包括他本身)比他本身小,那么 x 可以变成 y,y 也可以变成 x。
例如,4可以变为 3,1 可以变为 7。
限定所有数字变换在不超过 n的正整数范围内进行,求不断进行数字变换且不出现重复数字的最多变换步数。
链接link
一个数的约数之和和它是多对一的关系,即一个数只有一个约数之和,但一个数可以是多个数的约数之和,这正好是树的结构,而题目中所需的最多变化路径,就相当于求树的直径。
要解出本题的关键在于看出这是一道考察关系的图,一般涉及到各种关系就得想到用图论来解决,而这题所需的图正好满足一对多的关系,所以是一种特殊的图:树
2.树的中心
树的中心
树的直径可以理解为从一个点出发的最长和次长路径之和而树的中心可以只是从一个点出发的 一条最短路径
1.旅游规划
链接: link
旅游规划严格来说属于树的直径的问题,但将这两题归于一类是因为这两题的解题方式很相似,旅游规划中要求在直径上的点,思路是先求出直径,再判断一个点的最长和次长路径是否等于直径在求一个点的最长和次长路径,其实就转化成了求树的中心的过程
对于求树的中心,可以抽象成求树中的某一个节点到其他节点的路径,这时候考虑的对象就是节点,可以理解为dp集合划分的过程,对于一个节点而言,依据它可走的路径作为划分依据。它可以向dfs过程中的父节点的方向走,也可以向子节点的方向走。
先记为:
情况1:当前节点往父节点走 2.当前节点往子节点方向走
在处理树形dp问题时,我觉得有一个很重要的原则:一定要保证dfs过程中的顺序是直线向下的,尤其是在状态机模型时,如果采取很复杂,跨度很大(例如将父节点子节点甚至子子节点的关系杂糅在一起),甚至负节点的状态需要子节点回溯来处理,这样的关系就很难做,所以尽量要把关系保持在子节点和父节点之间
在满足这一原则的前提下:可以发现对于情况2,正好是一个dfs的过程,即dfs到底再回溯的过程,也就是子节点更新父节点的过程,因为我们是先知道了子节点的信息,再得到的父节点的信息
对于情况2来说,则正好相反,这是一个先知道父节点信息再知道子节点信息的过程
对于情况1,2的处理方式最大的不同点在于,情况1是在更新信息前先dfs子节点,情况2是在更新完后再dfs子节点
两道题关键处(求一个点的最长或最短路径)的代码模板
//dfs_d是向下走的过程
//d1记录最大值,d2记录次大值,p1记录对于节点u的最大路径来自哪个子节点
void dfs_d(int u,int father)
{
for(int i=h[u];i!=-1;i=ne[i])
{
int j=e[i];
if(j==father) continue;//无向图不能遍历父节点
dfs_d(j,u);
int d=d1[j]+w[i];
//对于新的d有三种情况,1.大于当前d1,2.介于d1,d2,3.小于d2
if(d>d1[u])//情况1
{
d2[u]=d1[u],d1[u]=d,p1[u]=j;
}
//情况2
else if(d>d2[u]) d2[u]=d;
//情况3不用更新
}
}
对于向父节点方向走的情况,就是用父节点的信息更新子节点,其实可以转换下思维,假设当前的子节点为父节点,父节点为字节点,就变成了熟悉的向下dfs的过程,之前一直都是考虑当前节点的子节点的选择,现在就相当于考虑当前节点父节点的选择,既然是递归,就得保持一致性,对于父节点来说,也有两种选择,一种还是向父节点,一种是向子节点
往父节点方向的距离用up[]记录,向子节点方向的就用上一步留下的d1[],d2[],以及p1[]
在考虑向的情况时,需要注意d1[u]是否就是父节点与子节点的这条连边,
如果是,在考虑父节点即其向上的距离时,就要比较再向上和往下的次大距离的关系,如果不是就直接比较与d1[u]的关系
在判断d1[u]是否是父节点与当前枚举的子节点的连边时,p1[],就起到了记录的作用
void dfs_u(int u,int father)
{
for(int i=h[u];i!=-1;i=ne[i])
{
int j=e[i];
if(j==father) continue;
if(p1[u]==j) up[j]=max(up[u],d2[u])+w[i];
else up[j]=max(up[u],d1[u])+w[i];
dfs_u(j,u);
}
}
3.状态机模型
状态机
1.没有上司的舞会
链接link
Ural 大学有 N名职员,编号为 1∼N。他们的关系就像一棵以校长为根的树,父节点就是子节点的直>接上司。
每个职员有一个快乐指数,用整数 Hi给出,其中 1≤i≤N。
现在要召开一场周年庆宴会,不过,没有职员愿意和直接上司一起参会。
在满足这个条件的前提下,主办方希望邀请一部分职员参会,使得所有参会职员的快乐指数总和最大,求这个最大值。
这一题的大意就是对于一条边的两个结点,最多只能取一个节点,所以考虑对象为每个节点,可以写出状态机,每个点有选或不选两种情况,记为f[u][1],f[u][0];
还是按照上文的原则,保证dfs的过程中只维护父节点和子节点的信息,所以接下来考虑当前节点和子节点的关系,同时以父节点的状态作为划分点
1.如果选择父节点即意味着子节点都不能选
用j表示子节点:f[u][1]+=f[j][0];
2.如果不选父节点,那么子节点就可选可不选
f[u][0]=max(f[j][1],f[j][0]);
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=6100;
int f[N][2];
bool has_father[N];
int h[N],e[N],ne[N],idx;
int happy[N];
int n;
void add(int a,int b)
{
e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
void dfs(int u)
{
f[u][1]=happy[u];
for(int i=h[u];i!=-1;i=ne[i])
{
int j=e[i];
dfs(j);
f[u][1]+=f[j][0];
f[u][0]+=max(f[j][0],f[j][1]);
}
}
int main()
{
memset(h,-1,sizeof h);
cin>>n;
for(int i=1;i<=n;i++) cin>>happy[i];
for(int i=0;i<n-1;i++)
{
int a,b;
cin>>a>>b;
has_father[a]=true;
add(b,a);
}
int root=1;
while(has_father[root]) root++;
dfs(root);
cout<<max(f[root][0],f[root][1])<<endl;
}
2.战略游戏
鲍勃喜欢玩电脑游戏,特别是战略游戏,但有时他找不到解决问题的方法,这让他很伤心。
现在他有以下问题。
他必须保护一座中世纪城市,这条城市的道路构成了一棵树。
每个节点上的士兵可以观察到所有和这个点相连的边。
他必须在节点上放置最少数量的士兵,以便他们可以观察到所有的边。
你能帮助他吗?
链接link
根据题意,相当于一棵树上的每一条边上的点至少有一个
考虑当前点的状态,有两种,没有士兵和有士兵,分别对应f[u][0],f[u][1],还是依据上文的原则,尽量考虑父节点与子节点的关系,写成状态机,
1.对于父节点没有士兵,那么它的所有子节点必须有士兵
f[u][0]+=f[j][1];
2.对于父节点有士兵,那么它的子节点就是可有可没有士兵
f[u][1]+=max(f[j][1],f[j][0]);
需注意,状态机中所有的状态转移一定要确保是合法,并且不遗漏,不重复
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=1510;
int T;
int n;
int f[N][2];
bool st[N];//用于确定root
int h[N],e[N],ne[N],idx;
void add(int a,int b)
{
e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
void dfs(int u)
{
f[u][1]=1;//选择根节点,边数+1
for(int i=h[u];i!=-1;i=ne[i])
{
int j=e[i];
dfs(j);
f[u][0]+=f[j][1];
f[u][1]+=min(f[j][0],f[j][1]);
}
}
int main()
{
while(cin>>n)
{
memset(h,-1,sizeof h);
memset(f,0,sizeof f);
memset(st,0,sizeof st);
idx=0;
for(int i=0;i<n;i++)
{
int id,cnt;
scanf("%d:(%d)",&id,&cnt);
for(int j=0;j<cnt;j++)
{
int a;
cin>>a;
add(id,a);
st[a]=true;//如果一个节点是其他节点的子节点,那就一定不是根
}
}
int root;
for(int i=0;i<n;i++)
{
if(!st[i])
{
root=i;
break;
}
}
dfs(root);
cout<<min(f[root][1],f[root][0])<<endl;
}
}
3.皇宫看守
太平王世子事件后,陆小凤成了皇上特聘的御前一品侍卫。
皇宫各个宫殿的分布,呈一棵树的形状,宫殿可视为树中结点,两个宫殿之间如果存在道路直接相连,则该道路视为树中的一条边。
已知,在一个宫殿镇守的守卫不仅能够观察到本宫殿的状况,还能观察到与该宫殿直接存在道路相连的其他宫殿的状况。
大内保卫森严,三步一岗,五步一哨,每个宫殿都要有人全天候看守,在不同的宫殿安排看守所需的费用不同。
可是陆小凤手上的经费不足,无论如何也没法在每个宫殿都安置留守侍卫。
帮助陆小凤布置侍卫,在看守全部宫殿的前提下,使得花费的经费最少。
链接link
这题乍一看和上一题很像,但实际解决方案的差别很大。上一题只需要考虑一条边上的状态,但这题需要考虑一个点和它周围所有邻边的关系,这就不是简单的考虑父节点选或者不选就能推出子节点的状态,可以简单试下:
如果父节点不选,那么子节点也不是不一定要选,因为还可以让子节点的子节点选,也是合法的。
所以本题还需仔细划分。
这个问题看似很复杂,对应了上文提到的需要联合子节点和父节点,如果处理不当,会造成状态机过于复杂,但再复杂的问题还是可以试着从一个点切入,还是和上题一样,先考虑当前点的状态:
当前点还是可以划分成选或不选两种大类,
对于不选,如果要满足条件,就需要父节点选,或者子节点选,这就又细分成了两种状态;
对于选,根据前面的原则,只考虑和子节点的关系,因为和父节点的关系是上一层考虑的,那么子节点就是可选可不选
我们先将这三种状态记为f[u][0],f[u][1],f[u][2],分别为1.不选当前节点,选父节点 ,2.不选当前节点,选子节点,3.选当前节点
对于状态1,对于它的子节点可以确定的是,它的父节点是一定不选的,所以它要满足条件,只能是自己选,或者它的子节点选
写成状态机f[u][0]+=min(f[j][1],f[j][2]);
对于状态3,它的子节点就是可选可不选,并且可以确定的是父节点是选的,
f[u][2]+=min(f[j][1],f[j][2],f[j][0])
对于状态2,就一定需要一个一个子节点必须选,其他节点可选可不选,枚举是哪个节点选的,并且维护一个变量,记录其他节点可选可不选的最小值
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=1510;
int n;
int h[N],e[N],ne[N],w[N],idx;
int f[N][3];
bool st[N];
void add(int a,int b)
{
e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
void dfs(int u)
{
f[u][2]=w[u];//如果当前节点有守卫,加上权值
for(int i=h[u];~i;i=ne[i])
{
int j=e[i];
dfs(j);
f[u][0]+=min(f[j][2],f[j][1]);
f[u][2]+=min(min(f[j][1],f[j][2]),f[j][0]);
}
f[u][1]=1e9;
for(int i=h[u];~i;i=ne[i])
{
int j=e[i];
f[u][1]=min(f[u][1],f[u][0]-min(f[j][1],f[j][2])+f[j][2]);
//公式中的sum就等于f[u][0],因为要有一个点必须有守卫,所以sum中要去掉当前枚举点
}
}
int main()
{
cin>>n;
memset(h,-1,sizeof h);
for(int i=1;i<=n;i++)
{
int id,cost,cnt;
cin>>id>>cost>>cnt;
w[id]=cost;
while(cnt--)
{
int ver;
cin>>ver;
add(id,ver);
st[ver]=true;
}
}
int root;
for(int i=1;i<=n;i++)
{
if(!st[i])
{
root=i;
break;
}
}
dfs(root);
cout<<min(f[root][1],f[root][2])<<endl;
}
4.背包模型
二叉苹果树
链接link
有一棵二叉苹果树,如果树枝有分叉,一定是分两叉,即没有只有一个儿子的节点。这棵树共 N个节点,编号为 1至 N,树根编号一定为 1。我们用一根树枝两端连接的节点编号描述一根树枝的位置。一棵苹果树的树枝太多了,需要剪枝。但是一些树枝上长有苹果,给定需要保留的树枝数量,求最多能留住多少苹果。这里的保留是指最终与1号点连通。
背包问题的实质就是组合问题,题目给出了需要保留树枝的数量,并需要求最大的苹果树,就相当于给出一种组合,满足数量等于n,并且价值最大,典型的背包问题
本题很类似于有依赖的背包问题link
题目其实给出很强的暗示,对于dp的划分依据是数量n,相当于背包问题中的体积,在有依赖的背包问题中,划分体积其实并没有那么明显,比较显然的是按照物品的枚举划分,但这样划分效率是远低于按体积划分的,所以找出合理的划分依据是解决dp问题的关键,但本题在枚举体积时还需注意,如果当前体积是j,子节点最多只能分配j-1,点体积,因为这题的体积是边,子节点要选到,必须有一条与父节点的边。
这题的背包是一个多重背包,每个物品可以多次取,但有取的上限
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
int n,q;
const int N=110,M=N*2;
int f[N][N];
int h[N],ne[M],e[M],w[M],idx;
void add(int a ,int b, int c)
{
e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
}
void dfs(int u,int father)
{
for(int i=h[u];i!=-1;i=ne[i])
{
int j=e[i];
if(j==father) continue;
dfs(j,u);
for(int v=q;v>=0;v--)
for(int k=0;k+1<=v;k++)
{
f[u][v]=max(f[u][v],f[u][v-1-k]+f[j][k]+w[i]);
}
}
}
int main()
{
cin>>n>>q;
memset(h,-1,sizeof h);
for(int i=0;i<n-1;i++)
{
int a,b,c;
cin>>a>>b>>c;
add(a,b,c),add(b,a,c);
}
dfs(1,-1);
cout<<f[1][q]<<endl;
}