最小生成树 Minimum Spanning Tree

本文来自我的博客 最小生成树 Minimum Spanning Tree - Snow’s Blog (ivansnow02.github.io)

最小生成树 Minimum Spanning Tree

加权无向图的最小生成树(Minimum Spanning Tree,简称MST)是一棵生成树,其权(所有边的权值之和)不会大于其它任何生成树的权。

一个带权连通图G(假定每条边上的权值均大于零)可能有多棵生成树.
每棵生成树中所有边上的权值之和可能不同。
其中边上的权值之和最小的生成树称为图的最小生成树。

MST算法有很多,但其中最知名的是Prim算法和Kruskal算法

Prim算法

基本思路

从任何一个顶点开始作一棵单顶点MST,再为之增加V-1条边,每次增加的边都是将MST上的一个顶点和尚未在此MST上的一个顶点相连接的最小边。

假设G=(V,E)是一个具有n个顶点的带权连通图,T=(U,TE)是G的最小生成树,其中U是T的顶点集,TE是T的边集,则由G构造从起始点v出发的最小生成树T的步骤如下:

  1. 初始化U={v}。以v到其他顶点的所有边为候选边。
  2. 重复以下步骤n-1次,使得其他n-1个顶点被加入到U中:
    1. 从候选边中挑选权值最小的边加入TE(所有候选边一定是连接两个顶点集U和V-U的边),设该边在V-U中的顶点是k,将顶点k加入U中。
    2. 考察当前V-U中的所有顶点j,修改候选边:若(k,j)的权值小于原来和顶点j关联的候选边,则用(k,j)取代后者作为候选边

寻找最小边在这里使用小根堆

基于堆的Prim算法代码

struct Edge {
    int from, to, cost;
    Edge(int u, int v, int w):from(u), to(v), cost(w) {}
};
struct HeapNode {
    int d, u; // d表示点u到MST的距离
    bool operator< (const HeapNode& rhs) const {
        return d > rhs.d; // 小根堆
    }
};
vector<Edge> edges; // edges存所有边
vector<int> G[MAXN]; // G[i]是顶点i发出的所有边
bool done[MAXN]; // i是否已经加入了MST
int d[MAXN]; // 每个点到MST的最小距离
int Prim() {
    priority_queue<HeapNode> Q;
    int ans = 0; // 总权值
    for (int i = 0; i < V; ++i) d[i] = INF; // V是图的节点数
    d[0] = 0; // 从点0开始
    memset(done, 0, sizeof(done));
    Q.push((HeapNode){0, 0});
    while (!Q.empty()) {
        HeapNode x = Q.top(); Q.pop;
        int u = x.u; // u为队头元素的序号
        if (done[u]) continue; // 已经在MST中就跳过
        ans += x.d; // 加上出队的队头元素到MST的距离
        done[u] = true;
        for (int i = 0; i < G[u].size; ++i) { // 更新最小生成树的权值
            Edge& e = edges[G[u][i]]; // 顶点to能修改最小距离
            if (d[e.to] > e.cost) {
                d[e.to] = e.cost;
                Q.push((HeapNode){d[e.to], e.to});
            }
        }
    }
    return ans;
}
注意

本段代码用邻接表来存图,输入可以使用

for (int i = 0; i < E; ++i) { // E是边数 
    int u, v, cost;
    cin >> u >> v >> cost;  // 输入边的起点、终点和权值
    G[u].push_back(i);  // 存储边的索引
    edges.push_back(Edge(u, v, cost));
    G[v].push_back(i);  // 存储边的索引
    edges.push_back(Edge(v, u, cost));
}

分析

Prim时间复杂度为 O ( V l o g V + E l o g V ) O(VlogV + ElogV) O(VlogV+ElogV)。在稀疏图的情况下,E的数量通常远小于 V 2 V^2 V2,因此可以将时间复杂度近似为 O ( E l o g V ) O(ElogV) O(ElogV)。而在稠密图的情况下,E的数量接近 V 2 V^2 V2,时间复杂度会接近 O ( V 2 l o g V ) O(V^2logV) O(V2logV)

Kruskal算法

基本思路

以边的长度(从小到大)为顺序来处理,若一条边与前面加入到MST中的边未形成环,则将这样的边加入到MST中,增加了V-1条边后停止。也就是说开始是一个森林,每个顶点就是一棵独立的树,然后逐渐把这些树合并(通过一条最小边),最后形成的一棵树就是MST。

假设G=(V,E)是一个具有n个顶点的带权连通图,T=(U,TE)是G的最小生成树,则构造最小生成树的步骤如下:

  1. 置U的初值等于V(即包含有G中的全部顶点),TE的初值为空集(即图T中每一个顶点都构成一个分量)。
  2. 将图G中的边按权值从小到大的顺序依次选取:若选取的边未使生成树T形成回路,则加入TE;否则舍弃,直到TE中包含n-1条边为止。

如果把一棵树看成一个集合,那么对于新加入的边,要判断它的两个顶点是否已经在同一个集合了?是的话就跳过,处理下一条边;不是的话就把这条边加入到MST,同时把该边两顶点所处的两个集合合并。这个实际就是不相交集合(Disjoint Set)的并查(Union-Find)操作。

Kruskal算法代码

struct edge { 
    int u, v, cost; 
};

bool cmp(const edge& e1, const edge& e2) {
    return e1.cost < e2.cost;
}

edge es[MAX_E]; // 存储边的信息
int par[MAX_V]; // 存储父节点

// 初始化并查集
void init_union_find(int V) {
    for (int i = 0; i < V; ++i) {
        par[i] = i; // 初始时每个节点的父节点是自身
    }
}

// 查找根节点
int find(int x) {
    if (par[x] == x) {
        return x; // 根节点的父节点是自身
    } else {
        return par[x] = find(par[x]); // 路径压缩,将x的父节点设为根节点,加速后续查找
    }
}

// 合并集合
void unite(int x, int y) {
    x = find(x); // 查找x的根节点
    y = find(y); // 查找y的根节点
    if (x != y) {
        par[x] = y; // 将x的根节点设为y,合并两个集合
    }
}

// 判断两个节点是否属于同一个集合
bool same(int x, int y) {
    return find(x) == find(y); // 若两个节点的根节点相同,则属于同一个集合
}

int Kruskal(int V, int E) { 
    sort(es, es + E, cmp); // 按权值从小到大排序
    init_union_find(V); // 并查集初始化 
    int res = 0;
    for (int i = 0; i < E; ++i) { 
        edge e = es[i];
        if (!same(e.u, e.v)) { // u和v不属于一个集合
        	unite(e.u, e.v); // 合并u和v集合的元素
        	res += e.cost;
        }
    }
    return res;
}

分析

Kruskal算法的时间复杂度为 O ( E l o g E + E α ( V ) ) O(ElogE + Eα(V)) O(ElogE+Eα(V))。在稀疏图的情况下,E的数量通常远小于 V 2 V^2 V2,因此可以将时间复杂度近似为 O ( E l o g E ) O(ElogE) O(ElogE)。而在稠密图的情况下,E的数量接近 V 2 V^2 V2,时间复杂度会接近 O ( E α ( V ) ) O(Eα(V)) O(Eα(V))

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值