完整教程:旅游(牛客)
写在前面:

牛客每日一题持续更新中!
今天给彦祖和亦菲们带来的是 旅游
题目如下:
题目描述
有
n个城市和m条双向道路。每条道路有一个损坏值 aiai。
牛牛有
c元预算。修复道路需要花钱:第 k 次修复需要花费 k×aik×ai 元。
国家会免费修复所有损坏值 ≤ p 的道路。
牛牛可以自己决定修复哪些道路以及修复顺序。
目标:让所有城市连通(即修好的道路构成连通图),且牛牛的总花费 ≤ c。
问题
求 p 的最小值,使得牛牛能在预算内完成目标。
如果国家不需要修任何路(即牛牛自己就能承担全部费用),输出0。这是一个典型的 图论 + 二分答案 (Binary Search) 问题。我们需要结合 最小生成树 (MST Minimum Span Tree) 的思想和 贪心策略 来解决。
核心思路分析
1. 问题本质
这是一个带预算限制的最小生成树(MST)问题,且修复成本与修复顺序相关。
2. 关键洞察
修复成本公式:第 k 次修复花费
k × 边权值最优修复顺序:按边权值从小到大修复,这样总成本最小
国家援助的影响:损坏值 ≤ p 的边免费,相当于把这些边的成本降为0
3. 问题转化
我们需要找到最小的 p,使得:
MST中所有损坏值 > p 的边,按最优顺序修复的总成本 ≤ c算法步骤
步骤1:构建最小生成树
1. 将所有边按损坏值从小到大排序 2. 使用Kruskal算法构建MST 3. 记录MST中的所有边权值为什么用MST?
要连通n个城市至少需要n-1条边
MST提供了成本最小的边集合(按损坏值衡量)
步骤2:确定修复顺序和成本计算
1. 将MST中的边按损坏值从小到大排序 2. 最优修复顺序就是按这个顺序修复 3. 总成本 = 1×a₁ + 2×a₂ + 3×a₃ + ... + (n-1)×aₙ₋₁步骤3:寻找最小的p
如果总成本 ≤ c: p = 0 国家不需要帮忙 否则: 从最贵的边开始向前检查: 如果某条边由国家免费修,那么所有比它便宜的边也都免费 我们需要找到第一条边,使得剩下的边修复成本 ≤ c算法实现技巧
高效查找p的方法
代码中使用了一个巧妙的反向计算法:
// 将MST边按损坏值从小到大排序后存储在u中 int k = 1; int sum = 0; for(int i = u.size()-1; i >= 0; i--) { sum += u[i] * (k++); // 从贵到便宜累加成本 if(sum > c) { printf("%lld", u[i]); // 当前边就是p的最小值 return 0; } } printf("0");为什么这样有效?
如果从贵到便宜累加时,在边
u[i]处总花费超过c说明如果
u[i]及所有比它贵的边都要牛牛修,预算不够因此国家必须免费修
u[i]及所有损坏值 ≤u[i]的边所以p至少为
u[i]正确性证明
单调性保证
p 越大 → 国家免费修的边越多 → 牛牛要修的边越少 → 总花费越小
最优性保证
MST保证:使用最便宜的边集实现连通
修复顺序保证:先修便宜边使总花费最小化
p的选择保证:找到刚好满足预算的临界点
代码实现
C/C++版本:
#include
#include
#include
#define int long long // 将int定义为long long类型,防止溢出
using namespace std;
const int N=1e5+10;
int p[N]; // 并查集数组,用于判断连通性
vector u; // 存储最小生成树中的边权值
int n,m,c; // n:城市数, m:道路数, c:牛牛拥有的钱数
int cnt; // 记录已选择的边数
// 定义边的结构体
struct node{
int a; // 边的起点
int b; // 边的终点
int w; // 边的损坏值
}edge[N];
// 比较函数,按损坏值从小到大排序
bool cmp(node a,node b){
return a.w>n>>m>>c;
// 初始化并查集
for(int i=0;i>a>>b>>w;
edge[i]={a,b,w};
}
// 按损坏值从小到大排序
sort(edge,edge+m,cmp);
// 执行Kruskal算法,得到最小生成树的所有边权值
kruskal();
// 关键部分:计算最小需要的p值
int k=1; // 修复操作的次数,第k次修复需要k*a_i元
int sum=0; // 累计总花费
// 从损坏值最大的边开始遍历(因为我们要找临界点)
for(int i=u.size()-1;i>=0;i--){
// 计算修复当前边需要的花费:第k次操作 * 边的损坏值
sum+=u[i]*(k++);
// 如果总花费超过了牛牛的钱数c
if(sum>c) {
// 输出当前边的损坏值,这就是p的最小值
// 因为国家会免费修复所有损坏值<=p的道路
// 我们需要让这条昂贵的边由国家来修
printf("%lld",u[i]);
return 0;
}
}
// 如果所有边修复完总花费都不超过c,说明国家不需要修复任何道路
printf("0");
}
Python版本:
import sys
def main():
def find(x, parent):
if parent[x] != x:
parent[x] = find(parent[x], parent)
return parent[x]
# 读取输入
data = sys.stdin.read().strip().split()
n, m, c = int(data[0]), int(data[1]), int(data[2])
edges = []
idx = 3
for i in range(m):
u = int(data[idx]) - 1 # 转换为0-based索引
v = int(data[idx+1]) - 1
w = int(data[idx+2])
idx += 3
edges.append((u, v, w))
# 按边权值排序
edges.sort(key=lambda x: x[2])
# Kruskal算法求最小生成树
parent = list(range(n))
mst_edges = []
for u, v, w in edges:
pu = find(u, parent)
pv = find(v, parent)
if pu != pv:
parent[pu] = pv
mst_edges.append(w)
# 如果MST边数不足n-1,说明图不连通(但题目保证连通)
if len(mst_edges) < n - 1:
print(0)
return
# 将MST边按权值排序(其实已经是有序的,但为了清晰还是排序)
mst_edges.sort()
# 从大到小计算总花费,寻找临界点
k = 1
total_cost = 0
# 从最贵的边开始向前计算
for i in range(len(mst_edges) - 1, -1, -1):
# 计算修复当前边需要的花费:第k次操作 × 边的损坏值
total_cost += mst_edges[i] * k
k += 1
# 如果总花费超过预算
if total_cost > c:
# 当前边的损坏值就是p的最小值
# 因为国家需要免费修这条边和所有比它便宜的边
print(mst_edges[i])
return
# 如果所有边修复完总花费都不超过c
print(0)
if __name__ == "__main__":
main()
Java版本:
import java.util.*;
import java.io.*;
public class Main {
static int[] parent;
// 并查集查找函数
static int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]);
}
return parent[x];
}
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 c = Long.parseLong(st.nextToken());
// 存储所有边
int[][] edges = new int[m][3];
for (int i = 0; i < m; i++) {
st = new StringTokenizer(br.readLine());
int u = Integer.parseInt(st.nextToken()) - 1; // 转换为0-based
int v = Integer.parseInt(st.nextToken()) - 1;
int w = Integer.parseInt(st.nextToken());
edges[i] = new int[]{u, v, w};
}
// 按边权值排序
Arrays.sort(edges, (a, b) -> Integer.compare(a[2], b[2]));
// 初始化并查集
parent = new int[n];
for (int i = 0; i < n; i++) {
parent[i] = i;
}
// Kruskal算法求最小生成树
List mstEdges = new ArrayList<>();
for (int[] edge : edges) {
int u = edge[0], v = edge[1], w = edge[2];
int pu = find(u);
int pv = find(v);
if (pu != pv) {
parent[pu] = pv;
mstEdges.add(w);
}
}
// 如果MST边数不足n-1,说明图不连通(但题目保证连通)
if (mstEdges.size() < n - 1) {
System.out.println(0);
return;
}
// 将MST边按权值排序(其实已经有序,但为了清晰还是排序)
Collections.sort(mstEdges);
// 从大到小计算总花费,寻找临界点
int k = 1;
long totalCost = 0;
int result = 0;
// 从最贵的边开始向前计算
for (int i = mstEdges.size() - 1; i >= 0; i--) {
int weight = mstEdges.get(i);
// 计算修复当前边需要的花费:第k次操作 × 边的损坏值
totalCost += (long) weight * k;
k++;
// 如果总花费超过预算
if (totalCost > c) {
// 当前边的损坏值就是p的最小值
// 因为国家需要免费修这条边和所有比它便宜的边
result = weight;
break;
}
}
// 输出结果:如果result为0表示国家不需要帮忙,否则输出p的最小值
System.out.println(result);
}
}
复杂度分析
排序:O(m log m)
Kruskal:O(m α(n)),其中α是反阿克曼函数
p的查找:O(n)
总复杂度:O(m log m),在题目约束下完全可行
好了,各位码友,代码已经调试通过,文章也已commit,就等各位的push了。点赞不要 //TODO,关注务必 star!
写在后面:


浙公网安备 33010602011771号