最小生成树

无向图:图的边没有标明方向
无向连通图:满足任意两点均可经过任意条边相互到达的无向图
树:有 \(n\) 个点和 \(n-1\) 条边的无向连通图
无向连通图的生成树:选定图上的一些边,将所有点连成树的形状
最小生成树:假定边带有权值,在所有生成树中,选定边的边权之和最小的生成树

最小生成树一定包含权值最小的边

证明考虑反证法
如果不包含的话,一定能找到一条将这条边的两个点割成两个部分的边
切掉这条边加入权值最小的边,肯定会让权值更小,与“最小”矛盾

推广:
如果已经确认了 \(k\) 条边,那么剩下的 \(n-1-k\) 条边一定满足:
1.连接了两个之前不连通的节点 2.权值最小

Prim

类似于Dijsktra算法
考虑每次加入一个点,假设已经确定最小生成树的点集为 \(V\) 剩下的部分为 \(S\)
贪心地选取满足 \(min{w_{x,y}},x\in S,y\in V\) 取到最小值的点 \(x\)
\(x\) 加入 \(V\) 连带这条边算进总权值里,然后用 \(x\) 更新一下集合 \(S\)

类似的,选边权最小值的暴力是 \(O(n^2)\)
这个过程同样可以用堆维护,时间复杂度 \(O(m\log m)\)
不过同复杂度下,我们有另外一个算法,相对而言更加简洁

Prim
#include <bits/stdc++.h>
using namespace std;
const int p=222;
int m[p][p],dis[p];
int n;
int ans;
void in(){
    scanf("%d",&n);
    for(int i=1;i<=n;i++){
        for(int j=1;j<=n;j++){
            scanf("%d",&m[i][j]);
        }
    }
}
void prim(int x){
    ans=0;
    for(int i=1;i<=n;i++){
        dis[i]=m[x][i];
    }
    dis[x]=0;
    for(int i=2;i<=n;i++){
        int min=0x7f7f7f7f,k;
        for(int j=1;j<=n;j++){
            if(dis[j]<min&&dis[j]!=0){
                min=dis[j];
                k=j;
            }
        }
        ans+=dis[k];
        dis[k]=0;
        for(int j=1;j<=n;j++){
            if(dis[j]>m[k][j]){
                dis[j]=m[k][j];
            }
        }
    }
}
void out(){
    printf("%d",ans);
}
int main(){
    in();
    prim(2);
    out();
    return 0;
}

Kruscal

采用并查集维护最小生成树

贪心地选取边权尽量小的边,如果这两个点没有被连通就选上,同时维护新的连通性

正确性显然,时间复杂度为 \(O(m\log m)\)

Kruskal
#include <bits/stdc++.h>
using namespace std;
const int p=100001;
struct edge{
    int s;
    int t;
    int v;
}e[p];
int f[p],n,m;
int ans,t;
int find(int x){
    if(f[x]!=x){
        f[x]=find(f[x]);
    }
    return f[x];
}
bool cmp(const edge &a,const edge &b){
    return a.v<b.v;
}
void in(){
    for(int i=1;i<=m;i++){
        scanf("%d%d%d",&e[i].s,&e[i].t,&e[i].v);
    }
    sort(e+1,e+m+1,cmp);
}
void renew(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++){
        f[i]=i;
    }
}
void work(){
    for(int i=1;i<=m;i++){
        int x=e[i].s,y=e[i].t;
        int fx=find(x),fy=find(y);
        if(fx!=fy){
            f[fx]=fy;
            ans=e[i].v;
            t++;
        }
        if(t==n-1){
            break;
        }
    }
}
void out(){
    printf("%d %d\n",t,ans);
}
int main(){
    renew();
    in();
    work();
    out();
    return 0;
}

例题

P1195

维护一个 \(K\) 个连通块的森林

考虑克鲁斯卡尔的过程,本质上是由森林合并成树的过程
每合并两个连通块就会减少一个连通块
初始有 \(n\) 个连通块,维护一下当前连通块的总数即可

P1194

新建一个点 \(S\),从 \(1\)\(n\) 连一条 \((S,i)\) 边权为 \(A\) 的边
正常做最小生成树即可

P1396

看到最大值最小,大家可能会想到二分答案
需要二分吗?
从小到大加边直到两点连通就可以了

为什么?
因为任何小于边权 \(w\) 的路径都不可能使 \(s\) 到达 \(t\)
而且走别的路径只会让最大值更大
也就是最大值是单调不降的

CF1245D

前面建虚点的套路重了
注意到 \(n\leq 2000\) 并且是完全图
考虑边数的量级
用 Prim

P2700

答案要求最小化删边,所有边权已经知道
那么如果删干净之后,知道最大化加边即可求出答案
最大生成树同样是类似的过程
注意并查集维护的时候需要标记该集合是否包含特殊节点


习题

posted @ 2022-02-10 20:28  2K22  阅读(131)  评论(0)    收藏  举报