生成树(最小生成树,次小生成树,kruskal重构树)

一.最小生成树

前言

  • 在一个含有 n n n 个节点的无向图中,找到一棵含有 n n n 个节点的树,且该树边权总和最小

kruskal 算法学习

  • 算法描述:kruskal 算法,基于贪心。对所有边从小到大排序,并依次使用小的边去将图上的点连接起来(构成环则不连),总时间复杂度: O ( m l o g m ) O(mlogm) O(mlogm)
  • 性质:
    • 边权和最小
    • 就是最大的边最小。‘

模板代码

int find(int x) {
	if(x==fa[x])return x;
	else return fa[x]=find(fa[x]);
}
int cmp(ppp x,ppp y) {
	return x.c>y.c;
}
int main() {
	int n,m,u,v,c;
	cin>>n>>m;
	for(int i=1; i<=m; i++) {
		cin>>u>>v>>c;
		add(u,v,c);
	}
	for(int i=1; i<=n; i++)fa[i]=i;
	sort(e+1,e+k+1,cmp);
	int cnt=1,dissum=0;
	for(int i=1; i<=k; i++) {
		int p1=find(e[i].u),p2=find(e[i].v);
		if(p1!=p2){
			fa[p1]=p2;
			cnt++;
			dissum+=e[i].c;
			if(cnt>=n)break;
		}
	}
}

prim 算法学习

  • 算法描述: 开始时任意选择一个点作为起点,每次操作:从剩下未选择的点构成的点集中,找到一个与已选择点距离最小的点 v v v,将 v v v 加入已选择的点集中,并添加边。总时间复杂度: O ( n 2 ) O(n^2) O(n2)

模板代码:

void prim(){
	for(int i=1;i<=n;i++)dis[i]=inf;
	dis[1]=0;
	int ans=0;
    for(int i=1;i<=n;i++){
    	int minn=inf,pos=0;
    	for(int j=1;j<=n;j++){
    		if(vis[j]==0&&dis[j]<minn){
    			minn=dis[j];
    			pos=j;
			}
		}
		vis[pos]=1;
		ans+=minn;
		for(int j=1;j<=n;j++){
			if(vis[j])continue;
			if(dis[j]>dist(pos,j))dis[j]=dist(pos,j);
		}
	}
}

例题

例题1:求最近距离最远的 d i s ( s , t ) dis(s,t) dis(s,t)

  • 题目描述: 无向连通图中有 n 个点,求最近距离最远的 d i s ( s , t ) dis(s,t) dis(s,t)
  • 问题分析: 无向连通图中建一个最大生成树,求最大生成树的直径即可

例题2:k 条一线道路

例题链接

  • 题目描述: n 个点,m 条道路,每条道路可以选择建设一线道路,花费 c i c_i ci;或者建设二线道路,花费 d i d_i di ,且保证 c i > = d i c_i>=d_i ci>=di 。要求从 m m m 条道路中选择 n − 1 n-1 n1 条道路,使得所有点均互相可达,且至少选择 k k k 条一线道路。求选择所有道路的最大花费最小为多少。
  • 问题分析: 将每条道路拆分成两条路,花费分别为 c i , d i c_i,d_i ci,di 。先用 kruskal 从 c c c 中选择 k k k 条一线道路,再对剩余的 c c c 以及 d d d 中选择 n − 1 − k n-1-k n1k 条即可。注意:同一条道路只能选一次。

核心代码:

void kruskal(){
	int maxans=-1e9;
    for(int i=1;i<=n;i++)fa[i]=i;
	for(int i=1;i<=m;i++){
		int p1=find(e1[i].u),p2=find(e1[i].v);
		if(p1!=p2){
			fa[p1]=p2;
			cnt++;
			ans[++top].x=e1[i].id;
			ans[top].y=1;
			vis[e1[i].id]=1;
			maxans=max(maxans,e1[i].w);
		}
		if(cnt==k+1)break; 
	}
	for(int i=1;i<=m;i++){
		if(vis[e2[i].id])continue;
		int p1=find(e2[i].u),p2=find(e2[i].v);
		if(p1!=p2){
			fa[p1]=p2;
			cnt++;
			ans[++top].x=e2[i].id;
			ans[top].y=2;
			maxans=max(maxans,e2[i].w);
		}
		if(cnt==n)break; 
	}
	cout<<maxans<<endl; 
	for(int i=1;i<=top;i++)cout<<ans[i].x<<" "<<ans[i].y<<endl;
}

