ybtAu「图论」第7章 双连通问题与圆方树

A. 【例题1】矿场搭建

如果图中没有割点,即整个图是一个点双,那么任取两个点即可。
如果一个点双中只有一个割点,那么这个割点一定是关键点,另一个点可以从该点双中任取。这是因为,一个割点一定在至少两个点双中,把它设为关键点可以对多个点双造成贡献。
如果一个点双中有两个及以上个割点,那么无论它们中的哪个被砍掉了,该点双里的点都能通过其他割点到达该点双外的关键点,因此无需额外设置关键点。

#include <iostream>
#include <vector>
#define int long long
#define N 2005
int n,m,hed[N],tal[N],nxt[N],cnte;
std::vector<int> buc[N];
void de(int u,int v) {tal[++cnte]=v,nxt[cnte]=hed[u],hed[u]=cnte;}
int dfn[N],low[N],st[N],cut[N],tp,idx,cnt,rt;
void dfs(int x)
{
	dfn[x]=low[x]=++idx,st[++tp]=x;
	int cson=0;
	for(int i=hed[x];i;i=nxt[i])
	{
		if(!dfn[tal[i]])
		{
			cson++;
			dfs(tal[i]);
			low[x]=std::min(low[x],low[tal[i]]);
			if((x==rt&&cson>1)||(x!=rt&&dfn[x]<=low[tal[i]])) cut[x]=1;
			if(dfn[x]<=low[tal[i]])
			{
				cnt++;
				buc[cnt].clear();
				do buc[cnt].push_back(st[tp--]); while(st[tp+1]!=tal[i]);
				buc[cnt].push_back(x);
			}
		}
		else low[x]=std::min(low[x],dfn[tal[i]]);
	}
}
signed main()
{
	std::ios::sync_with_stdio(0);
	std::cin.tie(0),std::cout.tie(0);
	for(int cases=1;std::cin>>m;cases++)
	{
		if(!m) break;
		for(int i=1;i<=n;i++) hed[i]=cut[i]=dfn[i]=low[i]=0;
		n=idx=cnt=cnte=0;
		for(;cnte;cnte--) tal[cnte]=nxt[cnte]=0;
		for(int i=1,u,v;i<=m;i++) std::cin>>u>>v,de(u,v),de(v,u),n=std::max(n,std::max(u,v));
		for(int i=1;i<=n;i++) if(!dfn[i]) rt=i,dfs(i);
		int ans=0,acnt=1;
		for(int i=1;i<=cnt;i++)
		{
			int tc=0,siz=buc[i].size();
			for(int j:buc[i]) tc+=cut[j];
			if(!tc) ans+=2,acnt*=siz*(siz-1)/2;
			else if(tc==1) ans++,acnt*=siz-1; 
		}
		std::cout<<"Case "<<cases<<": "<<ans<<' '<<acnt<<'\n';
	}
}

B. 【例题2】铁人两项

对于每一对 \((s,f)\),可行的 \(c\) 的个数即它们的路径并 \(-2\)
考虑对图建出圆方树,两点路径并即它们树上路径上方点的点集并。如果令每个方点的权值为点双大小,圆点权值为 \(-1\),则两点路径权值和就是可行的 \(c\) 的个数。
考虑经过每个点的路径的贡献,分为两种:两端点都在该点子树内的,和一端点在子树内,另一点在子树外的。直接求解即可。

