SDSC整理(Day5 树形dp)
树形dp
我才不会告诉你这是第六天讲的
\(pre-knowledge\)
没啥前置知识,递归算吗?存图算吗?简单dp算吗?
\(description\)
树形\(dp\),顾名思义,就是在树上进行\(dp\) 。
因为树的定义本身就具有递归性,而且树有很明显的层次性,必须求出所有的子节点之后才可以转移到父节点。
所以树形 \(dp\) 一般采用递归来实现。
而且树形 \(dp\) 的第一维通常都是子树的规模,算是比较套路的一点。
下面讲 \(4\) 个树形 \(dp\) 的典型例子。
1.最大独立集问题
很明显,职员与上司之间形成了一棵有根树,规则是选了儿子就不能选父亲,选了父亲就不能选儿子,要求选的点的权值和最大,这类问题被称为最大独立集问题。
我们设 \(dp[i][0/1]\) 表示以 \(i\) 为根的子树中所能选的最大值, \(0\) 代表该节点选, \(1\) 代表该节点不选。
如果我们选了父亲,那么我们就一定不能选儿子。
如果我们没有选父亲,那么儿子可选可不选,取最大值。
所以我们有:
\(dfs\) 到叶子节点之后,在递归回退的时候从下往上转移。
/*
树形dp(求最大独立集问题)
date:2022.7.31
worked by respect_lowsmile
*/
#include<iostream>
#include<algorithm>
using namespace std;
const int N=6e3+5;
struct node
{
int to,next;
};
node edge[N<<2];
int dp[N][2],val[N],fa[N],head[N];
int n,r=1,num;
void add(int u,int v)
{
num++;
edge[num].to=v;
edge[num].next=head[u];
head[u]=num;
}
void dfs(int now)
{
dp[now][0]=0,dp[now][1]=val[now];
for(int i=head[now];i;i=edge[i].next)
{
int v=edge[i].to;
if(v) dfs(v);
dp[now][0]+=max(dp[v][1],dp[v][0]);
dp[now][1]+=dp[v][0];
}
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;++i)
scanf("%d",&val[i]);
for(int i=1;i<n;++i)
{
int u,v;
scanf("%d %d",&u,&v);
add(v,u),fa[u]=v;
}
while(fa[r]) r=fa[r];
dfs(r);
printf("%d",max(dp[r][0],dp[r][1]));
return 0;
}
2.最大独立集问题+唯一性判断。
例题:UVA1220 Hali-Bula的晚会 Party at Hali-Bula
这题我写题解了,详情见题解
我再不要脸地要个赞
3.背包类树形 \(dp\) (点权)
简单描述就是一共选 \(m\) 个点,选择顺序满足树的层次性,要求选的点的点权和最大。
我们设 \(dp[i][j]\) 表示以 \(i\) 为根的子树中选 \(j\) 个节点所能得到的最大得分。
如果一棵子树能选 \(j\) 个点,一个孩子选了 \(k\) 个点,那么这个子树内只能再选 \(j-k\) 个点,其中 \(0 \le j \le m\) , \(0 \le k \le j-1\) 。
所以我们有状态转移方程:
我们枚举 \(j\) 和 \(k\) ,转移即可。
注意,因为每个节点只能选一次,所以我们要保证转移唯一,所以枚举 \(j\) 的时候要倒着枚举。
是不是像极了 \(01\) 背包???
tips
因为每一个子树的节点个数都有限,有可能枚举的 \(j\) 和 \(k\) 会大于当前子树的节点数,
特别是 \(j\) 或者 \(k\) 很大的时候,会产生很多没有必要的枚举,
所以我们可以处理一个 \(siz[]\) 来处理每棵子树的节点数,然后枚举的边界设为 \(min(siz[now],m)\) 和 \(min(siz[son],k)\) 。
/*
树形dp
date:2022.8.1
worked by respect_lowsmile
*/
#include<iostream>
#include<vector>
using namespace std;
const int N=305;
int dp[N][N],val[N],siz[N];
vector<int> E[N];
int n,m;
void dfs(int now)
{
siz[now]=1,dp[now][1]=val[now];
for(int i=0;i<E[now].size();++i)
{
int v=E[now][i];
dfs(v);
siz[now]+=siz[v];
for(int j=min(siz[now],m);j>=1;--j)
for(int k=0;k<=min(siz[v],j-1);++k)
dp[now][j]=max(dp[now][j],dp[v][k]+dp[now][j-k]);
}
}
int main()
{
scanf("%d %d",&n,&m);
m++;
for(int i=1;i<=n;++i)
{
int k;
scanf("%d %d",&k,&val[i]);
E[k].push_back(i);
}
dfs(0);
printf("%d",dp[0][m]);
return 0;
}
4.背包类树形dp(边权)
例题:P2015 二叉苹果树
其实和点权的差不多。
我们用\(dp[i][j]\)表示以\(i\)为根的子树中选\(j\)条边能获得的最大值。
和上面的那个题一个套路,如果一棵子树能选 \(j\) 条边,一个孩子选了 \(k\) 条边,那么这个子树内只能再选 \(j-k-1\) 条边,因为该节点和它的儿子之间也连了一条边,其中 \(0 \le j \le m\) , \(0 \le k \le j-1\) 。
我们有状态转移方程:
/*
树形dp(最近在练vector,所以用vector存图比较多)
date:2022.8.1
worked by respect_lowsmile
*/
#include<iostream>
#include<vector>
using namespace std;
const int N=105;
struct node
{
int to,w;
};
int dp[N][N],siz[N];
int n,m;
vector<node> E[N];
void dfs(int now,int fa)
{
siz[now]=1;
for(int i=0;i<E[now].size();++i)
{
int v=E[now][i].to,w=E[now][i].w;
if(v==fa) continue;
dfs(v,now);
siz[now]+=siz[v];
for(int j=min(siz[now],m);j>=1;j--)
for(int k=0;k<=min(j-1,siz[v]);++k)
dp[now][j]=max(dp[now][j],dp[now][j-k-1]+dp[v][k]+w);
}
}
int main()
{
scanf("%d %d",&n,&m);
for(int i=1;i<n;++i)
{
int u,v,w;
scanf("%d %d %d",&u,&v,&w);
E[u].push_back(node{v,w});
E[v].push_back(node{u,w});
}
dfs(1,0);
printf("%d",dp[1][m]);
return 0;
}
换根法
不会。。。
留坑