图论·最小生成树

最小生成树(MST)

最小生成树是无向图中边权之和最小的生成树,显然有全部n个点与n1条边。

因为MST一定包含图中权值最小的边,所以可以贪心构造MST。

Kruskal算法

kruskal是对边进行贪心,每次选取权值最小的边,用并查集维护两个结点的连通性。

kruskal编码简单,复杂度为 O(m log2 m)

板子代码:

bool cmp(node x,node y) {  return x.w<y.w;  }
int find_fa(int x)
{
	if (fa[x]==x) return x;
	return fa[x]=find_fa(fa[x]);
}
void kruskal()
{
	for (int i=1;i<=n;i++) fa[i]=i;
	sort(e+1,e+1+m,cmp);
	for (int i=1;i<=m;i++)
	{
		int u=e[i].u,v=e[i].v,w=e[i].w;
		int fa1=find_fa(u),fa2=find_fa(v);
		if (fa1==fa2) continue;
		cnt++;
		ans+=w;
		fa[fa1]=fa2;
		if (cnt==n-1) break;
	}
}

Prim算法

Prim是对点进行贪心,思想与Dijkstra基本相同。prim之中没有dijkstra的松弛操作,每次用小根堆找出目前点集中最近的点。

复杂度为 O(m log2 n)

板子代码:

priority_queue < pii,vector <pii>,greater <pii> > q;
void prim()
{
	memset(vis,0,sizeof vis);
	q.push(mp(0,1));
	while (!q.empty())
	{
		pii u=q.top();
		q.pop();
		if (vis[u.second]) continue;
		vis[u.second]=true;
		
		sum+=u.first(),cnt++;
		if (cnt==n) break;
		
		int _size=e[u.second].size();
		for (int i=0;i<_size;i++)
		{
			int v=e[u.second][i].nxt;
			if (vis[v]) continue;
			q.push(mp(e[u.second][i].val,v));
		}
	}
} 

严格次小生成树

洛谷原题链接

求出MST后,用LCA计算每两点间路径上的最大边长与次大边长,再枚举每条不在MST上的边,替换MST上原来的边即可。

长成一坨的代码
#incIude <bits/stdc++.h>
#define int long long
#define pii pair<int,int>
#define mp make_pair
using namespace std;
const int N=1e5+5;
const int M=3e5+5;
int n,m;
struct node{
	int u,v,w;
}e[M];
struct NODE{
	int nxt,val;
};
vector <NODE> tr[N];
bool flag[M];
int sum;
int ans=0x3f3f3f3f3f3f3f3f;

