超级好题,完美考核了2个算法:(最小生成树+lca);
当然,也可以用“树链剖分”来替代“lca”。如果想学树链剖分,请拉到最后!
题目大意:
n个点的无向图内,m次询问:x 点到 y 点的路径中,最大值是多少?如果 x与y不连通,输出-1;
解法分析:
1、在无向图中,求两点的连通性,还要求两点之间道路的最值,应该先建树(最小生成树);
2、多次询问 x与 y之间的道路最值,就是树上的两点的关系,用“最近公共祖先(lca)”来实现;
3、用倍增数组来优化求 lca的过程。
算法介绍:
1、最小生成树:就是用最少的边(n-1条),把全图连通;
这里用k算法,方法是:对原图的边进行排序,再取其中的最多(n-1)条边来连接 n个点(就是一棵树(无环)了);
2、lca:求树上的两个点的最近公共祖先;
这里用倍增数组进行优化跳跃,倍增在这里表现在一个二维数组(如果之前没接触过的孩子,但看本题解比较难懂,加油哦)
上代码:(部分细节,还是在代码注解中)
#include<cstdio>
#include<algorithm>
using namespace std;
const int mx=100005,inf=999999999;//n的最值
int n,m,len=0;;
struct nod{int x,y,c,gg;}e[mx*5],b[mx*2];
int h[mx],d[mx],f[mx],fa[mx],p[mx][30],w[mx][30];
bool cmp(nod x,nod y) { return x.c>y.c; }//排序的参数
int ch(int x) { if(f[x]==x) return x; return f[x]=ch(f[x]); }//并查集的板子
void ins(int x,int y,int c)
{
len++;b[len].x=x;b[len].y=y;b[len].c=c;b[len].gg=h[x];h[x]=len;
}
void dfs(int x)//对树分层的函数:同时初始化倍增的p和w数组
{
for(int i=h[x];i>0;i=b[i].gg)
{
int y=b[i].y;
if(d[y]==0&&p[x][0]!=y)
{
d[y]=d[x]+1;//更新y点
p[y][0]=x; w[y][0]=b[i].c;//w[y][0]指y到父亲的这段路的最小价值
dfs(y);
}
}
}
void ycl()
{
for(int i=1;i<=n;i++) d[i]=0;//对树分层
for(int i=1;i<=n;i++) //可能有多棵树,对每棵树,任意选根
if(d[i]==0) { d[i]=1;p[i][0]=0; dfs(i);}//p[i][0]等同于 i的父亲节点
//下面循环是倍增的核心代码,规模从小到大填写p数组和w数组(思维类似合并石子)
for(int i=1;i<=20;i++)
{
for(int x=1;x<=n;x++)//x点的第i跳能到达的地方,记录为p[x][i];
{ //w[x][i]则表示这个过程的最小值
p[x][i]=p[p[x][i-1]][i-1];
w[x][i]=min(w[x][i-1],w[p[x][i-1]][i-1]);
}//以上两句需要自行理解,我懒得画图讲解了~~
}
//到此为止,每棵树都已经分层完毕,倍增数组也弄好了!!
//剩下就是树上的查询工作(lca)了 ~~
}
int lca(int x,int y)//题目求的是 x-y之间的最小值,现在让x和y跳到他们的lca上相遇
{
if(d[x]<d[y]) swap(x,y);//人为定义方向,让 x 在下, y在上,让x找y
int ans=inf;
for(int i=20;i>=0;i--)
{
if(d[p[x][i]]>=d[y])//先让 x 跳到 y的同层
{
ans=min(ans,w[x][i]);//注意先维护,再更新
x=p[x][i];
}
}
if(x==y) return ans;//同层又相同,说明 y 本身就是 x的 祖先
//如果x、y不同子树,就一起跳上去
for(int i=20;i>=0;i--)
{
if(p[x][i]!=p[y][i])
{
ans=min(ans,w[x][i]); x=p[x][i]; //注意先维护,再更新
ans=min(ans,w[y][i]); y=p[y][i]; //注意先维护,再更新
}
}//最终他们跳到lca的下一层停住
ans=min(ans,min(w[x][0],w[y][0]));//更新最后一层数据
return ans;
}
int main()
{
scanf("%d %d",&n,&m);int x,y;
//最小生成树部分
for(int i=1;i<=m;i++) scanf("%d %d %d",&e[i].x,&e[i].y,&e[i].c);
sort(e+1,e+1+m,cmp);//e是原图的边,以c作为关键字排序:从大到小
for(int i=1;i<=n;i++) { f[i]=i;h[i]=0; }//初始化,用来构树
int kk=0;
for(int i=1;i<=m;i++)//最多取 n-1 条边构树
{
x=ch(e[i].x); y=ch(e[i].y);
if(x!=y) //如果x、y不在同一个集合,说明当前边有用
{
ins(e[i].x,e[i].y,e[i].c);
ins(e[i].y,e[i].x,e[i].c);//双向边
f[x]=y;kk++;if(kk==n-1) break;
}
}//本题可能会建成多棵树
//对树进行预处理
ycl();
scanf("%d",&m);//m次询问
while(m--)
{
scanf("%d %d",&x,&y);
int xx=ch(x);
int yy=ch(y);
if(xx!=yy) printf("-1\n");//两点不在同一棵树上,不能到达
else printf("%d\n",lca(x,y)); //输出 x-y之间的道路最小值
}
return 0;
}
··········································································以上是LCA的代码·······················································································
···································································以下是树链剖分的代码·······················································································
算法介绍:
1、构树的过程是一样的:
最小生成树:就是用最少的边(n-1条),把全图连通;
这里用k算法,方法是:对原图的边进行排序,再取其中的最多(n-1)条边来连接 n个点(就是一棵树(无环)了);
2、树链剖分:
2.1 把一棵树(从上而下)看成很多条链,对于一条特定的链(重链),给他连续的,新的编号;
2.2 因为连续编号,把树划分为很多编号连续的(重链);
2.3 以前我们学过线段树,可以高效管理连续的区间;
2.4 直接用线段树来管理(树链);
3、树剖的优势:
因为连续区间已经用线段树来管理了,查找复杂度时间大大缩小。如果上下两个区间不在同一条链内,就做个跳跃吧。
4、代码思路:
4.1 构造最小生成树;
4.2 深搜,求出每个点管理的节点数(包括自己),每个父亲都有一个重儿子;
4.3 给树上的点赋予新的编号:重儿子们得到连续的编号,其他点也得到新编号;
4.3 建立线段树,用来管理新编号;
4.4 求 x-〉y 之间的值,通过线段树来快速查找连续区间,区间之间,就直接跳上去。
上AC代码:(之前错误已经差出来了,感谢小六的同学们!!!)
#include<cstdio>
#include<algorithm>
using namespace std;
const int mx=100005;
struct nodx{int f,tot,son,t,d,c,n;}a[mx];//原图的点
struct nodb{int x,y,c,gg;}b[mx*6],e[mx*3];//双向边
struct nodt{int l,r,ls,rs,c;}t[mx*2];//线段树的点 (c是最小值)
int n,m,len=0,nx=0,lt=0,la[mx],f[mx];//last数组,f数组用于并查集
bool cmp(nodb x,nodb y){ return x.c>y.c; }
int ch(int x) { if(x==f[x]) return x; return f[x]=ch(f[x]); }//并查集
void ins(int x,int y,int c)
{
len++;b[len].x=x;b[len].y=y;b[len].c=c;b[len].gg=la[x];la[x]=len;
}
void fc(int x)//将树分层,同时记录重儿子
{
a[x].tot=1;//管理自己
a[x].son=0;//重儿子在回溯时更新
for(int i=la[x];i>0;i=b[i].gg)
{
int y=b[i].y;
if(a[x].f!=y)
{
a[y].c=b[i].c;//将边上的值,给下层的点
a[y].f=x;a[y].d=a[x].d+1;
fc(y);
//回溯:做更新
a[x].tot+=a[y].tot;//更新统计数量
if(a[y].tot>a[a[x].son].tot) a[x].son=y; //更新重儿子
}
}
}
void bl(int x,int tou)//x表示当前点,tou表示当前链的顶端
{
a[x].t=tou;a[x].n=++nx;//获取新编号
if(a[x].son!=0) bl(a[x].son,tou); //优先更新重儿子,使编号连续
for(int i=la[x];i>0;i=b[i].gg)
{
int y=b[i].y;
if(a[x].f!=y&&a[x].son!=y)//非重.儿子
{
bl(y,y);//新开一条链,头是他自己
}
}
}
void bt(int l,int r)//建立空的线段树
{
lt++;int x=lt;t[x].l=l;t[x].r=r;t[x].ls=t[x].rs=-1;
t[x].c=0;//c是和
if(l<r)
{
int m=(l+r)/2;
t[x].ls=lt+1; bt(l,m);
t[x].rs=lt+1; bt(m+1,r);
}
}
void xg(int x,int y,int k)// 改点
{ // 到叶子了:
if(t[x].l==t[x].r) { t[x].c=k; return ;}
//下探:
int m=(t[x].l+t[x].r)/2,ls=t[x].ls,rs=t[x].rs;
if(y<=m) xg(ls,y,k);
else xg(rs,y,k);
//维护:
t[x].c=min(t[ls].c,t[rs].c);
}
int fmin(int x,int l,int r)
{
if(t[x].l==l&&t[x].r==r) return t[x].c;
int m=(t[x].l+t[x].r)/2,ls=t[x].ls,rs=t[x].rs;
if(r<=m) return fmin(ls,l,r);
else if(l>m) return fmin(rs,l,r);
else return min(fmin(ls,l,m),fmin(rs,m+1,r));
}
int smin(int x,int y)
{
int tx=a[x].t, ty=a[y].t,ans=999999999;
//目标是让两点跳到同一条链上 :
while(tx!=ty)
{
if(a[tx].d>a[ty].d) { swap(x,y); swap(tx,ty); }
ans=min(ans,fmin(1,a[ty].n,a[y].n));//y在链内往上跳
y=a[ty].f;ty=a[y].t;
}
//以上循环结束后,x与 y 在同一条链内
if(x==y) return ans;
if(a[x].d>a[y].d) swap(x,y);
return min(ans,fmin(1,a[a[x].son].n,a[y].n));
}
int main()
{
scanf("%d %d",&n,&m);int x,y,c;
//最小生成树部分 =======================
for(int i=1;i<=n;i++) { la[i]=0;f[i]=i; }
for(int i=1;i<=m;i++)
{
scanf("%d %d %d",&x,&y,&c);
e[i].x=x;e[i].y=y;e[i].c=c;
}
sort(e+1,e+m+1,cmp);
int su=0;
for(int i=1;i<=m;i++)
{
x=ch(e[i].x); y=ch(e[i].y);
if(x!=y)
{
ins(e[i].x,e[i].y,e[i].c);
ins(e[i].y,e[i].x,e[i].c);
f[x]=y;su++;if(su==n-1) break;
}
}
//分层:找重儿子
for(int i=1;i<=n;i++) a[i].tot=0;
for(int i=1;i<=n;i++)
{
if(a[i].tot==0) { a[i].f=0;fc(i);}
}
//建立重链关系,赋予新编号
for(int i=1;i<=n;i++)
{
if(a[i].f==0) bl(i,i);
}
bt(1,n);//建立空的线段树
for(int i=1;i<=n;i++) xg(1,a[i].n,a[i].c);//将点的信息填入树内
scanf("%d",&c);//c 次的询问
for(int i=1;i<=c;i++)
{
scanf("%d %d",&x,&y);
int tx=ch(x);
int ty=ch(y);
if(tx!=ty) printf("-1\n");
else printf("%d\n",smin(x,y));
}
return 0;
}