圆方树
构建
在将图变为树的方法里,圆方树与 v-dcc 类似。
圆方树中,原来的每个点对应一个 圆点,每个点双对应一个 方点。
故圆方树的节点数为 \(n+c\),其中 \(n=|V|\),\(c=|\text{v-dcc}|\).
对于每个点双,其方点向这个点双里的每个点连边,形成一个菊花图,多个菊花图通过割点连接。
割点的数量小于 \(n\),故圆方树的点数小于 \(2n\).
若原图有 \(k\) 个连通分量显然圆方树森林有 \(k\) 颗树。
过程
Algorithm
考虑对每个连通子图构建它的圆方树。
圆方树使用的算法是 tarjan 的变体:对图 dfs,得到两个数组 \(dfn\) 和 \(low\).
-
\(dfn[u]\) 为 \(u\) 的 dfs 序。
-
\(low[u]\) 为 \(u\) 的 dfs 树的子树中的某个点 \(v\) 通过 最多一次返祖边或向父亲的树边 能访问到的 最小 dfs 序。
和割点不同的是规定了可以通过父边向上,实际上和 tarjan 的实现基本相同。
Observation
-
每个点双在 dfs 树上是一颗连通子树,且至少包含两个点。特别地,最顶端节点仅往下接一个点。
-
每条树边恰好在一个点双内。
考虑一个点双在 dfs 树中的最顶端节点 \(u\),那么 \(u\) 的子树包含了整个点双的信息。
再看点双的下一个点 \(v\),那么 \(u,v\) 之间存在树边。
- 此时有 \(low[v]=dfn[u]\).
也就是,对于树边 \(u\rightarrow v\),\(u,v\) 在一个点双里,且 \(u\) 在点双中的深度最浅当且仅当 \(low[v]=dfn[u]\).
确定点双的点集用类似 tarjan 的方法即可。
在栈中被弹出的节点,将其和新建的方点连边。最后让 \(u\) 和方点连边。
code
处理有多个连通子图的情况。
int n,m,cnt;
vector<int>e[N],T[N<<1];
int dfn[N],low[N],dfc;
int st[N],top;
void tarjan(int u){
low[u]=dfn[u]=++dfc;
st[++top]=u;
for(int v:e[u]){
if(!dfn[v]){
tarjan(v),low[u]=min(low[u],low[v]);
if(low[v]==dfn[u]){
cnt++;
for(int x=0;x!=v;top--){
x=st[top];
T[cnt].pb(x),T[x].pb(cnt);
}
T[cnt].pb(u),T[u].pb(cnt);
}
}
else low[u]=min(low[u],dfn[v]);
}
}
int main(){
n=read(),m=read();
cnt=n;
for(int i=1,u,v;i<=m;i++){
u=read(),v=read();
e[u].pb(v),e[v].pb(u);
}
for(int i=1;i<=n;i++)
if(!dfn[i])tarjan(i),top--;
return 0;
}
例题
P4630 [APIO2018] 铁人两项
一张简单无向图,问有多少三元组 \((s,c,f)\),满足 \(s\not=c\not=f\),且存在一条从 \(s\) 出发,经过 \(c\) 到达 \(f\) 的简单路径。
- 点双中的两点之间简单路径的并集恰好完全等于这个点双。
这个性质证明比较困难。
也是就是点双中的两个不同点 \(u,v\) 之间一定存在一条简单路径经过点双内的另一点 \(w\).
然后推出另一个结论:
- 对于两圆点在圆方树上的路径,“与路径上经过的方点相邻的圆点” 的集合等于原图中两点简单路径上的点集。
固定 \(s,f\),合法的 \(c\) 的数量等于 \(s,f\) 之间简单路径的并集的点数 \(-2\).
考虑在圆方树上计数。
在圆方树上有一个 trick:路径统计时给点赋一个合适的权值。
对方点赋值为对应点双的大小,圆点赋值为 \(-1\).
那么两圆点间路径点权和即原图中简单路径并集大小 \(-2\).
现在要对每对圆点求和。
把它变成权值 \(\times\) 经过它的路径数,简单树形 DP。
我的圆方树写了个逆天错误调了一个小时。
#include<bits/stdc++.h>
#define ll long long
#define N 100010
#define pb push_back
using namespace std;
int read(){
int x=0,w=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')w=-1;ch=getchar();}
while(isdigit(ch))x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
return x*w;
}
int n,m,cnt;
vector<int>e[N],T[N<<1];
int dfn[N],low[N],tim;
int st[N],top;
int val[N<<1],siz[N<<1];
int num;
void tarjan(int u){
low[u]=dfn[u]=++tim;
st[++top]=u;
num++;
for(int v:e[u]){
if(!dfn[v]){
tarjan(v),low[u]=min(low[u],low[v]);
if(low[v]==dfn[u]){
cnt++;
for(int x=0;x!=v;top--){
x=st[top];
T[cnt].pb(x),T[x].pb(cnt);
++val[cnt];
}
T[cnt].pb(u),T[u].pb(cnt);//这一步一开始搞错地方了
++val[cnt];
}
}
else low[u]=min(low[u],dfn[v]);
}
}
ll ans;
void dfs(int u,int fa){
siz[u]=(u<=n);
for(int v:T[u]){
if(v==fa)continue;
dfs(v,u);
ans+=2ll*val[u]*siz[u]*siz[v];
siz[u]+=siz[v];
}
ans+=2ll*val[u]*siz[u]*(num-siz[u]);
}
int main(){
n=read(),m=read();
for(int i=1;i<=n;i++)val[i]=-1;
cnt=n;
for(int i=1,u,v;i<=m;i++){
u=read(),v=read();
e[u].pb(v),e[v].pb(u);
}
for(int i=1;i<=n;i++)
if(!dfn[i]){
num=0;
tarjan(i),top--;
dfs(i,0);
}
printf("%lld\n",ans);
return 0;
}
Tourists
简单无向连通图。支持:
-
修改点权
-
询问两点之间所有简单路径上点权的最小值
令方点权值为相邻圆点权值的最小值,问题即路径上最小值。
容易用树剖和线段树维护,考虑修改。
修改圆点的点权需修改所有与其相邻的方点,修改可能有 \(O(n)\) 个。
因为圆方树是颗树,令方点权值为自己的儿子圆点的权值最小值,则只需要修改父亲方点。
对每个方点开一个 multiset 即可。
若 lca 为方点,还需要查询 lca 的父亲圆点的权值。
#include<bits/stdc++.h>
#define ll long long
#define N 100010
#define pb push_back
#define inf 0x3f3f3f3f
using namespace std;
int read(){
int x=0,w=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')w=-1;ch=getchar();}
while(isdigit(ch))x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
return x*w;
}
bool gets(){
char ch=getchar();
while(ch!='A'&&ch!='C')ch=getchar();
return ch=='C';
}
int n,m,q,cnt,w[N<<1];
vector<int>e[N],T[N<<1];
int dfn[N<<1],low[N],tim;
int st[N],tp;
void tarjan(int u){
low[u]=dfn[u]=++tim;
st[++tp]=u;
for(int v:e[u]){
if(!dfn[v]){
tarjan(v),low[u]=min(low[u],low[v]);
if(low[v]==dfn[u]){
cnt++;
for(int x=0;x!=v;tp--){
x=st[tp];
T[cnt].pb(x),T[x].pb(cnt);
}
T[cnt].pb(u),T[u].pb(cnt);
}
}
else low[u]=min(low[u],dfn[v]);
}
}
int fa[N<<1],dep[N<<1],siz[N<<1],son[N<<1];
int idf[N<<1],top[N<<1],dfc;
void dfs1(int u){
siz[u]=1;
for(int v:T[u]){
if(dep[v])continue;
fa[v]=u,dep[v]=dep[u]+1;
dfs1(v),siz[u]+=siz[v];
if(siz[son[u]]<siz[v])son[u]=v;
}
}
void dfs2(int u,int t){
dfn[u]=++dfc,idf[dfc]=u,top[u]=t;
if(son[u])dfs2(son[u],t);
for(int v:T[u])
if(fa[v]==u&&v!=son[u])dfs2(v,v);
}
multiset<int>S[N<<1];
struct Tree{
int l,r,dat;
#define l(x) tr[x].l
#define r(x) tr[x].r
#define dat(x) tr[x].dat
}tr[N<<3];
#define ls p<<1
#define rs p<<1|1
void update(int p){
dat(p)=min(dat(ls),dat(rs));
}
void build(int p,int l,int r){
l(p)=l,r(p)=r;
if(l==r){
dat(p)=w[idf[l]];
return;
}
int mid=(l+r)>>1;
build(ls,l,mid);
build(rs,mid+1,r);
update(p);
}
void modify(int p,int x,int k){
if(l(p)==r(p)){
dat(p)=k;
return;
}
int mid=(l(p)+r(p))>>1;
if(x<=mid)modify(ls,x,k);
else modify(rs,x,k);
update(p);
}
int query(int p,int l,int r){
if(l<=l(p)&&r>=r(p))return dat(p);
int mid=(l(p)+r(p))>>1,ret=inf;
if(l<=mid)ret=min(ret,query(ls,l,r));
if(r>mid)ret=min(ret,query(rs,l,r));
return ret;
}
int getans(int u,int v){
int ret=inf;
while(top[u]!=top[v]){
if(dep[top[u]]<dep[top[v]])swap(u,v);
ret=min(ret,query(1,dfn[top[u]],dfn[u]));
u=fa[top[u]];
}
if(dfn[u]>dfn[v])swap(u,v);
ret=min(ret,query(1,dfn[u],dfn[v]));
if(u>n)ret=min(ret,w[fa[u]]);
return ret;
}
int main(){
n=read(),m=read(),q=read();
for(int i=1;i<=n;i++)w[i]=read();
cnt=n;
for(int i=1,u,v;i<=m;i++){
u=read(),v=read();
e[u].pb(v),e[v].pb(u);
}
tarjan(1);
dep[1]=1,dfs1(1),dfs2(1,1);
for(int i=1;i<=n;i++)
if(fa[i])S[fa[i]].insert(w[i]);
for(int i=n+1;i<=cnt;i++)w[i]=*S[i].begin();
build(1,1,cnt);
for(int opt,x,y;q;q--){
opt=gets(),x=read(),y=read();
if(opt){
modify(1,dfn[x],y);
if(fa[x]){
int u=fa[x];
S[u].erase(S[u].find(w[x]));
S[u].insert(y);
if(w[u]!=*S[u].begin()){
w[u]=*S[u].begin();
modify(1,dfn[u],w[u]);
}
}
w[x]=y;
}
else{
int ans=getans(x,y);
printf("%d\n",ans);
}
}
return 0;
}
P4334 [COI2007] Policija
2023.8.11 模拟题记录。
一张无向连通图,问
-
割边后两点的连通性
-
割点后两点的连通性
\(n\le 10^5\),\(m\le 5\times 10^5\),\(q\le 3\times 10^5\).
luogu \(\rm ML=62.5MiB\).
tarjan 的话第一问更简单,但是考场上过载了。
先来看一下 part2.
一个普通的想法是点双之后判断删点在不在两点的路径上,这个直接拍到圆方树上做就好了。
具体来说直接倍增把三者的 lca 弄出来判一判即可。当然也可以写个树剖。
再看下 part1.
只需要考虑边为桥的情况。那么这两个点应该与同一个方点相邻。
这样问题就又变成了 part2,把这个桥对应的方点拉出来做一遍即可。
时间复杂度 \(O(n\log n)\),空间复杂度 \(O(n\sim n\log n)\).
求桥时圆方树的算法里做 low[u]=min(low[u],dfn[v])
不能爬父边,有一个问题是会不会对圆方树的结构产生影响。
#include<bits/stdc++.h>
#define ll long long
#define N 100010
#define pb push_back
#define mp make_pair
#define mit map<pair<int,int>,int>::iterator
using namespace std;
int read(){
int x=0,w=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')w=-1;ch=getchar();}
while(isdigit(ch))x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
return x*w;
}
bool gets(){
char ch=getchar();
while(ch!='A'&&ch!='C')ch=getchar();
return ch=='C';
}
int n,m,q,cnt;
vector<int>e[N],T[N<<1];
int dfn[N],low[N],tim;
int st[N],tp;
map<pair<int,int>,int>bri;
void tarjan(int u,int fa){
low[u]=dfn[u]=++tim;
st[++tp]=u;
for(int v:e[u]){
if(!dfn[v]){
tarjan(v,u),low[u]=min(low[u],low[v]);
if(low[v]>=dfn[u]){
cnt++;
if(low[v]>dfn[u])
bri[minmax(u,v)]=cnt;
for(int x=0;x!=v;tp--){
x=st[tp];
T[cnt].pb(x),T[x].pb(cnt);
}
T[cnt].pb(u),T[u].pb(cnt);
}
}
else if(v!=fa)low[u]=min(low[u],dfn[v]);
}
}
int fa[N<<1],dep[N<<1],siz[N<<1],son[N<<1];
int top[N<<1],dfc;
void dfs1(int u){
siz[u]=1;
for(int v:T[u]){
if(dep[v])continue;
fa[v]=u,dep[v]=dep[u]+1;
dfs1(v),siz[u]+=siz[v];
if(siz[son[u]]<siz[v])son[u]=v;
}
}
void dfs2(int u,int t){
top[u]=t;
if(son[u])dfs2(son[u],t);
for(int v:T[u])
if(fa[v]==u&&v!=son[u])dfs2(v,v);
}
bool crs(int x,int y,int z){
while(top[x]!=top[y]){
if(dep[top[x]]<dep[top[y]])swap(x,y);
if(top[x]==top[z]&&dep[z]<=dep[x])return true;
x=fa[top[x]];
}
if(dep[x]>dep[y])swap(x,y);
if(top[x]==top[z]&&dep[z]>=dep[x]&&dep[z]<=dep[y])return true;
return false;
}
int main(){
n=read(),m=read();
cnt=n;
for(int i=1,u,v;i<=m;i++){
u=read(),v=read();
e[u].pb(v),e[v].pb(u);
}
tarjan(1,0);
dep[1]=1,dfs1(1),dfs2(1,1);
q=read();
for(int opt,a,b,c,d;q;q--){
opt=read(),a=read(),b=read(),c=read();
if(opt==1){
d=read();
mit it=bri.find(minmax(c,d));
if(it==bri.end())puts("yes");
else puts(crs(a,b,it->second)?"no":"yes");
}
else puts(crs(a,b,c)?"no":"yes");
}
return 0;
}
[ABC318G] Typical Path Problem
给定一张简单连通无向图,问是否存在经过 \(B\) 的从 \(A\) 到 \(C\) 的简单路径。
\(n\le 2\times 10^5\),\(A\not=B\not=C\).
在考场上一眼想到是板的圆方树但是没弄出来。
这个条件相当于从 \(A\) 到 \(B\) 的路径可以不经过 \(C\) 而且 \(B\) 到 \(C\) 的路径不经过 \(A\).
可以像上一题一样做,但是会发现有个问题就是如果 \(A,B,C\) 都在同一个 v-dcc 里面等几种情况就会爆炸。
至于圆方树都是树了,不妨从 \(A\) 开始 dfs,遍历到每个方点就将其连接的圆点 \(cnt+1\),走到 \(C\) 是若发现 \(cnt_b\) 非 \(0\) 那么说明存在题目所给的路径。
#include<bits/stdc++.h>
#define N 200010
#define pb push_back
using namespace std;
int read(){
int x=0,w=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')w=-1;ch=getchar();}
while(isdigit(ch))x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
return x*w;
}
int n,m,node,A,B,C;
vector<int>e[N],T[N<<1];
int dfn[N],low[N],tim;
int st[N],tp;
void tarjan(int u){
low[u]=dfn[u]=++tim;
st[++tp]=u;
for(int v:e[u]){
if(!dfn[v]){
tarjan(v),low[u]=min(low[u],low[v]);
if(low[v]>=dfn[u]){
node++;
for(int x=0;x!=v;tp--){
x=st[tp];
T[node].pb(x),T[x].pb(node);
}
T[node].pb(u),T[u].pb(node);
}
}
else low[u]=min(low[u],dfn[v]);
}
}
int cnt[N];
void solve(int u,int fa){
if(u>n)for(int v:T[u])cnt[v]++;
if(u==C){
puts(cnt[B]?"Yes":"No");
exit(0);
}
for(int v:T[u])
if(v!=fa)solve(v,u);
if(u>n)for(int v:T[u])cnt[v]--;
}
int main(){
n=read(),m=read(),A=read(),B=read(),C=read();
node=n;
for(int i=1,u,v;i<=m;i++){
u=read(),v=read();
e[u].pb(v),e[v].pb(u);
}
tarjan(A);
solve(A,0);
return 0;
}