【学习笔记】树型DP学习笔记
省流:被吊打了
自己开的一个坑,死也要填完它。
希望我随手写下的笔记对您的学习有所帮助(也不太可能)。
更改日志
2024/01/08:开坑,写了树的直径和换根DP,写不动了(((
2024/01/08 晚上:更新了最小点覆盖和最大独立集,看来精神还可以,顶着明天做手术的风险
2024/01/09:修改错误+增补解释说明(主要在换根DP处)。
学习背景
看着那么多dalao会树型DP,我这个小蒟蒻也来挑战一下。
结果还是被dalao吊打(悲
正片开始
了解树型DP
字面意思,树型DP就是在树上做 DP
或是 DP
过程中有树的特征。树形结构拥有递归特征,所以常以子树作为划分阶段的单位。
树型DP的状态常常形如 DP[u][...]
,表示以
树型DP实现常常采用记忆化搜索,由子节点提供给父节点信息去转移到答案,与 DP
的性质类似(由子节点(子问题)转移到父节点(较大的问题)),也会有父节点为子节点或兄弟节点提供信息的情况。
树的直径
树的直径就是树上最远的两个点之间的距离,连接这两点的路径被称为树的最长链
例题
P2610 [ZJOI2012] 旅游
(别看我一上来就丢出一道紫题,这可是水紫(逃))
题目描述
到了难得的暑假,为了庆祝小白在数学考试中取得的优异成绩,小蓝决定带小白出去旅游~~
经过一番抉择,两人决定将 T 国作为他们的目的地。
T 国的国土可以用一个凸
T 国包含
为了能够买到最好的纪念品,小白希望旅游路线上经过的城市尽量多。作为小蓝的好友,你能帮帮小蓝吗?
输入格式
每个输入文件中仅包含一个测试数据。
第一行包含两个由空格隔开的正整数
接下来有
输出格式
输出文件共包含一行,表示最多经过的城市数目。(一个城市被当做经过当且仅当其与线路有至少两个公共点)
说明/提示
对于
对于
解法(怎么感觉开始写题解了)
分析
第一眼看到这题没想到怎么用树型DP,连建图都不会,后面想到可以把一个城市看成一个点,如果两个城市有发生冲突的公共边,就在这两个点之间建一条边。
那怎么知道是不是公共边呢?用个 map
统计就好啦(要先把三个数排序哦!)。
这里是可以确定这是一颗树的,那不就可以树型DP了吗。
那这个要求“旅游路上经过的城市尽量多”就可以抽象为“在树上找一条最长路径”,就可以直接求树的直径了。
那么树的直径又要何去求呢?
其实可以找一个点
但是不知道点 DFS
维护每个点的从此点出发的最长链长度和从此点出发的次长链长度,最后取个 max
即可。
设状态
我们设
转移
设点
-
若
,说明有更优的最长链,则将当前的最长链贬变为次长链,再更新最长链,就是 。 -
再看,若
且 ,说明有更优的次长链,则将当前的次长链扔掉,再更新次长链,就是 。
解
边界为
CODE(不就是题解吗):
#include<bits/stdc++.h>
using namespace std;
map<pair<int,int>,int>mp;//记录的map
int n,ret,x,y,z,dp[200010][2];
vector<int>g[200010];
void sort1(int &x,int &y,int &z){//这个不必解释,三个数排序
if(x>y){
swap(x,y);
}if(y>z){
swap(y,z);
}if(x>y){
swap(x,y);
}
}void dfs(int u,int x){
for(int i=0;i<g[u].size();i++){
int v=g[u][i];
if(v!=x){
dfs(v,u);//先要DFS
if(dp[v][0]+1>dp[u][0]){//转移
dp[u][1]=dp[u][0],dp[u][0]=dp[v][0]+1;
}else if(dp[v][0]+1>dp[u][1]){
dp[u][1]=dp[v][0]+1;
}
}
}ret=max(ret,dp[u][0]+dp[u][1]);//取max
}int main(){
scanf("%d",&n);
for(int i=1;i<=n+2;i++){
scanf("%d%d%d",&x,&y,&z);
sort1(x,y,z);//排序
if(mp[make_pair(x,y)]!=0){//若之前统计到过这条边了
g[i].push_back(mp[make_pair(x,y)]);
g[mp[make_pair(x,y)]].push_back(i);//与之前map里记录的城市建边
}if(mp[make_pair(y,z)]!=0){//同上
g[i].push_back(mp[make_pair(y,z)]);
g[mp[make_pair(y,z)]].push_back(i);
}if(mp[make_pair(x,z)]!=0){//同上
g[i].push_back(mp[make_pair(x,z)]);
g[mp[make_pair(x,z)]].push_back(i);
}mp[make_pair(x,y)]=i,mp[make_pair(y,z)]=i,mp[make_pair(x,z)]=i;//把标记改为这个城市
}dfs(1,-1);
printf("%d",ret+1);//还要加一,就是点(城市)的数量=边的数量+1
return 0;
}
换根DP
2024/01/08:我可能暂时就写这俩了,树的重心还没学会,到时候再写啊。
有一些树型DP中,转移不仅和子树内有关,还有可能和子树外的节点有关(注意这里,后面有考),此时就要二次扫描与换根:
第一次扫描:任选一个点 DFS
,回溯时自底向上转移,由子节点转移父节点,就是普通的一次树型DP。
第二次扫描:以 DFS
,递归前使用自顶向下更新,用父节点的值更新子节点的值,计算出“换根”后得到的解。
例题
P2986 [USACO10MAR] Great Cow Gathering G
题目描述
Bessie 正在计划一年一度的奶牛大集会,来自全国各地的奶牛将来参加这一次集会。当然,她会选择最方便的地点来举办这次集会。
每个奶牛居住在
道路
在选择集会的地点的时候,Bessie 希望最大化方便的程度(也就是最小化不方便程度)。
比如选择第
帮助 Bessie 找出最方便的地点来举行大集会。
输入格式
第一行一个整数
第二到
第
输出格式
一行一个整数,表示最小的不方便值。
提示
解法
朴素做法
这题我们可以先来考虑朴素的做法,枚举每一个节点作为集会地点,然后 DFS
统计出不方便程度,接着取 min
,时间复杂度巨大,考虑优化。
设状态
设
设
转移
可以思考由
所以,设
解
这样可求出每个点为根时的所有奶牛到此点的不方便程度,最后取 min
即可,时间复杂度
二次扫描+换根法
可以看到就算朴素做法就算优化也无法满分,
我们可以画一张假设的图:
(真的大大小小,画技不好,嫌丑轻喷!)
一次转移
首先 DFS
一遍以
二次转移
根据上面那张图,第一遍 DFS
后
考虑怎么求虚线框出来的所有节点到点
其实可以想,虚线框出来的所有节点到点
设从点
-
所有虚线框出来的点到点
(就是点 的父节点)的距离就是 ,代表以点 为根的子树中所有点到点 的不方便值 以点 为根的子树中所有点到点 的不方便值 点 为根的子树中所有奶牛一起从点 到点 的总不方便值(就是以点 为根的子树中的奶牛数 从点 到点 的距离)。 -
虚线框出来的所有奶牛一起从点
到点 的总不方便值就是 ,代表以点 为根的子树中的奶牛只数减去以点 为根的子树中的奶牛只数 点 到点 之间的距离(解释同上朴素算法转移)。
最后将这两个加一下就行了:
这里可以推出公式,设
解
这样我们就可以在第二次 DFS
是求出每个节点作为根时,所有奶牛到这个点的不方便程度了,最后答案为
其实这里的二次扫描就是就是一开始讲二次扫描+换根法时提到的“转移不仅和子树内有关,还有可能和子树外的节点有关”。
二次扫描+换根法CODE(朴素的就不放了):
#include<bits/stdc++.h>
using namespace std;
int n,x,y,z,c[100010];
long long ret,dp[100010],sum[100010];
struct node{
int v,w;
};
vector<node>g[100010];
void dfs(int u,int x){
sum[u]=c[u];
for(int i=0;i<g[u].size();i++){
int v=g[u][i].v,w=g[u][i].w;//v就是儿子,w是距离
if(v!=x){/特判
dfs(v,u);//先回溯,再转移,这就是自底向上
sum[u]+=sum[v];//统计以u为根的子树中的奶牛只数
dp[u]+=dp[v]+sum[v]*w;//先求出以u为根的子树中所有节点到点u的不方便程度和以u为根的子树中所有节点住的奶牛的总只数。
}
}
}void dfs1(int u,int x){
for(int i=0;i<g[u].size();
int v=g[u][i].v,w=g[u][i].w;//同上
if(v!=x){//同上
dp[v]+=(dp[u]-dp[v]-1ll*sum[v]*w)+1ll*(sum[u]-sum[v])*w;//再加上没有加上的(就如上面图片的虚线框出部分)不方便程度
sum[v]=sum[u];//整个的奶牛数
ret=min(ret,dp[v]);//找答案取min
dfs1(v,u);//先转移,再递归,这就是自顶向下
}
}
}int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%d",&c[i]);
}for(int i=1;i<=n-1;i++){
scanf("%d%d%d",&x,&y,&z);
g[x].push_back((node){y,z});//双向建边
g[y].push_back((node){x,z});
}dfs(1,0);//先统计以它为根的子树
ret=dp[1];//设初值
dfs1(1,0);//再统计没统计到的,二次扫描
printf("%lld",ret);
return 0;
}
练习题
P1364 医院设置
题目描述
设有一棵二叉树,如图:
其中,圈中的数字表示结点中居民的人口。圈边上数字表示结点编号,现在要求在某个结点上建立一个医院,使所有居民所走的路程之和为最小,同时约定,相邻接点之间的距离为
输入格式
第一行一个整数
接下来的
输出格式
一个整数,表示最小距离和。
提示
数据规模与约定
对于
做法
这不就是来送AC的吗
其实可以发现,这题和例题很像,只是点之间的距离是 没有一点难度好吧。
输入怎么改就不需要我说了吧,懂得都懂。
CODE(这还是练习题吗):
#include<bits/stdc++.h>//解释同上
using namespace std;
int n,x,y,z,c[100010];
long long ret,dp[100010],sum[100010];
struct node{
int v,w;
};
vector<node>g[100010];
void dfs(int u,int x){
sum[u]=c[u];
for(int i=0;i<g[u].size();i++){
int v=g[u][i].v,w=g[u][i].w;
if(v!=x){
dfs(v,u);
sum[u]+=sum[v];
dp[u]+=dp[v]+sum[v]*w;
}
}
}void dfs1(int u,int x){
for(int i=0;i<g[u].size();i++){
int v=g[u][i].v,w=g[u][i].w;
if(v!=x){
dp[v]+=(dp[u]-dp[v]-1ll*sum[v]*w)+1ll*(sum[u]-sum[v])*w;
sum[v]=sum[u];
ret=min(ret,dp[v]);
dfs1(v,u);
}
}
}int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%d%d%d",&c[i],&x,&y);
if(x!=0){
g[i].push_back((node){x,1});//1代表距离为1,正好就满足了两题的共同点
g[x].push_back((node){i,1});
}if(y!=0){
g[i].push_back((node){y,1});
g[y].push_back((node){i,1});
}
}dfs(1,0);
ret=dp[1];
dfs1(1,0);
printf("%lld",ret);
return 0;
}
最小点覆盖
最小点覆盖是指从图中选出尽量少的点,使得图中所有的边都有和选中的点相连。
设状态
可以这样想:
设
转移
于是,我们可以推到(设
-
代表点 被选中了,那么所有与点 相连的边都被覆盖了,那么子节点 选不选均可,则有 。 -
代表点 没被选中,那么所有与点 相连的边都没有被覆盖,为了所有边都被覆盖,那么子节点 必须选,则有 。
解
设根为
例题
P2016 战略游戏
好多绿+的题啊
题目背景
Bob 喜欢玩电脑游戏,特别是战略游戏。但是他经常无法找到快速玩过游戏的办法。现在他有个问题。
题目描述
他要建立一个古城堡,城堡中的路形成一棵无根树。他要在这棵树的结点上放置最少数目的士兵,使得这些士兵能瞭望到所有的路。
注意,某个士兵在一个结点上时,与该结点相连的所有边将都可以被瞭望到。
请你编一程序,给定一树,帮 Bob 计算出他需要放置最少的士兵。
输入格式
第一行一个整数
第二行至第
对于一个
输出格式
输出文件仅包含一个整数,为所求的最少的士兵数目。
提示
数据规模与约定
对于全部的测试点,保证
解法
这不就是最小点覆盖模板题吗??直接套进去就行了!!
就是把放士兵的节点看作选中的点就行了。
CODE(其实可以直接当模板了):
#include<bits/stdc++.h>
using namespace std;
int n,m,dp[2010][2];
vector<int>g[2010];
void dfs(int u,int x){
dp[u][1]=1;//初始化
for(int i=0;i<g[u].size();i++){
int v=g[u][i];
if(v==x){//特判
continue;
}dfs(v,u);//自底向上
dp[u][0]+=dp[v][1];//可爱的转移,解释见上文
dp[u][1]+=min(dp[v][0],dp[v][1]);
}
}int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++){
int u,v,k;
scanf("%d%d",&u,&k);
for(int j=1;j<=k;j++){//这题建图有点奇怪……
scanf("%d",&v);
g[u].push_back(v);
g[v].push_back(u);
}
}dfs(0,-1);
printf("%d",min(dp[0][0],dp[0][1]));
return 0;
}
最大独立集
最小点覆盖是指从图中选出尽量多的点,使得这些点之间没有边相连。
设状态
设
转移
于是,我们可以推到(设
-
代表点 被选中了,那么子节点 不可能被选中,则有 。 -
代表点 没被选中,那么子节点 选不选均可,则有 。
解
设根为
例题
P1352 没有上司的舞会
众所周知的题目
题目描述
某大学有
他们之间有从属关系,也就是说他们的关系就像一棵以校长为根的树,父结点就是子结点的直接上司。
现在有个周年庆宴会,宴会每邀请来一个职员都会增加一定的快乐指数
所以,请你编程计算,邀请哪些职员可以使快乐指数最大,求最大的快乐指数。
输入格式
输入的第一行是一个整数
第
第
输出格式
输出一行一个整数代表最大的快乐指数。
提示
数据规模与约定
对于
又是一道模板题,直接套最大独立集模板就行了。
只是这里的选择顶点的价值不再是
CODE(又是可以直接当模板了):
#include<bits/stdc++.h>
using namespace std;
int n,u,v,mx,fa[20010],r[20010],dp[20010][10];
vector<int>g[20010];
void dfs(int u){
dp[u][1]=r[u];//初始化为快乐程度
for(int i=0;i<g[u].size();i++){
int v=g[u][i];
dfs(v);
dp[u][0]+=max(dp[v][0],dp[v][1]);//转移
dp[u][1]+=dp[v][0];
}
}int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%d",&r[i]);
}for(int i=1;i<n;i++){
scanf("%d%d",&u,&v);
fa[u]=1;//标记他有上司
g[v].push_back(u);
}for(int i=1;i<=n;i++){
if(fa[i]==0){//是根
dfs(i);
mx=max(dp[i][0],dp[i][1]);//取max
break;
}
}printf("%d",mx);
return 0;
}
咕咕咕
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构