题目链接
题目描述
给定一个 个点、
条边的带权无向连通图。你有
次操作机会,每次操作可以选择一条边,使其权值加 1。目标是在完成所有操作后,找出一棵生成树,使得这棵生成树中边权最小的边的权值尽可能大。
任务是输出这个最大可能的“最小边权”。
解题思路
这是一个典型的“最大化最小值”问题,非常适合使用二分答案的技巧来解决。
我们的目标是找到一个最大的整数 ans
,使得我们存在一种方案,通过不超过 次操作,让图中存在一棵所有边的权值都
的生成树。
二分答案框架
我们在一个可能的值域(例如 )中对
ans
进行二分查找。对于二分过程中的每一个猜测值 mid
,我们需要一个 check(mid)
函数来判断这个值是否可行。
- 如果
check(mid)
返回true
,说明mid
是一个可能达到的“最小边权”,我们应该尝试更大的值,因此left = mid
。 - 如果
check(mid)
返回false
,说明mid
太大了,无法达到,我们必须减小目标,因此right = mid - 1
。
check(x)
函数的设计
check(x)
函数的核心是回答:我们能否通过不超过 次操作,构造一棵所有边权都
的生成树?
要构造一棵所有边权都 的生成树,我们只能使用那些“原始边权
”或者“可以通过操作提升到
”的边。为了以最少的总操作次数连通整个图,我们应该优先选择那些“提升成本”最低的边。这启发我们借鉴 Kruskal 算法的思想。
check(x)
算法步骤如下:
-
初始化: 创建一个并查集(Union-Find)数据结构。初始化总操作成本
total_cost = 0
。 -
筛选与合并:
- 创建一个列表
cost_edges
用于存放需要提升的边。 - 遍历原图所有边
:
- 如果
,这条边可以免费使用。我们直接用它来连接
和
(如果它们尚未连通)。
- 如果
,则将它的提升成本
和端点
存入
cost_edges
。
- 如果
- 创建一个列表
-
构建生成树(Kruskal 过程):
- 将
cost_edges
按照提升成本从小到大排序。 - 遍历排序后的
cost_edges
。对于每条边,如果它的两个端点尚未连通:- (防溢出检查) 在累加成本前,先判断
total_cost + current_cost
是否会超过。由于
total_cost
和current_cost
都可能很大,直接相加可能导致溢出。更安全的方式是检查total_cost > k - current_cost
。如果这个条件成立,说明预算不足,直接判定check(x)
失败。 - 如果预算充足,则累加成本,并连接这两个端点。
- (防溢出检查) 在累加成本前,先判断
- 将
-
判断可行性:
- 在上述过程结束后,检查图是否完全连通(即并查集中只有一个连通分量)。
- 同时需要保证总成本未超预算。
- 只有两个条件都满足,
check(x)
才返回true
。
通过二分查找,我们就能找到满足 check
函数的最大 x
。
代码
#include <iostream>
#include <vector>
#include <numeric>
#include <algorithm>
using namespace std;
struct Edge {
int u, v;
long long w;
};
struct DSU {
vector<int> parent;
int components;
DSU(int n) {
parent.resize(n + 1);
iota(parent.begin(), parent.end(), 0);
components = n;
}
int find(int i) {
if (parent[i] == i) return i;
return parent[i] = find(parent[i]);
}
void unite(int i, int j) {
int root_i = find(i);
int root_j = find(j);
if (root_i != root_j) {
parent[root_i] = root_j;
components--;
}
}
};
bool check(long long x, int n, long long k, const vector<Edge>& edges) {
DSU dsu(n);
long long cost = 0;
vector<pair<long long, pair<int, int>>> cost_edges;
for (const auto& edge : edges) {
if (edge.w < x) {
cost_edges.push_back({x - edge.w, {edge.u, edge.v}});
} else {
dsu.unite(edge.u, edge.v);
}
}
sort(cost_edges.begin(), cost_edges.end());
for (const auto& p : cost_edges) {
long long current_cost = p.first;
int u = p.second.first;
int v = p.second.second;
if (dsu.find(u) != dsu.find(v)) {
if (cost > k - current_cost) { // 防溢出检查
return false;
}
cost += current_cost;
dsu.unite(u, v);
}
}
return dsu.components == 1;
}
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int n, m;
long long k;
cin >> n >> m >> k;
vector<Edge> edges(m);
long long max_w = 0;
for (int i = 0; i < m; ++i) {
cin >> edges[i].u >> edges[i].v >> edges[i].w;
max_w = max(max_w, edges[i].w);
}
long long low = 0, high = max_w + k, ans = 0;
while (low <= high) {
long long mid = low + (high - low) / 2;
if (check(mid, n, k, edges)) {
ans = mid;
low = mid + 1;
} else {
high = mid - 1;
}
}
cout << ans << endl;
return 0;
}
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.StringTokenizer;
public class Main {
static class Edge {
int u, v;
long w;
Edge(int u, int v, long w) {
this.u = u;
this.v = v;
this.w = w;
}
}
static class DSU {
int[] parent;
int components;
DSU(int n) {
parent = new int[n + 1];
for (int i = 0; i <= n; i++) parent[i] = i;
components = n;
}
int find(int i) {
if (parent[i] == i) return i;
return parent[i] = find(parent[i]);
}
void unite(int i, int j) {
int root_i = find(i);
int root_j = find(j);
if (root_i != root_j) {
parent[root_i] = root_j;
components--;
}
}
}
static boolean check(long x, int n, long k, List<Edge> edges) {
DSU dsu = new DSU(n);
long cost = 0;
List<long[]> costEdges = new ArrayList<>();
for (Edge edge : edges) {
if (edge.w < x) {
costEdges.add(new long[]{x - edge.w, edge.u, edge.v});
} else {
dsu.unite(edge.u, edge.v);
}
}
Collections.sort(costEdges, (a, b) -> Long.compare(a[0], b[0]));
for (long[] p : costEdges) {
long currentCost = p[0];
int u = (int) p[1];
int v = (int) p[2];
if (dsu.find(u) != dsu.find(v)) {
if (cost > k - currentCost) { // 防溢出检查
return false;
}
cost += currentCost;
dsu.unite(u, v);
}
}
return dsu.components == 1;
}
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StringTokenizer st = new StringTokenizer(br.readLine());
int n = Integer.parseInt(st.nextToken());
int m = Integer.parseInt(st.nextToken());
long k = Long.parseLong(st.nextToken());
List<Edge> edges = new ArrayList<>();
long maxW = 0;
for (int i = 0; i < m; i++) {
st = new StringTokenizer(br.readLine());
int u = Integer.parseInt(st.nextToken());
int v = Integer.parseInt(st.nextToken());
long w = Long.parseLong(st.nextToken());
edges.add(new Edge(u, v, w));
maxW = Math.max(maxW, w);
}
long low = 0, high = maxW + k, ans = 0;
while (low <= high) {
long mid = low + (high - low) / 2;
if (check(mid, n, k, edges)) {
ans = mid;
low = mid + 1;
} else {
high = mid - 1;
}
}
System.out.println(ans);
}
}
import sys
class DSU:
def __init__(self, n):
self.parent = list(range(n + 1))
self.components = n
def find(self, i):
if self.parent[i] == i:
return i
self.parent[i] = self.find(self.parent[i])
return self.parent[i]
def unite(self, i, j):
root_i = self.find(i)
root_j = self.find(j)
if root_i != root_j:
self.parent[root_i] = root_j
self.components -= 1
return True
return False
def check(x, n, k, edges):
dsu = DSU(n)
cost = 0
cost_edges = []
for u, v, w in edges:
if w < x:
cost_edges.append((x - w, u, v))
else:
dsu.unite(u, v)
cost_edges.sort()
for current_cost, u, v in cost_edges:
if dsu.find(u) != dsu.find(v):
if cost > k - current_cost:
return False
cost += current_cost
dsu.unite(u,v)
return dsu.components == 1
def main():
try:
n, m, k = map(int, sys.stdin.readline().split())
edges = []
max_w = 0
for _ in range(m):
u, v, w = map(int, sys.stdin.readline().split())
edges.append((u, v, w))
max_w = max(max_w, w)
low, high = 0, max_w + k
ans = 0
while low <= high:
mid = (low + high) // 2
if mid < 0: mid = 0
if check(mid, n, k, edges):
ans = mid
low = mid + 1
else:
high = mid - 1
print(ans)
except (IOError, ValueError):
pass
if __name__ == "__main__":
main()
算法及复杂度
- 算法: 二分答案 + Kruskal + 并查集
算法及复杂度
- 算法: 二分答案 + Kruskal + 并查集
- 时间复杂度:
- 二分答案需要
次迭代。
- 在每次
check
函数中,主要开销来自于对“待提升”边的排序,其复杂度为。
- 二分答案需要
- 空间复杂度:
,用于存储图的边和并查集数据结构。