蓝桥杯历届试题-城市建设

题目


oj平台

解题基础知识:并查集+最小生成树

读完题目,它的题意首先我们要清楚,就是需要以最少的预算用水路或者陆路的方式将所有城市连在一起,我们把建设道路的成本看作是两个结点之间的权值,则把所有点连在一起的最小成本情况可以看作是一颗最小生成树,关于什么是最小生成树,做着这题之前我们一定要先了解一下,我将我以前写过的 [关于最小生成树的一切] 以及 [关于并查集的一切] 写在下面的链接里,建议先看看这两个方法。

关于最小生成树的一切

关于并查集的一切

此题最大的两个坑点

一、需要分类讨论为两种情况:建设码头 和 不建设码头

原因:

  • 由于有 n 个城市,按照最小生成树的定理,选取 n-1 条边最小生成树便形成,但是由于码头如果按照普通的城市点对点的边方式存储,则会形成 O(n^2) 的时间复杂度,这样是会超时的。

  • 所以我们换一种思路对码头的建设成本进行存储,我们虚拟一个 0 结点,把建设 i 码头的成本看作是 0 结点到 i 结点的建设成本,则所有城市的码头都可以通过这个 0 结点进行连接,也就形成了城市间的连接,这样将问题就化为了 O(n)

  • 但随之而来的是,我们相当于增加了一个中间城市,也就是 n+1 个城市了,而要把 n+1 个城市都连起来,则需要选取 n 条边形成最小生成树。

二、只要权值为负数便可以继续形成环

原因:

  • 题目中只要求用最小的预算,而并未限定不能形成多条回路,而按照最小生成树走下去,肯定是会把形成环的情况给跳过,只要某条边的权值是负数,那么管它形不形成环,建就完了,我建它反而会带来收益何乐而不为呢?

解题代码(仅供参考,重要是掌握最小生成树)

写并查集我一般喜欢写个类,感觉操作比较直观。如果堆也不会用的话,那么也是可以用数组自定义排序来实现的。

效率害行✔

#include<iostream>
#include <queue>
//edge1用于得到不用码头时候的答案
//edge2用于得到用了码头后的答案
using namespace std;

struct Edge {
    int a;
    int b;
    int len;

    //类的构造函数,方便赋值
    Edge(int a1, int b1, int c1) : a(a1), b(b1), len(c1) {}

    //重载小于号,便于堆的排序
    bool operator<(const Edge &x) const {
        return len > x.len;
    }
};

class UniFind {
public:
    UniFind(int n) : cnt(0), sum_num(n) {
        memo = new int[n + 1];
        for (int i = 0; i <= n; i++) {
            memo[i] = i;
        }
    }

    int Find(int x) {
        if (memo[x] == x)
            return x;
        return memo[x] = Find(memo[x]);
    }

    void merge(int x, int y) {
        int rootX = Find(x);
        int rootY = Find(y);
        if (rootX != rootY) {
            memo[rootX] = rootY;
            cnt++;
        }
    }

    bool isConnected(int x, int y) {
        return Find(x) == Find(y);
    }

    int get_cnt() {
        return cnt;
    }

    //用于重复使用这一个对象节省空间
    void clear() {
        for (int i = 0; i <= sum_num; i++) {
            memo[i] = i;
        }
        cnt = 0;
    }

private:
    int sum_num;
    int *memo;
    int cnt;
};

int main() {
    //取消与C语言io流的绑定,加快cin\cout速度
    ios::sync_with_stdio(false);
    int n;
    cin >> n;
    int m;
    cin >> m;
    //建立两个不同的优先队列存储边的数据,以及一个并查集查询连通
    priority_queue<Edge> edge1, edge2;
    UniFind check(n);
    int res1 = 0, res2 = 0;
    int cnt1 = 0;
    //存储建设道路的权值
    for (int i = 0; i < m; i++) {
        int a, b, c;
        cin >> a >> b >> c;
        edge1.push(Edge(a, b, c));
        edge2.push(Edge(a, b, c));
    }
    //计算不用码头时的最小预算,如果跳出循环后用于连通的边仍少于n-1则无法连通
    while (check.get_cnt() <= n - 1 && !edge1.empty()) {
        Edge x = edge1.top();
        edge1.pop();
        if (!check.isConnected(x.a, x.b)) {
            check.merge(x.a, x.b);
            res1 += x.len;
        } else if (x.len < 0) {    //即使顶点都已被连接,只要建了它还能赚一笔,继续建就完了
            res1 += x.len;
        }
    }
    cnt1 = check.get_cnt();
    //输入码头到edge2中
    for (int i = 1; i <= n; i++) {
        int x;
        cin >> x;
        if (x != -1) {
            edge2.push(Edge(0, i, x));
        }
    }
    //计算用码头情况的答案
    check.clear();
    while (check.get_cnt() <= n && !edge2.empty()) {
        Edge x = edge2.top();
        edge2.pop();
        if (!check.isConnected(x.a, x.b)) {
            check.merge(x.a, x.b);
            res2 += x.len;
        } else if (x.len < 0) {
            res2 += x.len;
        }
    }
    //打印小的那个,如果 cnt1<n-1则 res1无效。
    if (cnt1 < n - 1)cout << res2;
    else cout << min(res1, res2);

    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值