例题3:一条 最 大 值 最 小 值 \frac{最大值}{最小值} 最小的道路

例题链接

  • 题目描述: 给定一个 n 个点,m 条边,有边权 w i w_i wi 的图。找到 s s s t t t 中, 最 大 值 最 小 值 \frac{最大值}{最小值} 最小的路径,求得该比值。 n < = 500 , w i < = 30000 , m < = 5000 n<=500,w_i<=30000,m<=5000 n<=500,wi<=30000,m<=5000
  • 问题分析: 排个序,枚举最小边 i i i,kruskal 求出 s , t s,t s,t 有通路的最小边 j j j,check 一下比值即可。

核心代码:

void kruskal(){
	for(int i=1;i<=m;i++){
		for(int j=1;j<=n;j++)fa[j]=j;
		for(int j=i;j<=m;j++){
			int p1=find(e[j].u),p2=find(e[j].v);
			if(p1!=p2)fa[p1]=p2;
			if(find(s)==find(t)){
				check(e[j].w,e[i].w);
				break;
			}
		}
	}
	if(ansfz==1e9)cout<<"IMPOSSIBLE";
	else if(ansfm==1)cout<<ansfz;
	else cout<<ansfz<<"/"<<ansfm;
}

例题4:一棵完全图中找到一棵最小生成树

例题链接

  • 题目描述: 一个平面图上有 n n n 个顶点,每个点的坐标分别为 ( x i , y i ) (x_i,y_i) (xi,yi),求一棵最小生成树,输出最小边权和。 n < = 5000 n<=5000 n<=5000
  • 问题分析: prim 板子,时间复杂度 O ( n 2 ) O(n^2) O(n2)。【注意】用 kruskal 需要 O ( n 2 l o g n ) O(n^2logn) O(n2logn),prim 的优越性体现出来了。

例题5:双限制

例题链接

  • 题目描述: n n n 个点,每个点都有高度 h i h_i hi m m m 条无向边 ( u , v , w ) (u,v,w) (u,v,w) 。从 1 1 1 号结点出发,每次不能走到当前高度高的点,但是可以回到已走过的点(瞬移)。求经过最多个结点的前提下走的最小距离为多少。
  • 问题分析: 最多经过结点是固定的,可以先用 d f s dfs dfs ,并打上标记(表示未来要生成一个覆盖所有标记点的生成树)。再使用 kruskal 求最小生成树,以边的终点高度为第一关键字,以边权为第二关键字排序。这样就保证了所遍历的点一定是从高到低遍历的。(不会出现多个起点的情况)
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10,M=1e6+10;

struct E {
	int u,v,w,next;
} e[M*2];
int vex[N],tot,n,m,p[N],vis[N],h[N];
void add(int u,int v,int w) {
	tot++;
	e[tot].u=u;
	e[tot].v=v;
	e[tot].w=w;
	e[tot].next=vex[u];
	vex[u]=tot;
}
int cmp(E x,E y) {
	if(h[x.v]==h[y.v])return x.w<y.w;
	else return h[x.v]>h[y.v];
}
int find(int x) {
	if(x==p[x])return x;
	else return p[x]=find(p[x]);
}
void dfs(int u) {
	if(vis[u])return;
	vis[u]=1;
	for(int i=vex[u]; i; i=e[i].next) {
		int v=e[i].v;
		dfs(v);
	}
}
void kruskal() {
	long long cnt=0,ans=0,cnt2=1;
	for(int i=1; i<=n; i++) {
		if(vis[i]==1)cnt++;
	}
	sort(e+1,e+tot+1,cmp);
	for(int i=1; i<=tot; i++) {
		if(!vis[e[i].u]||!vis[e[i].v])continue;
		int p1=find(e[i].u),p2=find(e[i].v);
		if(p1!=p2) {
			p[p1]=p2;
			ans+=e[i].w;
			if(cnt2==cnt)break;
		}
	}
	cout<<cnt<<" "<<ans;
}
int main() {
	int u,v,w;
	cin>>n>>m;
	for(int i=1; i<=n; i++)p[i]=i;
	for(int i=1; i<=n; i++)cin>>h[i];
	for(int i=1; i<=m; i++) {
		cin>>u>>v>>w;
		if(h[u]>h[v])add(u,v,w);
		else if(h[u]<h[v])add(v,u,w);
		else {
			add(u,v,w);
			add(v,u,w);
		}
	}
	dfs(1);
	kruskal();
}

二.kruskal重构树

