树形DP详解
哈喽大家好,我是 doooge,今天给大家带来的是 树形DP 详解。
\]
1.树形DP是什么
想必大家都学过动态规划吧,树形DP 就是 DP 的一部分。顾名思义,树形DP 就是在树上做动态规划。
2.树形DP的转移
知道了 树形DP 是什么后,想必聪明的读者肯定想问:在树上怎么才能转移 \(dp\) 数组呢?
我们可以想想,不难想出,有两个转移的方法:
- 父节点转移到子节点,这种更新方法一般作为树上链路问题,比如说求每个节点的深度可以用 \(dp_x=dp_{fa}+1\) 来更新。
- 子节点转移到父节点,这种更新方法一般用于树上选点问题(详见两道例题)。
这两种转移方法的代码也很好写。
子节点转移父节点:
写法跟树上 DFS 一样:for(auto i:v[x]){
if(i==fa)continue;
dfs(i,x);//继续dfs
//dp[x]=dp[i]...转移
}
父节点转移子节点:
//dp[x]=dp[fa]...转移
3.树形DP 的例题
3.1 P1122 最大子树和
3.1.1 题目描述
小明对数学饱有兴趣,并且是个勤奋好学的学生,总是在课后留在教室向老师请教一些问题。一天他早晨骑车去上课,路上见到一个老伯正在修剪花花草草,顿时想到了一个有关修剪花卉的问题。于是当日课后,小明就向老师提出了这个问题:
一株奇怪的花卉,上面共连有 \(n\) 朵花,共有 \(n-1\) 条枝干将花儿连在一起,并且未修剪时每朵花都不是孤立的。每朵花都有一个“美丽指数”,该数越大说明这朵花越漂亮,也有“美丽指数”为负数的,说明这朵花看着都让人恶心。所谓“修剪”,意为:去掉其中的一条枝条,这样一株花就成了两株,扔掉其中一株。经过一系列“修剪“之后,还剩下最后一株花(也可能是一朵)。老师的任务就是:通过一系列“修剪”(也可以什么“修剪”都不进行),使剩下的那株(那朵)花卉上所有花朵的“美丽指数”之和最大。
老师想了一会儿,给出了正解。小明见问题被轻易攻破,相当不爽,于是又拿来问你。
3.1.2 思路
咳咳,这道题的题面稍微有点乱,我来简化一下:
有一棵树,每个点都有点权,你可以去掉该树的一些子树,使剩余部分的点权和最大
直接考虑 树形DP 怎么写。
不难想到,\(dp_x\) 表示子树 \(x\) 去掉部分子树最大的点权和。首先肯定的,\(dp_x=a_x\),也就是必须选自己。
那我们怎么转移呢?
因为是要选择子树,所以肯定 \(dp_x\) 肯定是从 \(x\) 的子树转移而来的。我们设 \(x\) 的子树为 \(i\),因为点 \(i\) 的点权可能为负,\(dp_i\) 也可能为负,我们既然想要选择最有的子树,也就是要选择点权和大于 \(0\) 的子树,所以可以得到 \(dp_x=\max(dp_x,dp_x+dp_i)\)。
放在代码上也就是:
for(auto i:v[x]){
if(i==fa)continue;
dfs(i,x);
dp[x]=max(dp[x],dp[x]+dp[i]);
}
当然我们也知道,当 \(dp_x>0\) 时,\(dp_x+dp_i\) 肯定会大于 \(dp_x\),所以代码也可以写成:
for(auto i:v[x]){
if(i==fa)continue;
dfs(i,x);
if(dp[i]>0)dp[x]+=dp[i];
}
当然,不要忘了初始化 \(dp_x=a_x\),整个 DFS 代码也就出来了:
void dfs(int x,int fa){
dp[x]=a[x];
for(auto i:v[x]){
if(i==fa)continue;
dfs(i,x);
if(dp[i]>0)dp[x]+=dp[i];
}
return;
}
相信读者如果会了 DFS 的代码,完整代码也不难了吧!
3.1.3 代码
代码:
#include<bits/stdc++.h>
using namespace std;
int dp[100010],a[100010];
vector<int>v[100010];
void dfs(int x,int fa){
dp[x]=a[x];
for(auto i:v[x]){
if(i==fa)continue;
dfs(i,x);
if(dp[i]>0)dp[x]+=dp[i];
}
return;
}
int main(){
int n,ans=-1e18;
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
}
for(int i=1;i<n;i++){
int x,y;
cin>>x>>y;
v[x].push_back(y);
v[y].push_back(x);
}
dfs(1,-1);
for(int i=1;i<=n;i++){
ans=max(ans,dp[i]);
}
cout<<ans<<endl;
return 0;
}
3.2 P1352 没有上司的舞会
某大学有 \(n\) 个职员,编号为 \(1\ldots n\)。
他们之间有从属关系,也就是说他们的关系就像一棵以校长为根的树,父结点就是子结点的直接上司。
现在有个周年庆宴会,宴会每邀请来一个职员都会增加一定的快乐指数 \(r_i\),但是呢,如果某个职员的直接上司来参加舞会了,那么这个职员就无论如何也不肯来参加舞会了。
所以,请你编程计算,邀请哪些职员可以使快乐指数最大,求最大的快乐指数。
3.2.2 思路
这道题的题面并没有多长,所以就不做简化题意了其实是我懒。
我们同样考虑 树形DP 怎么写,这道题的情况就比较复杂了。
先不管别的,我们设 \(dp_x\) 表示选了 \(x\) 职员,在 \(x\) 职员关系的子树内所能达成的最大快乐值。我们考虑怎么转移。
这个时候我们会发现,转移并不好写,因为我们选择了 \(x\),那么 \(x\) 的父节点和子节点都选不了。也就是说得要从这样才能转移过来:
这样不仅难写,还容易写错。
我们重新想想:如果选择了 \(i\),如果选择了 \(i\),就不能选择 \(i\) 相邻的节点。那我们可不可以设计一个状态表示不选 \(i\),包括 \(i\) 的子树最大的快乐值。这样转移也好写多了。
于是!我们可以增加一维状态 \(1\) 和 \(0\) 来表示选或不选的最大快乐值,转移,但是需要注意,有些职员的快乐值为负大概是整顿职场的吧,我们还要对自己取 \(\max\)。
\(dp_{x,1}\) 因为选了自己,必须不选子节点,所以:
- \(dp_{x,1}=\max(dp_{x,1},dp_{x,1}+dp_{i,1})\)
然而 \(dp_{x,0}\) 因为没有选自己,选不选子节点都没问题,所以:
- \(dp_{x,0}=\max(dp_{x,0},dp_{x,0}+\max(dp_{i,0},dp_{i,1}))\)
转移的代码也就很好写了,在这里就不多细讲了。
3.2.3 代码
代码:
#include<bits/stdc++.h>
using namespace std;
int a[100010],dp[100010][5];
vector<int>v[100010];
void dfs(int x,int fa){
dp[x][1]=a[x];
for(int i:v[x]){
if(i==fa)continue;
dfs(i,x);
dp[x][1]+=max(0,dp[i][0]);
dp[x][0]+=max(0,max(dp[i][0],dp[i][1]));
}
return;
}
int main(){
int n;
cin>>n;
for(int i=1;i<=n;i++)cin>>a[i];
for(int i=1;i<n;i++){
int x,y;
cin>>x>>y;
v[x].push_back(y);
v[y].push_back(x);
}
dfs(1,-1);
cout<<max(dp[1][0],dp[1][1])<<endl;
return 0;
}
什么?你说你不想在这里这些题太简单了?别急嘛,这还有更难的等着你呢
3.3 P2585 [ZJOI2006] 三色二叉树
3.3.1 题目描述
一棵二叉树可以按照如下规则表示成一个由 \(0\)、\(1\)、\(2\) 组成的字符序列,我们称之为“二叉树序列 \(S\)”:
\begin{cases}
0& \text表示该树没有子节点\\
1S_1& 表示该树有一个节点,S_1 为其子树的二叉树序列\\
2S_1S_2& 表示该树有两个子节点,S_1 和 S_2 分别表示其两个子树的二叉树序列
\end{cases}\]
例如,下图所表示的二叉树可以用二叉树序列 \(S=\texttt{21200110}\) 来表示。
你的任务是要对一棵二叉树的节点进行染色。每个节点可以被染成红色、绿色或蓝色。并且,一个节点与其子节点的颜色必须不同,如果该节点有两个子节点,那么这两个子节点的颜色也必须不同。给定一颗二叉树的二叉树序列,请求出这棵树中最多和最少有多少个点能够被染成绿色。
3.3.2 思路
我们先来考虑要求最大值该怎么写。
因为染绿色蓝色和红色都是一样的,所以我们求染绿色蓝色红色都是一样的。
于是我们可以设 \(dp_{x,1}\),\(dp_{x,2}\) 和 \(dp_{x,3}\) 为 \(x\) 节点染绿色,红色和蓝色,\(x\) 及所在的子树中的绿色节点最大的个数。
接下来想想如何转移,我们可以先想想将 \(x\) 染成绿色,子节点可以怎么染,如图:
因为这题是个二叉树,如果我们把左儿子表示成 \(ls\),右儿子表示成 \(rs\),转移应该这样:
\]
同样,我们也能得到:
\]
\]
转移代码(复制粘贴大法好!):
dp[x][1]+=max(dp[lc[x]][2]+dp[rc[x]][3],dp[lc[x]][3]+dp[rc[x]][2]);
dp[x][2]=max(dp[lc[x]][1]+dp[rc[x]][3],dp[lc[x]][3]+dp[rc[x]][1]);
dp[x][3]=max(dp[lc[x]][1]+dp[rc[x]][2],dp[lc[x]][2]+dp[rc[x]][1]);
当然,不要忘了初始化 \(dp_{x,1}\) 为 \(1\)。由于染绿色蓝色红色都是一样的,所以答案就是 \(\max(dp_{x,1},dp_{x,2},dp_{x,3})\) 啦。
求最小值同理,DFS 代码如下(码风一坨):
void dfs(int x){
dp[x][1]=dp2[x][1]=1,dp2[x][2]=dp2[x][3]=0;
if(lc[x]!=0)dfs(lc[x]);
if(rc[x]!=0)dfs(rc[x]);
if(lc[x]==0&&rc[x]==0)return;
dp[x][1]+=max(dp[lc[x]][2]+dp[rc[x]][3],dp[lc[x]][3]+dp[rc[x]][2]);
dp[x][2]=max(dp[lc[x]][1]+dp[rc[x]][3],dp[lc[x]][3]+dp[rc[x]][1]);
dp[x][3]=max(dp[lc[x]][1]+dp[rc[x]][2],dp[lc[x]][2]+dp[rc[x]][1]);
dp2[x][1]+=min(dp2[lc[x]][2]+dp2[rc[x]][3],dp2[lc[x]][3]+dp2[rc[x]][2]);
dp2[x][2]=min(dp2[lc[x]][1]+dp2[rc[x]][3],dp2[lc[x]][3]+dp2[rc[x]][1]);
dp2[x][3]=min(dp2[lc[x]][1]+dp2[rc[x]][2],dp2[lc[x]][2]+dp2[rc[x]][1]);
}
值得一提的是,这道题的建树挺特别的,在本文主要讲的是 树形DP,所以建树就不多提了,大家可以自行参考题解或者我的代码(绝对不是因为我懒)
3.3.3 代码
代码:
#include<bits/stdc++.h>
using namespace std;
int lc[500010],rc[500010],dp[500010][5],dp2[500010][5],n=1;
int build(int x,int pos){
char c=getchar();
if(c=='0')return pos;
else if(c=='1'){
lc[x]=++n;
pos=build(n,pos+1);
}
else{
lc[x]=++n;
pos=build(n,pos+1)+1;
rc[x]=++n;
pos=build(n,pos);
}
return pos;
}
void dfs(int x){
dp[x][1]=dp2[x][1]=1,dp2[x][2]=dp2[x][3]=0;
if(lc[x]!=0)dfs(lc[x]);
if(rc[x]!=0)dfs(rc[x]);
if(lc[x]==0&&rc[x]==0)return;
dp[x][1]+=max(dp[lc[x]][2]+dp[rc[x]][3],dp[lc[x]][3]+dp[rc[x]][2]);
dp[x][2]=max(dp[lc[x]][1]+dp[rc[x]][3],dp[lc[x]][3]+dp[rc[x]][1]);
dp[x][3]=max(dp[lc[x]][1]+dp[rc[x]][2],dp[lc[x]][2]+dp[rc[x]][1]);
dp2[x][1]+=min(dp2[lc[x]][2]+dp2[rc[x]][3],dp2[lc[x]][3]+dp2[rc[x]][2]);
dp2[x][2]=min(dp2[lc[x]][1]+dp2[rc[x]][3],dp2[lc[x]][3]+dp2[rc[x]][1]);
dp2[x][3]=min(dp2[lc[x]][1]+dp2[rc[x]][2],dp2[lc[x]][2]+dp2[rc[x]][1]);
}
signed main(){
build(1,0);
dfs(1);
cout<<max(dp[1][1],max(dp[1][2],dp[1][3]))<<' '<<min(dp2[1][1],min(dp2[1][2],dp2[1][3]))<<endl;
return 0;
}
4.拓展-树形DP求树的直径
不难发现,一棵树的直径一定是由某一个节点 \(x\) 和两条或一条与 \(x\) 相连的路径所组成的。
所以,树的直径必定在距离每个点 \(x\) 的最长的路径和次长的路径之中,我们可以维护一个数组 \(dp\) 和 \(dp2\),\(dp_i\) 和 \(dp2_i\) 分别表示距离 \(i\) 最长和次长的路径的长度,经过 \(i\) 的答案就是 \(dp_i+dp2_i\),最后遍历求 \(\max(dp_i)\) 找答案。
代码很好写,就不推 \(dp\) 和 \(dp2\) 的数组的转移了,代码:
#include<bits/stdc++.h>
using namespace std;
int dp[100010],dp2[100010];
vector<int>v[100010];
void dfs(int x,int fa){
// dp[x]=dp2[x]=-1e9;如果有负边权就需要初始化,但是叶节点的dp要初始化成0,具体代码看下面。
// bool flag=false;判断该节点是不是叶节点,如果能向下遍历就不是叶节点
for(auto i:v[x]){
if(i==fa)continue;//不能走回头路
// flag=true;//如果可以继续向下搜索就不是叶节点
dfs(i,x);//继续搜索
if(dp[i]+1>dp[x]){//注意这里如果有边权要加上边权
//如果从这条路走的长度比dp[x]要长
dp2[x]=dp[x];//更新,把dp[x]传下去给dp2[x]
dp[x]=dp[i]+1;//更新dp[x]
}
else if(dp[i]+1>dp2[x]){
//如果从这条路走的长度比dp2[x]要长
dp2[x]=dp[i]+1;//直接更新dp2[x]
}
}
//if(!flag)dp[i]=dp2[i]=0;如果是叶节点就要初始化成0
return;
}
int main(){
int n,ans=0;//ans用于最后求max,所以如果有负边权就要初始化成-1e9
cin>>n;
for(int i=1;i<n;i++){
int x,y;
cin>>x>>y;
v[x].push_back(y);
v[y].push_back(x);//建图
}
dfs(1,-1);//从什么点遍历都可以
for(int i=1;i<=n;i++){//注意最后要遍历找答案
ans=max(ans,dp[i]+dp2[i]);//dp[i]+dp2[i]就是经过点i的最长的路
}
cout<<ans<<endl;
return 0;
}
5.作业
- P2016 战略游戏 难度:\(2/5\)。
- P4084 [USACO17DEC] Barn Painting G 难度:\(3/5\)。
- P3177 [HAOI2015] 树上染色 难度:\(4.5/5\)。
6.闲话
蒟蒻不才,膜拜大佬,如果文章有什么问题,请在评论区@我。
树形DP详解的更多相关文章
- 树形DP详解+题目
关于树形dp 我觉得他和线性dp差不多 总结 最近写了好多树形dp+树形结构的题目,这些题目变化多样能与多种算法结合,但还是有好多规律可以找的. 先说总的规律吧! 一般来说树形dp在设状态转移方程时都 ...
- 数位DP 详解
序 天堂在左,战士向右 引言 数位DP在竞赛中的出现几率极低,但是如果不会数位DP,一旦考到就只能暴力骗分. 以下是数位DP详解,涉及到的例题有: [HDU2089]不要62 [HDU3652]B-n ...
- 动态规划晋级——HDU 3555 Bomb【数位DP详解】
转载请注明出处:http://blog.csdn.net/a1dark 分析:初学数位DP完全搞不懂.很多时候都是自己花大量时间去找规律.记得上次网络赛有道数位DP.硬是找规律给A了.那时候完全不知数 ...
- 数位DP详解
算法使用范围 在一个区间里面求有多少个满足题目所给的约束条件的数,约束条件必须与数自身的属性有关 下面用kuangbin数位dp的题来介绍 例题 不要62 题意:在一个区间里面求出有多少个不含4和6 ...
- 状压DP详解(位运算)
前言: 状压DP是一种非常暴力的做法(有一些可以排除某些状态的除外),例如dp[S][v]中,S可以代表已经访问过的顶点的集合,v可以代表当前所在的顶点为v.S代表的就是一种状态(二进制表示),比如 ...
- 状态压缩dp 状压dp 详解
说到状压dp,一般和二进制少不了关系(还常和博弈论结合起来考,这个坑我挖了还没填qwq),二进制是个好东西啊,所以二进制的各种运算是前置知识,不了解的话走下面链接进百度百科 https://baike ...
- 线性DP详解
顾名思义,线性DP就是在一条线上进行DP,这里举一些典型的例子. LIS问题(最长上升子序列问题) 题目 给定一个长度为N的序列A,求最长的数值单调递增的子序列的长度. 上升子序列B可表示为B={Ak ...
- 数位dp详解&&LG P2602 [ZJOI2010]数字计数
数位dp,适用于解决一类求x~y之间有多少个符合要求的数或者其他. 例题 题目描述 杭州交通管理局经常会扩充一些的士车牌照,新近出来一个好消息,以后上牌照,不再含有不吉利的数字了,这样一来,就可以消除 ...
- 状态压缩动态规划(状压DP)详解
0 引子 不要999,也不要888,只要288,只要288,状压DP带回家.你买不了上当,买不了欺骗.它可以当搜索,也可以卡常数,还可以装B,方式多样,随心搭配,自由多变,一定符合你的口味! 在计算机 ...
- 状压DP详解+题目
介绍 状压dp其实就是将状态压缩成2进制来保存 其特征就是看起来有点像搜索,每个格子的状态只有1或0 ,是另一类非常典型的动态规划 举个例子:有一个大小为n*n的农田,我们可以在任意处种田,现在来描述 ...
随机推荐
- 【教程】Windows10系统激活
Windows10系统激活 一.找一个激活码 到百度搜索,筛选发表日期在最近一个月或者一周之内的 二.以管理员身份打开cmd 按Win+R键,输入cmd打开命令行窗口 按Ctrl+Shift+Esc键 ...
- Browser-use:基于 Python 的智能浏览器自动化 AI 工具调研与实战
Browser-use:基于 Python 的智能浏览器自动化 AI 工具调研与实战 一.概述 Browser-use 是一个旨在将 AI "智能体"(Agents)与真实浏览器进 ...
- 🎀Charles激活
简介 Charles激活码计算 激活 Help -> Register Charles 添加 Registered Name 和计算出的 License key 点击 Register Java ...
- C++数据的共享和保护
1.函数原型作用域:C++中最小的作用域 ①在函数原型声明时,形参的作用范围就是函数原型作用域. 2.局部作用域/块作用域 3.类作用域 类可以被看做是一组有名成员的集合,类X的成员m具有类作用域,对 ...
- MySQL 中 count(*)、count(1) 和 count(字段名) 有什么区别?
MySQL 中 count(*).count(1) 和 count(字段名) 的区别 在 MySQL 中,COUNT() 函数用于统计记录数.虽然 COUNT(*).COUNT(1) 和 COUNT( ...
- LangPipe大语言模型Pipeline应用框架案例介绍
LangPipe介绍 LangPipe是一个轻量级的大模型工作流应用框架LangPipe,可以轻松解决下面各种问题: text generation(文本生成) chat with LLM(与LLM对 ...
- sql学习day3——case when的使用
1,当前表 course_master open_course ...
- MySQL开启general_log
General_log 详解 1.介绍 开启 general log 将所有到达MySQL Server的SQL语句记录下来. 一般不会开启开功能,因为log的量会非常庞大.但个别情况下可能会临时的开 ...
- 通过Linux包管理器提升权限
免责声明:本文所涉及的技术仅供学习和参考,严禁使用本文内容从事违法行为和未授权行为,如因个人原因造成不良后果,均由使用者本人负责,作者及本博客不承担任何责任. 前言 在Linux系统中,apt和yum ...
- linux期末考试题(1)
linux期末考试题 一.选择题(共20分,每小题2分) 1.以下哪个环境变量表示shell搜索外部命令或程序路径(C) A.ENV B.PWD C.PATH D.ROOT 解答: ENV用于显示当前 ...