树的特征
1.N个点 只有N-1条边的无向图
2.无向图里 任意两点有且只有一条路
3.一个点只有一个前驱 但可以有多个后继
4.无向图没有环
树形DP
由于树有着天然的递归结构 父子结构 而且它作为一种特殊的图 可以描述许多复杂的信息 因此在树就成了一种很适合DP的框架
问题:给你一棵树 要求用最少的代价(最大的收益)完成给定的操作
树形DP 一般来说都是从叶子从而推出根 当然 从根推叶子的情况也有 不过很少(本蒟蒻还没有做到过~)
一般实现方式: DFS(包括记忆化搜索),递推等
例题
1.二叉苹果树
二叉树 很爽的一种dp结构 由于二叉树父亲节点只用管它的左右儿子 状态转移变得较为轻松
在遇到多叉树时 我们时常会考虑把多叉树转化为二叉树来做
而这道题直接是二叉树
首先考虑给DP数组下定义 一般来说树形DP的DP数组的第一维都是当前节点的编号
这道题光一维肯定是不够的 那么加维 发现dp[i][j]表示当前节点为i 保留j个节点的最大苹果数量比较ok
那么方程就显而易见了 dp[i][j]=max(dp[i][j],dp[i.lson][k]+dp[i.rson][j-k-1]+apple[i])
很明显 该问题具有很明显的最优子结构性质 也具备无后效性(每一步只与儿子有关系 而与爸爸之类的没有关系 )
另外 还可以在dfs时运用记忆化 可以大大提高速度
再提一句 由于题目中给的权值在边上 让人特别难受 于是 我们把权值转化到儿子上会方便操作
//f[i][j] 当前在i点 保留j个节点
//f[i][j]=max(f[i][j],f[tree[i].l][k]+f[tree[i].r][j-k-1]+tree[i].v);
#include<bits/stdc++.h>
using namespace std;
const int N=150;
int n,q,dp[N][N];
struct node
{
int lson;
int rson;
int val;
}tree[N*20];
int dfs(int now,int point)
{
if(now==0||point==0)
{
return 0;
}
if(tree[now].lson==0&&tree[now].rson==0)
{
return tree[now].val;
}
if(dp[now][point]>0) return dp[now][point];//记忆化
for(int k=0;k<point;k++) //枚举方程中的k
{
dp[now][point]=max(dp[now][point],dfs(tree[now].lson,k)+dfs(tree[now].rson,point-k-1)+tree[now].val);
}
return dp[now][point];
}
int main()
{ cin>>n>>q;
for(int i=1;i<=n-1;i++)
{
int fa,son,v;
cin>>fa>>son>>v;
tree[son].val=v;
if(tree[fa].lson==0) tree[fa].lson=son;
else tree[fa].rson=son;
}
cout<<dfs(1,q+1);//注意是q+1 因为把边变成了点
return 0;
}
2.选课
这道题就是采用刚才提到过的 把多叉树转化为二叉树来做
关于如何把多叉树转化为二叉树 有个口诀 叫做左儿子不变 右儿子兄♂弟
详细的不多说 可以去参考一下相关资料
等转化为二叉树了过后 让我们来琢磨一下
左儿子:原根节点的孩子
右儿子:原根节点的兄♂弟
也就是说 不能直接套用第一题的方程 但是可以对dp数组进行相同的定义
对于一个根节点 都可以 选 或者 不选
当给左儿子分配资源时 根节点必须选 而与右儿子无关
因此 方程就显而易见了 dp[i][j]=max(dp[i][j],dp[i.rson][j],dp[i.lson][k]+dp[i.rson][j-k-1]+val[i]) (0<=k<j)
//dp[i][j]: i的所有兄弟和i的所有儿子 和i自己 学j门课的最大学分总和。
//dp[i][j]=max(dp[rson][j],dp[lson][k]+dp[rson][j-k-1]+val[i])
#include<bits/stdc++.h>
using namespace std;
const int N=305;
int n,m,bigson[N],dp[N][N];
struct node
{
int val,lson,rson;
}tree[N*4];
int dfs(int now,int point)
{
if(!now||!point) return 0;
if(dp[now][point]) return dp[now][point];
dp[now][point]=dfs(tree[now].rson,point);
for(int k=0;k<point;k++)
{
dp[now][point]=max(dp[now][point],dfs(tree[now].lson,k)+dfs(tree[now].rson,point-k-1)+tree[now].val);
}
return dp[now][point];
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(NULL);
cout.tie(NULL);
cin>>n>>m;
for(int i=1;i<=n;i++)
{
int k,s; //爸爸 权值
cin>>k>>s;
tree[i].val=s;
if(bigson[k]==0) tree[k].lson=i; //如果k还没有其他儿子 那么i就是儿子了
else tree[bigson[k]].rson=i;
bigson[k]=i;
}
cout<<dfs(tree[0].lson,m);
return 0;
}
3.树的直径
这是解析...然而我觉得bfs或者dfs就够了 何苦dp
4.战略游戏
假设dp[i]表示以i为根的子树上需要安放的最少士兵 希望能从i的儿子推出i的情况
然而无法做到 考虑加维
由于每个节点可以选 或者不选
如果选了的话 那他的儿子可选可不选
如果没选的话 那他的儿子就必须选
因此dp[i][0]表示选了节点i所需要安防的最少士兵 dp[i][1]表示不选
方程显而易见 dp[i][0]=sigma min(,dp[i.son][0],dp[i.son][1]) dp[i][1]=sigma min(dp[i][1],dp[i.son][0])
//dp[i][0] 选i dp[i][1] 不选i 的所需最小个数
//如果选了i 意味着可以选或者不选他的儿子
//如果没选 意味着必须选所有的儿子
//dp[i][1]=sigma(dp[i.son][0])
//dp[i][0]=sigma(min(dp[i.son][0],dp[i.son][1]))
#include<bits/stdc++.h>
using namespace std;
const int N=1505;
int n,dp[N][2],first[N],tot;
struct Edge
{
int to,next;
}edge[N*4];
inline void addedge(int x,int y)
{
edge[++tot].to=y;
edge[tot].next=first[x];
first[x]=tot;
}
inline void dfs(int now)
{
dp[now][0]=1,dp[now][1]=0;
for(int u=first[now];u;u=edge[u].next)
{
int vis=edge[u].to;
// if(vis==fa) continue;
dfs(vis);
dp[now][1]+=dp[vis][0];
dp[now][0]+=min(dp[vis][0],dp[vis][1]);
}
}
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
{
int a,k;
cin>>a>>k;
for(int j=1;j<=k;j++)
{
int son;
cin>>son;
addedge(a,son);
// addedge(son,a);
}
}
dfs(0);
cout<<min(dp[0][1],dp[0][0])<<endl;
return 0;
}
5.皇宫看守
网址实在没找到....我提交的地方是学校题库
(题面)
对于每个点 都有三种情况
1.自己放
2.父亲放(被父亲看到)
3.儿子放(被儿子看到)
这意味着什么呢?
对于一个i
if 自己放了 也就是说儿子一定被父亲看到 也可以安排警卫 也可以被它的儿子看见
else
如果父亲放了 也就是说儿子可以安♂排 也可以被它的儿子看见
如果儿子放了 它的儿子必定有一个安排了的 否则被它的儿子看见 具体可以进行一些 特♂判
其实这道题很像上一道题的升级版
点到为止 不多说了(其实只是懒)
6.消息传递
由于根是不一定的 所以需要遍历所有点 作为根
设dp[i]是以i为根的子树传遍它所有子树需要的最少时间
dp[i]取决于花费时间最多的那颗子树(当然还要加上每次一秒的传递时间) 不过也不是一定的 万一话费时间最多的和次多的只差了一秒之类的情况也会出现 所以需要遍历所有的儿子~
方程:dp[i]=max{dp[i.son]+i.son.number(传递时间)}
#include<bits/stdc++.h>
using namespace std;
const int N=3005;
int n,tot,first[N],dp[N],son[N],cnt,ans=0,num;
struct Edge
{
int to,next;
}edge[N*10];
inline void addedge(int x,int y)
{
tot++;
edge[tot].to=y;
edge[tot].next=first[x];
first[x]=tot;
}
inline bool cmp(const int &a,const int &b)
{
return a>b;
}
inline void dfs(int now,int fa)
{
for(int u=first[now];u;u=edge[u].next)
{
int vis=edge[u].to;
if(vis==fa) continue;
dfs(vis,now);
}
int cnt=0;
for(int u=first[now];u;u=edge[u].next)
{
int vis=edge[u].to;
if(vis==fa) continue;
son[++cnt]=dp[vis];
}
sort(son+1,son+cnt+1);
int ret=0;
for(int i=1;i<=cnt;i++)
{
ret=max(ret,son[i]+cnt-i);
}
dp[now]=ret+1; //加1是因为仔细看了样例后发现默认时间是从一秒开始的orz
}
vector <int> con;
int main()
{
cin>>n;
for(int i=2;i<=n;i++)
{
int x;
cin>>x;
addedge(i,x);
addedge(x,i);
}
for(int i=1;i<=n;i++)
{
dfs(i,0);
int tmp=dp[i];
if(!ans||tmp<ans)
{
ans=tmp;
con.clear();
con.push_back(i);
}
else if(tmp==ans) con.push_back(i);
}
cout<<ans<<endl;
for(int i=0;i<con.size();i++) cout<<con[i]<<" ";
return 0;
}
7.有线电视网
树上背包。
这道对于本蒟蒻来说比较难
背包的总容量相当于该点为根节点的子树中所有的用户数量。然后,把该节点的每个儿子看成一组,每组中的元素为选一个,选两个...选n个用户。
转移方程 dp[i][j]=max(dp[i][j],dp[i][j-k]+dp[v][k]-这条边的花费) i,j不解释了,v表示枚举到这一组(即i的儿子),k表示枚举到这组中的元素:选k个用户。
//dp[i][j] 当前节点为i选j个用户 所能得到的最大收益
//dp[i][1]=pay[i];(叶子节点)
//dp[i][某个儿子的编号]=max{dp[i][某个儿子的编号-k(需要枚举)]+dp[vis][k]-val} (j>k>=1,)
#include<bits/stdc++.h>
using namespace std;
const int N=3005;
int n,m,first[N],pay[N],tot,dp[N][N];
struct Edge
{
int to,next,v;
}edge[N*2];
inline void addedge(int x,int y,int z)
{
tot++;
edge[tot].to=y;
edge[tot].next=first[x];
edge[tot].v=z;
first[x]=tot;
}
int dfs(int now)
{
dp[now][0]=0;
if(now>n-m) //用户一定为叶子
{
dp[now][1]=pay[now];
return 1;
}
int j=0;
for(int u=first[now];u;u=edge[u].next)
{
int vis=edge[u].to;
// if(fa==vis) continue;
j+=dfs(vis);
for(int i=j;i;i--)//枚举每一个j
{
for(int k=0;k<=i;k++)
{
dp[now][i]=max(dp[now][i],dp[now][i-k]+dp[vis][k]-edge[u].v); //倒序来压维 如果正序的话 dp[now][i-k]就被更新过了 并不是上一个儿子的值
}
}
}
return j;
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n-m;i++)
{
int k;
cin>>k;
for(int j=1;j<=k;j++)
{
int son,val;
cin>>son>>val;
addedge(i,son,val);
// addedge(son,i,val);
}
}
for(int i=1;i<=m;i++)
{
cin>>pay[i+n-m];
}
memset(dp,128,sizeof(dp)); //128是负无穷大
dfs(1);
for(int i=m;i>=0;i--)
{
if(dp[1][i]>=0)
{
cout<<i;
break;
}
}
return 0;
}
总结:
通常来说 把一棵树转化为二叉树 然后整个问题的最优只涉及到左右儿子的最优 然后考虑根节点随之的变化 这样化简了问题 也很容易推出状态转移方程
当然 也不是所有问题都要这样 我们应该仔细推敲每个结点的状态 以及相应状态与父子结点的联系等 就是如何从子节点的最优值推出父节点的最优值