文章目录
Gardening Friends
问题建模
给定一棵无向树,一棵树有一个价值,其价值为根节点到其余节点的距离最大值乘以k,现在有一个操作,可以将根节点由1转为其他节点,花费成本为根节点到该节点的路径边数乘以c,问该树可获得的最大价值-花费的最大值为多少
思路分析
1.分析所求
对于所求,我们需要知道每一个点为根时,到其余节点的最大距离,以及该点到节点1的边数。对于任意点到节点1的边数可以由根节点开始做一次dfs得到,而任意点为根到其余节点的最大距离有三种方法可得。
2.方法1利用树的直径的性质求任意节点到其余节点的最大距离
树的直径为:树上任意两节点之间最长的简单路径。其性质为:在一棵树上,从任意节点y开始进行一次 DFS,到达的距离其最远的节点z必为直径的一端。证明可以通过分三种情况反证,每次比较起始点所找路基与最近直径端点比较可得。
-
起始点在该直径上
-
起始点不在直径上但路径有重合
-
起始点不在该直径上,且所找到路径与直径不重合
通过树的直径性质,我们可以通过两次dfs预处理出来树的直径两个端点到所有点的距离,然后遍历所有点计算
代码
#include<bits/stdc++.h>
#define x first
#define y second
#define C(i) str[0][i]!=str[1][i]
using namespace std;
typedef unsigned long long ULL;
typedef long long LL;
typedef pair<int, int> PII;
typedef pair<LL, LL> PLL;
const int N =2e5+10,INF=0x3f3f3f3f;
int n,k,c;
vector<int> e[N];
int d1[N],d2[N],d3[N];
void dfs(int d[],int u,int fa){
for(auto v:e[u]){
if(v!=fa){
d[v]=d[u]+1;
dfs(d,v,u);
}
}
}
void solve() {
cin >>n >>k >>c;
for(int i=1;i<=n;i++) e[i].clear();
for(int i=0;i<n-1;i++){
int u,v;
scanf("%d %d",&u,&v);
e[u].push_back(v);
e[v].push_back(u);
}
d1[1]=0;
///以根节点1为源点进行DFS找到距离该点最远的点,该点即为树的直径的一个端点
dfs(d1,1,1);
int p1=max_element(d1+1,d1+n+1)-d1;
d2[p1]=0;
///以树的直径的一个端点为起始点,处理得到其余点到树的直径其中一个端点的距离
dfs(d2,p1,p1);
int p2=max_element(d2+1,d2+n+1)-d2;///与树的直径的一个端点相距最远的点为,直径的另一个端点
d3[p2]=0;
dfs(d3,p2,p2);
LL ans=0;
for(int i=1;i<=n;i++){
///遍历每一个节点作为根节点的情况,其价值为到直径端点的最远距离*k-到节点1的边数*c
ans=max(ans,(LL)max(d2[i],d3[i])*k-(LL)c*d1[i]);
}
cout <<ans <<"\n";
}
int main() {
int t = 1;
cin >> t;
while (t--) solve();
return 0;
}
3.方法2使用换根DP,每次当前结点为根时,维护向下走的两个最长距离以及向上走的最长距离
将每个节点的最大距离分情况讨论,一种为向下走,一种为向上走,通过两次搜索,得到每个节点向下走和向上走的最长距离,然后通过遍历每个节点来分别计算最大价值
代码
#include<bits/stdc++.h>
#define x first
#define y second
#define C(i) str[0][i]!=str[1][i]
using namespace std;
typedef unsigned long long ULL;
typedef long long LL;
typedef pair<int, int> PII;
typedef pair<LL, LL> PLL;
const int N =2e5+10,INF=0x3f3f3f3f;
int n,k,c;
vector<int> e[N];
int d1[N],d2[N],up[N],p[N],d3[N];
int dfs_d(int u,int fa){
d1[u]=d2[u]=-INF;
for(auto v:e[u]){
if(v!=fa){
d3[v]=d3[u]+1;
int val=dfs_d(v,u);///先更新子节点
///更新最大距离和次大距离
if(d1[u]<=val){
d2[u]=d1[u];
d1[u]=val;
p[u]=v;
}else if(d2[u]<val){
d2[u]=val;
}
}
}
if(d1[u]==-INF){
d1[u]=0;
p[u]=0;
}
return d1[u]+1;
}
void dfs_u(int u,int fa){
for(auto v:e[u]){
if(v!=fa){
///向外走的路径不能再折回来,故当v向上走的父节点其向下走的最大距离所对应节点为v
///则需要更换为次大距离
if(p[u]!=v) up[v]=max(up[u],d1[u])+1;
else up[v]=max(up[u],d2[u])+1;
dfs_u(v,u);///更新完父节点后再更新子节点
}
}
}
void solve() {
cin >>n >>k >>c;
for(int i=1;i<=n;i++) e[i].clear();
for(int i=0;i<n-1;i++){
int u,v;
scanf("%d %d",&u,&v);
e[u].push_back(v);
e[v].push_back(u);
}
d3[1]=0;
///获得每个节点向下走的两个最大距离
dfs_d(1,1);
up[1]=0;
///获得每个节点向上走的最大距离
dfs_u(1,1);
LL ans=0;
for(int i=1;i<=n;i++){
ans=max(ans,(LL)max(up[i],d1[i])*k-(LL)d3[i]*c);
}
cout <<ans <<"\n";
}
int main() {
int t = 1;
cin >> t;
while (t--) solve();
return 0;
}
4.方法3,采用树形DP的思想,考虑每个节点为根节点的子树内可得的最大距离
若要每个节点都考虑该节点为根的子树内所产生的最大距离,则可以分成两种情况,一种为该节点做根向下的最大距离,另一种为有两段该节点向下的路径合并在一起的最大距离,以其中一个较短的路径端点为根。
代码
#include<bits/stdc++.h>
#define x first
#define y second
#define C(i) str[0][i]!=str[1][i]
using namespace std;
typedef unsigned long long ULL;
typedef long long LL;
typedef pair<int, int> PII;
typedef pair<LL, LL> PLL;
const int N =2e5+10,INF=0x3f3f3f3f;
int n,k,c;
vector<int> e[N];
LL maxval;
LL dfs(int u,int fa,int d){
int ma1=0,ma2=0,res=0;
for(auto v:e[u]){
if(v!=fa){
///先更新子节点,获得子节点为根的子树可得到的最大距离
int val=dfs(v,u,d+1);
res=max(res,val);
///更新最大值和次大值
ma2=max(ma2,val);
if(ma1<ma2) swap(ma1,ma2);
}
}
///计算当前结点为根时可得到的最大价值,d+ma2是以m2路径所在端点点为根
maxval=max({maxval,(LL)k*ma1-(LL)c*d,(LL)k*(ma1+ma2)-(LL)c*(d+ma2)});
return res+1;
}
void solve() {
cin >>n >>k >>c;
for(int i=1;i<=n;i++) e[i].clear();
for(int i=0;i<n-1;i++){
int u,v;
scanf("%d %d",&u,&v);
e[u].push_back(v);
e[v].push_back(u);
}
maxval=0;
dfs(1,1,0);
cout <<maxval<<"\n";
}
int main() {
int t = 1;
cin >> t;
while (t--) solve();
return 0;
}