最小生成树、次小生成树、Kruskal重构树

专门开个博客一是因为没地放了,二是以后次小生成树什么的就一块扔这了。

最小生成树

点数n,边数m的图的最小生成树大概有两个算法:

  1. Kruskal算法(\(O(m\log m)\))

思路非常简单粗暴,把所有边扔出来按照边权排个序,然后拿并查集维护点的连通关系,最后选出n-1条边。

int kruskal(int x){
    sort(edge+1,edge+t+1,cmp);
    chushihua();
    int sum=0,cnt=0;
    for(int i=1;i<=t;i++){
        int x=edge[i].u,y=edge[i].v;
        if(find(x)!=find(y)){
            father[y]=x;
            sum+=edge[i].w;
            cnt++;
        }
        if(cnt==n-1)break;
    }
    return sum;
}
  1. Prim算法(\(O(n^2)\)

这个是每次维护最小生成树的一部分边,类似Dijkstra。(这玩意真没啥意思)

同样的,用堆优化可以到\(O(mlogn)\),但是你还不如直接写kruskal。

int prim(int x){
    int sum=0;memset(a,0x3f,sizeof(a));
    for(int i=1;i<=n;i++)dis[i]=a[x][i];
    v[x]=true;
    for(int i=2;i<=n;i++){
        int min=2147483647,k;
        for(int j=1;j<=n;j++){
            if(!v[j]&&dis[j]<min)min=dis[j];k=j;
        }
        sum+=dis[k];v[k]=true;//找一条与当前生成树相连的最小的边记录答案 
        for(int j=1;j<=n;j++){
            if(!v[j]&&dis[j]>a[k][j])dis[j]=a[k][j];//更新边 
        }
    }
    return sum;
}
  1. Boruvka算法(听完林老师讲课看看原题题解看到的)(\(O(m\log n)\)

首先定义最小边是一个连通块向其他连通块连的边中边权最小的一个。这个算法的大体思路是初始将每个点视作一个连通块,通过最小边合并连通块(共\(\log n\)次合并),最终形成最小生成树。这个东西一般不会让你写,但是不太好建边的时候可能有奇效(比如这道题)。

int link[5010],val[5010];
void boruvka(int n){
	int ans=0,num=0;
	bool jud=true;
	while(jud){
		jud=false;
		memset(link,0,sizeof(link));
		memset(val,0x3f,sizeof(val));
		for(int i=1;i<=n;i++){
			int x=find(i);
			for(int j=head[i];j;j=edge[j].next){
				int y=find(edge[j].v);
				if(val[x]>edge[j].w&&x!=y){//找到连通块i的最小边 
					val[x]=edge[j].w;//如果该边两端点不都属于连通块且边权更小则更新 
					link[x]=y;
				}
			}
		}
		for(int i=1;i<=n;i++){
			int x=find(i);
			if(find(i)==i){
				if(link[x]&&x!=find(link[x])){
					merge(x,link[x]);ans+=val[x];//连接最小边两端的两个连通块 
					jud=true;num++;//计入合并次数(洛谷的无解要特判一下) 
				}
			}
		}
	}
	if(num==n-1)printf("%d",ans);
	else printf("orz");
}

还有一个小定理就是一张图的所有最小生成树的每种权值的边的数量是相同的。

关于oi-wiki上那个语焉不详的最小生成树的唯一性,实际上就是对于每个相同权值的边进行这样一个过程:

  1. 先扫一遍找到可能向最小生成树里加多少边(就是两端点属于的边集不同)
  2. 跑Kruskal,看实际加入了多少边
  3. 如果不相同则不唯一

次小生成树

首先最小生成树和次小生成树只差一条边。

  1. 非严格次小生成树:把最小生成树的每条边标记一下,然后对所有没标记的边 \((u,v)\) 求出 \((u,v)\) 路径上边权的最大值,用这条边替换,得到的所有答案的最小值就是非严格次小生成树。倍增即可,代码就不放了。
  2. 严格次小生成树:这个为了保证选的这条边和边权最大值不同还得记录一个次大值。这个刚写了放一下。


#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cstring>
#define int long long
using namespace std;
const int inf=0x3fffffff;
int n,m;
struct node{
    int v,w,next;
}edge[600010];
struct E{
    int u,v,w;
    bool operator<(const E &s)const{
        return w<s.w;
    }
}g[300010];
int t,ans=__LONG_LONG_MAX__,sum,head[100010],f[100010];
bool v[300010];
int find(int x){
    return x==f[x]?f[x]:f[x]=find(f[x]);
}
void add(int u,int v,int w){
    edge[++t].v=v;edge[t].w=w;
    edge[t].next=head[u];head[u]=t;
}
int dep[100010],fa[100010][20],mx[100010][20],se[100010][20];
void dfs(int x,int f){
    dep[x]=dep[f]+1;fa[x][0]=f;se[x][0]=-inf;
    for(int i=1;i<=__lg(dep[x]);i++){
        fa[x][i]=fa[fa[x][i-1]][i-1];
        int ret[4]={mx[x][i-1],mx[fa[x][i-1]][i-1],se[x][i-1],se[fa[x][i-1]][i-1]};
        sort(ret,ret+4);
        mx[x][i]=ret[3];
        int p=2;
        while(p>=0&&ret[p]==ret[3])p--;
        se[x][i]=(p==-1?-inf:ret[p]);
    }
    for(int i=head[x];i;i=edge[i].next){
        if(edge[i].v!=f){
            mx[edge[i].v][0]=edge[i].w;
            dfs(edge[i].v,x);
        }
    }
}
int lca(int x,int y){
    if(dep[x]>dep[y])swap(x,y);
    for(int i=__lg(n);i>=0;i--)if(dep[fa[y][i]]>=dep[x])y=fa[y][i];
    if(x==y)return x;
    for(int i=__lg(n);i>=0;i--)if(fa[x][i]!=fa[y][i])x=fa[x][i],y=fa[y][i];
    return fa[x][0];
}
int query(int x,int y,int val){
    int ans=-inf;
    for(int i=__lg(n);i>=0;i--){
        if(dep[fa[x][i]]>=dep[y]){
            if(val!=mx[x][i])ans=max(ans,mx[x][i]);
            else ans=max(ans,se[x][i]);
            x=fa[x][i];
        }
    }
    return ans;
}
signed main(){
    scanf("%lld%lld",&n,&m);
    for(int i=1;i<=m;i++)scanf("%lld%lld%lld",&g[i].u,&g[i].v,&g[i].w);
    sort(g+1,g+m+1);
    for(int i=1;i<=n;i++)f[i]=i;
    int cnt=0;
    for(int i=1;i<=m;i++){
        int x=find(g[i].u),y=find(g[i].v);
        if(x!=y){
            sum+=g[i].w;f[y]=x;
            add(g[i].u,g[i].v,g[i].w);add(g[i].v,g[i].u,g[i].w);
            v[i]=true;
        }
    }
    dfs(1,0);
    for(int i=1;i<=m;i++){
        if(!v[i]){
            int lc=lca(g[i].u,g[i].v);
            int tmpa=query(g[i].u,lc,g[i].w),tmpb=query(g[i].v,lc,g[i].w);
            if(max(tmpa,tmpb)!=-inf)ans=min(ans,sum-max(tmpa,tmpb)+g[i].w);
        }
    }
    printf("%lld\n",ans);
    return 0;
}

Kruskal重构树

实际上并不是什么很高深的东西。

考虑我们Kruskal的过程,我们每次选边的时候新建一个节点,它的权值是这条边的边权,左右儿子是这条边的两个端点,这样最后会出现一棵二叉树,就是Kruskal重构树。

一个重要性质是:原图中两个点之间的所有简单路径上最大边权的最小值 = 最小生成树上两个点之间的简单路径上的最大值 = Kruskal 重构树上两点之间的 LCA 的权值。可能有点绕,不过很好理解。

posted @ 2022-09-03 19:05  gtm1514  阅读(19)  评论(0编辑  收藏  举报