最小生成树

无向图:图的边没有标明方向
无向连通图:满足任意两点均可经过任意条边相互到达的无向图
树:有 \(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  阅读(185)  评论(0)    收藏  举报