浅说树形dp
文章目录
- 前言
- 树形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;
}
相关文章:
浅说树形dp
文章目录 前言树形dp的转移方式树形dp的使用的场景小结 初步感知——简单的树形dp例题1例题2 深入分析——树形dp的经典模型最大独立集最小点覆盖最小支配集树上直径 前言 因为树的形式非常适合递归,他所带来的访问顺序也是非常符合拓扑排序的,故而在处…...
DeepSeek R1本地部署 DeepSeek Api接口调用 DeepSeek RAG知识库工作流详解
DeepSeek R1本地部署 DeepSeek Api接口调用 DeepSeek RAG知识库工作流详解全集: 第一部分:Windows本地部署DeepSeek R1 第二部分:Macos本地部署DeepSeek R1 第三部分:Linux本地部署DeepSeek R1 第四部分:DeepSeek …...
使用Charles进行mock请求
在 Charles 中,“Compose”(构建请求)功能允许你手动创建和发送 HTTP 或 HTTPS 请求,方便进行 API 测试、调试等操作。以下是 Compose 功能的详细使用方式: 1. 打开 Compose 界面 启动 Charles 后,在菜单…...
linu软件编程——IO
函数接口 fputc: man 3 fputc 原型:int fputc(int c, FILE *stream); 功能: 向stream流中写入 字符c 参数:c:要写入的字符的ASCII码值 stream:文件流指针 返回值:成功返回写入字符的ASCII码值 失败返回-1 fputc(ch, stdout) put…...
RAMinit 程序编译运行考古记录
本科的时候浅浅研究了DOSBox,今天看到网上挺多关于雷军代码的新闻,我把雷军代码在web上编译出来了,但是我是业余选手,所以做得比较差,不过大家感兴趣可以关注我的dosplay项目,旨在利用js-dos实现汇编语言在…...
2.【线性代数】——矩阵消元
二 矩阵消元 1. 消元法2. 单行或者单列的矩阵乘法2.1 单行矩阵乘法2.2 单列矩阵乘法 3. 用矩阵记录消元过程(初等矩阵) 【行的线性组合(数乘和加法)】3.1 row2-3row1的矩阵描述3.2 row3-2row2的矩阵描述3.3 矩阵乘法的性质 4. 用矩…...
Vue 3 30天精进之旅:Day 25 - PWA支持
一、引言 在前面的24天中,我们已经深入探讨了Vue 3的许多核心概念和高级特性。今天,我们将进入一个全新的领域——PWA(Progressive Web App)。PWA是一种现代Web应用程序的开发模式,它结合了Web和原生应用的优点&#…...
【Pico】使用Pico进行无线串流搜索不到电脑
使用Pico进行无线串流搜索不到电脑 官串方式:使用Pico互联连接电脑。 故障排查 以下来自官方文档 请按照以下步骡排除故障: 确认电脑和一体机连接了相同的路由器WiFi网络(相同网段) IP地址通常为192.168.XX,若两设备的IP地址前三段相同&…...
Golang Model 字段自动化校验设计
背景 在我们日常开发中,不可避免的总要去进行各种参数校验,但是如果在某个场景中,要校验的字段非常多,并且在其中还有耦合关系,那么我们手写校验逻辑就变得非常的低效且难以维护。本篇文档就基于 DDD 领域模型设计的思…...
移动端测试的挑战与解决方案:兼容性、网络问题及实战策略
引言 移动应用已成为用户触达服务的核心入口,但移动端测试面临设备多样性、网络波动、用户场景复杂等多重挑战。据Statista统计,2023年全球活跃移动设备超180亿台,操作系统(Android/iOS)版本碎片化率超30%,这对测试工程师提出了极高要求。本文深度解析移动端测试的核心痛…...
AI agent 未来好的趋势:AI医疗影像、智能客服、个性化推荐
AI agent 未来好的趋势:AI医疗影像、智能客服、个性化推荐 目录 AI agent 未来好的趋势:AI医疗影像、智能客服、个性化推荐比特币AI Agents稳定币扩容区块链AI基础设施AI驱动的软件应用AI赋能的行业应用AI医疗影像、智能客服、个性化推荐AI药物研发比特币 市场与机构化:2024…...
如何使用Three.js制作3D月球与星空效果
目录 1. 基本设置2. 创建星空效果3. 创建月球模型4. 添加中文3D文字5. 光照与相机配置6. 动画与控制7. 响应式布局8. 结语 在本文中,我们将一起学习如何利用Three.js实现一个3D月球与星空的效果,并添加一些有趣的元素,比如中文3D文字和互动功…...
Spring安装和使用(Eclipse环境)
一、Spring框架概述 1、 什么是Spring Spring是一个开源框架,Spring是于2003 年兴起的一个轻量级的Java 开发框架,由Rod Johnson 在其著作Expert One-On-One J2EE Development and Design中阐述的部分理念和原型衍生而来。它是为了解决企业应用开发的复…...
网络安全-攻击流程-传输层
传输层攻击主要针对OSI模型的第四层,涉及TCP和UDP协议的安全漏洞。以下是常见攻击类型及其流程,以及防御措施: 1. SYN洪水攻击(TCP半连接攻击) 攻击流程: 目标选择:确定目标服务器的IP地址和开…...
图论- Dijkstra算法
Dijkstra算法 前言概念BFS基础模版DijkstraDijkstra函数签名State类distTo 记录最短路径伪代码模版第一个问题解答第二个问题解答第三个问题解答 前言 学习这个算法之间,必须要对BFS遍历比较熟悉,它的本质就是一个特殊改造过的BFS算法. 概念 Dijkstra算法是一种计算图中单源…...
CAS单点登录(第7版)9.属性
如有疑问,请看视频:CAS单点登录(第7版) 属性 属性定义 概述 属性定义 从身份验证或属性存储库源获取和解析 CAS 中属性的定义时,往往使用其名称进行定义和引用,而无需任何其他元数据或修饰。例如&#…...
一些常用的Yum源
一些常用的Yum源 # CentOS-Stream-AppStream.repo [appstream] nameCentOS Stream $releasever - AppStream baseurlhttps://mirrors4.tuna.tsinghua.edu.cn/centos-vault/8-stream/AppStream/x86_64/os/ gpgcheck0 enabled1# CentOS-Stream-BaseOS.repo [baseos] nameCentOS …...
【电路笔记】-双向计数器
双向计数器 文章目录 双向计数器1、概述2、双向计数器双向计数器能够通过任何给定的计数序列向上或向下方向计数。 1、概述 双向计数器是同步向上/向下二进制计数器,能够在两个方向上向或从某个预设值以及零进行计数。 除了从零开始“向上”计数并增加或递增到某个预设值之外…...
Python PyCharm DeepSeek接入
Python PyCharm DeepSeek接入 创建API key 首先进入DeepSeek官网,https://www.deepseek.com/ 点击左侧“API Keys”,创建API key,输出名称为“AI” 点击“创建",将API key保存,复制在其它地方。 在PyCharm中下…...
LeetCode 232: 用栈实现队列
LeetCode 232: 用栈实现队列 题目描述 使用栈实现队列的操作。支持以下操作: MyQueue():初始化队列。push(x):将元素 x 推入队列。pop():从队列中移除元素。peek():返回队列头部的元素。empty():检查队列…...
P6792 [SNOI2020] 区间和 Solution
Description 给定序列 a ( a 1 , a 2 , ⋯ , a n ) a(a_1,a_2,\cdots,a_n) a(a1,a2,⋯,an),有 m m m 个操作分两种: chmax ( l , r , v ) \operatorname{chmax}(l,r,v) chmax(l,r,v):对每个 i ∈ [ l , r ] i \in [l,r] i∈[l,…...
基于智能体和RWA的分布式商业生态商业模型架构设计
引言 在数字化和智能化的浪潮下,传统商业生态正经历着深刻的变革。如何通过技术的赋能推动商业模式的升级,成为各行各业的共识。**智能体(AI Agents)与现实世界资产(RWA)**的结合,为分布式商业生…...
从ARM官方获取自己想要的gcc交叉编译工具链接(Arm GNU Toolchain),并在Ubuntu系统中进行配置
前言 本文是博文 https://blog.csdn.net/wenhao_ir/article/details/145547974 的分支博文。 在本博文中我们完成gcc交叉编译工具gcc-arm-9.2-2019.12-x86_64-arm-none-linux-gnueabihf.tar.xz的下载、配置、测试。 下载自己想要的gcc交叉编译工具的源码 目标文件的名字及说…...
Linux上Elasticsearch 集群部署指南
Es 集群部署 Es 集群部署 Es 集群部署 准备好三台服务器。示例使用:110.0.5.141/142/143 1、es用户和用户组创建,使用root账号 groupadd esuseradd -g es es2、将es安装包和ik分词器上传到:/home/es/目录下(任意目录都行&#…...
【系统架构设计师】虚拟机体系结构风格
目录 1. 说明2. 解释器体系结构风格3. 规则系统体系结构风格4. 例题4.1 例题1 1. 说明 1.p263。2.虚拟机体系结构风格的基本思想是人为构建一个运行环境,在这个环境之上,可以解析与运行自定义的一些语言,这样来增加架构的灵活性。3.虚拟机体…...
Python 字典思维导图
在本章中,你将学习能够将相关信息关联起来的Python字典。你将学习如何访问和修改字典中的信息。鉴于字典可存储的信息量几乎不受限制,因此我们会演示如何遍 历字典中的数据。另外,你还将学习存储字典的列表、存储列表的字典和存储字典的字典。…...
前台、后台、守护进程对比,进程组的相关函数
前台进程,后台进程,守护进程的对比 在前面我们已经了解了前台进程,后台进程,守护进程。 直接在终端中输入命令: 这是最常见的启动前台进程的方式。例如,在终端中输入 ./myprogram 就可以启动 myprogram 程…...
openAI最新o1模型 推理能力上表现出色 准确性方面提升 API如何接入?
OpenAI o1模型在回答问题前会进行深入思考,并生成一条内部推理链,使其在尝试解决问题时可以识别并纠正错误,将复杂的步骤分解为更简单的部分,并在当前方法无效时尝试不同的途径。据悉,o1不仅数学水平与美国奥林匹克竞赛…...
跨平台键鼠共享免费方案--Deskflow!流畅体验用MacBook高效控制Windows设备
在混合办公场景中,多设备协同已成为提升效率的关键需求。对于同时使用Mac与Windows设备的用户,如何通过一套键盘和触控板实现无缝切换,避免桌面空间浪费与操作冗余?本文将基于开源工具Deskflow,提供一套专业级解决方案…...
CAS单点登录(第7版)27.开发人员
如有疑问,请看视频:CAS单点登录(第7版) 开发人员 Javadocs文档 group org.apereo.cas has published 42 artifact(s) with total 8210 version(s) org.apereo.cas org apereo.cas 小组已出版 42 件作品,共 8210 个版…...
