文章目录
- 前言
- 树形dp的转移方式
- 树形dp的使用的场景
- 小结
- 初步感知——简单的树形dp
- 例题1
- 例题2
- 深入分析——树形dp的经典模型
- 最大独立集
- 最小点覆盖
- 最小支配集
- 树上直径
前言
因为树的形式非常适合递归,他所带来的访问顺序也是非常符合拓扑排序的,故而在处理子树类问题时,dp可以很好的利用相邻层级之间的关系和逻辑,非常符合dp的“口味”,所以我们才有了这个树形dp。
树形dp和线性dp没有什么本质上的区别,只不过一个是在树上,一个是在线上,唯一的一个不同点就是树形dp可以大致的定形,而线性dp却不可以。
树形dp的转移方式
一般情况下的树形dp只会有两种方式,要么从上到下(父亲到儿子),要么从下到上(儿子到父亲),一般情况下从下到上的可能性更多,因为儿子更多,可选性也更大,自然答案也更多,出题人也更愿意考。那么我们在判断出来一道题是树形dp的时候,我们就要主要去关注父亲和儿子之间的关系了,也就是说要关注相邻层级的关系,这也就是我所说的可以大致定形。但是这些都建立在一个前提之下——我们知道这个题要用树形dp。
树形dp的使用的场景
我们一般在涉及树形结构的最优解的时候,会使用树形dp。我在这里大致总结几个,但是不一定是全部的,还是要自己总结才行。
-
涉及树形的最优策略的情况(最大值,最小值,最优方案等),并且答案可以从已知的子树中转移或合并得到,那么这个就非常适合树形dp来做。下面是一些例子。
- 树的直径:求最长路径,可以通过子树的最长路径来计算。
- 最大独立集:选出最多不相邻的节点,当前点选或不选的情况依赖于子树的选择情况。
-
涉及子树之间要传递信息或相互转移的情况,并且对于一个节点而言的最优解会依赖于其子树的计算结果的时候,可以考虑使用树形dp,同时这里也可以简单的定个型:这里一般都是采用后序DP,也就是说从下而上的计算。下面是一些例子。
- 树上背包问题:使得总价值最大但是会受到某些条件的限制,类似于01背包。
- 树上最小支配集:要求覆盖整棵树的最小节点集,他的状态也依赖于子树的选或不选。
-
涉及从根或父亲节点向子节点进行转移的时候,同时答案也是在子节点的上面的时候,也可以考虑树形dp,并且一般情况下,这个都是采用先序DP,也就说从上而下的计算。下面是一些例子。
- 重复计算树的每个值(比如说子树):当以不同点为根的时候,如何快速记录出最短路径或子树和。
- 换根dp(Re-rooting DP):当以不同点为根节点的时候,所要求的值,比如说高度和。
-
当需要避免重复计算来提高效率的时候可以采用树形dp的方式来优化,而优化方式也就是记忆化或递推,这两种非常经典的方法。下面是一些例子。
- 树上路径问题:求树上一个点到其他所有点的最短路径的时候,我们也可以考虑树形dp来转移。
- 二叉树的最优构造方案:当只给你一棵树的中序遍历和一些限制条件的时候,要求你求出符合条件的树有多少种,我们也可以考虑树形dp来解决。
小结
我们可以在拿到树的问题的时候,问自己以下几个问题,以便来让我们判断改用什么方法来做。
- 树上的某个值能不能通过子树的值来计算? → \to → 树形dp
- 问题的最优解是否需要子树的答案来合并? → \to → 树形dp(后序)
- 是否可以通过递归的方式,把大问题分解成小问题? → \to → 树形dp(先序)
- 有没有大量的计算? → \to → 树形dp(优化)
初步感知——简单的树形dp
例题1
洛谷——最大子树和
这道题是要求一个为根所组成的树的最大值,这个问题的最优解需要子树的答案来合并,所以我们采用树形dp。
顺着他的题意,我们不妨设
d
p
[
i
]
dp[i]
dp[i] 表示以
i
i
i 为根节点的树的最大值,因为这个点的答案依赖于他儿子的答案,所以我们很容易得到这个状态转移方程:
d
p
[
i
]
=
∑
j
∈
s
o
n
[
i
]
j
≠
f
a
i
max
(
d
p
[
j
]
,
0
)
dp[i]=\sum_{j \in son[i]}^{j \neq fa_i}\max(dp[j],0)
dp[i]=j∈son[i]∑j=faimax(dp[j],0)
但是这样的方式我们只求了当前点关于他的孩子所得到的答案,他自己还没有算,所以我们还要调整一下。
d
p
[
i
]
=
max
(
d
p
[
i
]
+
n
u
m
[
i
]
,
0
)
dp[i]=\max(dp[i]+num[i],0)
dp[i]=max(dp[i]+num[i],0)
通过上下两个状态转移方程,我们就可以很轻松的得到答案。
#include<bits/stdc++.h>
using namespace std;
const int INF=1e5+10;
int num[INF],dp[INF],ans=INT_MIN;
vector<int> mp[INF];
void dfs(int x,int fa){
int len=mp[x].size();
for (int i=0;i<len;i++){
if (mp[x][i]==fa)continue;
int t=mp[x][i];
dfs(t,x);
dp[x]+=max(dp[t],0);
}
dp[x]=max(0,dp[x]+num[x]);
ans=max(dp[x],ans);
}
int main(){
int n;
cin>>n;
for (int i=1;i<=n;i++){
cin>>num[i];
ans=max(ans,num[i]);
}
if (ans<0){
cout<<ans;
return 0;
}
for (int i=1;i<n;i++){
int u,v;
cin>>u>>v;
mp[u].push_back(v);
mp[v].push_back(u);
}
dfs(1,-1);
cout<<ans;
return 0;
}
例题2
洛谷——时态同步
这个问题的最优解需要子树的答案来合并,所以我们采用树形dp。
我们不妨设
d
p
[
x
]
dp[x]
dp[x] 表示x的孩子到达x的最大时间。那么我们就要关注一下父亲和孩子之间的关系,可以很轻松的得到以下这个状态转移方程。
d
p
[
i
]
=
max
d
p
[
j
]
+
m
p
[
i
]
[
j
]
.
w
(
j
∈
m
p
[
i
]
)
dp[i]=\max{dp[j]+mp[i][j].w}~~(j\in mp[i])
dp[i]=maxdp[j]+mp[i][j].w (j∈mp[i])
由此我们只需要在求得答案之后对每个儿子节点做差然后求和即可。
#include<bits/stdc++.h>
using namespace std;
const int INF=5e5+10;
struct Node{
long long point,num;
};
vector<Node> mp[INF];
long long ans,dp[INF];
void dfs(int x,int fa){
int len=mp[x].size();
for (int i=0;i<len;i++){
if (mp[x][i].point==fa)continue;
int t=mp[x][i].point;
dfs(t,x);
dp[x]=max(dp[x],dp[t]+mp[x][i].num);
}
for (int i=0;i<len;i++){
if (mp[x][i].point==fa)continue;
int t=mp[x][i].point;
ans+=dp[x]-dp[t]-mp[x][i].num;
}
}
int main(){
int n,st;
cin>>n>>st;
for (int i=1;i<n;i++){
int a,b,t;
cin>>a>>b>>t;
mp[a].push_back({b,t});
mp[b].push_back({a,t});
}
dfs(st,-1);
cout<<ans;
return 0;
}
深入分析——树形dp的经典模型
最大独立集
什么是最大独立集?
顾名思义,就是所选出来的点,两两之间没有直接联系,也就说没有直接的上下层级关系。我们就要在满足这个条件下找到可行的最大的方案。
而没有上司的舞会就是一道典型的此类题目,所以我们就以这道题来讲:
洛谷——没有上司的舞会
因为相邻两点不能同时存在,所以说应该我们只需要关注一下父亲和儿子之间的关系即可。
我们可以分成两个方面来思考,如果当前点选会怎样,不选又会怎样。因此我们就可以设
d
p
[
x
]
[
0
]
dp[x][0]
dp[x][0] 表示当前点不选,
d
p
[
x
]
[
1
]
dp[x][1]
dp[x][1] 表示当前点要选。
如果当前点要选的话,他的孩子肯定都不选,,但是不要忘了还有自身的值,所以有:
d
p
[
x
]
[
1
]
=
∑
j
∈
s
o
n
[
x
]
j
≠
f
a
d
p
[
j
]
[
0
]
dp[x][1]=\sum_{j\in son[x]}^{j\neq fa}dp[j][0]
dp[x][1]=j∈son[x]∑j=fadp[j][0]
如果当前点不选的话,他的孩子选或不选都可以,所以取个最大值就可以了。
d
p
[
x
]
[
0
]
=
∑
j
∈
s
o
n
[
x
]
j
≠
f
a
max
(
d
p
[
j
]
[
0
]
,
d
p
[
j
]
[
1
]
)
dp[x][0]=\sum_{j\in son[x]}^{j\neq fa}\max (dp[j][0],dp[j][1])
dp[x][0]=j∈son[x]∑j=famax(dp[j][0],dp[j][1])
此时答案就一定是
max
(
d
p
[
1
]
[
1
]
,
d
p
[
1
]
[
0
]
)
\max (dp[1][1],dp[1][0])
max(dp[1][1],dp[1][0])。
#include<bits/stdc++.h>
using namespace std;
const int INF=1e4+10;
int a[INF],p[INF],root;
int dp[INF][2];//0为不选,1为选
vector<int> mp[INF];
void dfs(int x){
int len=mp[x].size();
for (int i=0;i<len;i++){
int t=mp[x][i];
dfs(t);
dp[x][0]+=max(dp[t][0],dp[t][1]);
dp[x][1]+=dp[t][0];
}
dp[x][1]+=a[x];
}
int main(){
int n;
cin>>n;
for (int i=1;i<=n;i++){
cin>>a[i];
}
for (int i=1;i<n;i++){
int u,v;
cin>>u>>v;
mp[v].push_back(u);
p[u]++;
}
for (int i=1;i<=n;i++){
if (p[i]==0){
root=i;
break;
}
}
dfs(root);
cout<<max(dp[root][0],dp[root][1]);
return 0;
}
变式练习
一本通——周年纪念晚会
这道题和没有上司的舞会基本上是一模一样,所以说就不讲了,自己摸索摸索吧。关键点上面都提到了,所以就放个代码吧。
#include<bits/stdc++.h>
using namespace std;
const int INF=1e4+10;
int a[INF],p[INF],root;
int dp[INF][2];//0为不选,1为选
vector<int> mp[INF];
void dfs(int x){
int len=mp[x].size();
for (int i=0;i<len;i++){
int t=mp[x][i];
dfs(t);
dp[x][0]+=max(dp[t][0],dp[t][1]);
dp[x][1]+=dp[t][0];
}
dp[x][1]+=a[x];
}
int main(){
int n;
cin>>n;
for (int i=1;i<=n;i++){
cin>>a[i];
}
for (int i=1;i<=n;i++){
int u,v;
cin>>u>>v;
mp[v].push_back(u);
p[u]++;
}
for (int i=1;i<=n;i++){
if (p[i]==0){
root=i;
break;
}
}
dfs(root);
cout<<max(dp[root][0],dp[root][1]);
return 0;
}
内心OS:两份代码好像一模一样
最小点覆盖
什么是最小点覆盖?
最小点覆盖指在满足每条边的两个端点至少要有一个点被选中这个条件下,使选择的点最少,说的专业一点就是在一棵树中选择最小数量的节点使这些节点所覆盖的遍集包含了树中所有的边。
而战略游戏就是一道典型的此类题目,所以我们还是以这个题来讲。
洛谷——战略游戏
首先我们考虑一下,对于一条边,可能会由父亲来看管,也有可能被儿子所看管,所以我们就可以设
d
p
[
x
]
[
0
]
dp[x][0]
dp[x][0] 表示当前边他自己来看守,
d
p
[
x
]
[
1
]
dp[x][1]
dp[x][1] 表示当前边他不看守,也就是说让他的儿子来看收。
对于
d
p
[
x
]
[
0
]
dp[x][0]
dp[x][0] 这种情况,他的儿子的情况是可以随便的,所以就有
d
p
[
x
]
[
0
]
=
∑
j
∈
s
o
n
[
x
]
j
≠
f
a
min
(
d
p
[
j
]
[
0
]
,
d
p
[
j
]
[
1
]
)
+
1
dp[x][0]=\sum_{j\in son[x]}^{j \ne fa}\min(dp[j][0],dp[j][1])+1
dp[x][0]=j∈son[x]∑j=famin(dp[j][0],dp[j][1])+1
对于
d
p
[
x
]
[
1
]
dp[x][1]
dp[x][1] 这种情况,这条边一定是由他的儿子来看管,所以就是说一定是他儿看管的和,故而有。
d
p
[
x
]
[
1
]
=
∑
j
∈
s
o
n
[
x
]
j
≠
f
a
d
p
[
j
]
[
0
]
dp[x][1]=\sum_{j\in son[x]}^{j \ne fa}dp[j][0]
dp[x][1]=j∈son[x]∑j=fadp[j][0]
最后的答案就是在
min
(
d
p
[
r
o
o
t
]
[
0
]
,
d
p
[
r
o
o
t
]
[
1
]
)
\min (dp[root][0],dp[root][1])
min(dp[root][0],dp[root][1])
#include<bits/stdc++.h>
using namespace std;
const int INF=1e5+10;
int p[INF],dp[INF][2];
vector<int> mp[INF];
void dfs(int x,int fa){
dp[x][1]=1;
int len=mp[x].size();
for (int i=0;i<len;i++){
if (mp[x][i]==fa)continue;
int t=mp[x][i];
dfs(t,x);
dp[x][0]+=dp[t][1];
dp[x][1]+=min(dp[t][0],dp[t][1]);
}
}
int main(){
int n;
cin>>n;
for (int i=1;i<=n;i++){
int k,u;
cin>>u>>k;
for (int j=1;j<=k;j++){
int t;
cin>>t;
p[t]++;
mp[u].push_back(t);
mp[t].push_back(u);
}
}
for (int i=0;i<n;i++){
if (p[i]==0){
dfs(i,-1);
cout<<min(dp[i][0],dp[i][1]);
return 0;
}
}
return 0;
}
变式练习
小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为 root 。
除了 root 之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵树”。 如果 两个直接相连的房子在同一天晚上被打劫 ,房屋将自动报警。
给定树的 root 。返回 在不触动警报的情况下 ,小偷能够盗取的最高金额。
这道题类似与最小覆盖点,但其实严格来说的话应该是最大覆盖点,因为我是要求小偷能盗取到的中最高金额。也就是说当我从儿子转移到父亲的时候,我们应该取得的是max而不是上面的min
#include<bits/stdc++.h>
using namespace std;
const int INF=1e5+10;
int w[INF],dp[INF][2];//自己偷,自己不被偷
vector<int> mp[INF];
void dfs(int x,int fa){
dp[x][0]=w[x];
int len=mp[x].size();
for (int i=0;i<len;i++){
if (mp[x][i]==fa)continue;
int t=mp[x][i];
dfs(t,x);
dp[x][0]+=dp[t][1];
dp[x][1]+=max(dp[t][1],dp[t][0]);//这里和最小覆盖点不同
}
}
int main(){
int n,root;
cin>>n>>root;
for (int i=1;i<=n;i++){
cin>>w[i];
}
for (int i=1;i<n;i++){
int u,v;
cin>>u>>v;
mp[u].push_back(v);
mp[v].push_back(u);
}
dfs(root,0);
cout<<max(dp[root][0],dp[root][1]);
return 0;
}
最小支配集
什么是最小支配集?
这个有点不好说,可以通俗的理解为在公司中,员工之间的关系成一颗树的样子,而其中每个人要么自己就是领导,要么就是和领导有直接的联系,求最少要多少个领导。
Cell Phone Network G这道题就是最小支配集的问题,所以说我们一这道题来讲。
洛谷——Cell Phone Network G
还是像我上面讲的一样,要关注父亲和儿子之间的关系,那么这个地方我们怎么想?是考虑两种方式吗?肯定不是,对于一个点而言会有三种情况:自己有塔(
d
p
[
x
]
[
0
]
dp[x][0]
dp[x][0]),自己没塔但是只被儿子覆盖,(
d
p
[
x
]
[
1
]
dp[x][1]
dp[x][1]),自己没塔但是只被父亲覆盖(
d
p
[
x
]
[
2
]
dp[x][2]
dp[x][2]),基于此,我们是不是也可以设三个方程来分别表示?
如果自己有塔的话,他的儿子有塔和无塔是不是都可以,所以就是三种情况都可以,故而取个最小值就可以。
d
p
[
x
]
[
0
]
=
∑
j
∈
s
o
n
[
x
]
j
≠
f
a
min
(
d
p
[
j
]
[
0
]
,
d
p
[
j
]
[
1
]
,
d
p
[
j
]
[
2
]
)
dp[x][0]=\sum_{j\in son[x]}^{j \ne fa} \min(dp[j][0],dp[j][1],dp[j][2])
dp[x][0]=j∈son[x]∑j=famin(dp[j][0],dp[j][1],dp[j][2])
如果自己没塔,但是被儿子覆盖了,那么我们是不是就要从所有的儿子中选一个最小的来作为放置信号塔的位置?然后其他的儿子在第一种和第二种中选一个最小的来求和即可。(令v为当前选的儿子)
d
p
[
x
]
[
1
]
=
min
(
d
p
[
v
]
[
0
]
+
∑
j
∈
s
o
n
[
x
]
j
≠
f
a
min
(
d
p
[
j
]
[
0
]
,
d
p
[
j
]
[
1
]
)
−
min
(
d
p
[
v
]
[
0
]
,
d
p
[
v
]
[
1
]
)
)
dp[x][1]=\min(dp[v][0]+\sum_{j\in son[x]}^{j \ne fa}\min(dp[j][0],dp[j][1])-\min(dp[v][0],dp[v][1]))
dp[x][1]=min(dp[v][0]+j∈son[x]∑j=famin(dp[j][0],dp[j][1])−min(dp[v][0],dp[v][1]))
如果自己没塔,但是被父亲覆盖了,也就是说儿子一定是属于没有塔并且被儿子覆盖的一类,所以说我们只需要对其求和即可。
d
p
[
x
]
[
2
]
=
∑
j
∈
s
o
n
[
x
]
j
≠
f
a
d
p
[
j
]
[
1
]
dp[x][2]=\sum_{j\in son[x]}^{j \ne fa}dp[j][1]
dp[x][2]=j∈son[x]∑j=fadp[j][1]
因为根节点没有父亲节点,所以答案就是在
min
(
d
p
[
r
o
o
t
]
[
0
]
,
d
p
[
r
o
o
t
]
[
1
]
)
\min (dp[root][0],dp[root][1])
min(dp[root][0],dp[root][1])
#include<bits/stdc++.h>
using namespace std;
const int INF=5e5+10;
int dp[INF][3];
vector<int> mp[INF];
void dfs(int x,int fa){
dp[x][0]=1,dp[x][1]=1e8;
int tot=0;
int len=mp[x].size();
for (int i=0;i<len;i++){
if (mp[x][i]==fa)continue;
int t=mp[x][i];
dfs(t,x);
dp[x][0]+=min(dp[t][0],min(dp[t][1],dp[t][2]));
tot+=min(dp[t][0],dp[t][1]);
dp[x][2]+=dp[t][1];
}
if (len==1&&x!=1)return;
for (int i=0;i<len;i++){
if (mp[x][i]==fa)continue;
int t=mp[x][i];
dp[x][1]=min(dp[x][1],dp[t][0]+tot-min(dp[t][0],dp[t][1]));
}
}
int main(){
int n;
cin>>n;
for (int i=1;i<n;i++){
int u,v;
cin>>u>>v;
mp[u].push_back(v);
mp[v].push_back(u);
}
dfs(1,-1);
cout<<min(dp[1][0],dp[1][1]);
return 0;
}
变式练习
一本通——皇宫看守
这道题其实就是非常裸的最小点覆盖,我们只需要分清什么时候由父亲到儿子,什么时候由儿子到父亲就可以了。
#include<bits/stdc++.h>
using namespace std;
const int INF=1e5+10;
int p[INF],dp[INF][3],w[INF];
vector<int> mp[INF];
void dfs(int x,int fa){
int minch=1e8;
dp[x][1]=w[x];
int len=mp[x].size();
for (int i=0;i<len;i++){
if (mp[x][i]==fa)continue;
int t=mp[x][i];
dfs(t,x);
dp[x][0]+=min(dp[t][0],dp[t][1]);
minch=min(minch,dp[t][1]-min(dp[t][0],dp[t][1]));
dp[x][1]+=min(dp[t][0],min(dp[t][1],dp[t][2]));
dp[x][2]+=min(dp[t][0],dp[t][1]);
}
dp[x][0]+=minch;
}
int main(){
int n;
cin>>n;
for (int i=1;i<=n;i++){
int k,u;
cin>>u>>w[u]>>k;
for (int j=1;j<=k;j++){
int t;
cin>>t;
p[t]++;
mp[u].push_back(t);
mp[t].push_back(u);
}
}
for (int i=1;i<=n;i++){
if (p[i]==0){
dfs(i,-1);
cout<<min(dp[i][0],dp[i][1]);
return 0;
}
}
return 0;
}
树上直径
我们之前讲过用dfs求解直径,具体的见这里,但是用dfs来求的话,不能处理有负权的情况,所以说现在我们来说说怎么用树形dp来解决。
首先对于每个点而言,都有可能是在直径上的,那么也就是说每个点而言找到两条经过这个点并且没有交集的线段求和即可。
然而对于一个点我们会有三种情况,分别是向上的路(记作
u
1
u_1
u1),向下的最长路(记作
d
1
d_1
d1)和向下的次长路(记作
d
2
d_2
d2)。而直径就是这三条路径中最长两条。
但是我们仔细思考一下真的需要三个点吗?现在的答案无非就是
max
(
u
1
+
d
1
,
d
1
+
d
2
)
\max (u_1+d_1,d_1+d_2)
max(u1+d1,d1+d2) ,但是其中的
u
1
+
d
1
u_1+d_1
u1+d1 这种情况是多余的,如果当前点要选
u
1
u_1
u1 的话,在上面一定会有一个点的
d
1
d_1
d1 包含这种情况,就像如图所示:
所以说,我们完全可以把
u
1
u_1
u1 这种情况给去掉,只需要维护向下的最大值和次大值就可以了。所以说现在的问题就是怎么维护。
最大值还是很好维护的,如果
d
1
[
j
]
+
w
d1[j]+w
d1[j]+w 大于
d
1
[
x
]
d1[x]
d1[x] 就说明当前的最大值不是真正的最大值,更新就可以了。
d
1
[
x
]
=
max
(
d
1
[
j
]
+
w
x
→
j
)
(
j
∈
s
o
n
[
x
]
)
d1[x]=\max(d1[j]+w_{x\to j})(j \in son[x])
d1[x]=max(d1[j]+wx→j)(j∈son[x])
关键就是次大值怎么维护,其实现在有两种情况,第一种是在最大值被更新的时候,原本的最大值就是当前的次大值,第二种就是最大值没有被更新但是比当前次大值要大的时候,此时的
d
1
[
j
]
+
w
d1[j]+w
d1[j]+w 就是新的次大值。
d
2
[
x
]
=
d
1
[
x
]
o
l
d
(
d
1
[
j
]
+
w
x
→
j
>
d
1
[
x
]
o
l
d
&
j
∈
s
o
n
[
x
]
)
d2[x]=d1[x]_{old}(d1[j]+w_{x\to j}>d1[x]_{old} ~~\&~~j\in son[x])
d2[x]=d1[x]old(d1[j]+wx→j>d1[x]old & j∈son[x])
d
2
[
x
]
=
d
1
[
j
]
+
w
x
→
j
(
d
1
[
j
]
+
w
x
→
j
≤
d
1
[
x
]
&
j
∈
s
o
n
[
x
]
)
d2[x]=d1[j]+w_{x\to j}(d1[j]+w_{x\to j}\le d1[x]~~\&~~j\in son[x])
d2[x]=d1[j]+wx→j(d1[j]+wx→j≤d1[x] & j∈son[x])
那么答案就是在所有点中取
d
1
[
x
]
+
d
2
[
x
]
d1[x]+d2[x]
d1[x]+d2[x] 最大的点。
#include<bits/stdc++.h>
using namespace std;
const int INF=2e5+10;
struct Node{
int p,num;
};
int d1[INF],d2[INF];
vector<Node> mp[INF];
int maxn=INT_MIN;
void getdw(int x,int fa){
d1[x]=0,d2[x]=0;//最大值,次大值
int len=mp[x].size();
for (int i=0;i<len;i++){
if (mp[x][i].p==fa)continue;
int t=mp[x][i].p,w=mp[x][i].num;
getdw(t,x);
if (d1[t]+w>d1[x]){
d2[x]=d1[x];
d1[x]=d1[t]+w;
}else if (d1[t]+w>d2[x]){
d2[x]=d1[t]+w;
}
}
maxn=max(maxn,d1[x]+d2[x]);
}
int main(){
int n;
cin>>n;
for (int i=1;i<n;i++){
int u,v,w;
cin>>u>>v>>w;
mp[u].push_back({v,w});
mp[v].push_back({u,w});
}
getdw(1,-1);
cout<<maxn;
return 0;
}
变式练习
一本通——旅游规划
这道题我们可以换位思考一下,因为我们不可能把所有的直径都标记出来,所以说我们只能判定一个点在不在直径上,那么这里就要像上面所说的一样,对于一个点要记录三个参数,而不能只记录两个参数,然后在三个参数中选择最大的两个求和,看是否等于最大值就可以了。那么现在的问题就是如何维护
u
p
[
x
]
up[x]
up[x]。
u
p
[
x
]
up[x]
up[x] 难道只是简单的
u
p
[
x
]
=
u
p
[
f
a
]
+
1
up[x]=up[fa]+1
up[x]=up[fa]+1 吗?显然不是,我们是不是可以在
f
a
fa
fa 这个点稍微拐个弯,拐到另一个岔路去?这样的答案就是另外的一条路。
#include<bits/stdc++.h>
using namespace std;
const int INF=2e5+10;
int d1[INF],d2[INF],up[INF],point[INF];//point记录一个点的向下最长链所经过的点
vector<int> mp[INF];
int maxn=INT_MIN;
void getdw(int x,int fa){
d1[x]=0,d2[x]=0;//最大值,次大值
int len=mp[x].size();
for (int i=0;i<len;i++){
if (mp[x][i]==fa)continue;
int t=mp[x][i];
getdw(t,x);
if (d1[t]+1>d1[x]){
d2[x]=d1[x];
d1[x]=d1[t]+1;
point[x]=t;
}else if (d1[t]+1>d2[x]){
d2[x]=d1[t]+1;
}
}
maxn=max(maxn,d1[x]+d2[x]);
}
void getup(int x,int fa){
int len=mp[x].size();
for (int i=0;i<len;i++){
if (mp[x][i]==fa)continue;
int t=mp[x][i];
if (point[x]==t){
up[t]=max(up[x]+1,d2[x]+1);
}else {
up[t]=max(up[x]+1,d1[x]+1);
}
getup(t,x);
}
}
int main(){
int n;
cin>>n;
for (int i=1;i<n;i++){
int u,v;
cin>>u>>v;
mp[u].push_back(v);
mp[v].push_back(u);
}
getdw(0,-1);
getup(0,-1);
int cnt=0;
for (int i=0;i<n;i++){
int tot=d1[i]+d2[i]+up[i]-min({d1[i],d2[i],up[i]});
if (tot==maxn)cout<<i<<endl;
}
return 0;
}