题目链接
题目描述
公元 2044 2044 2044 年,人类进入了宇宙纪元。
L 国有 n n n 个星球,还有 n − 1 n-1 n−1 条双向航道,每条航道建立在两个星球之间,这 n − 1 n-1 n−1 条航道连通了 L 国的所有星球。
小 P 掌管一家物流公司, 该公司有很多个运输计划,每个运输计划形如:有一艘物流飞船需要从 u i u_i ui 号星球沿最快的宇航路径飞行到 v i v_i vi 号星球去。显然,飞船驶过一条航道是需要时间的,对于航道 j j j,任意飞船驶过它所花费的时间为 t j t_j tj,并且任意两艘飞船之间不会产生任何干扰。
为了鼓励科技创新, L 国国王同意小 P 的物流公司参与 L 国的航道建设,即允许小 P 把某一条航道改造成虫洞,飞船驶过虫洞不消耗时间。
在虫洞的建设完成前小 P 的物流公司就预接了 m m m 个运输计划。在虫洞建设完成后,这 m m m 个运输计划会同时开始,所有飞船一起出发。当这 m m m 个运输计划都完成时,小 P 的物流公司的阶段性工作就完成了。
如果小 P 可以自由选择将哪一条航道改造成虫洞, 试求出小 P 的物流公司完成阶段性工作所需要的最短时间是多少?
输入格式
第一行包括两个正整数 n , m n, m n,m,表示 L 国中星球的数量及小 P 公司预接的运输计划的数量,星球从 1 1 1 到 n n n 编号。
接下来 n − 1 n-1 n−1 行描述航道的建设情况,其中第 i i i 行包含三个整数 a i , b i a_i, b_i ai,bi 和 t i t_i ti,表示第 i i i 条双向航道修建在 a i a_i ai 与 b i b_i bi 两个星球之间,任意飞船驶过它所花费的时间为 t i t_i ti。
数据保证
接下来 m m m 行描述运输计划的情况,其中第 j j j 行包含两个正整数 u j u_j uj 和 v j v_j vj,表示第 j j j 个运输计划是从 u j u_j uj 号星球飞往 v j v_j vj号星球。
输出格式
一个整数,表示小 P 的物流公司完成阶段性工作所需要的最短时间。
样例 #1
样例输入 #1
6 3
1 2 3
1 6 4
3 1 7
4 3 6
3 5 5
3 6
2 5
4 5
样例输出 #1
11
提示
所有测试数据的范围和特点如下表所示
请注意常数因子带来的程序效率上的影响。
对于 100 % 100\% 100% 的数据,保证: 1 ≤ a i , b i ≤ n 1 \leq a_i,b_i \leq n 1≤ai,bi≤n, 0 ≤ t i ≤ 1000 0 \leq t_i \leq 1000 0≤ti≤1000, 1 ≤ u i , v i ≤ n 1 \leq u_i,v_i \leq n 1≤ui,vi≤n。
算法思想
根据题目描述,有 n n n 个星球, n − 1 n-1 n−1 条双向航道,这 n − 1 n-1 n−1 条航道连通了所有星球,即构成一棵树。驶过第 i i i条航道要所花费的时间为 t i t_i ti;又有 m m m 个运输计划,对于第 i i i个运输计划有起点 u i u_i ui和终点 v i v_i vi,会经过起点到终点的每条航道。问若选择将一条航道改造成虫洞(花费时间变为0),最短需要多长时间完成所有运输计划。
二分答案
不妨假设改造一条航道后,最短需要
t
t
t时间完成所有运输计划,那么对于任意给定的时间
T
T
T,只要满足
T
≥
t
T\ge t
T≥t,也能够完成所有运输计划。也就是说答案满足两段性,如下图所示:
满足这个性质,可以使用二分答案求解。二分得到mid
后,需要判断是否可以将一条航道边权变成
0
0
0,使得每个运输计划花费的时间t
都满足t <= mid
。这样就要预处理出每个运输计划所花费的时间。
树上前缀和
一个运输计划从
u
u
u点出发到
v
v
v点结束所花费的时间,就是求这条路径的长度,可以使用树上前缀和来实现,如下图所示:
其基本思想是计算出每个节点到根节点的距离,例如
d
i
s
[
u
]
dis[u]
dis[u]表示节点
u
u
u到根节点的距离,那么
d
i
s
u
→
v
=
d
i
s
[
u
]
+
d
i
s
[
v
]
−
2
×
d
i
s
[
p
]
dis_{u\rightarrow v}=dis[u]+dis[v]-2\times dis[p]
disu→v=dis[u]+dis[v]−2×dis[p],其中
p
p
p是
u
u
u和
v
v
v的最近公共祖先。如何求解公共祖先,可以参考博主的这篇文章——每周一算法:倍增法求最近公共祖先(LCA)
求出每个运输计划的路径长度之后,对于路径长度<= mid
的运输计划无需处理;而对于路径长度> mid
的,都需要从中至少选择一条航道将其边权变成
0
0
0。因为题目要求只能改造一条航道,因此所选的航道必须是这些运输计划的公共边。
树上差分
每个运输计划都会经过若干条航道。那么可以采用计数的思想,对于不满足要求的运输计划,将其经过的每条航道计数 1 1 1次。假设有 c c c条不满足要求的运输计划,那么它们的公共边一定被计数 c c c次。
朴素的计数做法时间复杂度为
O
(
n
×
m
)
O(n\times m)
O(n×m),这里可以采用树上差分进行优化。
不妨设
d
[
]
d[]
d[]为树中每个节点向上连接的边经过次数的差分数组,如果要将从
u
u
u到
v
v
v的路径中每条边的经过次数
+
1
+1
+1,如下图所示:
可以让d[u] += 1
,d[v] += 1
,d[p] -= 2
,其中
p
p
p为
u
、
v
u、v
u、v的最近公共祖先。
经过若干次操作之后,再自底向上累加每棵子树的和,就可以求出每条边经过的总次数。这就是树上差分。
求出每条边的经过次数之后,就可以判断出公共边。对于每条公共边,将其边权变成
0
0
0,如果所有路径长度都不大于mid
,则返回true
。这里可以优化为判断最长路径减去该公共边的长度是否满足要求即可。
时间复杂度
- 二分答案时间复杂为 O ( n × l o g n ) O(n\times logn) O(n×logn)
- 求每个路径的长度 O ( n × l o g n ) O(n\times logn) O(n×logn)
- 预处理公共边 O ( n ) O(n) O(n)
代码实现
#include <bits/stdc++.h>
using namespace std;
const int N = 3e5 + 5, M = N * 2, K = 19;
int n, m, cnt, seq[N], depth[N], f[N][K], dis[N], d[N];
int h[N], e[M], ne[M], w[M], idx;
struct P
{
int u, v, p, dist;
} q[N];
void add(int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++;
}
void dfs(int u, int fa, int de) //预处每个节点的深度、到根节点的距离和倍增数组
{
seq[cnt ++] = u; //保存深搜序列,方便自底向上求树上前缀和
depth[u] = de;
for(int i = h[u]; ~ i; i = ne[i])
{
int v = e[i];
if(v == fa) continue;
f[v][0] = u; //计算倍增数组
for(int k = 1; k < K; k ++) f[v][k] = f[f[v][k - 1]][k - 1];
dis[v] = dis[u] + w[i]; //v节点到根节点的距离
dfs(v, u, de + 1);
}
}
int lca(int u, int v)
{
if(depth[u] < depth[v]) swap(u, v); //如果v更深,交换u、v
for(int i = K - 1; i >= 0; i -- ) //将u跳到和v同层
if(depth[f[u][i]] >= depth[v]) u = f[u][i];
if(u == v) return u;
for(int i = K - 1; i >= 0; i --) //将u和v同时向上跳,直到父节点相同
{
if(f[u][i] != f[v][i]) u = f[u][i], v = f[v][i];
}
return f[u][0]; //返回共同的父节点
}
bool check(int mid)
{
memset(d, 0, sizeof d); //初始化差分数组
int c = 0, max_d = 0; //统计不满足要求的运输计划的个数,以及最大的路径长度
for(int i = 0; i < m; i ++)
{
int u = q[i].u, v = q[i].v, p = q[i].p, dist = q[i].dist;
if(dist > mid)
{
c ++;
max_d = max(max_d, dist);
d[u] += 1, d[v] += 1, d[p] -= 2; //差分处理,将u->v路径上的每条边计数1次
}
}
if(c == 0) return true; //没有不满足要求的运输计划
for(int i = n - 1; i >= 0; i --) //自底向上统计每条航道的经过次数
{
int u = seq[i];
d[f[u][0]] += d[u]; //求子树的前缀和
}
for(int i = 1; i <= n; i ++)
//如果i是公共边,并且最长的路径减去公共边的长度满足要求
if(d[i] == c && max_d - (dis[i] - dis[f[i][0]]) <= mid)
return true;
return false;
}
int main()
{
scanf("%d%d", &n, &m);
memset(h, -1, sizeof h);
for(int i = 1; i < n; i ++)
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
add(a, b, c), add(b, a, c);
}
dfs(1, -1, 1); //预处每个节点的深度、到根节点的距离和倍增数组
for(int i = 0; i < m; i ++)
{
int u, v;
scanf("%d%d", &u, &v);
int p = lca(u, v); // 计算最近公共祖先
int dist = dis[u] + dis[v] - 2 * dis[p]; //计算u->v的路径长度
q[i] = {u, v, p, dist}; //保存运输计划
}
int L = 0, R = 3e8; //二分查找,R = n * max(w)
while(L < R)
{
int mid = (L + R) / 2;
if(check(mid)) R = mid;
else L = mid + 1;
}
printf("%d\n", L);
return 0;
}