最小生成树与严格次小生成树 从生成到生存

一.前言

本文仍是应室友间的学习交流而生可能有些私货(但并不影响阅读) 请谅解

作者是个蒟蒻 如果本文有错别字 错理解 错表达 欢迎来评论区指出 谢谢!

那么我们现在就进入最小生成树的世界吧

二.简介及定义

(在学习此之前建议先学了解一点关于 生成子图和生成树 的基本定义 如果条件允许还可以了解下简单运用 当然由于我的精力有限 不知道的可以自行出门右转上百度)

我们定义无向连通图的 最小生成树(Minimum Spanning Tree,MST)为边权和最小的生成树

通俗易懂的讲就是最小生成树包含所有节点而只最少的边最小的权值距离。因为n个节点最少需要n-1个边联通,而距离就需要采取某种策略选择恰当的边

PS.只有连通图才有生成树!!!(对于非连通图只存在生成森林)

三.Kruskal 算法

Kruskal算法采用的是边贪心思想 是通过从小到大加入边来实现

步骤如下

1.先对图中所有的边按照权值进行排序
2.如果当前这条边的两个顶点不在一个连通块里面,那么咋就用并查集的Union函数把他们合并在一个连通块里面(也就是把他们放在最小生成树里面),如果再在一个并查集(lmc的文章应该写了吧)里面,我们就舍弃这条边,不需要这条边。
3.重复执行步骤2,知道当边数等于n-1(n代表点的个数),那就说明这n个顶点就连合并在一个集合里面了;反之,如果边数不等于顶点数目减去1,那么说明这些边就不连通。

是不是很简单?现在我们来通过几张手绘图来加深对此过程的理解吧
 

理论证明

(在你没有多余时间时可以选择性跳过 此板块设计只是为了加深对算法的理解 方便应用) 

