浅说树形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的经典模型最大独立集最小点覆盖最小支配集树上直径 前言 因为树的形式非常适合递归,他所带来的访问顺序也是非常符合拓扑排序的,故而在处…...
Matlab 多项式曲线拟合(三维)
文章目录 一、简介二、实现代码三、实现效果参考资料一、简介 对于高维空间曲线的拟合,参数化是一种非常好的方式,可以让我们很容易得到我们想要的目标曲线。 假设给定一组数据点 ( u i , x i ) 、 ( u i ...
大语言模型推理中的显存优化 有哪些
大语言模型推理中的显存优化 有哪些 目录 大语言模型推理中的显存优化 有哪些显存优化背景Offloading/Checkpoint原理举例显存优化背景 在大语言模型推理时,显存是显著瓶颈。以开源的BLOOM 176B模型为例,在8张A100计算卡上,通常对话设置下仅能进行批量为10左右的推理。为缓…...
机器学习:k均值
所有代码和文档均在golitter/Decoding-ML-Top10: 使用 Python 优雅地实现机器学习十大经典算法。 (github.com),欢迎查看。 在“无监督学习”中,训练样本的标记信息是未知的,目标是通过对无标记训练样本的学习来揭示数据的内在性质及规律&…...
【图像加密解密】空间混沌序列的图像加密解密算法复现(含相关性检验)【Matlab完整源码 2期】
1、说明 本文给出详细完整代码、完整的实验报告和PPT。 环境:MATLAB2019a 复现文献:[1]孙福艳,吕宗旺.Digital image encryption with chaotic map lattices[J].Chinese Physics B,2011,20(04):136-142. 2、部分报告内容 3 部分源码与运行步骤 3.1 部…...
Unity学习part3
此为b站视频【【Unity教程】零基础带你从小白到超神】 https://www.bilibili.com/video/BV1gQ4y1e7SS/?p55&share_sourcecopy_web&vd_source6e7a3cbb802eb986578ad26fae1eeaab的笔记 1、反向动力学 打开ik处理 public class PlayerMoveController : MonoBehaviour {…...
【2025最新版】软件测试面试题总结(150道题含答案解析)
接口测试面试题 1:你平常做接口测试的过程中发现过哪些 bug? 2:平常你是怎么测试接口的? 3:平常用什么工具测接口? 4: webService 接口是如何测试的? 5:没有接口文档,如何做接口测试? 6&…...
双轴伺服电机驱动控制器AGV、AMR专用双伺服电机驱动控制器解决方案
工业机器人数控机床XY机械手双轴机器人堆垛机专用双轴伺服电机驱动控制器48V 14ARMS带有STO功能,隔离高压CAN/RS485/USB通讯支持编码器和霍尔输入 双伺服电机驱动控制器TMCM2611功能介绍 集成2个伺服电机的控制和驱动于一体供电电压48V,驱动电流14A RM…...
知识图谱数据库 Neo4j in Docker笔记
下载 docker pull neo4j:community官方说明 https://neo4j.com/docs/operations-manual/2025.01/docker/introduction/ 启动 docker run \--restart always \--publish7474:7474 --publish7687:7687 \--env NEO4J_AUTHneo4j/your_password \--volumeD:\files\knowledgegrap…...
Kubernetes实战教程:基于Vue前端与Java后端的应用部署
在云原生时代,Kubernetes 已成为管理容器化应用的核心平台。本文不仅详细介绍了 Kubernetes 的背景、架构和核心特性,还将通过一个具体的案例——基于 Vue 前端和 Java 后端的应用部署,带你一步步了解如何在 Kubernetes 集群中构建和运行一个…...
完全数和质数算法详解
完全数是指一个正整数,它等于其所有真约数(即除了自身以外的所有正因数)之和。例如,6 是一个完全数,因为它的真约数是 1、2 和 3,且 1 2 3 6。 1 计算约数和 1.1 遍历 遍历其所有可能的约数并计算它们…...
PHP本地商家卡券管理系统
本地商家卡券管理系统 —— 引领智慧消费新时代 本地商家卡券管理系统,是基于ThinkPHPUni-appuView尖端技术匠心打造的一款微信小程序,它彻底颠覆了传统优惠方式,开创了多商家联合发行优惠卡、折扣券的全新模式,发卡类型灵活多变…...
使用动态规划解决 0/1 背包问题
1. 背景 背包问题是计算机科学和优化领域中的经典问题之一,它被广泛应用于资源分配、任务调度等问题。在最简单的形式下,0/1背包问题描述的是: 你有一个背包,能够容纳一定的重量,而你有若干个物品,每个物品都有一个重量和价值,问你应该如何选择物品,使得在不超过背包…...
探索Java中的集合类_特性与使用场景
1. 引言 1.1 Java集合框架概述 Java集合框架(Java Collections Framework, JCF)是Java中用于存储和操作一组对象的类和接口的统称。它提供了多种数据结构来满足不同的需求,如列表、集合、映射等。JCF的核心接口包括Collection、List、Set、Queue和Map,以及它们的各种实现…...
动态DNS神器nip.io使用指南:快速实现域名与IP的动态映射--告别配置本地hosts
动态DNS神器nip.io使用指南:快速实现域名与IP的动态映射--告别配置本地hosts 一、项目简介二、快速入门三、进阶配置四、典型应用场景 本文基于开源项目 v1.2.1版本撰写,适用于开发测试、CI/CD等场景 一、项目简介 nip.io 是由Exentrique Solutions开发…...
Obsidian及Zotero常用的插件
Obsidian插件 Minimal Theme Settings(Life,zotero)【必需】 界面样式设置所需插件 Style Settings(Life,zotero)【必需】界面样式设置所需插件 Recent Files(Life,zotero…...
自学Java-面向对象高级(final、单例类、枚举类、抽象类、接口)
自学Java-面向对象高级(final、单例类、枚举类、抽象类、接口) 一、final关键字1、认识final关键字2、final修饰变量的注意3、常量 二、单例类(设计模式)1、设计模式的概念2、单例设计模式3、单例类有很多形式4、懒汉式单例类5、小…...
数据结构与算法之排序算法-归并排序
排序算法是数据结构与算法中最基本的算法之一,其作用就是将一些可以比较大小的数据进行有规律的排序,而想要实现这种排序就拥有很多种方法~ 那么我将通过几篇文章,将排序算法中各种算法细化的,详尽的为大家呈现出来: …...
Springboot整合ES
添加依赖 在 pom.xml 中添加以下依赖: <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-elasticsearch</artifactId> </dependency>配置 Elasticsearch 在 application.proper…...
文件夹上传到github分支最后github上面还是没有文件和文件夹
环境: github 问题描述: 文件夹上传到github分支最后github上面还是没有文件和文件夹, 和这样一样 解决方案: 从 git ls-tree -r HEAD 的输出中可以看到,metahuman-stream 文件夹显示为如下内容: 160000 commi…...
生成式聊天机器人 -- 基于Transformer实现的SeqToSeq模型 -- 上
生成式聊天机器人 -- 基于Transformer实现的SeqToSeq模型 -- 上 引言数据预处理下载并处理数据数据加载 Transformer模型嵌入层&位置编码层多头注意力机制EncoderLayerDecoderLayerPoint-wise Feed Forward NetworkTransformer 引言 在此之前,我们已经了解了如…...
【Java 面试 八股文】Spring Cloud 篇
Spring Cloud 篇 1. Spring Cloud 5大组件有哪些?2. 服务注册和发现是什么意思?Spring Cloud 如何实现服务注册发现?3. 我看你之前也用过nacos,你能说下nacos与eureka的区别?4. 你们项目负载均衡如何实现的?…...
CAS单点登录(第7版)10.多因素身份验证
如有疑问,请看视频:CAS单点登录(第7版) 多因素身份验证 概述 多因素身份验证 (MFA) 多因素身份验证(Multifactor Authentication MFA)是一种安全机制,要求用户提供两种…...
【16】思科AireOS:创建使用 LWA 认证的 WLAN
1. 概述 LWA(Local Web Authentication)是一种基于 Web 认证的方式,允许无线客户端在连接 WLAN 后,使用 Web 认证页面进行身份验证。该方法适用于访客网络或需要身份认证的场景。 本指南详细介绍如何在 Cisco AireOS 无线控制器(WLC)上配置 LWA 认证的 WLAN,并确保认证…...
webassembly009 transformers.js 网页端侧推理 whisper-web
whisper-web https://github.com/xenova/whisper-web 页面结构 AudioManager: 该组件负责音频的录制和处理。它会使用 Web API 来访问麦克风,录制音频数据,并将其传递给 transcriber 进行转录。它通过 transcriber 管理转录状态,音频数据将…...
vscode使用常见问题处理合集
目录 一、使用vite创建的vue3项目,script和style首行代码不会缩进,且格式化属性字段等会换行问题 首行缩进情况如下: 属性、参数格式化换行情况如下: 解决方式: 一、使用vite创建的vue3项目,script和style首行代码不…...
EasyExcel提取excel文档
目录 一、前言二、提取excel文档2.1、所有sheet----获取得到headerList和总行数2.2、所有sheet----获取合并单元格信息2.3、读取某个sheet的每行数据一、前言 EasyExcel 是阿里巴巴开源的一个高性能 Excel 读写库,相比于 Apache POI 和 JXL,它有明显的优势,特别是在处理大数…...
DeepSeek v3 技术报告阅读笔记
注 本文参考 DeepSeek-v3 / v2 / v1 Technical Report 及相关参考模型论文本文不包括基础的知识点讲解,为笔记/大纲性质而非教程,建议阅读技术报告原文交流可发送至邮箱 henryhua0721foxmail.com 架构核心 核心: MLA 高效推理DeepSeekMOE 更…...
Python爬虫-猫眼电影的影院数据
前言 本文是该专栏的第46篇,后面会持续分享python爬虫干货知识,记得关注。 本文笔者以猫眼电影为例子,获取猫眼的影院相关数据。 废话不多说,具体实现思路和详细逻辑,笔者将在正文结合完整代码进行详细介绍。接下来,跟着笔者直接往下看正文详细内容。(附带完整代码) …...
每天五分钟深度学习框架pytorch:搭建谷歌的Inception网络模块
本文重点 前面我们学习了VGG,从现在开始我们将学习谷歌公司推出的GoogLeNet。当年ImageNet竞赛的第二名是VGG,而第一名就是GoogLeNet,它的模型设计拥有很多的技巧,这个model证明了一件事:用更多的卷积,更深的层次可以得到更好的结构 GoogLeNet的网络结构 如图所示就是Go…...
export default与export区别
1.定义: export default:用于导出模块中的默认成员。一个模块中只能有一个export default,通常用于导出模块的主要功能或对象。导入时可以使用任意名称,因为它没有具体的名称 export:用于导出模块中的多个成…...
当Ollama遇上划词翻译:我的Windows本地AI服务搭建日记
🚀 实现Windows本地大模型翻译服务 - 基于OllamaFlask的划词翻译实践 🛠️ 步骤概要1️⃣ python 环境准备2️⃣ Ollama 安装3️⃣ 一个 Flask 服务4️⃣ Windows 服务化封装5️⃣ 测试本地接口6️⃣ 配置划词翻译自定义翻译源7️⃣ 效果展示8️⃣ debug…...
5G与物联网的协同发展:打造智能城市的未来
引言 随着科技的不断进步,智能城市的概念已经不再是科幻小说中的幻想,它正在逐步走进我们的生活。而这背后的两大驱动力无疑是 5G和 物联网(IoT)。5G网络以其高速率、低延迟、大容量的优势,与物联网的强大连接能力相结…...
并发编程---synchronized关键字,以及synchronized同步锁
文章目录 Synchronized 的使用synchronized 在普通方法上的使用(对象锁)synchronized 在静态方法上的使用(类锁)synchronized 在代码块上的使用 JVM 中锁的优化锁的类型自旋锁与自适应自旋锁自旋锁(Spin Lockÿ…...
Vue学习笔记5(Vue3)
Vue3学习笔记 一、create-vue搭建vue3项目 create-vue是vue官方新的脚手架工具,底层切换到了vite 步骤: 查看环境条件 node -v版本需要在16.0及以上创建一个vue应用 npm init vuelatest 这一指令会安装并执行create-vue 二、项目目录和关键文件 in…...
VoIP之音视频会议中的混音技术
在VoIP音视频会议中,需要将多路参会方音频流混合成一路音频流再发送给各参会方,以达到参会方可以听到每个与会人声音的目的,这种技术叫混音。 一、混音基础原理 在实际生活中,我们所处的生活和工作环境就是一个自然的混音场&…...
Baklib一站式云平台:全场景赋能企业知识资产激活
内容概要 在数字化浪潮推动下,企业知识资产的高效管理与价值释放成为核心议题。Baklib作为一站式云平台,以全场景赋能为核心定位,通过构建知识中台架构,为企业提供从资源整合到应用落地的闭环解决方案。该平台不仅支持文本、图像…...
基于nuScenes数据集和DeepSeek模型的端到端自动驾驶解决方案
结合DeepSeek模型进行知识蒸馏,以提高模型性能。这需要将nuScenes中的多模态数据(如摄像头图像、雷达点云、车辆状态等)整合到模型中,同时使用DeepSeek的生成能力进行蒸馏。 接下来,我需要考虑用户可能的背景。用户可能…...
《AI大模型开发笔记》deepseek提示词技巧
为什么你的 AI 助手总是答非所问? 「写篇产品分析」 → 收到一堆不知所云的文字 「做个竞品对比」 → 得到几页没有重点的废话 揭秘:不是 AI 不够聪明,而是你的指令太“高冷”! 一、新手进阶: 5 大法则,让…...
学习笔记-人脸识别相关编程基础
通过编程实现人脸识别功能,需要掌握一定的技术基础,包括编程语言、图像处理、机器学习以及相关的库和框架: 1. 编程语言 Python:Python 是实现人脸识别最常用的语言之一,因为它有大量的库和框架支持,如 Op…...
Java发展史
JavaEE的由来 语言的诞生 Java的前身是Oak语言,其目的是搞嵌入式开发开发智能面包机 叮~~~🍞🍞🍞 产品以失败告终 巅峰 网景公司需要网景浏览器打开网页,Oak->Java,进行前端开发(相关技…...
SAP-ABAP:SAP中REPORT程序和online程序的区别对比
在SAP中,REPORT程序和Online程序(通常指Dialog程序)是两种常见的ABAP程序类型,它们在用途、结构和用户交互方式上有显著区别。以下是它们的详细对比: 1. 用途 REPORT程序Online程序主要用于数据查询、报表生成和批量数…...
【第2章:神经网络基础与实现——2.1 前馈神经网络的结构与工作原理】
老铁们好!今天我们要来一场长达两万字的超详细技术探险,我会像拆解乐高积木一样把前馈神经网络(Feedforward Neural Network)的每个零件摆在台面上,用最接地气的方式让你彻底搞懂这个深度学习基石的工作原理。准备好了吗?我们开始吧! 第一章:神经网络的 “乐高积木” 1…...
Pythong 解决Pycharm 运行太慢
Pythong 解决Pycharm 运行太慢 官方给Pycharm自身占用的最大内存设低估了限制,我的Pycharm刚开始默认是256mb。 首先找到自己的Pycharm安装目录 根据合适自己的改 保存,重启Pycharm...
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,…...
基于ArduPilot开发无人机飞控自动驾驶仪
目录 1、项目参数 2、硬件设计解析 2.1、主控与协处理器架构 2.2、高精度传感器集成 2.3、数据存储与恢复 2.4、电源管理与保护 2.5、通信与接口 本项目基于开源飞行控制固件 ArduPilot 开发,设计并实现了一款高度集成的 自动驾驶仪,可广泛应用于…...
Kotlin Lambda
Kotlin Lambda 在探索Kotlin Lambda之前,我们先回顾下Java中的Lambda表达式,Java 的 Lambda 表达式是 Java 8 引入的一项强大的功能,它使得函数式编程风格的代码更加简洁和易于理解。Lambda 表达式允许你以一种更简洁的方式表示实现接口&…...
UniApp 中制作一个横向滚动工具栏
前言 最近在用 UniApp 开发项目时,需要一个横向滑动的工具栏。常见的工具栏一般都是竖着的,但横向滑动的工具栏不仅能展示更多内容,还能让界面看起来更加丰富。不过很多朋友可能会发现,如何让内容“横着”展示又不变形、能流畅滚…...
Qt的QListWidget样式设置
以下是关于QListWidget样式设置的详细说明,包含常用样式配置和进阶技巧: 1. 基础列表样式 // 设置整体列表容器样式 listWidget->setStyleSheet("QListWidget {"" background-color: #f5f5f5;" // 背景颜色" borde…...
OpenCV 模板匹配
模板匹配算法是一种在目标图像中寻找与模板图像相似区域的方法,模板匹配就是拿一个模板图片在一张比模板图像要大的搜索图像上寻找与模板图像相似的区域,以此来得到目标在搜索图像上的位置,其核心是将模板图像在待搜索图像上从左到右、从上到下依次逐像素平移滑动,每次滑动…...