算法学习

构造方法:

  • 最小生成树的边,作为重构树上的非叶子结点
  • 最小生成树的点,作为重构树上的叶子结点
  • u , v u,v u,v 连接时,寻找 u u u 所在的树的根 r o o t u root_u rootu,与 v v v 所在的树的根 r o o t v root_v rootv,(这个可以用并查集实现),新增结点 n o d e node node,添加边 ( u , r o o t ) , ( v , r o o t ) (u,root),(v,root) (u,root),(v,root)

性质:

  • 原树上两点之间的边权最大值为 l c a lca lca 的权值

模板代码:

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10,M=1e5+10;

struct E {
	int u,v,w,next;
} e[N*4],e2[M];
int vex[N*2],b[N*2],tot,f[N*2][25],p[N*2];
int node=0;
void add(int u,int v) {
	tot++;
	e[tot].u=u;
	e[tot].v=v;
	e[tot].next=vex[u];
	vex[u]=tot;
}
int cmp(E x,E y) {
	return x.w<y.w;
}
void dfs(int u,int fa) {
	f[u][0]=fa;
	for(int i=1; i<=20; i++)f[u][i]=f[f[u][i-1]][i-1];
	for(int i=vex[u]; i; i=e[i].next) {
		int v=e[i].v;
		if(v==fa)continue;
		dfs(v,u);
	}
}
int find(int x){
	if(x==p[x])return x;
	else return p[x]=find(p[x]);
}
int main() {
	int n,m,u,v,w;
	cin>>n>>m;
	for(int i=1; i<=n; i++)p[i]=i;
	for(int i=1; i<=m; i++) {
		cin>>u>>v>>w;
		e2[++tot].u=u;
		e2[tot].v=v;
		e2[tot].w=w;
	}
	sort(e2+1,e2+m+1,cmp);
	node=n;
	tot=0;
	for(int i=1; i<=m; i++) {
		int p1=find(e2[i].u),p2=find(e2[i].v);
		if(p1!=p2) {
			node++;
			add(p1,node),add(node,p1);
			add(p2,node),add(node,p2);
			p[p1]=p[p2]=p[node]=node;
			b[node]=e2[i].w;
		}
	}
	dfs(node,0);
	b[0]=2e9;
}

例题

例题1:kruskal重构树+倍增

例题链接

  • 题目描述: n 个结点 m 条边的一个图,点有点权 a i a_i ai,边有边权 w w w。开始时有价值 k k k。当经过一个点 i i i 后,可以获取它的值 a i a_i ai k + = a i k+=a_i k+=ai,但不可重复获取。当 k k k 大于边权时,才可以走对应的边。 q q q 次询问,每次询问给定起点 s s s 和初始价值 k k k ,求最多可以得到多少的价值。 n , m , q < = 1 e 5 , a i < = 1 e 4 , w i < = 1 e 9 n,m,q<=1e5,a_i<=1e4,w_i<=1e9 n,m,q<=1e5ai<=1e4,wi<=1e9
  • 问题分析:
  • 建造 kruskal 重构树,树上任意点的点权为以其为根的树的所有叶子的权值和。对于起点 u u u,不停的让 u u u 往上跳,直到不能跳为止,那么对应的点的点权 + k +k +k 即为答案。
  • 优化跳的过程:倍增往上跳,要注意
    • 不是直接一次倍增 w [ f [ x ] [ i ] ] < = a [ f [ x ] [ i ] ] + k w[f[x][i]]<=a[f[x][i]]+k w[f[x][i]]<=a[f[x][i]]+k,因为其还不一定能经过 x − > f [ x ] [ i ] x->f[x][i] x>f[x][i] 之间的所有点。
    • 而是多次倍增,每次都以起点 t e m p = x temp=x temp=x 为最大价值,倍增 w [ f [ x ] [ i ] ] < = a [ t e m p ] + k w[f[x][i]]<=a[temp]+k w[f[x][i]]<=a[temp]+k

倍增部分的代码:

int jump(int x,int k) {
	while(x!=node) {
		int temp=x;
		for(int i=20; i>=0; i--) {
			if(b[f[x][i]]<=k+a[temp])x=f[x][i];
		}
		if(x==temp)break;
	}
    return k+a[x];
}

二.次小生成树

算法学习

