【ACWing】858. Prim算法求最小生成树

题目地址:

https://www.acwing.com/problem/content/860/

给定一个 n n n个点 m m m条边的无向图,图中可能存在重边和自环,边权可能为负数。求最小生成树的树边权重之和,如果最小生成树不存在则输出impossible。

数据范围:
1 ≤ n ≤ 500 1\le n\le 500 1n500
1 ≤ m ≤ 1 0 5 1\le m\le 10^5 1m105
1 ≤ ∣ e ∣ ≤ 10000 1\le |e|\le 10000 1e10000
e e e是边长

可以用Prim算法。这个图是稠密图,所以可以用朴素版Prim算法。这个算法的思路是这样的。维护一个集合 S S S和一个数组 d d d S S S是已经挑过的点,它们的最小生成树已经求出,而 d d d维护的是每个点到 S S S的最短距离,初始化为 ∞ \infty (这个最短距离的定义是 d [ v ] = min ⁡ w ∈ S { v , w } d[v]=\min_{w\in S}\{v,w\} d[v]=minwS{ v,w},对于 v ∈ S v\in S vS的情况无定义),每次都挑一个 d d d最小的点,将其加入 S S S(加入了 S S S的同时,就把它与 S S S的距离所对应的 S S S的那个点所连的边也加入了最小生成树),并以它来更新所有还没加入 S S S的点的最短距离。接着重复上述过程,每次循环都加一个点进 S S S,当所有点都加进去的时候,就得出了最小生成树。第一个加入 S S S的点没有限制,可以随便挑。但是如果在第 k > 1 k>1 k>1次挑的点,发现它与 S S S的最短距离是 ∞ \infty ,那说明这个点与 S S S里的点不连通,则最小生成树不存在,输出impossible。这里还需要注意一个问题。一定要先把点加入 S S S并累加最小生成树总权重,再用这个点更新最短距离。原因是,如果这个点自己到自己有个负环的话,更新完距离之后,它的 d d d值会把负环也给累加进去,这就不对了。代码如下:

#include <iostream>
#include <cstring>
using namespace std;

const int N = 510, INF = 0x3f3f3f3f;

int n, m;
int g[N][N];
int dist[N];
bool st[N];

int prim() {
    
    
    memset(dist, 0x3f, sizeof dist);

    int res = 0;
    for (int i = 0; i < n; i++) {
    
    
        int t = -1;
        // 找到还没入S的点中与S距离最短的点
        for (int j = 1; j <= n; j++)
            if (!st[j] && (t == -1 || dist[t] > dist[j])) t = j;
		
		// 如果不是第一次挑的点,并且所有不在S中的点与S距离都是正无穷,
		// 说明原图不连通,则不存在最小生成树
        if (i && dist[t] == INF) return INF;

		// 把点t加入S,并累加边权
        if (i) res += dist[t];
        st[t] = true;

        for (int j = 1; j <= n; j++) 
            if (!st[j]) dist[j] = min(dist[j], g[t][j]);
    }

    return res;
}

int main() {
    
    
    scanf("%d%d", &n, &m);
    
    memset(g, 0x3f, sizeof g);

    while (m--) {
    
    
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        // 平行边只选边权最小的那个边
        g[a][b] = g[b][a] = min(g[a][b], c);
    }

    int t = prim();

    if (t == INF) cout << "impossible" << endl;
    else cout << t << endl;

    return 0;
}

时空复杂度 O ( n 2 ) O(n^2) O(n2)

也可以用堆优化版的Prim算法来做。思路和朴素版一样,只是在挑点的时候,可以用最小堆来每次挑出一个距离 S S S最近的点。代码如下:

#include <iostream>
#include <cstring>
#include <queue>
using namespace std;

typedef pair<int, int> PII;

const int N = 100010, M = 2 * N, INF = 0x3f3f3f3f;

int n, m;
int h[N], w[M], e[M], ne[M], idx;
int cnt;
int dist[N];
bool st[N];

void add(int a, int b, int c) {
    
    
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}

int prim() {
    
    
    memset(dist, 0x3f, sizeof dist);
    
    priority_queue<PII, vector<PII>, greater<PII> > heap;
    heap.push({
    
    INF, 1});
    dist[1] = 0;

    int res = 0;
    while (!heap.empty()) {
    
    
        auto t = heap.top();
        heap.pop();

        int v = t.second, c = t.first;

        if (st[v]) continue;

        if (v != 1) {
    
    
            res += c;
            cnt++;
            if (cnt == n - 1) break;
        }

        st[v] = true;

        for (int i = h[v]; i != -1; i = ne[i]) {
    
    
            int j = e[i], c = w[i];
            if (!st[j] && dist[j] > c) {
    
    
                dist[j] = c;
                heap.push({
    
    c, j});
            }
        }
    }

    return cnt < n - 1 ? INF : res;
}

int main() {
    
    
    scanf("%d%d", &n, &m);

    memset(h, -1, sizeof h);

    while (m--) {
    
    
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        if (a == b) continue;
        add(a, b, c), add(b, a, c);
    }

    int t = prim();

    if (t == INF) cout << "impossible" << endl;
    else cout << t << endl;

    return 0;
}

时间复杂度 O ( m log ⁡ n ) O(m\log n) O(mlogn),空间 O ( n + m ) O(n+m) O(n+m)

猜你喜欢

转载自blog.csdn.net/qq_46105170/article/details/113824532