#include <iostream>
#define int long long
#define N 800005
int n,m,hed[N],deh[N],tal[N],nxt[N],cnte,cnt,wt[N],siz[N];
int dfn[N],low[N],idx,st[N],tp,ans,sz;
void de(int u,int v) {tal[++cnte]=v,nxt[cnte]=hed[u],hed[u]=cnte;}
void ed(int u,int v) {tal[++cnte]=v,nxt[cnte]=deh[u],deh[u]=cnte;}
void dfs1(int x)
{
	dfn[x]=low[x]=++idx,st[++tp]=x;
	sz++;
	for(int i=hed[x];i;i=nxt[i])
	{
		if(!dfn[tal[i]])
		{
			dfs1(tal[i]),low[x]=std::min(low[x],low[tal[i]]);
			if(low[tal[i]]==dfn[x])
			{
				cnt++;
				do wt[cnt]++,ed(cnt,st[tp]),ed(st[tp],cnt),tp--;while(st[tp+1]!=tal[i]);
				wt[cnt]++,ed(cnt,x),ed(x,cnt);
			}
		}
		else low[x]=std::min(low[x],dfn[tal[i]]);
	}
}
void dfs2(int x,int fa)
{
	siz[x]=x<=n;
	for(int i=deh[x];i;i=nxt[i]) if(tal[i]!=fa)
	{
		dfs2(tal[i],x);
		ans+=wt[x]*siz[x]*siz[tal[i]]*2;
		siz[x]+=siz[tal[i]];
	}
	ans+=wt[x]*siz[x]*(sz-siz[x])*2;
}
signed main()
{
	std::ios::sync_with_stdio(0);
	std::cin.tie(0),std::cout.tie(0);
	std::cin>>n>>m,cnt=n;
	for(int i=1;i<=n;i++) wt[i]=-1;
	for(int i=1,u,v;i<=m;i++) std::cin>>u>>v,de(u,v),de(v,u);
	for(int i=1;i<=n;i++) if(!dfn[i]) sz=0,dfs1(i),dfs2(i,i);
	std::cout<<ans;
}

C. 建造军营

如果一条边不是割边,那么看不看守对图的连通性没有影响。因此考虑边双缩点,得到一棵树,进行树形 DP。
\(f_{i,0}\) 表示节点 \(i\) 子树内不修建军营的方案数,\(f_{i,1}\) 表示节点 \(i\) 子树内修建军营的方案数,令 \(V_i\)\(E_i\) 分别表示节点 \(i\) 包含的节点和边的数量。
首先,对于每个边双,有初始化:

\[f_{i,0}=2^{E_i} \\f_{i,1}=2^{E_i+V_i}-f_{i,0} \]

接下来考虑从子树转移,对每条边讨论。
如果子树里不修建军营,那么我们不需要看守这条边,存在看守与不看守两种方案:

\[f_{i,0}\leftarrow 2f_{i,0}f_{v,0} \]

如果节点 \(i\) 之前遍历的子树里都不修建军营,而当前子树修建了军营,则需要看守这条边,有:

\[f_{i,1}\leftarrow f_{i,0}f_{v,1} \]

如果之前遍历的子树里修建了军营,当前子树不修建,则不需要看守:

\[f_{i,1}\leftarrow 2f_{i,1}f_{v,0} \]

如果之前遍历的子树和当前子树里都修建了军营,则需要看守:

\[f{i,1}\leftarrow f_{i,1}f_{v,1} \]

注意这里只考虑了每个点至少有两棵不同的子树内修建军营,或者至少一棵子树和该节点修建军营的情况,如果该节点不修建军营,并且只有一颗子树内有军营,那么这颗子树外除了它连向父亲的那条边之外都可以不看守。于是答案为:

\[\large\sum_{v\neq root} 2^{m-E_v-1}f_v,1+f_{root,1} \]

根随便选一个就行。
由于必须至少修建一个军营,所以不能把 \(f_{i,0}\) 加入答案。

