题目描述
历史的车轮不断前进,国王在遥远的大陆上征服了一个新的区域。这个区域包含 NNN 个城镇(编号从 111 到 NNN),由若干条道路连接。可以确认任意两个城镇之间只有唯一的一条路径。
我们定义道路 (i,j)(i, j)(i,j) 的容量 C(i,j)C(i, j)C(i,j) 表示这两个城镇之间的道路最多允许运输 C(i,j)C(i, j)C(i,j) 单位的货物。对于城镇 iii 和 jjj 之间的路径,我们定义 S(i,j)S(i, j)S(i,j) 为这条路径上的最大交通容量,其值等于路径上所有道路容量的最小值。
国王希望选择一个中心城镇来储存战争资源,使得从该中心到其他 N−1N-1N−1 个城镇的总交通容量最大化。你的任务是帮助国王找到这个中心,并输出最大的总交通容量。
输入格式
- 多个测试用例
- 每个测试用例第一行包含整数 NNN (1≤N≤200,0001 \leq N \leq 200,0001≤N≤200,000)
- 接下来 N−1N-1N−1 行,每行包含三个整数 a,b,ca, b, ca,b,c,表示城镇 aaa 和 bbb 之间有一条容量为 ccc 的道路
输出格式
对于每个测试用例,输出一个整数表示所选中心城镇的最大总交通容量。
题目分析
问题本质
这是一个在树结构上的优化问题:
- 给定一棵 NNN 个节点的树,每条边有一个容量值
- 对于任意两个节点 iii 和 jjj,它们之间的交通容量 S(i,j)S(i, j)S(i,j) 是路径上所有边容量的最小值
- 需要找到一个中心节点 centercentercenter,使得 ∑j=1NS(center,j)\sum_{j=1}^{N} S(center, j)∑j=1NS(center,j) 最大
关键观察
- 树结构特性:任意两点间只有唯一路径,这简化了问题
- 瓶颈性质:S(i,j)S(i, j)S(i,j) 由路径上的最小容量边决定
- 贪心思想:容量大的边应该尽可能多地被利用
解题思路
本题的巧妙解法是使用并查集配合类似 Kruskal\texttt{Kruskal}Kruskal 算法的贪心策略:
- 逆向思维:不从中心点出发计算到其他点的容量,而是考虑每条边对总容量的贡献
- 排序策略:将所有边按容量从大到小排序,这样保证优先处理容量大的边
- 合并过程:使用并查集维护连通分量,每个分量记录:
- 分量大小(节点数)
- 当前分量作为中心时的总容量
- 合并决策:当合并两个连通分量时,计算两种可能的中心选择方案,选择总容量更大的方案
算法详解
核心思想
假设我们按容量降序处理边,当处理边 (u,v)(u, v)(u,v) 时:
- uuu 所在的连通分量为 AAA,大小为 sizeAsize_AsizeA,当前总容量为 sumAsum_AsumA
- vvv 所在的连通分量为 BBB,大小为 sizeBsize_BsizeB,当前总容量为 sumBsum_BsumB
有两种合并方案:
- 以 AAA 为中心:总容量 = sumA+sizeB×capsum_A + size_B \times capsumA+sizeB×cap
- 以 BBB 为中心:总容量 = sumB+sizeA×capsum_B + size_A \times capsumB+sizeA×cap
我们选择总容量更大的方案进行合并。
为什么这样正确?
- 贪心正确性:由于按容量降序处理,当前边是连接两个分量的最小容量边,因此两个分量中任意两点间的交通容量不会超过当前边容量
- 最优子结构:每次合并都保证了当前连通分量的总容量是局部最优的
- 最终结果:当所有边处理完成后,整个树形成一个连通分量,其记录的总容量就是全局最优解
时间复杂度
- 排序:O(NlogN)O(N \log N)O(NlogN)
- 并查集操作:O(Nα(N))O(N \alpha(N))O(Nα(N)),其中 α\alphaα 是反阿克曼函数
- 总体复杂度:O(NlogN)O(N \log N)O(NlogN),满足题目约束
代码实现
// Conquer a New Region
// UVa ID: 1664
// Verdict: Accepted
// Submission Date: 2025-11-26
// UVa Run Time: 0.200s
//
// 版权所有(C)2025,邱秋。metaphysis # yeah dot net
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 200010;
// 并查集数据结构
struct DSU {
int parent[MAXN];
long long size[MAXN]; // 连通分量的大小
long long sum[MAXN]; // 连通分量的总容量
void init(int n) {
for (int i = 1; i <= n; i++) {
parent[i] = i;
size[i] = 1;
sum[i] = 0;
}
}
int find(int x) {
if (parent[x] != x)
parent[x] = find(parent[x]);
return parent[x];
}
};
struct Edge {
int u, v, cap;
bool operator<(const Edge& other) const {
return cap > other.cap; // 按容量降序排序
}
};
Edge edges[MAXN];
DSU dsu;
int main() {
int n;
while (scanf("%d", &n) != EOF) {
// 读取所有边
for (int i = 0; i < n - 1; i++)
scanf("%d%d%d", &edges[i].u, &edges[i].v, &edges[i].cap);
// 初始化并查集
dsu.init(n);
// 按容量降序排序边
sort(edges, edges + n - 1);
// 处理每条边
for (int i = 0; i < n - 1; i++) {
int u = edges[i].u, v = edges[i].v, cap = edges[i].cap;
int rootU = dsu.find(u);
int rootV = dsu.find(v);
// 计算两个连通分量作为新中心的总容量
long long sumU = dsu.sum[rootU] + dsu.size[rootV] * cap;
long long sumV = dsu.sum[rootV] + dsu.size[rootU] * cap;
// 合并连通分量,选择总容量更大的作为新值
if (sumU > sumV) {
dsu.parent[rootV] = rootU;
dsu.size[rootU] += dsu.size[rootV];
dsu.sum[rootU] = sumU;
} else {
dsu.parent[rootU] = rootV;
dsu.size[rootV] += dsu.size[rootU];
dsu.sum[rootV] = sumV;
}
}
// 最终答案在唯一的连通分量的sum中
int root = dsu.find(1);
printf("%lld\n", dsu.sum[root]);
}
return 0;
}
总结
本题展示了如何将看似复杂的最优化问题转化为经典的并查集应用。关键在于:
- 利用树结构的特性简化问题
- 通过排序和贪心策略确保最优解
- 在并查集中维护额外的信息来支持决策
这种"逆向思维"和"按权重处理"的技巧在很多图论问题中都非常有用,值得深入理解和掌握。

1108

被折叠的 条评论
为什么被折叠?