思路很简单,为了造出一棵最小生成树,我们从最小边权的边开始,按边权从小到大依次加入,如果某次加边产生了环,就扔掉这条边,直到加入了n-1条边,即形成了一棵树(为什么不成环

 证明 归纳法,证明任何时候 K 算法选择的边集都被某棵 MST 所包含

 基础条件 对于算法刚开始时显然成立,即最小生成树存在

 假设某时刻成立,当前边集为 f,令 t 为这棵 MST,考虑下一条加入的边 e。如果 e 属于 t , 那 么   成立

 否则,t+e 一定存在一个环,考虑这个环上不属于 f 的另一条边 ff(一定只有一条)

 首先,ff 的权值一定不会比 e 小,不然  f 会在 e 之前被选取

 其次,ff 的权值一定不会比 e 大,不然 t+e-ff 就是一棵比 t 还优的生成树了

 所以, t+e-ff 包含了 f,并且也是一棵最小生成树,归纳成立

 代码模板

#include<bits/stdc++.h>
using namespace std;
const int maxn = 1000001;
int boss[maxn];//boss数组存放的是父亲结点
struct N {
	int u;//左端点
	int v;//右端点
	int dis;//权值
} a[maxn];

bool cmp(N a,N b) { //结构体排序
	return a.dis < b.dis;
}

int findd(int x) { //并查集核心操作
	if(x==boss[x]) return x;
	int temp = findd(boss[x]);//路径压缩
	return temp;
}
int Kruskal(int n,int m) {
	int ans = 0;//记入最小生成树的权值
	int cnt = 0;//边的数目
	
	for(int i=1; i<=n; i++) boss[i] = i; //初始化
	
	for(int i=1; i<=m; i++) {
		int fu = findd(a[i].u);//找a[i].u祖先
		int fv = findd(a[i].v);//找a[i].v祖先
		if(fu!=fv) { //两点不在同意集合(不联通)
			boss[fu] = fv;//合并标记祖先
			ans+=a[i].dis;//边权和增加
			cnt++;//边数增加
		}
		if(cnt==n-1) break;
		//选了n-1条边 最小生成树建好了
	}
}

四.Prim 算法

该算法的基本思想是从一个结点开始,不断加点(而不是 Kruskal 算法的加边)

步骤如下

1.寻找图中任意点,以它为起点,它的所有边V加入集合(优先队列)q1,设置一个boolean数组bool[]标记该位置(边有两个点,每次加入没有被标记那个点的所有边)。

2.从集合q1找到距离最小的那个边v1并 判断边是否存在未被标记的一点p ,如果p不存在说明已经确定过那么跳过当前边处理,如果未被标(访问)记那么标记该点p,并且与p相连的未知点(未被标记)

3.构成的边加入集合q1, 边v1(可以进行计算距离之类,该边构成最小生成树) .
重复1,2直到q1为空,构成最小生成树 。

 理论证明

从任意一个结点开始,将结点分成两类:已加入的,未加入的。

每次从未加入的结点中,找一个与已加入的结点之间边权最小值最小的结点。

然后将这个结点加入,并连上那条边权最小的边。

重复 n-1 次即可

证明:还是说明在每一步,都存在一棵最小生成树包含已选边集。

基础:只有一个结点的时候,显然成立。

归纳:如果某一步成立,当前边集为 f,属于 t 这棵 MST,接下来要加入边 e。

如果 e 属于 t,那么成立。

否则考虑 t+e 中环上另一条可以加入当前边集的边 ff。

首先,ff 的权值一定不会比 e 小,不然  f 会在 e 之前被选取

 其次,ff 的权值一定不会比 e 大,不然 t+e-ff 就是一棵比 t 还优的生成树了

 所以, t+e-ff 包含了 f,并且也是一棵最小生成树,归纳成立

 代码模板

(链表实现

#include<bits/stdc++.h>
#define INF 0x7f7f7f7f
using namespace std;

struct e {
	int v,w,nxt;
} g[10010];
int h[5001],d[5001],cnt=1,n,m,tot,p=1,c;
int u,v,w;
bool vi[5001];

void add(int u,int v,int w) {
	g[cnt].v=v;
	g[cnt].w=w;
	g[cnt].nxt=h[u];
	h[u]=cnt++;
}

void prim() {
	memset(d,0x3f,sizeof d);
	for(int i=h[1]; i; i=g[i].nxt) d[g[i].v]=min(d[g[i].v],g[i].w);
	while(++tot<n) {
		int mi=INF;
		vi[p]=1;
		for(int i=1; i<=n; ++i)
			if(!vi[i]&&mi>d[i]) mi=d[i],p=i;
		c+=mi;
		for(int i=h[p]; i; i=g[i].nxt) {
			int v=g[i].v;
			if(d[v]>g[i].w&&!vi[v]) d[v]=g[i].w;
		}
	}
	return ;
}

int main() {
	cin>>n>>m;
	for(int i=1; i<=m; i++) {
		cin>>u>>v>>w;
		add(u,v,w),add(v,u,w);
	}
	prim();
	if(c>0x3f) printf("no answer");
	else cout<<c;
	return 0;
}

(邻接矩阵实现

#include<bits/stdc++.h>
using namespace std;

const int N=5010;
int g[5010][5010],d[5010],n,m,c;
bool v[5010];

void prim() {
	memset(d,0x3f,sizeof(d));
	memset(v,0,sizeof v);
	d[1]=0;
	for(int i=1; i<n; i++) {
		int p=0;
		for(int j=1; j<=n; j++)
			if(!v[j]&&(p==0||d[j]<d[p])) p=j;
		v[p]=1;
		for(int j=1; j<=n; j++)
			if(!v[j]) d[j]=min(d[j],g[p][j]);
	}
}

int main() {
	cin>>n>>m;
	memset(g,0x3f,sizeof g);
	for(int i=1; i<=n; i++) g[i][i]=0;
	for(int i=1; i<=m; i++) {
		int x,y,z;
		scanf("%d%d%d",&x,&y,&z);
		g[y][x]=g[x][y]=min(g[x][y],z);
	}
	prim();
	for(int i=1; i<=n; i++) {
		if(d[i]==0x3f3f3f3f) {
			printf("orz");
			return 0;
		} else c+=d[i];
	}
	printf("%d",c);
	return 0;
}

五.拓展部分

1.Boruvka 算法

定义

该算法的思想是前两种算法的结合。它可以用于求解 边权互不相同 的无向图的最小生成森林。(无向连通图就是最小生成树)

思路

给定n个点,每个点都有点权,任意两个点之间有边权,边权为两个点权用过某种计算方式得出(例如两点权之差),求最小生成树
点的数量为n,边的数量为n^2,当n=1e5,prim和Krusal都会超时,现在用Boruvka求最小生成树的算法:
考虑维护当前的连通块(初始每个点为独立的一个连通块)
对于每个连通块,找到一条与该连通块相连的,且另一端点不在此连通块中的边权最小的边
将所有的这些边加入到最小生成树,注意,当加入一个边时需要判断该点的两端点是否在同一连通块内。
重复若干遍上述操作,直到图连通
复杂度分析:每次连通块的个数至少减半,复杂度为O((n+m)logn),并查集操作是O(1)
 

eg

动图源自 维基百科 

 代码
#include <bits/stdc++.h>
using namespace std;
#define LL long long
#define mp make_pair
const int N= 200005;
const int M= 500005;
const LL inf= 1e12;
int f[N], pd, n, m;
struct node {
	int a, b;
	LL c;
} e[M];
pair<LL, LL> E[N];
int find(int k) {
	return (k == f[k]) ? k : f[k]= find(f[k]);
}
LL Boruvka() {
	LL res= 0;
	pd= 1;
	int num= 0;
	for (int i= 1; i <= n; i++)
		f[i]= i;
	while (num < n - 1) {
		int tmp= 0;
		for (int i= 1; i <= n; i++)
			E[find(i)]= mp(inf, inf);
		for (int i= 1; i <= m; i++) {
			int fa= find(e[i].a);
			int fb= find(e[i].b);
			if (fa == fb)
				continue;
			tmp++;
			//取最小的边,最小边一样取最小编号
			E[fa]= min(E[fa], mp(e[i].c, i * 1ll));
			E[fb]= min(E[fb], mp(e[i].c, i * 1ll));
		}
		if (tmp == 0)
			break;
		for (int i= 1; i <= m; i++) {
			int fa= find(e[i].a);
			int fb= find(e[i].b);
			if (fa == fb)
				continue;
			if ((E[fa] == mp(e[i].c, i * 1ll)) || (E[fb] == mp(e[i].c, i * 1ll))) {
				f[fa]= fb;
				res+= e[i].c;
				num++;
			}
		}
	}
	if (num < n - 1)
		pd= 0;
	return res;
}

 2.严格次小生成树

定义

在无向图中,边权和最小的满足边权和 严格大于 最小生成树边权和的生成树

求解方法

简单求法
前置知识
树边:就是在生成树当中的边
非树边:未连接到该生成树上的边
定理: 对于一张无向图,如果存在最小生成树和次小生成树,那么对于任何一颗最小生成树都存在一颗次小生成树,使得这两棵树只有一条边不同。

假设我们求得了一颗如图的最小生成树, 那我们要如何求次小生成树呢?

 

如果我们得到一颗生成树,此时我们无论加入哪一条非树边, 都会构成一个环,如图, 我们加入了连接顶点3到5的一条边, 构成了橙色线条指示的环, 那这有什么用呢?

 

此时, 我们如果在这个环中去掉一条原树边, 便可以构成一颗不同的生成树,我们要求次小生成树, 最优的方案肯定是去掉环中最大的一条边, 但是如果最大树边和我们加入的非树边权值相等, 得到的答案和最小生成树相同怎么办?所以我们还需要加入一条次大边, 如果相等的话, 我们就判断一下删去次大边是不是最优解。

先求出最小生成树, 在求最小生成树的过程中, 将树边建图并标记,记录最小生成树的权值 res
在最小生成树构成的图中依次遍历每个顶点,求出最小生成树中任意两个顶点所通过的路径的最大值dis1[u][v] 和 dis2[u][v]
依次枚举非树边, 若该非树边权值 w[ i ] 大于环中最大边(防止求出非严格的次小生成树)就更新答案 ans = min(ans, res + w - dis1[u][v]) 如果与最大边相等的话, 就采用次大边更新答案 ans = min(ans, res + w - dis2[u][v])

  代码
#include<bits/stdc++.h>
using namespace std;
const int N = 505;
const int M = 1e4 + 10;
int n, m, f[N], dis1[N][N], dis2[N][N];
long long ans = 1e18, res;
int h[N], e[N * 2], wi[N * 2], ne[N * 2], idx;
bool st[M];

struct node {
	int u, v, w;
	bool operator < (const node &b) const {
		return w < b.w;
	}
} cyh[M];



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

void add(int u, int v, int w) {
	e[++idx] = v;
	wi[idx] = w;
	ne[idx] = h[u];
	h[u] = idx;
}

void dfs(int u, int flag, int max1, int max2, int d1[], int d2[]) {
	d1[u] = max1;
	d2[u] = max2;
	for (int i = h[u]; i; i = ne[i]) {
		int v = e[i], w = wi[i];
		if (v != flag) {
			int x=max1, y=max2;
			if(w > max1)
				y=max1, x=w;  //更改次大值和最大值
			else if(w != x && w > max2)
				y=w;
			dfs(v, u, x, y, d1, d2);
		}
	}
}

PS.思路大抵就这样时间复杂度大概是O(n^3) [没算错的情况下] 为防止超时我们做如下

倍增优化

考虑刚才的非严格次小生成树求解过程,为什么求得的解是非严格的?

因为最小生成树保证生成树中 u 到 v 路径上的边权最大值一定 不大于 其他从 u 到 v 路径的边权最大值。换言之,当我们用于替换的边的权值与原生成树中被替换边的权值相等时,得到的次小生成树是非严格的。

解决的办法很自然:我们维护到 2^i 级祖先路径上的最大边权的同时维护 严格次大边权,当用于替换的边的权值与原生成树中路径最大边权相等时,我们用严格次大值来替换即可。用倍增求解

  优化代码
#include<bits/stdc++.h>
#define INFI 0x7f7f7f7f7f7f7f7f
using namespace std;
long long n,m,boss[100010],sum=0,ans=INFI;
long long first[600010],nex[600010],to[600010],value[600010],tot=0,dep[100010],f[100010][21],mx[100010][21],mn[100010][21];
struct node {
	long long x,y,used,val;
	bool operator < (const node &rhs)const {
		return val<rhs.val;
	}
} cyh[400040];
long long beizeng(long long x, long long y) {
	if(dep[y]>dep[x])swap(x, y);
	for(long long i=20; i>=0; i--) {
		if(dep[f[x][i]]>=dep[y])x=f[x][i];
		if(x==y)return x;
	}
	for(long long i=20; i>=0; i--)
		if(f[x][i]!=f[y][i])
			x=f[x][i],y=f[y][i];
	return f[x][0];
}
void zhunbei(long long u,long long boss) {
	dep[u]=dep[boss]+1;
	for(long long i=1; i<=20; i++) {
		f[u][i]=f[f[u][i-1]][i-1];
		if(mx[u][i-1]==mx[f[u][i-1]][i-1]) {
			mx[u][i]=mx[u][i-1];
			mn[u][i]=max(mn[f[u][i-1]][i-1],mn[u][i-1]);
		}
		if(mx[u][i-1]>mx[f[u][i-1]][i-1]) {
			mx[u][i]=mx[u][i-1];
			mn[u][i]=max(mx[f[u][i-1]][i-1],mn[u][i-1]);
		}
		if(mx[u][i-1]<mx[f[u][i-1]][i-1]) {
			mx[u][i]=mx[f[u][i-1]][i-1];
			mn[u][i]=max(mn[f[u][i-1]][i-1],mx[u][i-1]);
		}
	}
	for(long long e=first[u]; e; e=nex[e]) {
		long long v=to[e];
		if(v==boss)continue;
		f[v][0]=u;
		mx[v][0]=value[e];
		zhunbei(v,u);
	}
}
void add(long long x,long long y,long long val) {
	nex[++tot]=first[x];
	first[x]=tot;
	to[tot]=y;
	value[tot]=val;
}
long long getboss(long long x) {
	if(boss[x]==x)return boss[x];
	boss[x]=getboss(boss[x]);
	return boss[x];
}
void kruskal() {
	for(long long i=1; i<=m; i++) {
		long long x=cyh[i].x,y=cyh[i].y;
		long long bossx=getboss(x),bossy=getboss(y);
		if(bossx==bossy)continue;
		long long val=cyh[i].val;
		add(x,y,val);
		add(y,x,val);
		sum+=val;
		cyh[i].used=1;
		boss[bossx]=bossy;
	}
}
long long anss(long long x,long long y,long long w) {
	long long lca=beizeng(x, y);
	long long numx=0,numn=0;
	for(long long i=20; i>=0; i--) {
		if(dep[f[x][i]]>=dep[lca]) {
			if(numx==mx[x][i])numn=max(numn,mn[x][i]);
			if(numx>mx[x][i])numn=max(numn,mx[x][i]);
			if(numx<mx[x][i])numx=mx[x][i],numn=max(numn,mn[x][i]);
			x=f[x][i];
		}
		if(dep[f[y][i]]>=dep[lca]) {
			if(numx==mx[y][i])numn=max(numn,mn[y][i]);
			if(numx>mx[y][i])numn=max(numn,mx[y][i]);
			if(numx<mx[y][i])numx=mx[y][i],numn=max(numn,mn[y][i]);
			y=f[y][i];
		}
	}
	if(w != numx)return sum-numx+w;
	else if(numn)return sum-numn+w;
	else return INFI;
}

暂时就写到这里啦 否则我在树上可能就无法生存了 掰掰

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值