最小生成树(MST, Minimum Spanning Tree)

一、概述

最小生成树问题顾名思义,概括来说就是路修的最短。

接下来引入几个一看就明白的定义:

最小生成树相关概念:

带权图:边赋以权值的图称为网或带权图,带权图的生成树也是带权的,生成树T各边的权值总和称为该树的权。

最小生成树(MST):权值最小的生成树。

最小生成树的性质:假设G=(V,E)是一个连通网,U是顶点V的一个非空子集。若(u,v)是一条具有最小权值的边,其中u∈U,v∈V-U,则必存在一棵包含边(u,v)的最小生成树。

完成构造网的最小生成树必须解决下面两个问题:

  • 尽可能选取权值小的边,但不能构成回路;
  • 选取n-1条恰当的边以连通n个顶点;

prim算法适合稠密图,kruskal算法适合简单图。

二、Prim算法

首先,假设有一棵只包含一个顶点v的树T,然后贪心地选取T和其它顶点之间相连的最小权值的边,并把它加到T中,不断进行这个操作,就可以得到一棵生成树了,可证这是一棵最小生成树

如何查找最小权值的边?假设已经求得的生成树的顶点的集合是X,把X集合和顶点v连接的边的最小权值记为mincost[v],在向X里添加顶点u时,只需要查看和u相连的边就可以了,对于每条边,更新mincost[v]=min(mincost[v],边(u,v)的权值)
如果每次都遍历为包含在X中的点的mincost[v],需要O(|V|2)的时间,不过和Dijkstra一样我们可以使用堆来维护mincost,时间复杂度就是O(|E|log|V|) 

#include <cstdio>
#include <queue>
#include <vector>
#include <utility>

using namespace std;

typedef pair<int, int> P;
struct Edge
{
    int to, cost;
};

const int MAX_V=5000;
const int INF=0x3f3f3f3f;
int V, E, N;
vector<Edge> G[MAX_V];
int res;
int mincost[MAX_V];
bool used[MAX_V];

void prim()
{
    for (int i=0; i<V; i++)
    {
        mincost[i]=INF;
        used[i]=false;
    }
    mincost[0]=0;
    priority_queue<P, vector<P>, greater<P> > que;
    que.push(P(0, 0));
    while (!que.empty())
    {
        P p=que.top(); que.pop();
        int v=p.second;
        if (mincost[v]<p.first) continue;
        res+=mincost[v];
        N++;
        used[v]=true;
        for (int i=0; i<G[v].size(); i++)//加入顶点v时只要查看和顶点v相连的边即可 
        {
            if (mincost[G[v][i].to]>G[v][i].cost && !used[G[v][i].to])//更新mincost,入队 
            {
                mincost[G[v][i].to]=G[v][i].cost;
                que.push(P(G[v][i].cost, G[v][i].to));
            }
        }
    }
}

int main()
{
    scanf("%d %d", &V, &E);
    for (int i=0; i<E; i++)
    {
        int s, t, c;
        scanf("%d %d %d", &s, &t, &c);
        Edge e;
        e.to=t-1; e.cost=c;
        G[s-1].push_back(e);
        e.to=s-1;
        G[t-1].push_back(e);
    }
    prim();
    if (N <V) puts("orz");
    else printf("%d
", res);
}

  

三、kruskal算法

kruskal远离更为简单粗暴,但是需要借助并查集这一知识,来判断是否存在圈。

Kruskal算法按照边的权值的大小从小到大查看一遍,如果不产生圈(重边等也算在内),就把当前这条边加入到生成树中,一共加V-1条边。

如何判断是否产生圈? 假设现在要把连接顶点u和顶点v的边e加入生成树中。如果加入之前u和v不在同一个连通分量里,那么加入e也不会产生圈,反之,如果u和v在同一个连通分量里,那么一定会产生圈,可以用并查集高效地判断是否属于同一个连通分量

Kruskal在边的排序上最费时,算法的复杂度是O(|E|log|V|). 

#include <cstdio>
#include <queue>
#include <vector>
#include <algorithm>
#include <cctype>
#define num s-'0'

using namespace std;

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

const int MAX_V=5000;
const int MAX_E=200000;
const int INF=0x3f3f3f3f;
int V,E,n;
edge es[MAX_E];
int res;
int par[MAX_V];
int r[MAX_V];

void kruskal();
void init();
int find(int);
void unite(int, int);
bool same(int, int);

void read(int &x){
    char s;
    x=0;
    bool flag=0;
    while(!isdigit(s=getchar()))
        (s=='-')&&(flag=true);
    for(x=num;isdigit(s=getchar());x=x*10+num);
    (flag)&&(x=-x);
}

void write(int x)
{
    if(x<0)
    {
        putchar('-');
        x=-x;
    }
    if(x>9)
        write(x/10);
    putchar(x%10+'0');
}

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

int main()
{
    read(V);read(E);
    for (int i=0; i<E; i++)
    {
        read(es[i].u);read(es[i].v);read(es[i].cost);
        es[i].u--;es[i].v--;
    }
    kruskal();
    write(res);
    putchar('
');
}

void init()
{
    for (int i=0; i<V; i++)
    {
        par[i]=i;
        r[i]=0;
    }
}

int find(int x)
{
    if (par[x]==x) return x;
    return par[x]=find(par[x]);
}

void unite(int x, int y)
{
    x=find(x);
    y=find(y);
    if (x==y) return;
    if (r[x]<r[y]) par[x]=y;
    else
    {
        par[y]=x;
        if (r[x]==r[y]) r[x]++; 
    } 
}

bool same(int x, int y)
{
    return (find(x)==find(y));
}

void kruskal()
{
    sort(es, es+E, compare);
    init();
    int n=0;
    for (int i=0; i<E; i++)
    {
        edge e=es[i];
        if (!same(e.u, e.v))
        {
            unite(e.u, e.v);
            res+=e.cost;
            ++n;
        }
        if (n==V-1) break;
    }
}

  

原文地址:https://www.cnblogs.com/jaszzz/p/12870282.html