一.前言
本文仍是应室友间的学习交流而生可能有些私货(但并不影响阅读) 请谅解
作者是个蒟蒻 如果本文有错别字 错理解 错表达 欢迎来评论区指出 谢谢!
那么我们现在就进入最小生成树的世界吧
二.简介及定义
(在学习此之前建议先学了解一点关于 生成子图和生成树 的基本定义 如果条件允许还可以了解下简单运用 当然由于我的精力有限 不知道的可以自行出门右转上百度)
我们定义无向连通图的 最小生成树(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=1e5,prim和Krusal都会超时,现在用Boruvka求最小生成树的算法:
考虑维护当前的连通块(初始每个点为独立的一个连通块)
对于每个连通块,找到一条与该连通块相连的,且另一端点不在此连通块中的边权最小的边
将所有的这些边加入到最小生成树,注意,当加入一个边时需要判断该点的两端点是否在同一连通块内。
重复若干遍上述操作,直到图连通
复杂度分析:每次连通块的个数至少减半,复杂度为O((n+m)logn),并查集操作是O(1)
动图源自 维基百科
代码
#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.思路大抵就这样时间复杂度大概是 [没算错的情况下] 为防止超时我们做如下
倍增优化
考虑刚才的非严格次小生成树求解过程,为什么求得的解是非严格的?
因为最小生成树保证生成树中 u 到 v 路径上的边权最大值一定 不大于 其他从 u 到 v 路径的边权最大值。换言之,当我们用于替换的边的权值与原生成树中被替换边的权值相等时,得到的次小生成树是非严格的。
解决的办法很自然:我们维护到 级祖先路径上的最大边权的同时维护 严格次大边权,当用于替换的边的权值与原生成树中路径最大边权相等时,我们用严格次大值来替换即可。用倍增求解
优化代码
#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;
}
暂时就写到这里啦 否则我在树上可能就无法生存了 掰掰