http://codevs.cn/problem/3287/
方法一三时间复杂度为O(mlogn),方法二时间复杂度为O(mlog2n)但在实际测试中速度方法三快于方法二快于方法一。
方法一:倍增lca+ST表
思路
由题设条件可知,要求求最大瓶颈路,答案一定在原图的最大生成树上,那么我们就建立一颗最大生成树,ST表(或者单纯归为树上倍增)记录每一点向上一段距离的最小值。因为树上两点间距离是唯一的,每次询问时,只需倍增求起点和终点的lca,再查询起点到lca,和终点到lca路径上的最小值即可。
代码
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
using namespace std;
typedef long long ll;
ll n,m,q,ru,rv,rw,tot,a,b,lca,cnt;
ll first[200010],nxt[200010],fa[100010],deep[200010];
ll anc[100010][24],mins[100010][24];
struct edge
{
ll u,v,w;
}l[200010];
struct edgs
{
ll u,v,w;
}p[200010];
void add(ll f,ll t,ll c)
{
p[++tot]=(edgs){f,t,c};
nxt[tot]=first[f];
first[f]=tot;
}
bool cmp(edge a,edge b)
{
return a.w>b.w;
}
ll find(ll x)
{
return fa[x]==x?x:fa[x]=find(fa[x]);
}
void Kruskal()
{
for(ll i=1;i<=n;i++)
fa[i]=i;
sort(l+1,l+m+1,cmp);
for(ll i=1;i<=m;i++)
{
ll u=find(l[i].u);
ll v=find(l[i].v);
if(u!=v)
{
cnt++;
fa[u]=v;
add(l[i].u,l[i].v,l[i].w);
add(l[i].v,l[i].u,l[i].w);
}
if(cnt==n-1) break;//所有点都连通了
}
}
void dfs(ll k,ll fa)
{
deep[k]=deep[fa]+1;
anc[k][0]=fa;
for(ll i=1;anc[k][i-1];i++)//注意为anc[k][i-1]
{
anc[k][i]=anc[anc[k][i-1]][i-1];
mins[k][i]=min(mins[k][i-1],mins[anc[k][i-1]][i-1]);
}
for(ll i=first[k];i!=-1;i=nxt[i])
{
ll x=p[i].v;
if(x!=fa)
{
mins[x][0]=p[i].w;//x-k间最小值即x-k间边权
dfs(x,k);
}
}
}
ll ask_min(ll x,ll y)
{
ll minx=1e11+7;
ll d=deep[x]-deep[lca];
for(ll i=20;i>=0;i--)
{
if(d&(1<<i))
{
minx=min(minx,mins[x][i]);
x=anc[x][i];
}
}
d=deep[y]-deep[lca];
for(ll i=20;i>=0;i--)
{
if(d&(1<<i))
{
minx=min(minx,mins[y][i]);
y=anc[y][i];
}
}
return minx;
}
ll ask_lca(ll x,ll y)
{
if(deep[x]<deep[y])
swap(x,y);
if(deep[x]>deep[y])
{
ll d=deep[x]-deep[y];
for(ll i=20;i>=0;i--)
{
if(d&(1<<i))
x=anc[x][i];
}
}
if(x!=y)
{
for(ll i=20;i>=0;i--)
{
if(anc[x][i]!=anc[y][i])
{
x=anc[x][i];
y=anc[y][i];
}
}
}
if(x==y)
lca=x;
else lca=anc[x][0];
return ask_min(a,b);
}
int main()
{
memset(first,-1,sizeof(first));
memset(mins,0x7f,sizeof(mins));
scanf("%lld%lld",&n,&m);
for(ll i=1;i<=m;i++)
{
scanf("%lld%lld%lld",&ru,&rv,&rw);
l[++tot]=(edge){ru,rv,rw};
}
tot=0;
Kruskal(),dfs(1,0);
scanf("%lld",&q);
for(ll i=1;i<=q;i++)
{
scanf("%lld%lld",&a,&b);
if(find(a)!=find(b))//并查集判断连通性
printf("-1\n");
else printf("%lld\n",ask_lca(a,b));
}
return 0;
}
方法二:树链剖分+线段树
思路
与方法一大致相同,只不过用树链剖分求lca,并用线段树维护树上两点间最小值。
为了便于处理我们将边权赋为点权,默认以一号点为根,一号点的权值赋极大值。
注意边权赋为点权后,跳链时查询和在一条重链上查询时的边界问题。
代码
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
using namespace std;
int n,m,q,ru,rv,rw,tot,a,b,lca,cnt;
int first[200010],nxt[200010],fas[100010];
int deep[100010],f[100010],siz[100010],son[100010],top[100010],totre[100010],toseg[100010],val[100010];
struct edge
{
int u,v,w;
}l[2000010];
struct edgs
{
int u,v,w;
}p[2000010];
struct inte
{
int l,r,minx;
}tree[5000010];
void add(int f,int t,int c)
{
p[++tot]=(edgs){f,t,c};
nxt[tot]=first[f];
first[f]=tot;
}
bool cmp(edge a,edge b)
{
return a.w>b.w;
}
int find(int x)
{
return fas[x]==x?x:fas[x]=find(fas[x]);
}
void Kruskal()
{
for(int i=1;i<=n;i++)
fas[i]=i;
sort(l+1,l+m+1,cmp);
for(int i=1;i<=m;i++)
{
int u=find(l[i].u);
int v=find(l[i].v);
if(u!=v)
{
cnt++;
fas[u]=v;
add(l[i].u,l[i].v,l[i].w);
add(l[i].v,l[i].u,l[i].w);
}
if(cnt==n-1) break;
}
}
void dfs_1(int k,int fa,int d)
{
deep[k]=d;
f[k]=fa;
siz[k]=1;//而非siz[k]++;
for(int i=first[k];i!=-1;i=nxt[i])
{
int x=p[i].v;
if(x==fa)
continue;
val[x]=p[i].w;//边权下发为点权
dfs_1(x,k,d+1);
siz[k]+=siz[x];
if(!son[k]||siz[x]>siz[son[k]])
son[k]=x;
}
}
void dfs_2(int k,int num)
{
top[k]=num;
toseg[k]=++tot;
totre[toseg[k]]=k;
if(!son[k])
return;
dfs_2(son[k],num);
for(int i=first[k];i!=-1;i=nxt[i])
{
int x=p[i].v;
if(x!=f[k]&&x!=son[k])
dfs_2(x,x);
}
}
void update(int now)
{
tree[now].minx=min(tree[now<<1].minx,tree[now<<1|1].minx);
}
void build(int now,int l,int r)
{
tree[now].l=l;
tree[now].r=r;
if(l==r)
{
tree[now].minx=val[totre[l]];
return;
}
int mid=(l+r)>>1;
build(now<<1,l,mid);
build(now<<1|1,mid+1,r);
update(now);
}
int ask_min(int now,int l,int r)
{
if(tree[now].l>=l&&tree[now].r<=r)
{
return tree[now].minx;
}
int mid=(tree[now].l+tree[now].r)>>1;
int ans=1e9+7;
if(l<=mid)
ans=min(ans,ask_min(now<<1,l,r));
if(r>mid)
ans=min(ans,ask_min(now<<1|1,l,r));
return ans;
}
int ask_lca(int x,int y)
{
int f1=top[x],f2=top[y],ans=1e9+7,tmp;
while(f1!=f2)
{
if(deep[f1]<deep[f2])
{
swap(f1,f2);
swap(x,y);
}
ans=min(ans,ask_min(1,toseg[f1],toseg[x]));//边界
x=f[f1]; //deep[top[x]]<deep[top[y]],top[x]上方的边一定会走到,toseg[top[x]]必定会被统计
f1=top[x];
}
if(deep[x]<=deep[y])
lca=x,tmp=y;
if(deep[x]>deep[y])
lca=y,tmp=x;
ans=min(ans,ask_min(1,toseg[son[lca]],toseg[tmp]));//边界
return ans;
}
int main()
{
memset(first,-1,sizeof(first));
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
{
scanf("%d%d%d",&ru,&rv,&rw);
l[++tot]=(edge){ru,rv,rw};
}
tot=0;
Kruskal();
dfs_1(1,0,1);
tot=0;
dfs_2(1,1);
val[1]=1e9+7;//极大值
build(1,1,n);
scanf("%d",&q);
for(int i=1;i<=q;i++)
{
scanf("%d%d",&a,&b);
if(find(a)!=find(b))
printf("-1\n");
else printf("%d\n",ask_lca(a,b));
}
return 0;
}
方法三:按秩合并+并查集树结构上暴力查询
先放三张图片,作为对代码的解释
1.此处为样例按秩合并后的树形
2.按秩合并降低树高——还要合并到同一根节点
3. ask_min(6,7)—3对取min得到的答案无影响
思路
第三种方法比较难以理解——但却是理解按秩合并的好机会。
我们定义一个rank[i],表示]以i为顶点的链的深度,是从0开始计的,这个定义很重要。
首先需要知道,对于rank不同的两条链按秩合并(事实上这个链的定义是不准确的,按秩合并的两条链中必定有一条深度不超过2),我们会将rank小的合并到rank大的下面,具体请见图片2,且每次合并为根节点的合并,用于降低树高。
注意是暴力查询暴力查询不找lca!,本题不使用路径压缩的优化方法是因为路径压缩会破坏并查集树形导致无法统计。但事实上按秩合并在在某些情况下依旧破坏了树形…但不影响正确性..仍可以进行统计 问题在于:
破坏树形后,无法确定两点的lca。但仍可以通过两点依次向上查询,直到找到按秩合并后树的根节点(可能与原根节点不同)为止,得到原路径上的最小值。
为什么这样做是正确的呢?——这个是由我们的建树方法决定的,牢记这是棵最大生成树,我们将边从大到小依次加入
1.按秩合并后的两点到根节点的路径一定包含了原树中两点到其lca路径(原有道路)。——按秩合并可以保证
2.原有道路在按秩合并的并查集树中的深度(注意与rank相反)一定大于等于其包含的其他的,lca到原树根节点的道路——按秩合并使得后加入的点的深度大于等于先加入点的深度
3.深度较大的点的权值较小,取min至根节点对答案无影响——后加入的点权值小
(我们将边权赋为点权了)
建议手推验证,可以参考图片3理解。
代码
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
using namespace std;
int n,m,q,a,b,ru,rv,rw,tot;
int first[200010],nxt[200010],fa[100010],rank[200010],val[200010],vis[200010];
struct edge
{
int u,v,w;
}l[200010];
void build(int f,int t,int c)
{
l[++tot]=(edge){f,t,c};
nxt[tot]=first[f];
first[f]=tot;
}
bool cmp(edge a,edge b)
{
return a.w>b.w;//最大生成树
}
int find(int x)
{
return fa[x]==x?x:find(fa[x]);
}
void Kruskal()
{
for(int i=1;i<=n;i++)//预处理
{
fa[i]=i;
vis[i]=1e9+1;
}
sort(l+1,l+2*m+1,cmp);
for(int i=1;i<=2*m;i++)
{
int x=find(l[i].u);
int y=find(l[i].v);
if(x!=y)//按秩合并降低树高
{
if(rank[x]<rank[y])//rank[i]表示以i为顶点的链的深度
{ //将深度小的与深度大的合并到同一父节点
fa[x]=y;//---见图片2
val[x]=l[i].w;//边权下放为点权
}
else
{
fa[y]=x;
val[y]=l[i].w;
if(rank[x]==rank[y])
rank[x]++;//调整rank
}
}
}
}
int ask_min(int x,int y)//暴力查询
{
int ans1=1e9,ans2=1e9+1,tmp=x;
while(1)
{
vis[x]=ans1;//到当前节点为止的最大值
if(x==fa[x])//找到根节点
break; //边权从大到小排序然后按秩合并,保证深度(注意与rank相反)较大的点的权值较小
ans1=min(ans1,val[x]);//取min至根节点无影响---见图片3
x=fa[x];
}
while(1)
{
if(vis[y]<=1e9)//x,y在同一条到根节点的链上且x在y下方
{ //即y的答案已经被找过,直接退出
ans2=min(ans2,vis[y]);//注意取min
break;
}
if(y==fa[y])
break;
ans2=min(ans2,val[y]);
y=fa[y];
}
x=tmp;
while(1)//多次询问清空vis
{
vis[x]=1e9+1;
if(x==fa[x])
break;
x=fa[x];
}
return ans2;
}
int main()
{
memset(first,-1,sizeof(first));
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
{
scanf("%d%d%d",&ru,&rv,&rw);//对于一条边只去边权,而其两端点在并查集中的上下位置无所谓,尽管双向边只会加入随机一条 ,但对答案没有影响
build(ru,rv,rw);//故只建单向边也可以
build(rv,ru,rw);
}
Kruskal();
scanf("%d",&q);
for(int i=1;i<=q;i++)
{
scanf("%d%d",&a,&b);
if(find(a)!=find(b))
printf("-1\n");
else printf("%d\n",ask_min(a,b));
}
return 0;
}
十分关键的一点是:对于树上问题—-按秩合并后直接在并查集树上进行统计的做法只适用于部分题目(如满足区间可加性的题目),不进行优化的并查集树则适用于大部分题目,所以对于此类方法还需慎用。
另外,理论上来说可以通过对边按起点或终点排序优化按秩合并,使根节点直接连接的点的数量尽可能多。但是本题主要按边权排序加边,按起点排序优化按秩合并意义不大,意为:起点排序后合并当然会使树形压缩优化查找,但主要按边权排序后仅对边权相同的边生效,意义不大 。