定义: 次小生成树,顾名思义就是第二小的生成树,有严格与非严格之分
定理: 如果存在次小生成树,则次小生成树与最小生成树一定只有一条边的差异
算法思路: 求出最小生成树,枚举没有使用过的每一条边 ( u , v , w ) (u,v,w) (u,v,w),来替换原树上的一条边。由于增加一条边必然成环,那么只需删去原最小生成树上 ( u , v ) (u,v) (u,v) 路径的最大边即可。对于非严格最小生成树:只需删去最大边即可;对于严格最小生成树,若最大边与 w w w 相同,则删除次大边,否则删除最大边。PS:以上的增边和删边不用真的增删
算法实现: 由于最小生成树是树型结构,那么路径 ( u , v ) = ( u , r o o t ) + ( v , r o o t ) (u,v)=(u,root)+(v,root) (u,v)=(u,root)+(v,root),因此我们可以倍增求最大值和次大值。

模板题链接

模板代码如下:

#include<bits/stdc++.h>
using namespace std;
const int N=6e5+10,M=6e5+10;

struct E {
	int u,v,w,next;
} e[M],e2[M];
int cmp(E x,E y) {
	return x.w<y.w;
}
int vex[N],f[N][25],Max[N][25][2],p[N],tot,vis[N],dep[N];
long long sum=0,ans=0;
void add(int u,int v,int w) {
	tot++;
	e[tot].u=u;
	e[tot].v=v;
	e[tot].w=w;
	e[tot].next=vex[u];
	vex[u]=tot;
}
int find(int x) {
	if(x==p[x])return x;
	else return p[x]=find(p[x]);
}
void update(int &Max0,int &Max1,int temp0,int temp1) { //更新最大值Max0和次大值Max1
	if(Max0>temp0) {
		Max1=max(Max1,temp0);
	} else if(Max0<temp0){
		Max1=max(Max0,temp1);
		Max0=temp0;
	}else{
		Max1=max(Max1,temp1);
	}
}
void dfs(int u,int fa,int w) {
	f[u][0]=fa;
	Max[u][0][0]=w;
	Max[u][0][1]=-1;
	dep[u]=dep[fa]+1;
	for(int i=1; i<=20; i++) {
		f[u][i]=f[f[u][i-1]][i-1];
		Max[u][i][0]=Max[u][i-1][0];
		Max[u][i][1]=Max[u][i-1][1];
		update(Max[u][i][0],Max[u][i][1],Max[f[u][i-1]][i-1][0],Max[f[u][i-1]][i-1][1]);
	}
	for(int i=vex[u]; i; i=e[i].next) {
		int v=e[i].v;
		if(v==fa)continue;
		dfs(v,u,e[i].w);
	}
}
void lca(int x,int y,int w) {
	if(dep[x]<dep[y])swap(x,y);
	int Max0=-1,Max1=-1;
	for(int i=20; i>=0; i--) {
		if(dep[f[x][i]]>=dep[y]) {
			update(Max0,Max1,Max[x][i][0],Max[x][i][1]);
			x=f[x][i];
		}
	}
	if(x!=y) {
		for(int i=20; i>=0; i--) {
			if(f[x][i]==f[y][i])continue;
			update(Max0,Max1,Max[x][i][0],Max[x][i][1]);
			update(Max0,Max1,Max[y][i][0],Max[y][i][1]);
			x=f[x][i];
			y=f[y][i];
		}
		update(Max0,Max1,Max[x][0][0],Max[x][0][1]);
		update(Max0,Max1,Max[y][0][0],Max[y][0][1]);
	}
	if(w==Max0) {
		if(Max1!=-1)ans=min(ans,sum+w-Max1);
	} else ans=min(ans,sum+w-Max0);
}
int main() {
	int n,m,u,v,w;
	cin>>n>>m;
	for(int i=1; i<=m; i++) {
		cin>>u>>v>>w;
		e2[i]= {u,v,w};
	}
	for(int i=1; i<=n; i++)p[i]=i;
	sort(e2+1,e2+m+1,cmp);
	for(int i=1; i<=m; i++) {
		int p1=find(e2[i].u),p2=find(e2[i].v);
		if(p1!=p2) {
			p[p1]=p2;
			add(e2[i].u,e2[i].v,e2[i].w);
			add(e2[i].v,e2[i].u,e2[i].w);
			vis[i]=1;
			sum+=e2[i].w;
		}
	}
	dfs(1,0,0);
	ans=1e16;
	for(int i=1; i<=m; i++) {
		if(vis[i]||e2[i].v==e2[i].u)continue;
		lca(e2[i].u,e2[i].v,e2[i].w);
	}
	cout<<ans;
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值