圆方树

树的形态很特殊,因此有很多便于利用的性质。一般的图却不太行。圆方树就是将一般无向图转化为树来处理。

模板

圆方树上的所有点分为两类:圆点和方点,同时我们一般使用的广义圆方树上的边都一定是连接了一个方点和一个圆点。
圆点是原来树上的点,方点是我们建出来的点,代表一个点双连通分量,每一个圆点都会与其归属的点双连通分量连边。注意一个点可以属于多个点双连通分量。

那这棵树上的路径的意义就比较明显了,路径上任意两个圆点间的路径上除了这两个点以外的所有点都一定是割点,也就是路径上必定要经过的点。这个性质一般来说在判断连通性之类的问题上比较有用。(比如给一些点问你要割掉哪些点使得这些点两两间不连通之类的)
一般而言我们用圆方树的方式是给方点和圆点赋恰当的权值然后去统计路径上的和或者最值之类的,比如给方点赋上其点双连通分量的大小之类的。

建树的过程本身实际上是比较简单的。考虑在 tarjan 的过程中直接求即可。但是求点双的 tarjan 写法有一些细节需要注意。

void tar(int u){
	dfn[u]=low[u]=++dfncnt;stk[++top]=u;
	for(int v:q[u]){
		if(!dfn[v]){
			tar(v);low[u]=min(low[u],low[v]);
			if(low[v]==dfn[u]){
				idcnt++;int tmp=0;
				while(tmp!=v){tmp=stk[top];p[idcnt].push_back(tmp),p[tmp].push_back(idcnt);top--;}
				p[idcnt].push_back(u),p[u].push_back(idcnt);//注意到是判断是否是 v,因为有可能会将其他点双中的点删掉 
			}
		}
		else low[u]=min(low[u],dfn[v]);
	}
}

P4630 [APIO2018] 铁人两项

这个就是比较模板的题了。

我们发现在一般图本身上计数是困难的。考虑在圆方树上用类似于树形 DP 的东西去计数。由于圆方树是树的形态,因此我们做的事情就是对于一个有序点对 $\left \langle s,f \right \rangle $,去计数可能的 \(c\) 的数量。

也就是说我们要计算的是 \(s,f\) 两个点所有可能路径的并集大小然后除去 \(s,f\) 这两个点本身,这个东西用圆方树是好算的。我们给方点的权值赋上点双的大小,给圆点权值赋上 \(-1\)(赋上 \(-1\) 是因为割点同时被路径上两个点双计数,要减掉)。然后路径的并集大小就是两个圆点之间路径权值和。
但是这样计数会导致路径端点被额外减(没有被两个点双重复计数),但是我们又发现我们恰好要将 \(s,f\) 两个点排除掉(其本身不能作为 \(c\) 可能的取值),因此方案数就是路径权值,皆大欢喜。

于是如果不 DP 就是暴力枚举 \(s,f\) 然后计算路径和。考虑用 DP 快速算。
我们对于圆方树上每一个点,用 \(\text{这个点被所有路径经过的次数}\times \text{这个点的权值}\) 的方式去计数。
于是我们将路径分为两种,一种是当前点 \(u\) 作为其不同子树中的 \(s,f\) 的 lca 的路径,一种是 \(s,f\) 一个在 \(u\) 子树内一个在子树外经过 \(u\) 的路径。
第一种就在每次 dfs 子树的时候计算即可,有

\[\begin{aligned} ans\gets ans+siz_u&\times siz_v,v\in son_u \\ siz_u\gets siz_u&+siz_v \end{aligned} \]

其中 \(siz_u\)\(u\) 及其子树中圆点的数量。注意方点显然不能作为 \(s,f\)
然后另外一种路径条数就是 \(u\) 子树内的圆点个数乘上子树外的圆点个数,有

\[ans\gets ans+siz_u\times (tot-siz_u) \]

其中 \(tot\)\(u\) 所在连通块的大小,因为图不保证连通。

code

相对简短。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+7;
int n,m,idcnt,dfn[N],low[N],dfncnt,stk[N],top,val[N],siz[N],rt;
long long ans=0;
vector <int> q[N],scc[N],p[N];
void tar(int u){
	dfn[u]=low[u]=++dfncnt;stk[++top]=u;val[u]=-1;
	for(int v:q[u]){
		if(!dfn[v]){
			tar(v);low[u]=min(low[u],low[v]);
			if(low[v]==dfn[u]){
				idcnt++;int tmp=0;
				while(tmp!=v){tmp=stk[top];p[idcnt].push_back(tmp),p[tmp].push_back(idcnt);val[idcnt]++;top--;}
				p[idcnt].push_back(u),p[u].push_back(idcnt);val[idcnt]++;
			}
		}
		else low[u]=min(low[u],dfn[v]);
	}
}
void dfs(int u,int fa){
	siz[u]=(u<=n);
	for(int v:p[u]){
		if(v==fa)continue;
		dfs(v,u);ans+=2ll*siz[u]*siz[v]*val[u];
		siz[u]+=siz[v];
	}
	ans+=2ll*siz[u]*(dfncnt-siz[u])*val[u];
}
signed main(){
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	cin>>n>>m;for(int i=1,u,v;i<=m;i++)cin>>u>>v,q[u].push_back(v),q[v].push_back(u);
	for(int i=1;i<=n;i++){
		if(!dfn[i]){
			rt=i;idcnt=n,dfncnt=0;tar(rt);
			dfs(rt,0);for(int j=n+1;j<=idcnt;j++)p[j].clear(),val[j]=siz[j]=0;
		}
	}
	cout<<ans;return 0;
}

