完整教程:旅游(牛客)

写在前面:

牛客每日一题持续更新中!

今天给彦祖和亦菲们带来的是 旅游

题目如下:

题目描述

  • 有 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 越大 → 国家免费修的边越多 → 牛牛要修的边越少 → 总花费越小

最优性保证

  1. MST保证:使用最便宜的边集实现连通

  2. 修复顺序保证:先修便宜边使总花费最小化

  3. 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!

写在后面:

posted @ 2025-12-16 09:04  gccbuaa  阅读(17)  评论(0)    收藏  举报