问题描述:
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);
}
分治+并查集的解法可以参见这篇博客,大致思想就是二分答案然后判断是否有解,判断采用类似线段树分治的方法将边设置时间范围,启发式合并、回退并查集。