P4606 [SDOI2018] 战略游戏

圆方树上建虚树,欢乐多又多!

我们将所谓“被占领的城市”称为关键点。实际上就是求在圆方树上关键点形成的生成树上有多少个圆点(这个生成树上的圆点被割掉一定令一对关键点不连通)
于是我们有了一个超级暴力。暴力找生成树然后数上面有多少个圆点即可。

考虑如何快速求这个生成树上的圆点个数。我们发现我们不需要知道整棵生成树长什么样子。由于路径和有分配律,因此我们直接求出虚树然后求其所有点与其父亲间圆点的个数。
于是将圆点的 权值赋为 1,方点赋为 0 直接算即可。

code

一个需要注意的点是,由于 1 并不一定是关键点同时建出来的虚树中也不一定就有 1,因此一般的虚树直接强制插入 1 作为根节点的方法行不通了,因此要记录一下建出来的虚树的根节点,因为我们记录路径上圆点的个数是不太好记录上根节点的信息的,因此我们最后要单独加上根节点的信息!(因为这个调了相对久)

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+7,inf=1e9+7;
int n,m,dfncnt=0,idcnt=0,dfn[N],low[N],stk[N],top,val[N],f[N][18],s[N],dep[N];
vector <int> q[N],p[N];
void init(){
	for(int i=1;i<=n;i++)q[i].clear(),low[i]=0;
	for(int i=1;i<=idcnt;i++){p[i].clear(),dfn[i]=val[i]=dep[i]=0;for(int j=0;j<=17;j++)f[i][j]=0;}
	dfncnt=idcnt=0;
}
void tar(int u){
	dfn[u]=low[u]=++dfncnt;stk[++top]=u;val[u]=1;
	for(int v:q[u]){
		if(!dfn[v]){
			tar(v);low[u]=min(low[u],low[v]);
			if(low[v]==dfn[u]){
				idcnt++;int tmp=0;
				while(tmp!=v){tmp=stk[top--],p[idcnt].push_back(tmp),p[tmp].push_back(idcnt);}
				p[idcnt].push_back(u),p[u].push_back(idcnt);
			}
		}
		else low[u]=min(low[u],dfn[v]);
	}
}
void dfs(int u,int fa){
	val[u]+=val[fa];f[u][0]=fa;dep[u]=dep[fa]+1;dfn[u]=++dfncnt;
	for(int i=1;i<=17;i++)f[u][i]=f[f[u][i-1]][i-1];
	for(int v:p[u]){
		if(v==fa)continue;
		dfs(v,u);
	}
}
int lca(int x,int y){
	if(dep[x]<dep[y])swap(x,y);
	for(int i=17;i>=0;i--)if(dep[f[x][i]]>=dep[y])x=f[x][i];
	if(x==y)return x;
	for(int i=17;i>=0;i--)if(f[x][i]!=f[y][i])x=f[x][i],y=f[y][i];
	return f[x][0];
}
bool cmp(int x,int y){return dfn[x]<dfn[y];}
int h[N];
int calc(int len){
	sort(s+1,s+len+1,cmp);int tmp=len,ans=0,rt=0;len=0;
	for(int i=1;i<tmp;i++)h[++len]=s[i],h[++len]=lca(s[i],s[i+1]);h[++len]=s[tmp];
	sort(h+1,h+len+1,cmp);len=unique(h+1,h+len+1)-(h+1);
	for(int i=1;i<len;i++){
		int lc=lca(h[i],h[i+1]);//注意到第一个点一定是根
		ans+=val[h[i+1]]-val[lc];if(dep[lc]<dep[rt]||rt==0)rt=lc;
	}
	for(int i=1;i<=len;i++)h[i]=s[i]=0;
	return ans+val[rt]-val[f[rt][0]];
}
void solve(){
	cin>>n>>m;for(int i=1,u,v;i<=m;i++)cin>>u>>v,q[u].push_back(v),q[v].push_back(u);
	idcnt=n;tar(1);dfncnt=0;dfs(1,0);int Q;cin>>Q;
	while(Q--){
		int o;cin>>o;for(int i=1;i<=o;i++)cin>>s[i];
		cout<<calc(o)-o<<'\n';
	}
}
signed main(){
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	int T;cin>>T;while(T--)solve(),init();
	return 0;
}
posted @ 2025-07-24 19:41  all_for_god  阅读(52)  评论(0)    收藏  举报