树形DP学习笔记
这篇是我学习使用的,里面引用了众多的大佬的思路和文章,如有冒犯,请告知我,我会删掉的,谢谢!!!
树形DP是以树的结构为基础,所进行的DP,其实现是用DFS,在DFS中使用DP,我认为DFS是辅助,DP才是重点,树形DP最重要的就是使用DP的思想,用着DFS使其答案最优。
大概有两种DP方程:
要的实现形式是dp[i][j][0/1],i是以i为根的子树,j是表示在以i为根的子树中选择j个子节点,0表示这个节点不选,1表示选择这个节点。有的时候j或0/1这一维可以压掉
第一种是选择节点:
第二种是树形背包:
(这一段,摘要:这位大佬,ta写得好好哇!!!,如有不满,麻烦联系我,我会快速的删掉)
第一题 没有上司的舞会
(题目点这里)
可以看出,这是节点选和不选的,因为如果你选择一位员工A,要是A害怕他的老板B,B还能选吗?不能吗?如果选B后快乐的指数比选A大,那A还选吗?不选了吗?上述过程符合上面的方程,这是一道基础的树形dp,dp[i][1]是选,dp[i][0]是不选。
代码如下:
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+100;
int fa[N],dp[N][2];
vector<int> e[N];
void dfs(int x)
{
dp[x][0]=0;
for(int i=0;i<e[x].size();i++)
{
int v=e[x][i];
dfs(v);
dp[x][0]+=max(dp[v][0],dp[v][1]);//不选该节点,它的子节点有两种情况,选和不选;
dp[x][1]+=dp[v][0];//选了该节点,它的子节点不能选了,因为它怕!!
}
}
int main()
{
int n;
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%d",&dp[i][1]);
for(int i=1;i<+n;i++)
{
int u,v;
scanf("%d%d",&u,&v);
e[v].push_back(u);
fa[u]=v;
}
int i;
for(i=1;i<=n;i++)
if(!fa[i]) break;
dfs(i);
printf("%d\n",max(dp[i][0],dp[i][1]));
return 0;
}
第二题 最大子树和
(题目看这里)
这题和上面一题是差不多的,但是它们的dp方程式不同的,方程为 dp[u]+=max(dp[v],0) ; 有些子节点相加会为负数,这朵花看着会恶心,因此直接不要即为0,让它与0相比较。
代码如下:
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+100;
int dis[N],dp[N];
vector<int> e[N];
int n,ans=0;
void dfs(int x,int fa)
{
dp[x]=dis[x];//x节点的美丽指数
for(int i=0;i<e[x].size();i++)
{
int v=e[x][i];
if(v!=fa)
{
dfs(v,x);//继续往下寻找
dp[x]+=max(dp[v],0);//状态转移
}
}
ans=max(ans,dp[x]);//跟新答案
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%d",&dis[i]);
for(int i=1;i<n;i++)
{
int u,v;
scanf("%d%d",&u,&v);
e[u].push_back(v);
e[v].push_back(u);
}
dfs(1,0);
printf("%d\n",ans);
return 0;
}
第三题 选课
(题目看这里)
如果这题没有要选M门这个限制条件,就和《没有上司的舞》 会很像。
言归正传,如果暂时抛开这是一棵树的结构,再来看看题目,有N门课,每门课都有大小不一的学分,从中选出M门课,并保证选出的M门的课的学分最大,这和背包问题是差不多的,因此方程就有:dp[u][j]=max(dp[u][j],dp[v][k]+dp[u][j-k]; v是以u为根节点的子节点,j是前j个节点选k门课的方案数。
代码如下:
#include<bits/stdc++.h>
using namespace std;
const int N=1e3+100;
int dp[N][N];
vector<int> e[N];
int n,m;
void dfs(int u)
{
for(int i=0;i<e[u].size();i++)
{
int v=e[u][i];
dfs(v);
for(int j=m+1;j>=1;j--)
for(int k=0;k<j;k++)
dp[u][j]=max(dp[u][j],dp[v][k]+dp[u][j-k]);
}
}
int main()
{
scanf("%d%d",&n,&m);
for(int v=1;v<=n;v++)
{
int u,val;
scanf("%d%d",&u,&val);
dp[v][1]=val;
e[u].push_back(v);
}
dfs(0);
printf("%d\n",dp[0][m+1]);
return 0;
}
第四题 战略游戏
(题目看这里)
简而言之,就选和不选该节点,这就用到上述的第一个方程了。
代码如下:
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+100;
int dp[N][2],cnt[N];
vector<int> e[N];
int n;
void dfs(int x)
{
dp[x][1]=1;
for(int i=0;i<e[x].size();i++)
{
int v=e[x][i];
dfs(v);
dp[x][1]+=min(dp[v][1],dp[v][0]);
dp[x][0]+=dp[v][1];
}
}
int main()
{
scanf("%d",&n);
for(int i=0;i<n;i++)
{
int node,k;
scanf("%d%d",&node,&k);
for(int j=0;j<k;j++)
{
int v;
scanf("%d",&v);
e[node].push_back(v);
cnt[v]++;
}
}
int node;
for(node=0;node<=n;node++)//找到一个根节点
if(!cnt[node]) break;
dfs(node);
printf("%d\n",min(dp[node][0],dp[node][1]));
return 0;
}
第五题 二叉苹果树
(题目看这里)
这题其实和上面一题挺像的,都是有N个节点,想要Q个节点的最多苹果树,但是这题比较麻烦的一点是这题的根节点(1)始终不能删去要保留着,并且u和v上的所有的边也是要保留的,虽然这样,我们也把问题简化看一下,先给出状态转移方程:dp[x][j]=max(dp[x][j],dp[x][j-k-1]+dp[v][k]+val);如果选的一条边上的数量cnt是小于Q,就有前cnt节点选k颗的方案数,如果选的一条边上的数量是大于Q的,题目说要保留Q颗,因此就有前Q个节点选K颗的方案数。(说得可能不是很好,要是有不懂的,私聊我吧,如果愿意的话。)
代码如下:
#include<bits/stdc++.h>
using namespace std;
typedef pair<int,int> pill;
const int N=1e3+100;
int dp[N][N],cnt[N],dis[N];
vector<pill> e[N];
int n,m;
void dfs(int x,int fa)
{
for(int i=0;i<e[x].size();i++)
{
int v=e[x][i].first;
int val=e[x][i].second;
if(v!=fa)
{
dfs(v,x);
cnt[x]+=cnt[v]+1;
for(int j=min(cnt[x],m);j>=0;j--)
for(int k=min(cnt[v],j-1);k>=0;k--)
dp[x][j]=max(dp[x][j],dp[x][j-k-1]+dp[v][k]+val);
}
}
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<n;i++)
{
int u,v,val;
scanf("%d%d%d",&u,&v,&val);
e[u].push_back(make_pair(v,val));
e[v].push_back(make_pair(u,val));
}
dfs(1,-1);
printf("%d\n",dp[1][m]);
return 0;
}
第六题 Cell Phone Network G
(题目看这里)
题目大致是说,如果我们选了这个点,那么与这条边相邻的所有的点都会信号,这题似乎很战略游戏那题挺像的,但是值得注意的是这题
重点是从节点开始选的,而那题重点是在边,选完这条边再选周围的边就完事了,可是点涉及到的就有点多了,如图:
题目要求要建立最小的站点,如果选中的是C,那么A,B,D都会信号,这三个点就不用建立站点了,也就是这三个点不能再使用了,但是战略游戏那题不能使用的是边,点还是可以使用的,所以这就是该题与战略游戏不同之处,回到该题,既然我们选择了C,A,B,D都不能使用,是不是有着三个状态,第一个:自己被自己覆盖了信号(即在该节点上建立站点);第二种:自己被自己的子节点覆盖了信号;第三种:自己被自己的父亲覆盖了信号;
1.自己被自己染色
这时我们可以想一下,u被自己染色可以由什么转移过来,如果u已经被自己染色了的话,他的儿子v可以选择自己染色,也可以选择被自己(v)的儿子染色,当然也可以被uu染色,当然,我们要选最小的,所以转移方程就是
2.被自己的父亲结点染色
如果被父亲结点(fafa)染色了,那么uu的儿子vv只能选择自己染色或者被它的儿子染色,转移方程为
3.被自己的儿子结点染色
这是最麻烦的一种情况,因为u可能有多个儿子,只要有一个儿子自己染色了,就可以将uu覆盖,这种情况就成立了
而现在它的儿子有两种情况,分别是自己染色和被它儿子染色
我们可以先假设每个儿子都是被它自己染色(v被自己染色)的,然后看一下u的每个儿子(v)被其儿子染色是否使结果变得更小,把能让结果更小的 自己染色(v自己染色)的儿子 替换为 被其儿子染色的儿子(v被它儿子染色)的儿子
(参考了ysnerysner大佬的思路)
那么怎么实现呢?
- 先让f[u][1]加上所有的f[v][0](也就是假设所有的v目前都是自己给自己染色的)
- 在进行一的同时,用一个gg数组,表示vv被儿子染色所需的价值减去vv被自己染色的价值的差值,同时用一个变量tot记录一下一共有多少个儿子,即g[++tot] = f[v][1] - f[v][0]g[++tot]=f[v][1]−f[v][0]
- 如果uu没有儿子,即tottot为00,说明uu是一个叶结点,那么就没有从儿子来的价值,因为转移的时候我们要取小的,所以就把f[u][1]f[u][1]设为一个极大值
- 如果u有儿子,就将g从小到大排序,如果是负值,我们就可以替换,因为是负值的话就说明,此时的f[v][1]比f[v][0]小,所以就可以替换,只要是负的,值越小越好,所以就排序一下,是负的就替换,否则就breakbreak,当然我们最多替换tot-1个,因为要保证u被染色,必须有一个儿子是自己染色的
至此主要部分就讲完了,需要注意的是每次dfs的时候先将f[u][0]设为1,因为自己给自己染色肯定至少为1
(此部分是大佬的思路,超级棒棒!! 如有不满,请麻烦联系我,我会快速删掉!!!)
代码如下:
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+100;
const int INF=0x3f3f3f;
int dp[N][3];
vector<int> e[N];
int n;
void dfs(int u,int fa)
{
int g[N]={0},tot=0;
dp[u][0]=1;
for(int i=0;i<e[u].size();i++)
{
int v=e[u][i];
if(v==fa) continue;
dfs(v,u);
dp[u][0]+=min(min(dp[v][1],dp[v][0]),dp[v][2]);
dp[u][1]+=dp[v][0];
dp[u][2]+=min(dp[v][0],dp[v][1]);
g[++tot]=dp[v][1]-dp[v][0];
}
if(!tot) dp[u][1]=INF;
else
{
sort(g+1,g+1+tot);
for(int i=1;i<tot;i++)
if(g[i]<0) dp[u][1]+=g[i];
else break;
}
}
int main()
{
scanf("%d",&n);
for(int i=1;i<n;i++)
{
int u,v;
scanf("%d%d",&u,&v);
e[u].push_back(v);
e[v].push_back(u);
}
dfs(1,-1);
printf("%d\n",min(dp[1][1],dp[1][0]));
return 0;
}
看到这里,对于什么是树形DP是不是有个大概的了解,其重点是DP的思想,再其次是利用树的结构,进行递归实现。
最后,还有一些题,会尽快补上,对于上述被我引用的大佬们,非常感谢,也非常抱歉没有经过你们的同意擅自使用你们的文章部分内容,如有不满,请联系我,我会尽快删掉,在上述文章中除了我特别说明部分,剩下部分都是我原创。如果不解,可以私信我,谢谢。
(编辑于2020.9.28 时间:1.38)