#include <iostream>
#define int long long
#define mod 1000000007
#define N 4000005
int n,m,deh[N],hed[N],tal[N],nxt[N],cnte,ans;
int dfn[N],low[N],col[N],idx,cnt,st[N],tp,siz[N],ex[N],f[N][2],ec[N],sz[N];
void de(int u,int v) {tal[++cnte]=v,nxt[cnte]=hed[u],hed[u]=cnte;}
void ed(int u,int v) {tal[++cnte]=v,nxt[cnte]=deh[u],deh[u]=cnte;}
void dfs1(int x,int fa)
{
	dfn[x]=low[x]=++idx,st[++tp]=x,ex[x]=1;
	for(int i=hed[x];i;i=nxt[i]) if(tal[i]!=fa)
	{
		if(!dfn[tal[i]]) dfs1(tal[i],x),low[x]=std::min(low[x],low[tal[i]]);
		else if(ex[tal[i]]) low[x]=std::min(low[x],dfn[tal[i]]);
	}
	if(low[x]==dfn[x])
	{
		cnt++;
		do col[st[tp]]=cnt,ex[st[tp--]]=0,siz[cnt]++;while(st[tp+1]!=x);
	}
}
void dfs2(int x,int fa)
{
	sz[x]=ec[x];
	for(int i=deh[x];i;i=nxt[i]) if(!sz[tal[i]]&&tal[i]!=fa) dfs2(tal[i],x),sz[x]+=sz[tal[i]]+1;
}
int qpow(int x,int y) {int r=1;for(;y;y>>=1,x=x*x%mod) if(y&1) r=r*x%mod;return r;}
void dfs3(int x,int fa)
{
	for(int i=deh[x];i;i=nxt[i]) if(tal[i]!=fa)
	{
		dfs3(tal[i],x);
		f[x][1]=(2*f[x][1]*f[tal[i]][0]+f[x][0]*f[tal[i]][1]+f[x][1]*f[tal[i]][1])%mod;
		f[x][0]=(2*f[x][0]*f[tal[i]][0])%mod;
	}
	if(x!=1) (ans+=f[x][1]*qpow(2,m-sz[x]-1))%=mod;
	else (ans+=f[x][1])%=mod;
}
signed main()
{
	std::ios::sync_with_stdio(0);
	std::cin.tie(0),std::cout.tie(0);
	std::cin>>n>>m;
	for(int i=1,u,v;i<=m;i++) std::cin>>u>>v,de(u,v),de(v,u);
	dfs1(1,1);
	for(int i=1;i<=n;i++) for(int j=hed[i];j;j=nxt[j])
	{
		if(col[i]!=col[tal[j]]) ed(col[i],col[tal[j]]);
		else ec[col[i]]++;
	}
	for(int i=1;i<=cnt;i++)
	{
		ec[i]>>=1;
		f[i][0]=qpow(2,ec[i]);
		f[i][1]=qpow(2,siz[i]+ec[i])-f[i][0];
	}
	dfs2(1,1);
	dfs3(1,1);
	std::cout<<ans;
}

D. 仙人题

仙人掌,路径加,单点查。
建出圆方树,原图上两点路径的并,就是圆方树上路径上所有与方点相连的圆点。我们并不能直接维护这些圆点的权值,因此考虑把圆点的权值放到方点上,这样就变成了树上路径加,可以用树剖维护。查询时,查询该圆点在圆方树上父节点的权值。
但是这样会出现一个问题:如果一条路径的 \(LCA\) 是圆点,那么这个点不会被修改。因此需要给每个圆点也维护一个点权,当路径 \(LCA\) 是圆点时,给该点的点权 \(+v\)。查询时查询父节点的权值加它自己的权值。
区间加单点查使用树状数组即可。
注意实现细节。

