最小生成树 (MST基础)

1.引言

第一篇blog,就挑点简单的东西写吧。

有一个这样的问题。有n个城市,先要在城市之间修路,城市u与城市v之间修路的成本是 au,v a_{u,v} au,v,求让所有城市建通所需要的最短成本。这就是经典的最小生成树(MST)问题。

2.MST基础

前置概念:

生成树:包含所有顶点,无环,联通的子图;

最小生成树:边权和最小的生成树;

MST本质就是给你一个图,让你求最小生成树,这种问题我们通常用贪心的策略解决。

下面介绍两种求最小生成树的方法。

1.Kruskal算法

贪心选边,将所有边按边权从小到大排序,每次考虑能不能将边权最小的边加入树。若加入这条边后不形成环,就允许加入树。直到选出n-1条边。具体动画演示可以看这里。那下面直接给出代码

ini 复制代码
struct Edge {
    int u, v, w;
    bool operator<(const Edge& other) const {
        return w < other.w;
    }
};

vector<Edge> edges; // 存所有边
vector<int> parent, sz;

int find(int x) {
    return parent[x] == x ? x : parent[x] = find(parent[x]);
}//找祖先

bool unite(int a, int b) {
    a = find(a), b = find(b);
    if (a == b) return false;//在同一个连通块,再加边则成环了
    if (sz[a] < sz[b]) swap(a, b);
    parent[b] = a;
    sz[a] += sz[b];
    return true;
}

ll kruskal(int n) {
    sort(edges.begin(), edges.end());
    parent.resize(n+1);
    sz.assign(n+1, 1);
    for(int i=0;i<parent.size();i++){
        parent[i]=i;//父节点指向自己
    }
    ll res = 0;
    int cnt = 0;//已经加入的边的个数
    for (auto [u, v, w] : edges) {
        if (unite(u, v)) {
            res += w;
            if (++cnt == n-1) break;
        }
    }
    return res;
}

设边数m,时间复杂度O(mlogm).n为点数,m为边数,后续分析一样。 Kruskal算法是从边出发的,但是存在稠密图(即边很多的图,尤其是完全图),m极大,对Kruskal很不友好,那么就需要从点出发了。

2.Prim算法

只关注于一个联通块,选择其连出去的边权最小的边,将边和新点加入,用优先队列维护候选边。动态演示上面视频有,直接给出代码

arduino 复制代码
vector<pair<int,int>> adj[N]; // adj[u] = {v, w} 邻接表,u连到v权值w
bool vis[N];

ll prim(int n, int start = 1) {
    priority_queue<pair<int,int>, vector<pair<int,int>>, greater<>> pq;
    //pq按第一个数(w,权值)顺序排序
    pq.push({0, start});
    ll res = 0;
    int cnt = 0;
    while (!pq.empty()) {
        auto [w, u] = pq.top(); pq.pop();
        if (vis[u]) continue;
        vis[u] = true;
        res += w;
        if (++cnt == n) break;
        for (auto [v, nw] : adj[u]) {
            if (!vis[v]) pq.push({nw, v});
        }//目标连通块的所有未选连出边都在pq里
    }
    return res;
}

时间复杂度O(mlogn).

各位注意方法选择,稠密图用Prim会更优

3.Borůvka 算法

具体步骤如下:

初始时,每个点都是一个连通分量

重复一下步骤,直至仅剩一个连通分量:

对于每一个连通分量,找到它连向其他边的最小边权

将这些最小边权全部加到MST中

如图:

下面给出代码

struct 复制代码
    int u, v, w;
};

int find(int x, vector<int>& parent) {
    return parent[x] == x ? x : parent[x] = find(parent[x], parent);
}

bool unite(int a, int b, vector<int>& parent, vector<int>& sz) {
    a = find(a, parent), b = find(b, parent);
    if (a == b) return false;
    if (sz[a] < sz[b]) swap(a, b);
    parent[b] = a;
    sz[a] += sz[b];
    return true;
}

ll boruvka(int n, vector<Edge>& edges) {
    vector<int> parent(n + 1), sz(n + 1, 1);
    iota(parent.begin(), parent.end(), 0);

    ll res = 0;
    int components = n;

    while (components > 1) {
        // minEdge[i] 存分量 i 的最小出边 {u, v, w}
        vector<Edge> minEdge(n + 1, {0, 0, INT_MAX});

        for (auto& e : edges) {
            int u = find(e.u, parent);
            int v = find(e.v, parent);
            if (u == v) continue;               // 同分量,跳过
            if (e.w < minEdge[u].w) minEdge[u] = e;  // 更新 u 的最小出边
            if (e.w < minEdge[v].w) minEdge[v] = e;  // 更新 v 的最小出边
        }

        bool merged = false;
        for (int i = 1; i <= n; i++) {
            if (minEdge[i].w == INT_MAX) continue;  // 这个分量没有出边了
            int u = minEdge[i].u, v = minEdge[i].v, w = minEdge[i].w;
            if (unite(u, v, parent, sz)) {
                res += w;
                components--;
                merged = true;
            }
        }

        if (!merged) break;  // 没有可以合并的了(图不连通)
    }

    return components == 1 ? res : -1;  // -1 表示图不连通
}

用于可以快速找出最小边的题目,不是很常用。 3.例题 \[\](造桥与砍树 - Problem - QOJ.ac) Prim和Borůvka都可以做,快去试试吧

相关推荐
月光船幽幽9 小时前
呼吸孔设计重塑系统稳定性
python·科技·算法
旖旎夜光10 小时前
LeetCode 991: 坏了的计算器(贪心算法) —— 题解
数据结构·c++·算法·leetcode·贪心算法
旖旎夜光10 小时前
LeetCode 553:最优除法(贪心算法) —— 题解
数据结构·c++·算法·leetcode·贪心算法
雾喔10 小时前
算法练习7
java·数据结构·算法
过期的秋刀鱼!10 小时前
重点-偏差方差与神经网络
人工智能·深度学习·神经网络·算法·机器学习·过拟合·l2正则化
可编程芯片开发11 小时前
基于模糊PID控制的六步逆变器供电无刷直流电机调速simulink仿真
算法
imperialeast12 小时前
WinAXP音乐播放器8月4日更新
windows·算法·ui
ZhouDevin12 小时前
算法论文/模型微调1——CNN架构做lora微调训练
人工智能·深度学习·算法·架构·cnn
小月土星12 小时前
LeetCode 21. 合并两个有序链表 超详细题解(虚拟头结点 + 逐行代码拆解)
算法
oier_Asad.Chen12 小时前
【OI学习笔记】Floyed-Warshall算法解决传递闭包问题
c++·笔记·学习·算法·图论·最短路·传递闭包