洛谷P4180 [BJWC2010]严格次小生成树

题目描述

(C)最近学了很多最小生成树的算法,(Prim)算法、(Kurskal)算法、消圈算法等等。正当小(C)洋洋得意之时,小(P)又来泼小(C)冷水了。小(P)说,让小(C)求出一个无向图的次小生成树,而且这个次小生成树还得是严格次小的,也就是说:如果最小生成树选择的边集是(EM),严格次小生成树选择的边集是(ES),那么需要满足:((value(e))表示边(e)的权值) (sum_{e in E_M}value(e)<sum_{e in E_S}value(e))

这下小(C)蒙了,他找到了你,希望你帮他解决这个问题。

输入格式

第一行包含两个整数(N)(M),表示无向图的点数与边数。 接下来(M)行,每行(3)个数(x) (y) (z) 表示,点 (x) 和点 (y) 之间有一条边,边的权值为 (z)

输出格式

包含一行,仅一个数,表示严格次小生成树的边权和。(数据保证必定存在严格次小生成树)

输入输出样例

输入 #1

5 6
1 2 1 
1 3 2 
2 4 3 
3 5 4 
3 4 3 
4 5 6

输出 #1

11

说明/提示

数据中无向图不保证无自环; (50\%) 的数据(N≤2 000) (M≤3 000)(80\%) 的数据(N≤50 000) (M≤100 000)(100\%) 的数据(N≤100 000) (M≤300 000) ,边权值非负且不超过 (10^9)

思路

首先要知道一个定理:严格次小生成树只有一条边与最小生成树不一样。但是我不会证明。OI又不考证明,感性理解一下即可。一共有(n)个点,首先我们用(n-1)条边构成最小生成树,然后将所有的边分成组成最小生成树的边和没有组成最小生成树的边,以下简称树边和非树边。那么方法就很显而易见了,首先求出最小生成树。然后枚举非树边。最小生成树加上一条非树边之后会形成一个环,删去环内除了新加入的边的最大边即可。而加入的边很显然是大于等于环内所有的边的,所以此时分类讨论。如果新加入的边等于最大边,因为我们求的是严格次小生成树,所以不能相等,此时删去环内次大边作为候选答案。而如果新加入的边大于环内除了新加入的非树边的最大边,那么直接删去最大边即可。即设最小生成树边权和为(sum),最后的严格次小生成树边权和为(ans)

if(val1 == edge[i].z){
      ans = min(ans, sum-val2+edge[i].z);//edge[i].z为非树边边权,val1为最大边权,val2为次大边权
}
if(val1 > edge[i].z){
      ans = min(ans, sum-val1+edge[i].z);
}

然后剩下的问题就是求两点之间的边权最大值和次大值了。其实就是找到两点的(LCA),然后记录路径上的最大边权和次大边权。还是可以用倍增来优化求解。在寻找(LCA)的同时,用三维数组(g_{i,j,k}),来表示当前点为(i),向上跳(2^j)到达的点,与当前点路径上的最大边权和最小边权。(k=0)记录最大边权,(k=1)记录次大边权。倍增求(LCA)的状态转移方程不再赘述。主要是探求(g)数组的转移方程。其实一共就只有四条边相互比较和更新。(g[i][j-1][0])(g[f[i][j-1]][j-1][0])(g[i][j-1][1])(g[f[i][j-1]][j-1][1])。首先考虑更新最大值,也就是(g[i][j][0])。最大值肯定是由两个最大值取最大值转移而来的。而次大值就要分类讨论了,详情见代码:

      g[y][j][0]=max(g[f[y][j-1]][j-1][0],g[y][j-1][0]);//最大值通过两个最大值比较来更新
      if(g[y][j-1][0]==g[f[y][j-1]][j-1][0]){//如果两个最大值相等,遵循**严格**次小的原则不能取等,所以用两个次小值来更新。
            g[y][j][1]=max(g[y][j-1][1],g[f[y][j-1]][j-1][1]);
      }
      if(g[y][j-1][0]>g[f[y][j-1]][j-1][0]){//如果一大一小,那肯定是用较大的最大值来更新最大值,用较大的最大值的次大值和较小的最大值来更新次大值(虽然很绕但很好理解)
            g[y][j][1]=max(g[f[y][j-1]][j-1][0],g[y][j-1][1]);
      }
      if(g[y][j-1][0]<g[f[y][j-1]][j-1][0]){//同理
            g[y][j][1]=max(g[y][j-1][0],g[f[y][j-1]][j-1][1]);
      }

到这里是预处理所有路径上的最大边权和次大边权。然后在走的时候实时更新路径上的最大边权和次大边权即可。

怒切紫题ed

