极差最小生成树(苗条的生成树,Urban Geography)【LCT】

问题描述:

vjudge题目链接:Urban Geography
给你一个n个点m条带权边的图,求一棵生成树,使得它的最大边减去最小边最小,输出任意一组符合条件的所选边的编号。

题目分析:

看了看我初一写的博客:苗条的生成树
不忍直视QWQ
这道题是有nlogn做法的,LCT或者分治回溯+并查集,这里主要说前一种。

最小极差很容易想到最小生成树,但是最小边不好确定,枚举的话复杂度是n2logn的。

将边从小到大排序后依次加边,假设上一条边加入后刚好构成最小生成树,那么加入当前边的时候,由于下一种树的最大值必然已经大于等于了这条边,所以最优的选择应该是尽量让这条边把最小的那条边替换掉。

具体做法是找到这条边在树上对应的两点,找到两点路径上的最小边,然后把它删掉,再加入当前边。

在边数==n-1的时候就可以计算答案,最大最小边的值可以用set删存(存编号比较方便),删边加边以及找最小边就是LCT的基本操作了。
最后得到答案对应的最小边编号后从那里开始做一遍kruskal就可以得到方案了。

Code:

#include <bits/stdc++.h>
#define maxn 100005
using namespace std;
int n,m;
namespace LCT{
	int ch[maxn][2],fa[maxn],mn[maxn];
	bool rev[maxn];
	#define il inline
	#define pa fa[x]
	il bool isr(int x){return ch[pa][0]!=x&&ch[pa][1]!=x;}
	il bool isc(int x){return ch[pa][1]==x;}
	il void upd(int x){
		mn[x]=x>n?x:maxn;
		if(mn[ch[x][0]]>n) mn[x]=min(mn[x],mn[ch[x][0]]);
		if(mn[ch[x][1]]>n) mn[x]=min(mn[x],mn[ch[x][1]]);
		if(mn[x]==maxn) mn[x]=0;
	}
	il void rot(int x){
		int y=fa[x],z=fa[y],c=isc(x);
		!isr(y)&&(ch[z][isc(y)]=x);
		(ch[y][c]=ch[x][!c])&&(fa[ch[y][c]]=y);
		fa[ch[x][!c]=y]=x,fa[x]=z;
		upd(y),upd(x);	
	}
	il void pd(int x){
		if(rev[x]){
			swap(ch[x][0],ch[x][1]),rev[x]=0;
			if(ch[x][0]) rev[ch[x][0]]^=1;
			if(ch[x][1]) rev[ch[x][1]]^=1;
		}
	}
	il void pdpath(int x){if(!isr(x)) pdpath(pa); pd(x);}
	il void splay(int x){
		for(pdpath(x);!isr(x);rot(x))
			if(!isr(pa)) rot(isc(pa)==isc(x)?pa:x);	
	}
	il void access(int x){
		for(int y=0;x;x=fa[y=x]) splay(x),ch[x][1]=y,upd(x);
	}
	il void bert(int x){access(x),splay(x),rev[x]^=1;}
	il int sert(int x){access(x),splay(x);for(;ch[x][0];x=ch[x][0]);return x;}
	il bool connect(int x,int y){return sert(x)==sert(y);}
	il void link(int x,int y){bert(x),fa[x]=y;}
	il void cut(int x,int y){bert(x),access(y),splay(y),ch[y][0]=fa[x]=0,upd(y);}
	il int split(int x,int y){bert(x),access(y),splay(y);return y;}
}
using namespace LCT;
struct node{
	int x,y,w,id;
	bool operator < (const node &p)const{return w<p.w;}
}e[maxn];
int Ans=1<<30,L,R;
bool mark[maxn];
set<int>E;
int F[maxn];
int Find(int x){return F[x]==x?x:F[x]=Find(F[x]);}
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++) scanf("%d%d%d",&e[i].x,&e[i].y,&e[i].w),e[i].id=i;
    sort(e+1,e+1+m);
    for(int i=1;i<=m;i++){
    	if(connect(e[i].x,e[i].y)){
    		int k=mn[split(e[i].x,e[i].y)]-n;
    		E.erase(k),cut(e[k].x,k+n),cut(e[k].y,k+n);
    	}
    	E.insert(i),link(e[i].x,n+i),link(n+i,e[i].y);
    	if(E.size()==n-1){
    		int l=*E.begin(),r=*E.rbegin(),tmp=e[r].w-e[l].w;
    		if(Ans>tmp) Ans=tmp,L=l,R=r;
    	}
    }
    for(int i=1;i<=n;i++) F[i]=i;
    for(int i=L,x,y;i<=R;i++) if((x=Find(e[i].x))!=(y=Find(e[i].y))) F[x]=y,mark[e[i].id]=1;
    for(int i=1;i<=m;i++) if(mark[i]) printf("%d ",i);
}

分治+并查集的解法可以参见这篇博客,大致思想就是二分答案然后判断是否有解,判断采用类似线段树分治的方法将边设置时间范围,启发式合并、回退并查集。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值