树形dp
概念类
树形dp是一种很优美的动态规划,真的很优美真的,前提是在你学会它之后。
实现形式
树形dp的主要实现形式是dfs,在dfs中dp,
主要的实现形式是dp[i][j][0/1],
i是以i为根的子树,j是表示在以i为根的子树中选择j个子节点,
0表示这个节点不选,1表示选择这个节点。有的时候j或0/1这一维可以压掉
基本的dp方程
选择节点类
dp[i][0]=dp[j][1]
dp[i][1]=max/min(dp[j][0],dp[j][1])
树形背包类
dp[v][k]=dp[u][k]+val
dp[u][k]=max(dp[u][k],dp[v][k-1])
DP入门第一题
第一题:Anniversary party 没有上司的舞会
洛谷链接:https://www.luogu.org/problemnew/show/P1352
//My AC Code:
#include <cstdio>
#include <vector>
#include <cstring>
using namespace std;
const int maxn = 6005;
int max(int a,int b){return a>b?a:b;}
vector<int> G[maxn];
int fad[maxn];
int dp[maxn][2];
int L,K,n;
void dfs(int zd){
for(int i=0;i<G[zd].size();i++){
dfs(G[zd][i]);
//dp[i][0]:表示不邀请i员工其子树达到的最大快乐值,dp[i][1]则表示邀请。
/*
若一个上司不来 那他的最大值取决自己的下属 下属又分两种情况 来或者不来 所以就是取这两个的最大值来加上
若一个上司来 那他的下属必然不会来 于是直接加上下属不来的情况就行了
dp转移考虑在dfs里面搞 dfs从整个树的根节点出发 一直往下dfs。。。
*/
dp[zd][0]+=max(dp[G[zd][i]][0],dp[G[zd][i]][1]);
dp[zd][1]+=dp[G[zd][i]][0];
}
}
int main(){
scanf("%d",&n);
memset(dp,0,sizeof(dp));
memset(fad,-1,sizeof(fad));
for(int i=1;i<=n;i++){
scanf("%d",&dp[i][1]);//读取当前节点(职员)去舞会的快乐点
G[i].clear();//一定要吧vector数组初始化
}
while(scanf("%d%d",&L,&K)&&L&&K){
G[K].push_back(L);//用vector数组 存储 每一个父节点 下面有几个子节点
fad[L]=K;//fad[L]=K,表示 L节点的父节点是K
}
int root=1;//找整颗树的根
while(fad[root]!=-1)root=fad[root];
dfs(root);
printf("%d",max(dp[root][0],dp[root][1]));
return 0;
}
第二题:Strategic game
洛谷链接:https://www.luogu.org/problemnew/show/UVA1292
//My AC Code:
#include <cstdio>
#include <vector>
using namespace std;
const int maxn = 1505;
int n,r,rm,zs;
char cr;
vector<int> G[maxn];
int dp[maxn][2];
int fad[maxn];
int min(int a,int b){return a>b?b:a;}
void dfs(int rt){
for(int i=0;i<G[rt].size();i++){
dfs(G[rt][i]);
dp[rt][0]+=dp[G[rt][i]][1];
dp[rt][1]+=min(dp[G[rt][i]][0],dp[G[rt][i]][1]);
/*
如果根节点 没放士兵,则,子节点一定要都放士兵,可脑补画面
如果根节点放了士兵,则 当前根节点的士兵可以 保卫 所有的子节点,
也就是 子节点可以有士兵,也可以没有士兵,所以要当根节点有士兵有两种情况
*/
}
}
int main(){
while(scanf("%d",&n)!=EOF){
for(int i=0;i<=n;i++){
G[i].clear();
dp[i][1]=1;dp[i][0]=0;
fad[i]=-1;
}
/*
树形DP之前一定要清空,所有相关存树的,数组,还有初始化要dp的数组
以及 存放每个节点的父节点的数组
*/
for(int i=1;i<=n;i++){
scanf("%d:(%d)",&r,&rm);
for(int j=0;j<rm;j++){
scanf("%d",&zs);
G[r].push_back(zs);
fad[zs]=r;
}
}
int root=0;
while(fad[root]!=-1)root=fad[root];
dfs(root);
printf("%d\n",min(dp[root][0],dp[root][1]));
}
return 0;
}
第三题:选课
洛谷链接:https://www.luogu.org/problemnew/show/P2014
/*
首先,观察题目,我们把这道题转换一下:给定一颗树,选出包含1号节点(根)的一颗子树,使得点权和最大。
我们可以这样子定义状态:
设f[i][j] 表示以i为根节点的子树,选出j个节点,所能达到的最大点权值。
对于二叉树来说,转移很显然,就是枚举左子树分配多少个节点,就可以对应的得出右子树能分配到多少个节点,对所有情况取最大值就好。
对于多叉树来说,问题就没有那么简单了,这里,我们有两个方案可以解决这个问题:
多叉树转二叉树,树上背包。
因为我不会多叉树转二叉树,所以在这里我主要讲一讲第二种方法。
我们在树上做背包本质上一般是多重背包问题。
我以本题为例子,讲一下树上如何做多重背包。
首先,我们肯定要一层循环枚举子树(可以类似为背包问题中枚举第几件物品)。
第二层循环我们得枚举当前以节点的子树能分配的节点数(可以类似为背包问题中枚举背包容量)
这一层循环一定要从后往前枚举,类似与背包压在一维做的做法
第三层循环我们就可以枚举当前子树分配多少个节点了(可以类似多重背包中枚举第i件物品要几件)
*/
#include <cstdio>
#include <vector>
using namespace std;
const int maxn = 305;
int n,m,ta,tb;
int dp[maxn][maxn];
vector<int> G[maxn];
int v[maxn];
int dfs(int x){
/*
一层循环枚举子树(可以类似为背包问题中枚举第几件物品)。
第二层循环我们得枚举当前以节点的子树能分配的节点数(可以类似为背包问题中枚举背包容量) 这一层循环一定要从后往前枚举,类似与背包压在一维做的做法
第三层循环我们就可以枚举当前子树分配多少个节点了(可以类似多重背包中枚举第i件物品要几件)
*/
int tot=1;//tot表示包括根节点,当前树的所有节点数
dp[x][1]=v[x];
for(int i=0;i<G[x].size();i++){//第一层循环 相当于 分别 对每个子树(求出当前树总节点数) 进行DP 枚举子树
int temp=dfs(G[x][i]);//先把子树的f递归下去算出来
tot+=temp;//tot记录到当前子树为止总节点数
for(int j=tot;j>=1;j--){//这层循环相当于 背包中的 枚举 容量,一定要倒着枚举 枚举自己这颗树的总分配数
for(int k=0;k<=temp;k++)//枚举子树分配多少个节点
if(j-k>=1)//j也就是 一共选的点数,减去k 也就是 要给当前这颗子树分配的节点数,一定要大于等于1,因为最少,也要选根树
dp[x][j]=max(dp[x][j],dp[x][j-k]+dp[G[x][i]][k]);
}
}
return tot;
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++){
scanf("%d%d",&ta,&tb);
G[ta].push_back(i);
v[i]=tb;
}
dfs(0);
printf("%d",dp[0][m+1]);
return 0;
}
第四题:有线电视
洛谷连接:https://www.luogu.org/problemnew/show/P1273
//My AC Code:
#include <cstdio>
#include <cstring>
using namespace std;
const int maxn = 3005;
struct node{
int to,back,cd;//这边 去哪个节点 这边的上一条边是啥 这条边长度多少
}nd[maxn];
int v[maxn];
int first[maxn];
int dp[maxn][maxn];//如dp[i][j],则表示 以第i节点为根节点,选j个用户 花了多少钱
int n,m,po,xn,xa,xb;
void addedge(int from,int to,int dis){
nd[++po].back=first[from];
nd[po].to=to;
nd[po].cd=dis;
first[from]=po;
}
int max(int a, int b){return a>b?a:b;}
int dfs(int x){
if(x>n-m){
dp[x][1]=v[x];
return 1;
}
/*
如果根节点 是大于n-m
也就是 用户节点,所以可以初始化dp[x][1]=v[x]
*/
int sum=0;
for(int i=first[x];i;i=nd[i].back){
int vv=nd[i].to;
int temp=dfs(vv);sum+=temp;//temp表示当前子树一共有多少用户,sum表示,以x为根,一共有多少用户,即当前子树可以选多少用户
for(int j=sum;j>=0;j--){
for(int k=1;k<=temp;k++){
/*
j循环表示 以x为根节点,一共可以选 sum个用户到0个用户
k循环表示 当前子树i,可以选择1-temp个用户,也可以是选择0-temp个用户
然后的话,j-k一定要大于等于零,因为 子树选择的节点数一定小于等于根树选择的节点数
*/
if(j-k>=0)dp[x][j]=max(dp[x][j],dp[x][j-k]+dp[vv][k]-nd[i].cd);
/*
dp[x][j]=max(dp[x][j],dp[x][j-k]+dp[vv][k]-nd[i].cd);
以x为根节点,选j个用户,可以分成
选x为根节点,选j-k个用户
选vv(x的子节点)为根节点,选k个用户,一定要减去 根节点和当前vv节点之间的消耗或者说是边权值
*/
}
}
}
return sum;
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n-m;i++){
scanf("%d",&xn);
for(int j=1;j<=xn;j++){
scanf("%d%d",&xa,&xb);
addedge(i,xa,xb);
}
}
for(int i=n-m+1;i<=n;i++)scanf("%d",&v[i]);
memset(dp,~0x3f,sizeof(dp));
for(int i=1;i<=n;i++)dp[i][0]=0;//因为 如果选0个用户,一定是花了0元,dp 如果有可以初始化的数据,一定要初始化
dfs(1);
for(int i=m;i>=1;i--){
if(dp[1][i]>=0){printf("%d",i);break;}
}
return 0;
}