int fa[N]; 
bool cmp(node a,node b) {  return a.w<b.w;  }
int find_fa(int x)
{
	if (fa[x]==x) return x;
	return fa[x]=find_fa(fa[x]);
}
void kruskal()
{
	for (int i=1;i<=n;i++) fa[i]=i;
	sort(e+1,e+1+m,cmp);
	
	int cnt=0;
	for (int i=1;i<=m;i++)
	{
		int u=e[i].u,v=e[i].v,w=e[i].w;
		int fa1=find_fa(u),fa2=find_fa(v);
		if (fa1==fa2) continue;
		
		fa[fa1]=fa2;
		sum+=e[i].w;
		flag[i]=true;
		cnt++;
		tr[u].push_back({v,w}),tr[v].push_back({u,w});
		if (cnt==n-1) break;
	}
}
int dep[N],st[N][22],g[N][22][4];
void dfs(int x,int _fa)
{
	dep[x]=dep[_fa]+1,st[x][0]=_fa;
	
	int _size=tr[x].size();
	for (int i=0;i<_size;i++)
	{
		int v=tr[x][i].nxt;
		if (v==_fa) continue;
		g[v][0][0]=tr[x][i].val,g[v][0][1]=0xc1c1c1c1c1c1c1c1;
		dfs(v,x);
	}
}
void initST()
{
	for (int j=1;j<=20;j++)
	{
		for (int i=1;i<=n;i++)
		{
			st[i][j]=st[st[i][j-1]][j-1];
			g[i][j][0]=max(g[i][j-1][0],g[st[i][j-1]][j-1][0]);
			if (g[i][j-1][0]==g[st[i][j-1]][j-1][0]) g[i][j][1]=max(g[i][j-1][1],g[st[i][j-1]][j-1][1]);
			else if (g[i][j-1][0]<g[st[i][j-1]][j-1][0]) g[i][j][1]=max(g[i][j-1][0],g[st[i][j-1]][j-1][1]);
			else g[i][j][1]=max(g[i][j-1][1],g[st[i][j-1]][j-1][0]);
		}
	}
}
int lca(int x,int y)
{
	if (dep[x]<dep[y]) swap(x,y);

	int delta=dep[x]-dep[y],lg=0;
	while (delta)
	{
		if (delta&1) x=st[x][lg];
		delta>>=1,lg++;
	}
	if (x==y) return x;
	
	for (int i=20;i>=0;i--)
	{
		if (st[x][i]==st[y][i]) continue;
		x=st[x][i],y=st[y][i];
	}

	return st[x][0];
}
int get(int x,int _lca,int lim)
{
	int res=0xc1c1c1c1c1c1c1c1;
	for (int i=20;i>=0;i--)
	{
		if (dep[st[x][i]]<dep[_lca]) continue;
		if (lim!=g[x][i][0]) res=max(res,g[x][i][0]);
		else res=max(res,g[x][i][1]);
		x=st[x][i];
	}
	return res;
}
signed main()
{
	scanf("%lld%lld",&n,&m);
	for (int i=1,u,v,w;i<=m;i++) scanf("%lld%lld%lld",&e[i].u,&e[i].v,&e[i].w);
	
	kruskal();
	dfs(1,0);
	initST();
	
	for (int i=1;i<=m;i++)
	{
		if (flag[i]) continue;
		int u=e[i].u,v=e[i].v,w=e[i].w;
		int _lca=lca(u,v);
		int maxu=get(u,_lca,w),maxv=get(v,_lca,w);
		ans=min(ans,sum+w-max(maxu,maxv));
	}
	printf("%lld",ans);
	return 0;
}

最优比率生成树

就是ybt的最后一道题。

这里要求的是 max hi=1ncisii=1ntisi,si=10

这就是01分数规划。一顿移项后,得F=hi1n(ci+x ti) si0

其中 x 就是二分的值。(容易发现这个东西具有单调性?)

于是,建一个新图,图的新边权即 ci+x ti,跑MST来check目前的 mid 是否合法,然后就做完了。


【YbtOj】例题

A.繁忙都市

板子题,秒了

#incIude <bits/stdc++.h>
#define int long long
using namespace std;
const int N=310;
const int M=1e5+5;
int n,m;
struct node{
	int u,v,w;
}e[M];
int ans1,ans2;
int fa[N];

bool cmp(node a,node b) {  return a.w<b.w;  }
int find_fa(int x)
{
	if (fa[x]==x) return x;
	return fa[x]=find_fa(fa[x]);
}
void kruskal()
{
	for (int i=1;i<=n;i++) fa[i]=i;
	sort(e+1,e+1+m,cmp);
	for (int i=1;i<=m;i++)
	{
		if (ans1>=n-1) return ;
		int u=e[i].u,v=e[i].v,w=e[i].w;
		int fa1=find_fa(u),fa2=find_fa(v);
		if (fa1==fa2) continue;
		fa[fa1]=fa2;
		ans2=w,ans1++;
	}
}
signed main()
{
	scanf("%lld%lld",&n,&m);
	for (int i=1;i<=m;i++) scanf("%lld%lld%lld",&e[i].u,&e[i].v,&e[i].w);
	kruskal();
	printf("%lld %lld",ans1,ans2);
	return 0;
}

