最小生成树 (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都可以做,快去试试吧

相关推荐
Daniel_1231 小时前
数组——总结篇
算法
不懒不懒1 小时前
【针对路面识别数据集,结合三轴加速度标准化数据及多路面识别需求,以下是算法选择与处理方案】
算法
Reart2 小时前
Leetcode 121. 买卖股票的最佳时机(717)
后端·算法
会编程的小孩2 小时前
初识数据类型以及变量定义
数据结构·算法
Reart2 小时前
Leetcode 337.打家劫舍3(717)
后端·算法
梅梅绵绵冰2 小时前
数据结构-时间复杂度
数据结构·算法
遥感知识服务2 小时前
盐碱地、白屋顶和裸土都很亮,卫星怎样分清?
人工智能·算法·机器学习
可编程芯片开发2 小时前
基于PID控制器的车辆控制系统simulink建模与仿真
算法
广州灵眸科技有限公司2 小时前
瑞芯微RV1126B开发板(EASY-EAI-PI2) 系统操作-线进程操作
数据库·单片机·嵌入式硬件·算法·php