代码

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#include<queue>
#include<cstdlib>
#include<ctime>
using namespace std;
typedef long long int ll;
const ll N=900005,M=900005;
ll n,m,sum,tot,tot2,root,val1,val2,ans=100000000000000,t;
ll g[N][20][2],fa[N],f[N][20];
ll ver[M],Next[M],head[N],edge[M],not_tree[M],d[N];
bool b[N];
queue<ll> q;
struct Edge{
	ll u,v,z;
}edge1[N];
ll cmp(Edge a,Edge b){
	return a.z<b.z;
}
ll get(ll x){
	if(fa[x]!=x) fa[x]=get(fa[x]);
	return fa[x];
}
void add(ll x,ll y,ll z){
	ver[++tot]=y;
	edge[tot]=z;
	Next[tot]=head[x];
	head[x]=tot;
}
void bfs(){
	d[root]=1;
	q.push(root);//别忘记从根节点开始bfs
	while(!q.empty()){
		ll x=q.front();
		q.pop();
		for(ll i=head[x];i;i=Next[i]){
			ll y=ver[i],z=edge[i];
			if(d[y]) continue;
			d[y]=d[x]+1;
			f[y][0]=x;
			g[y][0][0]=z;
			g[y][0][1]=-10000000;
			for(ll j=1;j<=t;j++){
				f[y][j]=f[f[y][j-1]][j-1];
			}//LCA预处理
			for(ll j=1;j<=t;j++){
				g[y][j][0]=max(g[f[y][j-1]][j-1][0],g[y][j-1][0]);
				if(g[y][j-1][0]==g[f[y][j-1]][j-1][0]){
					g[y][j][1]=max(g[y][j-1][1],g[f[y][j-1]][j-1][1]);
				}
				if(g[y][j-1][0]>g[f[y][j-1]][j-1][0]){
					g[y][j][1]=max(g[f[y][j-1]][j-1][0],g[y][j-1][1]);
				}
				if(g[y][j-1][0]<g[f[y][j-1]][j-1][0]){
					g[y][j][1]=max(g[y][j-1][0],g[f[y][j-1]][j-1][1]);
				}
			}//最大边权和次大边权预处理
			q.push(y);
		}
	}
}
void lca(ll x,ll y){
	if(d[x]>d[y]){
		swap(x,y);
	}
	for(ll i=t;i>=0;i--){
		if(d[f[y][i]]>=d[x]){
			if(g[y][i][0]>val1){
				val2=max(val1,g[y][i][1]);
				val1=g[y][i][0];
			}//路径中更新最大边权和次大边权,原理很简单,在此不再赘述。只要注意先更新次大值再更新最大值这一细节即可。
			if(g[y][i][0]<val1){
				val2=max(val2,g[y][i][0]);
			}
			y=f[y][i];
		}
	}
	if(x==y) return;
	for(ll i=t;i>=0;i--){
		if(f[x][i]!=f[y][i]){
			if(g[y][i][0]>val1){
				val2=max(val1,g[y][i][1]);
				val1=g[y][i][0];
			}
			if(g[y][i][0]<val1){
				val2=max(val2,g[y][i][0]);
			}
			if(g[x][i][0]>val1){
				val2=max(val1,g[x][i][1]);
				val1=g[x][i][0];
			}
			if(g[x][i][0]<val1){
				val2=max(val2,g[x][i][0]);
			}
			y=f[y][i];
			x=f[x][i];
		}
	}
	if(g[y][0][0]>val1){
		val2=max(val1,g[y][0][1]);
		val1=g[y][0][0];
	}
	if(g[y][0][0]<val1){
		val2=max(val2,g[y][0][0]);
	}
	if(g[x][0][0]>val1){
		val2=max(val1,g[x][0][1]);
		val1=g[x][0][0];
	}
	if(g[x][0][0]<val1){
		val2=max(val2,g[x][0][0]);
	}
}
int main()
{
	scanf("%lld%lld",&n,&m);
	t=log(n)/log(2)+1;
	for(ll i=1;i<=n;i++){
		fa[i]=i;
	}
	for(ll i=1,x,y,z;i<=m;i++){
		scanf("%lld%lld%lld",&x,&y,&z);
		edge1[i].u=x;
		edge1[i].v=y;
		edge1[i].z=z;
	}
	sort(edge1+1,edge1+1+m,cmp);
	for(ll i=1;i<=m;i++){
		ll x=get(edge1[i].u);
		ll y=get(edge1[i].v);
		if(x==y){
			not_tree[++tot2]=i;
			continue;
		}//记录非树边 
		fa[y]=x;
		add(edge1[i].u,edge1[i].v,edge1[i].z);
		add(edge1[i].v,edge1[i].u,edge1[i].z);
		sum+=edge1[i].z;//先求出最小生成树边权和
	}
	for(ll i=1;i<=n;i++){
		if(fa[i]==i){
			root=i;
			break;
		}
	}
	bfs();
	for(ll i=1;i<=tot2;i++){
		ll z1=edge1[not_tree[i]].z;
		val1=0;
		val2=0;
		lca(edge1[not_tree[i]].u,edge1[not_tree[i]].v);
		if(z1>val1){
			ans=min(ans,sum-val1+z1);
		}
		if(z1==val1){
			ans=min(ans,sum-val2+z1);
		}
	}
	printf("%lld
",ans);
	return 0;
}

Kruscal复杂度为(O(mlog_2 n)),倍增(LCA)预处理复杂度为(O(nlog_2 n)),每次询问复杂度为(O(log_2 n)),共有(m-n+1)条非树边,即有(m-n+1)次询问,所以总时间复杂度就是(O((2*m+1)*log_2 n))(1s)内解决绰绰有余。

调了一上午发现是(LCA)里写了(x=f[y][i]),都是(Ctrl+c,Ctrl+v)后少改变量名的后果,所以复制相似部分时一定要细心更改(QWQ)

十年OI一场空,不开longlong见祖宗

原文地址:https://www.cnblogs.com/57xmz/p/13501365.html