圆方树

构建

在将图变为树的方法里,圆方树与 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;
}
posted @ 2023-08-11 16:36  SError  阅读(19)  评论(0编辑  收藏  举报