圆方树
圆方树是将仙人掌图(每条边在不超过一个简单环的无向图中)转化为树的一种工具
建树方式
1、原图中的点我们称为圆点。
2、对于每一个环,我们将其转化为菊花图,新增一个节点作为菊花点,这个节点称为方点。(这个菊花点我们选取环的一个割点与其等价)。
3、去除所有环的边,保留其他边以及菊花图的边。
4、边权的处理:不在环上的边保留原边权。菊花点与每个点的边权为菊花点到每个点的最短路。
比如下面这个图
建完树应该是这个样子 6号节点为方点
代码实现
圆点即原图的点,我们不需要考虑
对于方点,我们在跑 t a r j a n tarjan tarjan 的时候判环 然后建点连边
对于原图中的非环边 我们直接在新图上连接即可
非环边 容易发现它是割边 t a r j a n tarjan tarjan 的时候进行判断
对于环边 先判环 求出环的起点和终点前的一个点 然后维护每个点的一个父亲 进行连边赋权即可
如何判断环的起点和终点前一个点:
对于环的起点 访问序一定是最小的 即 d f n [ u ] < d f n [ v ] dfn[u]<dfn[v] dfn[u]<dfn[v]
那我们只需要遍历 u u u 的儿子 如果 d f n [ u ] < d f n [ v ] dfn[u]<dfn[v] dfn[u]<dfn[v] 并且 f a [ v ] ≠ u fa[v]\neq u fa[v]=u 那么 u , v {u,v} u,v 就是一组环的起始点
当然第一个条件也可以换成 l o w [ v ] = d f n [ u ] low[v]=dfn[u] low[v]=dfn[u] 因为对于环的末尾节点 它的时间戳一定可以回溯到起点
void addsqare(int u,int v,int w){
int tv=v;
++n;
while(tv!=A.fa[u].first){
B.sum[tv]=w;
w+=A.fa[tv].second;
tv=A.fa[tv].first;
}
B.sum[n]=B.sum[u];
tv=v;
while(tv!=A.fa[u].first){
B.add(n,tv,min(B.sum[tv],B.sum[n]-B.sum[tv]));
B.add(tv,n,min(B.sum[tv],B.sum[n]-B.sum[tv]));
tv=A.fa[tv].first;
}
}
void tarjan(int x,int pre){
A.dfn[x]=A.low[x]=++A.time;
// cout<<x<<'\n';
for(int i=A.h[x];i;i=A.e[i].nxt){
int to=A.e[i].to,w=A.e[i].w;
if(to==pre)continue;
if(!A.dfn[to]){
A.fa[to]={x,w};
tarjan(to,x);
A.low[x]=min(A.low[x],A.low[to]);
}
else A.low[x]=min(A.low[x],A.dfn[to]);
if(A.low[to]>A.dfn[x]){
B.add(x,to,w);
B.add(to,x,w);
}
// if(A.fa[to].first!=x&&A.dfn[x]<A.dfn[to])addsqare(x,to,w);
if(A.fa[to].first!=x&&A.low[to]==A.dfn[x])addsqare(x,to,w);
}
}
A为原无向图 B为新树
a d d s q a r e addsqare addsqare 函数为建方点 并连边赋权值
P5236 【模板】静态仙人掌
给你一个有
n
n
n 个点和
m
m
m 条边的仙人掌图,和
q
q
q 组询问
每次询问两个点
u
,
v
u,v
u,v,求两点之间的最短路。
保证输入数据没有重边。
题目分析
对仙人掌建立圆方树
那么 u , v u,v u,v 间的最短路 即 u → l c a ( u , v ) + v → l c a ( u , v ) u\rightarrow lca(u,v) \ + \ v \rightarrow lca(u,v) u→lca(u,v) + v→lca(u,v)
需要注意的是
如果 l c a ( u , v ) lca(u,v) lca(u,v) 为方点 那么说明 u → v u \rightarrow v u→v 的最短路径需要经过这个方点所在的环 由于我们方点的创建是根据环的一个割点 所以直接作和未必是最短的 比如下面这个图
1 → 6 1\rightarrow 6 1→6 的最短路 一定会经过这个环 即他们的 l c a lca lca 一定是方点
容易发现最短路是 1 → 2 → 3 → 6 1 \rightarrow 2 \rightarrow 3 \rightarrow 6 1→2→3→6
2、3、5均为割点 都可作为建立方点的基准点 但选取5作为基准点按上述方式算出的最短路要大
因为5作为割点 那么 1 → 6 1\rightarrow6 1→6 一定会经过5 但实际上最短路不需要经过5
那这种情况如何处理呢
我们只需要处理出 l c a ( u , v ) lca(u,v) lca(u,v) 在环上 u , v u,v u,v 方向上的两个儿子 那么经过环的路只有两条 取最小值即可
在图上的即为 2 → 3 2 \rightarrow 3 2→3 或 2 → 4 → 5 → 3 2\rightarrow4 \rightarrow5\rightarrow3 2→4→5→3
代码实现
#include<iostream>
#include<algorithm>
#include<cstring>
#include<cmath>
#include<iomanip>
#include<vector>
#include<queue>
#include<map>
#include<set>
#include<stack>
#include<cassert>
//#include<random>
//#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define pii pair<int,int>
#define lowbit(x) x&-x
struct node{
int to,nxt,w;
};
int n,m,q,tn;
struct Graph{
vector<int>h,dfn,low,sum;
vector<node>e;
vector<pii>fa;
int cnt=0,time=0;
Graph(){}
Graph(int n,int m){
e=vector<node>(2*m+10);
h=vector<int>(2*n+10,0);
sum=dfn=low=h;
fa=vector<pii>(n+10,{0,0});
}
void add(int u,int v,int w){
e[++cnt].nxt=h[u];
h[u]=cnt;
e[cnt].to=v;
e[cnt].w=w;
}
}A,B;
void addsqare(int u,int v,int w){
int tv=v;
++n;
while(tv!=A.fa[u].first){
B.sum[tv]=w;
w+=A.fa[tv].second;
tv=A.fa[tv].first;
}
B.sum[n]=B.sum[u];
tv=v;
while(tv!=A.fa[u].first){
B.add(n,tv,min(B.sum[tv],B.sum[n]-B.sum[tv]));
B.add(tv,n,min(B.sum[tv],B.sum[n]-B.sum[tv]));
tv=A.fa[tv].first;
}
}
void tarjan(int x,int pre){
A.dfn[x]=A.low[x]=++A.time;
// cout<<x<<'\n';
for(int i=A.h[x];i;i=A.e[i].nxt){
int to=A.e[i].to,w=A.e[i].w;
if(to==pre)continue;
if(!A.dfn[to]){
A.fa[to]={x,w};
tarjan(to,x);
A.low[x]=min(A.low[x],A.low[to]);
}
else A.low[x]=min(A.low[x],A.dfn[to]);
if(A.low[to]>A.dfn[x]){
B.add(x,to,w);
B.add(to,x,w);
}
// if(A.fa[to].first!=x&&A.dfn[x]<A.dfn[to])addsqare(x,to,w);
if(A.fa[to].first!=x&&A.low[to]==A.dfn[x])addsqare(x,to,w);
}
}
int dep_v[200020],dep[200020];
int fa[200020][21];
void dfs(int x,int pre){
fa[x][0]=pre;
for(int j=1;j<=19;j++){
fa[x][j]=fa[fa[x][j-1]][j-1];
}
for(int i=B.h[x];i;i=B.e[i].nxt){
int to=B.e[i].to,w=B.e[i].w;
if(to==pre)continue;
dep[to]=dep[x]+1;
dep_v[to]=dep_v[x]+w;
dfs(to,x);
}
}
int cal(int u,int v){
int ans=0;
if(dep[u]<dep[v])swap(u,v);
int len=dep[u]-dep[v];
int tu=u,tv=v;
while(len){u=fa[u][__builtin_ffs(len)-1];len^=lowbit(len);}
ans+=dep_v[tu]-dep_v[u];
if(u==v)return ans;
ans+=dep_v[u]+dep_v[v];
for(int i=__lg(dep[u]);i>=0;i--){
if(fa[fa[u][i]][0]!=fa[fa[v][i]][0]){
u=fa[u][i],v=fa[v][i];
}
}
if(fa[u][0]!=fa[v][0])u=fa[u][0],v=fa[v][0];
if(fa[u][0]<=tn)return ans-2*dep_v[fa[u][0]];
if(B.sum[u]<B.sum[v])swap(u,v);
return ans-dep_v[u]-dep_v[v]+min(B.sum[u]-B.sum[v],B.sum[fa[u][0]]-(B.sum[u]-B.sum[v]));
}
void solve(){
int q;
cin>>n>>m>>q;
tn=n;
A=Graph(n,m);
B=Graph(2*n,m);
for(int i=1;i<=m;i++){
int u,v,w;
cin>>u>>v>>w;
A.add(u,v,w);
A.add(v,u,w);
}
tarjan(1,0);
dfs(1,0);
while(q--){
int u,v;
cin>>u>>v;
cout<<cal(u,v)<<'\n';
}
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
int t=1;
// cin>>t;
while(t--)solve();
return 0;
}