#include <iostream>
#define mod 998244353
#define N 4000005
#define int long long
int n,m,q,hed[N],deh[N],tal[N],nxt[N],cnte,cnt,a[N];
void de(int u,int v) {tal[++cnte]=v,nxt[cnte]=hed[u],hed[u]=cnte;}
void ed(int u,int v) {tal[++cnte]=v,nxt[cnte]=deh[u],deh[u]=cnte;}
namespace Tree
{
	int dfn[N],low[N],idx,st[N],tp;
	void dfs(int x)
	{
		dfn[x]=low[x]=++idx,st[++tp]=x;
		for(int i=hed[x];i;i=nxt[i])
		{
			if(!dfn[tal[i]])
			{
				dfs(tal[i]);
				low[x]=std::min(low[x],low[tal[i]]);
				if(low[tal[i]]>=dfn[x])
				{
					cnt++;
					do ed(cnt,st[tp]),ed(st[tp],cnt),tp--;while(st[tp+1]!=tal[i]);
					ed(cnt,x),ed(x,cnt);
				}
			}
			else low[x]=std::min(low[x],dfn[tal[i]]);
		}
	}
	void build() {dfs(1);}
};
namespace BIT
{
	int f[N],r;
	void c(int x,int t) {for(;x<=n;x+=x&-x) (f[x]+=t)%=mod;}
	void md(int l,int r,int t) {c(l,t),c(r+1,mod-t);}
	int q(int x) {for(r=0;x;x-=x&-x) (r+=f[x])%=mod;return r;}
};
namespace HLD
{
	int dfn[N],dep[N],fa[N],son[N],siz[N],top[N],idx;
	void dfs1(int x)
	{
		siz[x]=1;
		for(int i=deh[x];i;i=nxt[i]) if(!siz[tal[i]])
		{
			dep[tal[i]]=dep[x]+1,fa[tal[i]]=x,dfs1(tal[i]),siz[x]+=siz[tal[i]];
			if(siz[tal[i]]>siz[son[x]]) son[x]=tal[i];
		}
	}
	void dfs2(int x,int tp)
	{
		if(!x) return;
		dfn[x]=x>n?++idx:idx,dfs2(son[x],top[x]=tp);
		for(int i=deh[x];i;i=nxt[i]) if(!top[tal[i]]) dfs2(tal[i],tal[i]);
	}
	void init() {dfs1(1),dfs2(1,1);}
	void md(int x,int y,int t)
	{
		while(top[x]!=top[y])
		{
			if(dep[top[x]]<dep[top[y]]) std::swap(x,y);
			if(top[x]>n) BIT::md(dfn[top[x]],dfn[x],t);
			else BIT::md(dfn[top[x]]+1,dfn[x],t);
			x=fa[top[x]];
		}
		if(dep[x]>dep[y]) std::swap(x,y);
		if(x>n) BIT::md(dfn[x],dfn[y],t),(a[fa[x]]+=t)%=mod;
		else BIT::md(dfn[x]+1,dfn[y],t),(a[x]+=t)%=mod;
	}
	int q(int x) {/*printf("query %d\n",x);*/return (a[x]+(fa[x]?BIT::q(dfn[fa[x]]):0))%mod;}
};
signed main()
{
	std::ios::sync_with_stdio(0);
	std::cin.tie(0),std::cout.tie(0);
	std::cin>>n>>m>>q,cnt=n;
	for(int i=1,u,v;i<=m;i++) std::cin>>u>>v,de(u,v),de(v,u);
	Tree::build(),HLD::init();
	for(int i=1,op,x,y,z;i<=q;i++)
	{
		std::cin>>op>>x;
		if(op==0) std::cin>>y>>z,HLD::md(x,y,z);
		if(op==1) std::cout<<HLD::q(x)<<'\n';
	}
}

E. 战略游戏

可能需要用到一些虚树知识。
要想使两个点不连通,可以砍掉它们在圆方树上路径上除端点外的任何一个圆点。
然而直接两两枚举是 \(O(n^2\log n)\) 的,无法通过。
考虑将所给的点按照 \(dfn\) 排序,从第一个点依次走到下一个点,最后走回来,这样,除了所有所给的点的 \(LCA\),如果树上的某个圆点是某两个所给点的 \(LCA\),那么该点连向父亲的那条边一定被走了两次,一次是进入该点的子树,一次是出该点的子树。
因此,把点权转为边权,将所给的点按 \(dfn\) 排序,求出相邻两点(第一个和最后一个也算相邻)的路径和除以二,加上 \(LCA\)(如果是圆点),再减去所给点个数就是答案。