B.新的开始

因为一定要有发电站,所以在这建立一个虚点,虚点与其他点的路径权值就是v。这样,再跑一个kruskal就可以了(注意此时点数变为了n+1

建虚点的代码
#incIude <bits/stdc++.h>
#define int long long
using namespace std;
const int N=2005;
int n,m;
int x,tol;
int c[N];
bool b[N];
int k;
int mn,mx=0x3f3f3f3f3f3f3f3f;
int f[N][N];

signed main()
{
	scanf("%lld",&n);
	for (int i=1;i<=n;i++)
	{
		scanf("%lld",&f[i][0]);
		f[0][i]=f[i][0];
		c[i]=f[0][i];
	}
	for (int i=1;i<=n;i++)
	{
		for (int j=1;j<=n;j++) scanf("%lld",&f[i][j]);	
		f[i][i]=0x3f3f3f3f3f3f3f3f;
	}
	b[0]=true;
	for (int i=1;i<=n;i++)
	{
		mn=mx,k=0;
		for (int j=1;j<=n;j++) if (b[j]==0&&c[j]<mn) mn=c[j],k=j;
		tol+=c[k];
		b[k]=true;
		for (int j=1;j<=n;j++) if (f[k][j]<c[j]) c[j]=f[k][j];
	}
	printf("%lld",tol); 
	return 0;
}

附上我想的神奇方法:将问题转化为使原图分成若干个联通块,每个联通块都有一个代价最小的发电站,求最小代价。这里用到带权并查集,维护点集S形成联通块的最小代价发电站,刚开始每个点集都只有一个元素。每次对于不联通的两个点集Su,Sv,若它俩连通的代价小于分开的代价,那就合并,否则分着。

感谢sxht dalao的帮忙!

神奇做法的代码
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=305;
const int M=2e5;
int n;
int p[N];
struct node{
	int u,v,w;
}e[M];
int cnt;
int tol;
int ans;

int fa[N];
bool cmp(node x,node y) {  return x.w<y.w;  }
int find_fa(int x)
{
	if (fa[x]==x) return x;
	int ret=find_fa(fa[x]);
	p[x]=p[fa[x]];
	return fa[x]=ret;
} 
void kruskal()
{
	for (int i=1;i<=n;i++) fa[i]=i;
	sort(e+1,e+1+cnt,cmp);
	for (int i=1;i<=cnt;i++)
	{
		int u=e[i].u,v=e[i].v,w=e[i].w;
		int fa1=find_fa(u),fa2=find_fa(v);
		if (fa1==fa2) continue;
		if (w<max(p[u],p[v]))
		{
			tol++;
			ans=ans-max(p[u],p[v])+w;
			if (p[u]>p[v]) p[u]=p[v],fa[fa1]=fa2;
			else p[v]=p[u],fa[fa2]=fa1;
		}
		if (tol>=n-1) break;
	}
}
signed main()
{
	scanf("%lld",&n);
	for (int i=1;i<=n;i++) 
	{
		scanf("%lld",&p[i]);
		ans+=p[i];
	}
	for (int i=1;i<=n;i++)
	{
		for (int j=1;j<=n;j++)
		{
			int w;
			scanf("%lld",&w);
			e[++cnt]={i,j,w};
		}
	}
	kruskal();
	printf("%lld",ans);
	return 0;
}

C.公路建设

注意到n,m范围并不大。每次有新方案,将新边用插入排序插入当前的存边数组,再跑一遍kruskal即可。若有被替换的边k,那么最后将k弄到cnt之外就相当于踢出存边数组了。

#incIude <bits/stdc++.h>
#define int long long
using namespace std;
const int N=510;
const int M=2005;
int n,m;
int fa[N];
struct node{
	int u,v,w;
}e[M];
int cnt;

int find_fa(int x)
{
	if (fa[x]==x) return x;
	return fa[x]=find_fa(fa[x]);
}
double kruskal()
{
	for (int i=1;i<=n;i++) fa[i]=i;
	for (int i=cnt;i>=1;i--) //保持升序 
	{
		if (e[i-1].w>e[i].w) swap(e[i-1],e[i]);
	}
	int k=0,res=0;
	for (int i=1;i<=cnt;i++)
	{
		int fa1=find_fa(e[i].u),fa2=find_fa(e[i].v);
		if (fa1!=fa2)
		{
			fa[fa1]=fa2;
			res+=e[i].w;
		} 
		else k=i;
	}
	if (k)
	{
		cnt--;
		for (int i=k;i<=cnt;i++) swap(e[i],e[i+1]);
	}
	if (cnt!=n-1) return 0;
	return res;
}
signed main()
{
	scanf("%lld%lld",&n,&m);
	for (int i=1;i<=m;i++)
	{
		++cnt;
		scanf("%lld%lld%lld",&e[cnt].u,&e[cnt].v,&e[cnt].w);
		double out=kruskal();
		if (out==0) printf("0\n");
		else printf("%0.1lf\n",out/2.0);
	}
	return 0;
}

D.构造完全图

这里的并查集是个带权并查集(吧 吗?),维护每个集合的个数。升序排列树边,每一条新的边u,v的出现就意味着u,v所在集合的其他边组成的边被剔除,此时sum+=(numfa1×numfa21)×(w+1)即可。

#incIude <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+5;
int n;
struct node{
	int u,v,w;
}t[N];
int sum;
int fa[N];
int num[N];
set <int> st;

bool cmp(node a,node b) { return a.w<b.w; }
int find_fa(int x)
{
	if (fa[x]==x) return x;
	int ret=find_fa(fa[x]);
	num[x]+=num[fa[x]];
	return fa[x]=ret;
}
void _merge(int x,int y)
{
	int fa1=find_fa(x),fa2=find_fa(y);
	fa[fa1]=fa2;
	num[fa2]+=num[fa1];
	num[fa1]=0;
}
signed main()
{
	scanf("%lld",&n);
	for (int i=1;i<n;i++) 
	{
		scanf("%lld%lld%lld",&t[i].u,&t[i].v,&t[i].w);
		sum+=t[i].w;
	}
	
	for (int i=1;i<=n;i++) fa[i]=i,num[i]=1;
	sort(t+1,t+n,cmp);
	for (int i=1;i<n;i++)
	{
		int u=t[i].u,v=t[i].v,w=t[i].w;
		if (find_fa(u)==find_fa(v)) continue;
		int fa1=find_fa(u),fa2=find_fa(v);
		sum+=(num[fa1]*num[fa2]-1)*(w+1);
		_merge(u,v);
	}
	printf("%lld",sum);
	return 0;
}

E.最短时间

这道题需要将点权转换为边权。每条边一定会跑两次,跑两次的过程中一定会分别经过一次两个点,所以此时边权w=w×2+cu+cv。但是这样统计最后会漏掉起点的点权,我们跑一遍普通的kruskal后再钦定点权最小的点为起点、将它的点权加到答案中即可。

#incIude <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e4+5;
const int M=1e5+5;
int n,m;
int c[N];
struct node{
	int u,v,w;
}e[M];
int fa[N];
int cnt;
int ans=0x3f3f3f3f3f3f3f3f;

bool cmp(node x,node y) {  return x.w<y.w;	}	
int find_fa(int x)
{
	if (fa[x]==x) return x;
	return fa[x]=find_fa(fa[x]);
}
void kruskal()
{
	for (int i=1;i<=n;i++) fa[i]=i;
	sort(e+1,e+m+1,cmp);
	for (int i=1;i<=m*2;i++)
	{
		int u=e[i].u,v=e[i].v,w=e[i].w;
		int fa1=find_fa(u),fa2=find_fa(v);
		if (fa1==fa2) continue;
		fa[fa1]=fa2;
		ans+=w;
		cnt++;
		if (cnt>=n-1) return ;
	}
}
signed main()
{
	scanf("%lld%lld",&n,&m);
	for (int i=1;i<=n;i++) 
	{
		scanf("%lld",&c[i]);
		ans=min(ans,c[i]);
	}
	for (int i=1;i<=m;i++) 
	{
		scanf("%lld%lld%lld",&e[i].u,&e[i].v,&e[i].w);
		e[i].w+=e[i].w+c[e[i].v]+c[e[i].u];
	}
	kruskal();
	printf("%lld",ans);
	return 0;
} 

F.序列破解

sumi表示[1,i]的奇偶性,那么[l,r]的奇偶性即为suml1sumr。再接着感性理解一些,这样,就转换为有n2条连接l1,r、权值为ci,j的边,要使得该图连通且权值和最小,就转化为最小生成树板子了。

#incIude <bits/stdc++.h>
#define int long long
using namespace std;
const int N=2e3+5;
int n;
struct node{
	int u,v,w;
}t[N*N];
int fa[N];
int tol;
int cnt,ans;

bool cmp(node a,node b)
{
	return a.w<b.w;
}
int find_fa(int x)
{
	if (fa[x]==x) return x;
	return fa[x]=find_fa(fa[x]);
}
void kruskal()
{
	for (int i=1;i<=n;i++) fa[i]=i;
	sort(t+1,t+1+cnt,cmp);
	for (int i=1;i<=cnt;i++)
	{
		int u=t[i].u,v=t[i].v,w=t[i].w;
		int fa1=find_fa(u),fa2=find_fa(v);
		if (fa1==fa2) continue;
		fa[fa1]=fa2;
		ans+=w;
		tol++;
		if (tol==n) break;
	}
}
signed main()
{
	scanf("%lld",&n);
	for (int i=1;i<=n;i++)
	{
		for (int j=i;j<=n;j++) 
		{
			++cnt;
			scanf("%lld",&t[cnt].w);
			t[cnt].u=i-1,t[cnt].v=j;
		}
	}
	kruskal();
	printf("%lld",ans);
	return 0;
}

G.生物进化

容易发现,这就是道板子。跑一遍kruskal顺便记录一下作为答案的边,最后跑一遍dfs记录一下每个节点的父亲结点即可。

挂分小技巧:忘记双向建边。

#incIude <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1010;
int n;
struct node{
	int u,v,w;
}e[N*N];
int fa[N];
int tol;
int cnt;
vector <int> tr[N];
int ans[N];

bool cmp(node x,node y) {  return x.w<y.w;  }
int find_fa(int x)
{
	if (fa[x]==x) return x;
	return fa[x]=find_fa(fa[x]);
}
void kruskal()
{
	for (int i=1;i<=n;i++) fa[i]=i;
	sort(e+1,e+1+tol,cmp);
	for (int i=1;i<=tol;i++)
	{
		int u=e[i].u,v=e[i].v,w=e[i].w;
		int fa1=find_fa(u),fa2=find_fa(v);
		if (fa1==fa2) continue;
		fa[fa1]=fa2;
		tr[u].push_back(v);
		tr[v].push_back(u);
		cnt++;
		if (cnt==n-1) break;
	}
}
void dfs(int u,int _fa)
{
	ans[u]=_fa;
	int _size=tr[u].size();
	for (int i=0;i<_size;i++)
	{
		int v=tr[u][i];
		if (v==_fa) continue;
		dfs(v,u);
	}
}
signed main()
{
	scanf("%lld",&n);
	for (int i=1,x;i<=n;i++)
	{
		for (int j=1;j<=n;j++)
		{
			scanf("%lld",&x);
			e[++tol]={i,j,x};
		}
	}
	kruskal();
	dfs(1,0);
	for (int i=2;i<=n;i++) printf("%lld\n",ans[i]);
	return 0;
}

H.保留道路

与公路建设有点像。先按照g从小到大排序,枚举边u相当于枚举maxug。因为第i次枚举的MST一定是第i1次的MST和边i组成的,所以每次枚举按照s值将新边插入存边数组(存的是上次MST的边),接着跑kruskal即可,复杂度O(nm)

#incIude <bits/stdc++.h>
#define int long long
using namespace std;
const int N=405;
const int M=5e4+5;
const int inf=0x3f3f3f3f3f3f3f3f;
int n,m;
int g,s;
struct node{
	int u,v,g,s;
}e[M];
int top;
int st[N];
int num;
int ans=inf;

int fa[N];
bool cmp(node a,node b){  return a.g<b.g;  }
int find_fa(int x)
{
	if (fa[x]==0) return x;
	return fa[x]=find_fa(fa[x]);
}
signed main()
{
	scanf("%lld%lld%lld%lld",&n,&m,&g,&s);
	for (int i=1;i<=m;i++) scanf("%lld%lld%lld%lld",&e[i].u,&e[i].v,&e[i].g,&e[i].s);
	
	sort(e+1,e+1+m,cmp);
	for (int i=1;i<=m;i++)
	{
		int j;
		memset(fa,0,sizeof fa);
		for (j=top;j>=1;j--)//插入排序 
		{
			if (e[st[j]].s>e[i].s) st[j+1]=st[j];
			else break;
		}
		top++;
		st[j+1]=i;
		
		int num=0;
		for (int j=1;j<=top;j++)
		{
			int fa1=find_fa(e[st[j]].u),fa2=find_fa(e[st[j]].v);
			if (fa1!=fa2)
			{
				fa[fa1]=fa2;
				st[++num]=st[j];
			}
		}
		if (num==n-1) ans=min(ans,g*e[i].g+s*e[st[num]].s);
		top=num;
	}
	
	if (ans==inf) printf("-1");
	else printf("%lld",ans);
	return 0;
}

I.重建小镇

就是开篇讲的最优比率生成树!

神奇的代码
#incIude <bits/stdc++.h>
#define int long long
using namespace std;
const int N=405;
const int M=1e4+5;
const int MAX=2e15;
int n,m,f;
struct node{
	int u,v,c,t;//成本,时间 
	double w;
}e[M];

int fa[N];
bool cmp(node a,node b) {  return a.w<b.w;  }
int find_fa(int x)
{
	if (fa[x]==x) return x;
	return fa[x]=find_fa(fa[x]);
}
bool check(int x)
{
	int tmp=1;
	for (int i=1;i<=n;i++) fa[i]=i;
	for (int i=1;i<=m;i++) e[i].w=x/(3e6+0.0)*e[i].t+e[i].c;
	sort(e+1,e+1+m,cmp);
	
	double k=f+1e-12;
	for (int i=2;i<=n;i++)
	{
		while (tmp<=m&&find_fa(e[tmp].u)==find_fa(e[tmp].v)) tmp++;
		fa[find_fa(e[tmp].u)]=find_fa(e[tmp].v);
		k-=e[tmp].w;
		if (k<0) return false;
	}
	return true;
}
signed main()
{
	scanf("%lld%lld%lld",&n,&m,&f);
	for (int i=1;i<=m;i++) scanf("%lld%lld%lld%lld",&e[i].u,&e[i].v,&e[i].c,&e[i].t);
	
	int l=0,r=MAX;
	while (l<r)
	{
		int mid=l+r+1>>1;
		if (check(mid)) l=mid;
		else r=mid-1;
	}
	
	printf("%0.4lf",l/(3e6+0.0));
	return 0;
}
posted @   还是沄沄沄  阅读(47)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· NetPad:一个.NET开源、跨平台的C#编辑器
· PowerShell开发游戏 · 打蜜蜂
· 凌晨三点救火实录:Java内存泄漏的七个神坑,你至少踩过三个!
点击右上角即可分享
微信分享提示