背景:什么是树形dp
众所周知,树是一个有 n 个节点,n−1 条无向边的图。这种图可以表示一些事物之间的关系,且这种关系是联通的、无环的。当动态规划建立在一种依赖关系(或者其它互相的关系)之上,树形 dp 便是解决这类问题的好帮手。
通常对于树上的每一个节点,我们要求的 dp 的值通过其父亲节点/儿子节点推算过来。对于特殊的节点(根节点、叶子节点),有时需要初始化。
由于树是一种无向边的图,于是我们在建树的时候需要建立双向的,例如,即如果 u 与 v 相连,我们需要建立 u→v 以及 v→u 。
同时,由于树固有的递归性质,树形 DP 一般都是递归进行的。
树形dp
树概念的熟悉Facer的程序
建树
这里我们采用邻接表建图的方式来建立树,只需要在建图的基础上加上一条双向边就变成了一棵树
于是有如下代码
void add(int a,int b)
{
e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
//建树
add(u,v),add(v,u);
分析
首先我们引入一个分析不太复杂的树形dp题目来再熟悉一下树的结构
通过例子,子集(1),(2),(3),(1,2),(2,3),(1,2,3)满足,我们不难看出题目就是问该树有多少个子树
定义状态,表示以为根的子树的个数
当 为叶节点时,表示它只有他自己这棵子树。
当 不为叶节点时,注意到对于 的每个子节点 ,如果选上,我们有种选法。
如果我们不选上子树,这时候就只选择根,只有一种选法,因此转移方程就是:
选择子树 不选择子树
于是我们最后的答案就是所有的和
边界分析
,自己就构成一棵子树
代码
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=1e6+10,mod=1000000007;
typedef long long ll;
ll ans,f[N];
int e[N],ne[N],idx,h[N];
int n;
void add(int a,int b)
{
e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
//递归过程找树的根
void dfs(int x,int fa)
{
f[x]=1;//初始化自己也算一棵子树
for(int i=h[x];i!=-1;i=ne[i])
{
int y=e[i];
if(y!=fa)//如果取出的树不等于根,说明还没找到根,继续往下找
dfs(y,x),f[x]=(ll)(f[x]*(f[y]+1))%mod;
}
ans=(ans+f[x])%mod;
}
int main()
{
memset(h,-1,sizeof h);
cin>>n;n--;
while(n--)
{
int a,b;
cin>>a>>b;
add(a,b),add(b,a);
}
dfs(1,0);
cout<<ans;
return 0;
}
例题 树的直径
分析
首先我们建立这样一棵树,来方便我们分析理解题意
题目的大概意思是找到两个点,使得这两个点之间的距离最大。上图很明显可以看出,7号到6号的距离是最长的(7 5 2 1 5 6)。我们接下来分析最长距离是怎么找到的。
我们知道树的遍历是从根开始依次往下遍历,就比如上述以1为根遍历路线为1 2 5 7,1 3,1 4 5,事实上由很多种,这里只列举其中几种。由于是从根开始遍历的,那么两点之间的最长距离我们便可以利用如下方法计算,我们找到这个根节点的最大深度和次大深度,然后把他相加,就变成了两个点的最大距离,如上如,1的最大深度是最左边,1到7这条路径,次大深度是最右边,从1到6这条路径。
于是,我们便可以依次递归计算每一个子节点的深度最大值,由于这题边没有权重,于是,每向下递归一层,深度就要加1.
于是我们可以定义深度的状态转移为
对于最大值和次大值有如下转移,
如果存在比最大值还大的,那就更新最大值为当前值,次大值更新为上一次的次大值,注意,次大值的更新要写在前面,因为d1也要更新 当前这个d满足
代码
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=1e4+10;
int e[N*2],ne[N*2],idx,h[N];
int n,ans;
void add(int a,int b)
{
e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
int dfs(int x,int fa)
{
int dist=0;//以x为当前起点的初始路径为0,从当前往下走的最大长度
int d1=0,d2=0;//最大值和次大值
for(int i=h[x];i!=-1;i=ne[i])
{
int j=e[i];
if(j==fa)continue;
int d=dfs(j,x)+1;//每次往下一层就深度就要加一
dist=max(dist,d);
if(d>=d1) d2=d1,d1=d;//如果存在比最大值还大的,那就更新最大值为当前值,次大值更新为上一次的次大值,注意,次大值的更新要写在前面,因为d1也要更新
else if(d>=d2) d2=d;//当前这个d满足d1>d>d2
}
ans=max(ans,d1+d2);
return dist;
}
int main()
{
memset(h,-1,sizeof h);
cin>>n;n--;
while(n--)
{
int a,b;cin>>a>>b;
add(a,b),add(b,a);
}
dfs(1,-1);//初始没有父节点
cout<<ans;
return 0;
}
例题 数字转化
代码
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=50010;
int e[N*2],ne[N*2],idx,h[N];
int sum[N];//sum[i]表示i的所有约数之和
bool st[N];//由于不能有重复的于是我们得判重
int ans;
void add(int a,int b)
{
e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
int dfs(int u)
{
st[u]=true;
int d1=0,d2=0;//d1表示最大长度,d2表示次大长度
for(int i=h[u];i!=-1;i=ne[i])
{
int j=e[i];
if(!st[j])//没有遍历过我们才往下继续搜
{
int d=dfs(j)+1;//如果有权重应该加上权重
if(d>=d1)d2=d1,d1=d;
else if(d>d2)d2=d;
}
}
ans=max(ans,d1+d2);
return d1;
}
int main()
{
memset(h,-1,sizeof h);
int n;cin>>n;
for(int i=1;i<=n;i++)//类似于筛法
{
for(int j=2;j*i<=n;j++)
sum[i*j]+=i;
}
for(int i=2;i<=n;i++)
{
if(sum[i]<i)
add(sum[i],i),add(i,sum[i]);
}
dfs(1);
cout<<ans;
return 0;
}
例题 树的中心
题目描述
分析
代码
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=1e4+10;
int e[N*2],ne[N*2],idx,h[N],w[N];
int n,ans;
int d1[N],d2[N],p1[N],up[N];//记录当前位置往下走的最大路径和次大路径
void add(int a,int b,int c)
{
e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
}
//从u点向下走的dfs
int dfs_d(int u,int fa)
{
d1[u]=0,d2[u]=0;
for(int i=h[u];i!=-1;i=ne[i])
{
int j=e[i];//j是u的邻接点
if(j==fa)continue;//避免超过根节点还往上查找
int d=dfs_d(j,u)+w[i];//记录u经过j这个点往下走的最长长度
if(d>=d1[u])d2[u]=d1[u],d1[u]=d,p1[u]=j;
//p1[]记录u往下走的最长路径是从哪个点下去的
else if(d>d2[u])d2[u]=d;
}
return d1[u];///从u往下走的最大长度
}
//从u点向上走到up[]计算
void dfs_up(int u,int fa)
{
for(int i=h[u];i!=-1;i=ne[i])
{
int j=e[i];
if(j==fa)continue;
//up[j]表示从j点网上走的最大长度
if(p1[u]==j)//如果j再u往下走的最长,此时只需要拿到u向上走的最大值和向下走的次大值,因为最大值向下走的最大值已经被包含了
up[j]=max(up[u],d2[u])+w[i];
else
up[j]=max(up[u],d1[u])+w[i];
dfs_up(j,u);
}
}
int main()
{
memset(h,-1,sizeof h);
int n;cin>>n;
for(int i=1;i<n;i++)
{
int a,b,c;
cin>>a>>b>>c;
add(a,b,c),add(b,a,c);
}
dfs_d(1,-1);
dfs_up(1,-1);
int res=1e9;
for(int i=1;i<=n;i++)
{
res=min(res,max(up[i],d1[i]));
}
cout<<res;
}
例题 二叉苹果树
分析
题意,每条边有一个权值,保留若干条边,求去掉边后根节点能够到达的所有边的权值和最大是多少。
这道题明确给出了根的位置,也就确定了父子节点的关系,我们会发现,对于每个父节点的状态,都是由它的子节点转移过来的,所以我们大概可以得出这里有一个由子节点转移到父节点的状态转移方程,又因为父节点子树上选择的边数完全取决于子节点的子树选择的边数。
代码
//f[i][j]表示以i为根选择j条边的最大苹果数量
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=105;
int e[N*2],ne[N*2],w[N*2],idx,h[N];
int f[N][N];
int n,m;
void add(int a,int b,int c)
{
e[idx]=b,ne[idx]=h[a],w[idx]=c,h[a]=idx++;
}
void dfs(int u,int fa)
{
for(int i=h[u];i!=-1;i=ne[i])
{
int v=e[i];
if(v==fa)continue;
dfs(v,u);
for(int j=m;j>=0;j--)
{
for(int k=0;k<j;k++)
f[u][j]=max(f[u][j],f[u][j-k-1]+f[v][k]+w[i]);
}
}
}
int main()
{
memset(h,-1,sizeof h);
cin>>n>>m;
for(int i=1;i<n;i++)
{
int a,b,c;
cin>>a>>b>>c;
add(a,b,c),add(b,a,c);
}
dfs(1,-1);
cout<<f[1][m];
return 0;
}
例题 战略游戏
代码
//用f[u][1/0]表示这个点放还是不放士兵
//如果该点不放,那么该点的所有子节点都要放一名士兵d[u][0]+=d[v][1]
//如果当前根节点放置了士兵,那么子节点可以选择也可以不选择,dp[u][1]+=min(dp[v][1],dp[0])
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=1505;
int e[N*2],ne[N*2],h[N],idx;
int dp[N][2];
void add(int a,int b)
{
e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
void dfs(int u,int fa)
{
dp[u][1]=1,dp[u][0]=0;//初始化选择的话需要多少个士兵
for(int i=h[u];i!=-1;i=ne[i])
{
int v=e[i];
if(v==fa)continue;
dfs(v,u);
dp[u][0]+=dp[v][1];
dp[u][1]+=min(dp[v][1],dp[v][0]);
}
}
int main()
{
memset(h,-1,sizeof h);
int n;cin>>n;
for(int i=1;i<=n;i++)
{
int a,k;
cin>>a>>k;
for(int i=1;i<=k;i++)
{
int b;cin>>b;
add(a,b),add(b,a);
}
}
dfs(0,-1);
cout<<min(dp[0][0],dp[0][1]);
return 0;
}
例题 皇宫看守
分析
代码
//f[u][0/1/2]表示该点有没有放置士兵
//f[u][0] 表示自己不是守卫,父节点不是守卫,子节点是守卫
//f[u][1] 表示自己是守卫,父节点和子节点不知道
//f[u][2] 表示自己不是守卫,父节点是守卫,子节点不知道
//状态转移
//dp[u][1]=w[u],dp[u][0]=dp[u][2]=0;
//对于自己是守卫,那考虑他的子节点,对于子节点v,他的父节点是是守卫,所以dp[0]=0,
//dp[u][1]+=min(dp[v][1],dp[v][2]);
//对于父节点是守卫,dp[u][2]+=min(dp[v][1],dp[v][0])
//对于子节点是守卫
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=1505;
int e[N*2],ne[N*2],w[N*2],h[N],idx;
int dp[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)
{
int dd=1e9;
dp[u][1]=w[u],dp[u][0]=0,dp[u][2]=0;
for(int i=h[u];i!=-1;i=ne[i])
{
int v=e[i];
dfs(v);
dd=min(dd,dp[v][1]-min(dp[v][0],dp[v][1]));
dp[u][0]+=min(dp[v][0],dp[v][1]);//子节点放守卫了,那子节点的子节点就可以选择放或者不放
dp[u][1]+=min(dp[v][1],dp[v][2]);//表示当前节点选择放守卫,那他的子节点可以选择放也可以选择不放
dp[u][2]+=min(dp[v][1],dp[v][0]);//表示父节点是守卫了,那当前点一定能被守护发哦,那当前点可以选择放或者不放
}
dp[u][0]+=dd;
}
int main()
{
memset(dp,127,sizeof dp);
memset(h,-1,sizeof h);
int n;cin>>n;
for(int i=1;i<=n;i++)
{
int a,c,k;
cin>>a>>c>>k;
w[a]=c;
for(int i=1;i<=k;i++)
{
int b;cin>>b;
add(a,b);
st[b]=true;//不是根节点
}
}
int root=1;
while(st[root])root++;//找到根节点
dfs(root);
cout<<min(dp[root][0],dp[root][1]);//根节点没有父节点
return 0;
}