这应该是联赛以前图论领域最大的毒瘤了吧。
感觉不会再有比这个更难的了。
圆方树很好理解,把每一个环上的点间联边拆掉,再向代表这个环的方点建边,这样就能构成一棵树,我们叫它圆方树。
圆方树有广义的版本,用来处理一般无向图的的问题。
这里所说的是狭义的圆方树,只用来处理仙人掌上问题。
这两种圆方树有啥区别呢?主要区别就是一个只有两个点的点双联通分量要不要建方点的区别。(这只是我口胡出来的比较明显的特征)
我们处理静态仙人掌这个问题时候,用的就是后面这个比较狭义的圆方树。
题目也挺好理解的,就是给一个仙人掌,求两点距离。
方法就是把仙人掌转化成圆方树,然后通过LCA来求两点间距离。
那么这道题主要的处理难点在于:
1.新图之间的边权该如何处理。
2.求出lca之后怎么求出答案。
我们一一分析:
1.新图上只有两种边:
- 如果是圆-圆边,我们选择直接保留边权。
- 如果是方-圆边,我们再次分类:
- 如果是方点和它的父亲的连边,我们把权值设置为0.
- 如果是方点和其子结点的连边,我们把权值设置为这个子结点到方点父亲的最短距离长度。(简单环上两点最短距离可O(1)求)
2.基于以上的定义,我们已经可以绘制出新的图出来,那么我们考虑如何求树上连点距离。
- 直接LCA肯定是不行的,例如同一个环内的两点,他们的LCA是方点,而他们的距离却不是到方点的距离和。
- 我们需要分类讨论:
- 我们根据上面的边权定义可以知道,一个圆点和他的祖先圆点之间的距离一定是原图上两圆点之间的距离。
- 简单证明:(我们把一个环里的方点叫父亲结点,它的父亲就是爷爷结点了)
- 如果这两个点在一个环内,那么那个祖先一定是这个环上的爷爷,否则不可能成为祖先关系。那么这两点间在新图上距离就是子孙圆点-方点-爷爷圆点,权值则是子孙结点到爷爷结点的最短距离,这与我们的定义相吻合。
- 如果这两个点不在同一个环内,那么他们的距离为子孙结点-子孙结点环内的爷爷结点-环与环之间的连边-第二个环上的与第一个环的爷爷相连的点-第二个环的爷爷…………这样一直下去,我们的最开始选的那个祖先结点一定是某个环的爷爷,或是与某个爷爷相连的点,无论哪种情况,权值都是对的(爷-孙权值是对的,环环之间的连边一定是圆-圆边,权值也是原来的)
- 其他情况可以自己模拟一下,大多数会与仙人掌的性质不符而错误,最终只剩下这两种情况。
- 所以如果两个点的LCA是圆点,那么他们的距离就是各自到LCA的距离,与平常的树上距离是一个求法。
- 如果两个的LCA是方点,那么他们的距离是什么呢?
- 如果两个点的LCA是方点,那么某一个点的祖先中的LCA之前的那个祖先(也就是某个点到LCA这条链上的LCA的儿子结点)一定是与LCA这个方点在同一个环里面的。那么我们的最短距离就是:假设a,b是两个待求点,A,B是他们在LCA的环上距离他们最近的两个点(就是上面说的那个祖先),最后结果就是a到A的距离+b到B的距离+A到B的最短距离,由于a,b,A,B都是圆点,a到A,b到B距离可以直接求,剩下A到B的距离就是简单环上两点间最短距离,可以O(1)求。
3.这就是求仙人掌图上两点间距离的方法了,最后我们看看处理细节。
主要有三个难处理的地方:
- 简单环上两点间的最短距离预处理方法
- 我们用到前缀和的思想来处理这个问题。
- 我们对于一个环,从一个点出发,设他为起点,随便定一个时针顺序,把这个点到环上下一个点的边权加到下一个点的sum数组里,最后处理结果时候对于环上两点u,v答案是min(sum[u]-sum[v],sumtot-sum[u]+sum[v]);即u到v之间的边权与这个环上除u到v之间的边权以外的边权和(因为一个简单环上两点间只有两条路可走,sum[u]表示从起点到u的边权和,sum[v]表示起点到v的边权和,sum[u]-sum[v]就是v到u的边权和,sumtot是整个环的边权和,sumtot-sum[u]+sum[v]就是这个环上剩下的边权)
- 如何求一个点某个祖先前面那个祖先(就是处理方点时候的那个祖先的找法)
- 我习惯用树链剖分求LCA,所以这里说一下这种求LCA方法下的找祖先方法,如果是用倍增求LCA,据说处理会更简单一些。
- 我们假设已经知道了u,rt,要找u到rt这条链上rt前面的那个结点。
- 我们考虑,如果u,rt在一条重链上,那么他们的top相同,但是u和rt又不相同,所以直接return son[rt]即可,如果top不同,那就一直往上跳x=fa[top[x]],直到top相同,如果u和rt相同,那就说明我们要找的答案是rt的一个轻儿子,记录下上一个top是谁就ok了。
- tarjan函数的细节处理
- 这是和之前的简单环处理相结合使用的。
- 直接放个代码吧,有注释哒:
void tarjan(int u,int f){
dfn[u]=low[u]=++Time;
for(int i=head[u];i;i=edge[i].next){
int v=edge[i].to;
if(!dfn[v]){
fa[v]=u;
res[v]=edge[i].val;
//这个操作是记录一下每个点可能的环上的上一个结点,这里默认v的父亲是u,v上保存一下u到v的边权。
tarjan(v,u);
low[u]=min(low[u],low[v]);
}else if(v!=f){
low[u]=min(low[u],dfn[v]);
}
if(low[v]>dfn[u]){
add2(u,v,edge[i].val);
add2(v,u,edge[i].val);
}
//如果low[v]>dfn[u]而u与v还是相连的,那他们一定是圆-圆边,直接把原边权加入新图
}
for(int i=head[u];i;i=edge[i].next){
int v=edge[i].to;
if(fa[v]==u||dfn[v]<=dfn[u])continue;
//如果u是某个环的起点,那他一定连接这某个环的两个点(由于仙人掌的性质,u必须也只能连接两个点)。
//这两个点一个点是从u点出发的,他的fa值是u,剩下一个是转了一圈回来的,他一定在u之后被遍历。
//我们要在这里找那个转了一圈回来的点(他fa值不为u,一定是由某个u的子结点遍历到的)。
clac(u,v,edge[i].val);
//这里u是起点,也是这个环的爷爷结点
}
}
void clac(int u,int v,int val){
//这里u是起点,也是这个环的爷爷结点
belongcnt++;
//每clac一次就有一个环。
int preval=val,i=v;
//这个环上其他的边权已经被保存在各个点上了,只剩下这个目前的val,它是这个环上的最后一个边权。
while(i!=fa[u]){
sum[i]=preval;
//通过跳fa遍历这个环上的某一个点,并记录sum
preval+=res[i];
i=fa[i];
}
sum[belongcnt+n]=sum[u];
//方点的sum值代表整个环的权值和,相当于我上文所说的那个sumtot
//默认方点的是大于n的,之后好处理
i=v;
sum[u]=0;
//爷爷到自己的最短距离是0
int pw;
while(i!=fa[u]){
pw=min(sum[i],sum[belongcnt+n]-sum[i]);
//由于起点是爷爷结点,所以sum[i]就是i到爷爷结点的边权和
//找每个点到u,也就是这个环上爷爷结点的最短距离。
add2(belongcnt+n,i,pw);
add2(i,belongcnt+n,pw);
i=fa[i];
}
}
全码:
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e6+10;
int n,m,q;
struct E{
int to,next,val;
}edge[maxn],edge2[maxn];
int head[maxn],tot;
int head2[maxn],tot2;
void add(int from,int to,int val){
edge[++tot].to=to;
edge[tot].val=val;
edge[tot].next=head[from];
head[from]=tot;
}
void add2(int from,int to,int val){
edge2[++tot2].to=to;
edge2[tot2].val=val;
edge2[tot2].next=head2[from];
head2[from]=tot2;
}
int low[maxn],dfn[maxn],Time,belongcnt;
int fa[maxn],res[maxn],deepval[maxn],sum[maxn];
void clac(int u,int v,int val);
void tarjan(int u,int f){
dfn[u]=low[u]=++Time;
for(int i=head[u];i;i=edge[i].next){
int v=edge[i].to;
if(!dfn[v]){
fa[v]=u;
res[v]=edge[i].val;
tarjan(v,u);
low[u]=min(low[u],low[v]);
}else if(v!=f){
low[u]=min(low[u],dfn[v]);
}
if(low[v]>dfn[u]){
add2(u,v,edge[i].val);
add2(v,u,edge[i].val);
}
}
for(int i=head[u];i;i=edge[i].next){
int v=edge[i].to;
if(fa[v]==u||dfn[v]<=dfn[u])continue;
clac(u,v,edge[i].val);
}
}
void clac(int u,int v,int val){
belongcnt++;
int preval=val,i=v;
while(i!=fa[u]){
sum[i]=preval;
preval+=res[i];
i=fa[i];
}
sum[belongcnt+n]=sum[u];
i=v;
sum[u]=0;
int pw;
while(i!=fa[u]){
pw=min(sum[i],sum[belongcnt+n]-sum[i]);
add2(belongcnt+n,i,pw);
add2(i,belongcnt+n,pw);
i=fa[i];
}
}
int size[maxn],deep[maxn],son[maxn];
void dfs1(int u,int f){
fa[u]=f;size[u]=1;
for(int i=head2[u];i;i=edge2[i].next){
int v=edge2[i].to;
if(v==fa[u])continue;
deep[v]=deep[u]+1;
deepval[v]=deepval[u]+edge2[i].val;
dfs1(v,u);
size[u]+=size[v];
if(size[v]>size[son[u]])son[u]=v;
}
}
int top[maxn];
void dfs2(int u,int tp){
top[u]=tp;
if(son[u])dfs2(son[u],tp);
for(int i=head2[u];i;i=edge2[i].next){
int v=edge2[i].to;
if(v==fa[u]||v==son[u])continue;
dfs2(v,v);
}
}
int lca(int x,int y){
while(top[x]!=top[y]){
if(deep[top[x]]<deep[top[y]])swap(x,y);
x=fa[top[x]];
}
if(deep[x]>deep[y])swap(x,y);
return x;
}
int find(int x,int rt){
int res;
while(top[x]!=top[rt]){
res=top[x];
x=fa[top[x]];
}
if(x==rt)return res;
else return son[rt];
}
int solve(int x,int y,int LCA){
if(LCA<=n){
return deepval[x]+deepval[y]-2*deepval[LCA];
}else{
int X=find(x,LCA),Y=find(y,LCA);
int ans=deepval[x]-deepval[X]+deepval[y]-deepval[Y];
if(sum[X]<sum[Y])swap(X,Y);
int Min=min(sum[X]-sum[Y],sum[LCA]-sum[X]+sum[Y]);
return ans+Min;
}
}
int main(){
scanf("%d%d%d",&n,&m,&q);
for(int i=1;i<=m;i++){
int from,to,val;
scanf("%d%d%d",&from,&to,&val);
add(from,to,val);
add(to,from,val);
}
tarjan(1,0);
dfs1(1,0);
dfs2(1,1);
for(int i=1;i<=q;i++){
int x,y;
scanf("%d%d",&x,&y);
int LCA=lca(x,y);
printf("%d
",solve(x,y,LCA));
}
return 0;
}