问题描述
小蓝和小桥是两位花园爱好者,她们在自己的花园里种了一棵n个节点的树,每条边的长度为k。初始时,根节点为1号节点。她们想把这棵树卖掉,但是想卖个好价钱。树的价值被定义为根节点到所有节点的路径长度的最大值。
为了让这棵树更有价值,小蓝和小桥可以对这棵树进行一种操作: 花费C的代价,将根节点从当前的节点移动到它的一个相邻节点上。注意,这个操作不会改变树的形态,只是改变了根节点的位置。
她们希望通过尽可能地进行操作,使得卖出去的这棵树的盈利最大。盈利被定义为卖出去的树的价值减去操作的总代价。
请你帮助她们,找出她们能够获得的最大盈利。
输入格式
第一行包含一个整数t,表示测试数据组数。
每组数据第一行包含三个整数n、 k和c,表示树的节点数、每条边的长度和进行一次移动操作的代价。接下来n- 1行,每行描述树的一条边,包含两个整数 u i u_i ui和 v i v_i vi,表示树中连接 u i u_i ui和 v i v_i vi之间的一条边。
输出格式
对于每组数据,输出一个整数,表示最大盈利。
数据规模
对于100%的评测数据, 1 ≤ t ≤ 20 , 2 < n ≤ 1 0 5 , 1 ≤ k , c ≤ 1 0 9 , 1 ≤ u i , v i ≤ n 1≤t≤20,2< n≤10^5,1≤k,c≤10^9,1≤u_i,v_i≤n 1≤t≤20,2<n≤105,1≤k,c≤109,1≤ui,vi≤n。
样例输入
4
3 2 3
2 1
3 1
5 4 1
2 1
4 2
5 4
3 4
6 5 3
4 1
6 1
2 6
5 1
3 2
10 6 4
1 3
1 9
9 7
7 6
6 4
9 2
2 8
8 5
5 10
样例输出
2
12
17
32
与路径相关的树形dp题的思路,这里就不能用常规的板子了。这里用到了树的直径的知识点,首先了解一下树的直径的定义。
树的直径是树上最长的一条链,这条链并不是唯一的。我们可以画一下,看一看树的直径有什么样的特点。
如图所示,直径由树中的两个节点u和v决定,可以写作(u,v)其中u和v具有以下性质:
性质1:u和v的度数均为1。
性质2:在以任意一个点为根的树上,u和v中必然存在一个点作为最深的叶子节点。
对于性质1,假设u或者v不是度数为1的节点,说明u或者v还可以继续向下走,那么也就是说路径的长度还能增加,这和当前u走到v的路径是直径矛盾。所以u和v必然是度为1的节点。
对于性质2,假设u和v都不是最深的那个叶子节点,并且最深的叶子节点是w,根节点用r表示,u到v的路径是u-r-v,w到v的路径是w-r-v,因为w是最深的那个叶子节点,所以w到根节点的距离一定大于u到根节点的距离,即u-r的距离小于w-r的距离,此时u到v之间的路径就不是最长路径,和u走到v的路径是直径矛盾。所以u和v必然存在一个点作为最深的叶子节点。
回到题目,我们可以考虑求以每个节点为根节点时树的价值,假设用数组dis[i]表示以i为根节点时树的价值。将节点i变成根节点要花费的代价时节点i的深度,用dep[i]表示节点i的深度。那么将节点i作为根节点的盈利就是 d i s [ i ] − d e p [ i ] ∗ c dis[i]-dep[i]*c dis[i]−dep[i]∗c。注意一般我们在算深度或者长度时每条边的权值都是1,但是本题给了每条边的权值是k,所以盈利应该变成 d i s [ i ] ∗ k − d e p [ i ] ∗ c dis[i]*k-dep[i]*c dis[i]∗k−dep[i]∗c。
dep[i]表示的是节点i的深度。可以在一次深度搜索的过程中求出。但是dis[i]应该如何求呢?这里就要利用树的直径。假设我们已经求出树的直径为(u,v),那么根据树的直径的性质2,当i作为树的根节点时,u或者v必然是距离i最远的一个子节点。
接下来考虑如何求树的直径。分为两次dfs遍历,因为u和v必然有一个是当前节点为根节点时最深的那个节点,那么通过第一次dfs遍历,求出以当前节点为根节点时,每个节点对于的深度,那么深度最大的那个节点必然是构成直径的节点之一。代码如下,
dfs(1,0);
int u = 1;
for(int i = 1;i <= n;i++) {
if(dis[u]<dis[i]) u = i;
}
u表示的是构成直径的节点之一。此时再以u节点为根节点进行一次dfs遍历,求每个节点此时的深度,深度最深的节点即为另一个构成直径的节点。代码如下,
Arrays.fill(dis, 0);
dfs(u, 0);
int v = 1;
for(int i = 1;i <= n;i++) {
dis2[i] = dis[i];
if(dis[v]<dis[i]) v = i;
}
那么 d i s 2 dis2 dis2记录的是节点u和每个节点之间的距离,但是节点u并不一定是某个节点为根时最深的节点,还有可能是节点v。所以需要以节点v为根再进行一次dfs遍历。由于每次dfs遍历的过程中都是改变的 d i s dis dis数组,所以需要用 d i s 2 dis2 dis2数组暂存一下当前的遍历结果。遍历完后假设以节点u为根求得的节点深度为 d i s 2 [ i ] dis2[i] dis2[i],以节点v为根求得的节点深度为 d i s [ i ] dis[i] dis[i],那么以节点i为根时最深的深度为 d i s = m a x ( d i s [ i ] , d i s 2 [ i ] ) dis=max(dis[i],dis2[i]) dis=max(dis[i],dis2[i])代码如下,
Arrays.fill(dis,0);
dfs(v, 0);
for(int i = 1;i <= n;i++) {
dis[i] = Math.max(dis[i], dis2[i]);
}
那么最终的答案就是在枚举每个节点为根的情况下求所得盈利,也就是一开始说的 d i s [ i ] ∗ k − d e p [ i ] ∗ c dis[i]*k-dep[i]*c dis[i]∗k−dep[i]∗c。
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Scanner;
public class Main {
static HashMap<Integer, List<Integer>> map = new HashMap<Integer, List<Integer>>();
static long dis[],dis2[];
static long dep[];
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int t = scanner.nextInt();
while(t-- > 0) {
int n = scanner.nextInt();
int k = scanner.nextInt();
int c = scanner.nextInt();
map.clear();
dis = new long[n+1];
dis2 = new long[n+1];
dep = new long[n+1];
Arrays.fill(dis,0);
Arrays.fill(dep,0);
for(int i = 1;i < n;i++) {
int u = scanner.nextInt();
int v = scanner.nextInt();
add(v,u);
add(u,v);
}
dfs(1,0);
int u = 1;
long ans = 0;
for(int i = 1;i <= n;i++) {
dep[i] = dis[i];
if(dis[u]<dis[i]) u = i;//求直径的某一个端点
}
Arrays.fill(dis, 0);
dfs(u, 0);
int v = 1;
for(int i = 1;i <= n;i++) {
dis2[i] = dis[i];
if(dis[v]<dis[i]) v = i;//求直径的令一个端点
}
Arrays.fill(dis,0);
dfs(v, 0);
for(int i = 1;i <= n;i++) {
dis[i] = Math.max(dis[i], dis2[i]);//求以某个节点为根节点时最深的深度
}
for(int i = 1;i <= n;i++) {
ans = Math.max(ans, dis[i]*k-c*dep[i]);
}
System.out.println(ans);
}
}
private static void dfs(int u, int fa) {
// TODO Auto-generated method stub
if(map.get(u)==null) return;
for(Integer e:map.get(u)) {
if(e==fa) continue;
dis[e]=dis[u]+1;
dfs(e, u);
}
}
private static void add(int v, int u) {
// TODO Auto-generated method stub
if(!map.containsKey(v)) map.put(v, new ArrayList<Integer>());
map.get(v).add(u);
}
}