#include <iostream>
#include <algorithm>
#define N 2000005
int n,m,q,hed[N],deh[N],tal[N],nxt[N],cnte,cnt,li[N];
void de(int u,int v) {tal[++cnte]=v,nxt[cnte]=hed[u],hed[u]=cnte;}
void ed(int u,int v) {tal[++cnte]=v,nxt[cnte]=deh[u],deh[u]=cnte;}
namespace Tree
{
	int dfn[N],low[N],idx,st[N],tp;
	void dfs(int x)
	{
		dfn[x]=low[x]=++idx,st[++tp]=x;
		for(int i=hed[x];i;i=nxt[i])
		{
			if(!dfn[tal[i]])
			{
				dfs(tal[i]);
				low[x]=std::min(low[x],low[tal[i]]);
				if(low[tal[i]]>=dfn[x])
				{
					cnt++;
					do ed(cnt,st[tp]),ed(st[tp],cnt),tp--;while(st[tp+1]!=tal[i]);
					ed(cnt,x),ed(x,cnt);
				}
			}
			else low[x]=std::min(low[x],dfn[tal[i]]);
		}
	}
	void build()
	{
		idx=tp=0;
		for(int i=1;i<=n;i++) dfn[i]=low[i]=0;
		dfs(1);
	}
};
namespace HLD
{
	int dfn[N],idx,dep[N],d[N],fa[N],son[N],siz[N],top[N];
	void dfs1(int x)
	{
		siz[x]=1,dfn[x]=++idx;
		for(int i=deh[x];i;i=nxt[i]) if(!siz[tal[i]])
		{
			dep[tal[i]]=dep[x]+1,d[tal[i]]=d[x]+(tal[i]<=n),fa[tal[i]]=x,dfs1(tal[i]),siz[x]+=siz[tal[i]];
			if(siz[tal[i]]>siz[son[x]]) son[x]=tal[i];
		}
	}
	void dfs2(int x,int tp)
	{
		if(!x) return;
		dfs2(son[x],top[x]=tp);
		for(int i=deh[x];i;i=nxt[i]) if(!top[tal[i]]) dfs2(tal[i],tal[i]);
	}
	int lca(int x,int y)
	{
		while(top[x]!=top[y])
		{
			if(dep[top[x]]<dep[top[y]]) std::swap(x,y);
			x=fa[top[x]];
		}
		return dep[x]<dep[y]?x:y;
	}
	int dis(int x,int y) {return d[x]+d[y]-2*d[lca(x,y)];}
	void init()
	{
		idx=0;
		for(int x=1;x<=cnt;x++) dfn[x]=siz[x]=dep[x]=d[x]=fa[x]=top[x]=son[x]=0;
		dfs1(1),dfs2(1,1);
	}
	bool cm(int x,int y) {return dfn[x]<dfn[y];}
	int solve(int len)
	{
		std::sort(li+1,li+len+1,cm);
		int ans=0;
		li[0]=li[len];
		for(int i=1;i<=len;i++) ans+=dis(li[i-1],li[i]);
		ans=ans/2+(lca(li[1],li[len])<=n)-len;
		return ans;
	}
};
signed main()
{
	std::ios::sync_with_stdio(0);
	std::cin.tie(0),std::cout.tie(0);
	int T;
	std::cin>>T;
	while(T--)
	{
		for(int i=1;i<=cnt;i++) hed[i]=deh[i]=0;
		for(;cnte;cnte--) tal[cnte]=nxt[cnte]=0;
		std::cin>>n>>m;
		cnt=n;
		for(int i=1,u,v;i<=m;i++) std::cin>>u>>v,de(u,v),de(v,u);
		Tree::build();
		HLD::init();
		std::cin>>q;
		for(int i=1,l;i<=q;i++)
		{
			std::cin>>l;
			for(int j=1;j<=l;j++) std::cin>>li[j];
			std::cout<<HLD::solve(l)<<'\n';
		}
	}
}
posted @ 2025-06-26 16:07  整齐的艾萨克  阅读(11)  评论(0)    收藏  举报