很久没写过博客了今天来水一下
树形(DP)
顾名思义,就是在树上的dp。
树形dp常分为两种:选择节点(节点染色)问题 和 树形依赖背包问题
选择节点问题
先来看一道普通的treedp:
题目描述
某大学有 (n) 个职员,编号为 (1…n)。
他们之间有从属关系,也就是说他们的关系就像一棵以校长为根的树,父结点就是子结点的直接上司。
现在有个周年庆宴会,宴会每邀请来一个职员都会增加一定的快乐指数 (r_i)
但是呢,如果某个职员的直接上司来参加舞会了,那么这个职员就无论如何也不肯来参加舞会了。
所以,请你编程计算,邀请哪些职员可以使快乐指数最大,求最大的快乐指数。
输入格式
输入的第一行是一个整数 (n)。
第 (2) 行到第 ((n+1)) 行, 每行一个整数,第 ((i+1)) 的整数表示 (i) 号职员的快乐指数 (r_i)。
第 ((n+2)) 到第 (2n) 行,每行输入一对整数 (l,k) 代表 (k) 是 (l) 的直接上司。
输出格式
输出一行一个整数代表最大的快乐指数。
样例
输入
7
1
1
1
1
1
1
1
1 3
2 3
6 4
7 4
4 5
3 5
输出
5
(1<n<6 imes 10^3,-128leq r_ileq 127,1leq l,k leq n)且给出关系一定是树。
(DP)一般是在线性情况下进行,遇到这种树形结构该怎么办呢?
想想当初学习树时如何遍历树的. . . . . . 对了!DFS!
对一棵树,从根节点出发DFS,就是从父节点向子节点方向访问,回溯时候就是子节点向父节点访问。
利用DFS序,我们把一个树形结构拍平成了线性序列。于是(DP)就有了实现空间。
设(dp[x][0/1])代表第(x)个节点选 / 不选所能达到的最大快乐值。
对于第(x)个节点,设其子节点为(y)
-
若选,则它的所有子节点都不能选,(dp[x][1]=sum dp[y][0]+poi[x])。
-
若不选,则子节点可选可不选,(dp[x][0]=sum max(dp[y][0],dp[y][1]))。
其中 (poi[x]) 表示第(x)个人的快乐值。
代码如下:
#include<bits/stdc++.h>
using namespace std;
int head[1000010];
int ver[1000010];
int nxt[1000010];
int tot=0;
int e[1000010];
int dg[100010];//用来统计每个点的出度
int dp[6005][2];
int n;
void dfs(int u)
{
dp[u][0]=0;
dp[u][1]=e[u];
for(int i=head[u];i;i=nxt[i])//遍历子节点
{
int v=ver[i];
dfs(v);
dp[u][0]+=max(dp[v][0],dp[v][1]);/*状态转移*/
dp[u][1]+=dp[v][0];
}
return ;
}
void add(int x,int y)//邻接表存边
{
ver[++tot]=y;
nxt[tot]=head[x];
head[x]=tot;
}
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>e[i];
}
for(int i=1;i<n;i++)
{
int u,v;
cin>>u>>v;
add(v,u);
dg[u]++;//统计出度,方便找根
}
int root;
for(int i=1;i<=n;i++)
{
if(!dg[i])
{
root=i;
break;
}
}
dfs(root);
cout<<max(dp[root][0],dp[root][1]);//比较根节点放或不放哪个更优
return 0;
}
于是我们知道了,对于树形dp,最常用的是DFS来实现遍历。
当然,视不同情况,我们还可以用BFS+递推和拓补排序来做,这里就不展示了。
想看走这边→传送门
这里还有些额外的例题:
树形依赖背包。
一般的依赖背包我们或许已经接触过了,比如金明的预算方案
里面的依赖层数很少,就两层,直接暴力枚举没有问题
但当层数过多时,很明显就要使用到所讲的树形DP了。
先来了解一个概念:泛化物品 (以下有关概念摘自:徐持衡《浅谈几类背包问题》)
- 泛化物品定义: 考虑一种物品,它没有固定的费用和价值,而是其价值随着分配给它的费用变化而变化。
对于一个泛化物品,可以用一个一维数组(G_i)表示其费用与价值的关系:当费用为(i)时,相对应的价值为(G_i)。
- 泛化物品的和:把两个物品合在一起的运算,就是枚举费用分配给两个物品,
满足:
(G_j=max(G1_{j-k},G2_{k})(0 leq k leq j leq C))
时间复杂度为(O(C^2))。
- 对于一组成树形依赖关系的物品,我们可以将每个子树都看作一个泛化物品,那么一个子树的泛化物品就是子树根节点这件物品和它的子节点所在子树的泛化物品的和。
听起来有点魔幻,那就结合实际例题来看:P2014 [CTSC1997]选课
题目描述
在大学里每个学生,为了达到一定的学分,必须从很多课程里选择一些课程来学习,在课程里有些课程必须在某些课程之前学习,如高等数学总是在其它课程之前学习。现在有 (N) 门功课,每门课有个学分,每门课有一门或没有直接先修课(若课程 (a) 是课程 (b) 的先修课即只有学完了课程 (a),才能学习课程 (b))。一个学生要从这些课程里选择 (M) 门课程学习,问他能获得的最大学分是多少?
输入格式
第一行有两个整数(N,M)((1 leq N leq 300, 1leq Mleq 300))
接下来的(N)行,第((i+1))行包含两个整数(k_i)和(s_i),(k_i)表示第(i)门课的直接先修课,(s_i)表示第(i)门课的学分,若(k_i=0)则表示没有先修课。((1 leq k_i leq N, 1 leq s_i leq 20))
输出格式
只有一行,表示选(M)门课的最大学分。
样例
输入
7 4
2 2
0 1
0 4
2 1
7 1
7 6
2 2
输出
13
非常明显的树形结构。
不妨设(dp[i][j]) 为对于节点(i)总共选(j)门课所能达到的最大学分。
很明显我们要找到它的子节点的最大和,设其子节点为(y),我们可以枚举对于每个(y)分配的费用,取最大值处理。
状态转移方程为(dp[i][j]=max(dp[i][j],dp[i][j-k+1]+dp[y][k]+poi[y]); (0 leq k leq j))
看着有点复杂是不是? 其实只要普通背包问题熟练了,这个转移方程完全可以自己推导出来。
还是先放代码:
#include <bits/stdc++.h>
using namespace std;
int n,m;
int head[100010];
int ver[100010];
int nxt[100010];
int tot=0;
int poi[100010];
int dp[1010][1010];
void dfs(int x,int n)
{
for(int i=head[x];i;i=nxt[i])
{
int y=ver[i];
if(y==n) continue;
dfs(y,x);
for(int j=m;j>=0;j--)//倒着枚举,避免状态更新重叠
{
for(int k=0;k<j;k++)//枚举给y节点分配的费用,或者说课数
{
dp[x][j]=max(dp[x][j],dp[y][k]+poi[y]+dp[x][j-k-1]);
}
}
}
}
void add(int x,int y)
{
ver[++tot]=y;
nxt[tot]=head[x];
head[x]=tot;
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
{
int a,c;
cin>>a>>c;
add(i,a);
add(a,i);
poi[i]=c;
}
dfs(0,0);
cout<<dp[0][m];
}
看完之后最好再看看题自己再试着推一下状态转移方程~~
附上一道树形背包的变种练习题:click me