圆方树学习笔记

圆方树,就是圆方树。
一张图可以转化为一颗圆方树,有一些性质。

点双

图中任意两不同点之间都有至少两条点不重复的路径。

但是这里,我们把不存在割点的图看作点双

圆方树中,普通的点是圆点,一个点双是方点。方点向这个点双中包含的所有节点连边

看图就会一目了然:
image

圆方树的性质

  • 它是一棵树
  • 点数小于2n
  • 只有图是联通的在能保证圆方树不是森林(废话)
  • 所有度数≥1的圆点在原图中都是割点
  • 相邻的点形状必定不同

如何建圆方树

tarjan找点双

#include <algorithm>
#include <cstdio>
#include <vector>

constexpr int MN = 100005;

int N, M, cnt;
std::vector<int> G[MN], T[MN * 2];

int dfn[MN], low[MN], dfc;
int stk[MN], tp;

void Tarjan(int u) {
  printf("  Enter : #%d\n", u);
  low[u] = dfn[u] = ++dfc;                // low 初始化为当前节点 dfn
  stk[++tp] = u;                          // 加入栈中
  for (int v : G[u]) {                    // 遍历 u 的相邻节点
    if (!dfn[v]) {                        // 如果未访问过
      Tarjan(v);                          // 递归
      low[u] = std::min(low[u], low[v]);  // 未访问的和 low 取 min
      if (low[v] == dfn[u]) {  // 标志着找到一个以 u 为根的点双连通分量
        ++cnt;                 // 增加方点个数
        printf("  Found a New BCC #%d.\n", cnt - N);
        // 将点双中除了 u 的点退栈,并在圆方树中连边
        for (int x = 0; x != v; --tp) {
          x = stk[tp];
          T[cnt].push_back(x);
          T[x].push_back(cnt);
          printf("    BCC #%d has vertex #%d\n", cnt - N, x);
        }
        // 注意 u 自身也要连边(但不退栈)
        T[cnt].push_back(u);
        T[u].push_back(cnt);
        printf("    BCC #%d has vertex #%d\n", cnt - N, u);
      }
    } else
      low[u] = std::min(low[u], dfn[v]);  // 已访问的和 dfn 取 min
  }
  printf("  Exit : #%d : low = %d\n", u, low[u]);
  printf("  Stack:\n    ");
  for (int i = 1; i <= tp; ++i) printf("%d, ", stk[i]);
  puts("");
}

int main() {
  scanf("%d%d", &N, &M);
  cnt = N;  // 点双 / 方点标号从 N 开始
  for (int i = 1; i <= M; ++i) {
    int u, v;
    scanf("%d%d", &u, &v);
    G[u].push_back(v);  // 加双向边
    G[v].push_back(u);
  }
  // 处理非连通图
  for (int u = 1; u <= N; ++u)
    if (!dfn[u]) Tarjan(u), --tp;
  // 注意到退出 Tarjan 时栈中还有一个元素即根,将其退栈
  return 0;
}

例题:

铁人两项

路径 s->c->f,其中路径不能经过重复的点
求一张无向图中路径的不同的选取s,c,f的方案数


原题面:

比特镇的路网由 \(m\) 条双向道路连接的 \(n\) 个交叉路口组成。

最近,比特镇获得了一场铁人两项锦标赛的主办权。这场比赛共有两段赛程:选手先完成一段长跑赛程,然后骑自行车完成第二段赛程。

比赛的路线要按照如下方法规划:

  1. 先选择三个两两互不相同的路口 \(s\)\(c\)\(f\),分别作为比赛的起点、切换点(运动员在长跑到达这个点后,骑自行车前往终点)、终点。
  2. 选择一条从 \(s\) 出发,经过 \(c\) 最终到达 \(f\) 的路径。考虑到安全因素,选择的路径经过同一个点至多一次。

在规划路径之前,镇长想请你帮忙计算,总共有多少种不同的选取 \(s\)\(c\)\(f\) 的方案,使得在第 \(2\) 步中至少能设计出一条满足要求的路径。

提示

在第一个样例中,有以下 \(8\) 种不同的选择 \((s, c, f)\) 的方案:

  • \((1, 2, 3), (1, 2, 4), (1, 3, 4), (2, 3, 4), (3, 2, 1)\)
  • \((4, 2, 1), (4, 3, 1), (4, 3, 2)\)

在第二个样例中,有以下 \(14\) 种不同的选择 \((s, c, f)\) 的方案:

  • \((1, 2, 3), (1, 2, 4), (1, 3, 4), (1, 4, 3), (2, 3, 4)\)
  • \((2, 4, 3), (3, 2, 1), (3, 2, 4), (3, 4, 1), (3, 4, 2)\)
  • \((4, 2, 1), (4, 2, 3), (4, 3, 1), (4, 3, 2)\)

子任务(注:这里给出的子任务与本题在这里的最终评测无关,仅供参考)

  • Subtask 1(points: \(5\)):\(n \leq 10\)\(m \leq 100\)
  • Subtask 2(points: \(11\)):\(n \leq 50\)\(m \leq 100\)
  • Subtask 3(points: \(8\)):\(n \leq 100000\),每个交叉路口至多作为两条双向道路的端点。
  • Subtask 4(points: \(10\)):\(n \leq 1000\),在路网中不存在环(存在环是指存在一个长度为 \(k\)\(k \ge 3\))的交叉路口序列 \(v_1, v_2, \ldots, v_k\),序列中的路口编号两两不同,且对于 \(i\)\(1\)\(k - 1\),有一条双向道路直接连接路口 \(v_i\)\(v_{i+1}\),且有一条双向道路直接连接路口 \(v_k\)\(v_1\))。
  • Subtask 5(points: \(13\)):\(n \leq 100000\),在路网中不存在环。
  • Subtask 6(points: \(15\)):\(n \leq 1000\),对于每个交叉路口,至多被一个环包含。
  • Subtask 7(points: \(20\)):\(n \leq 100000\),对于每个交叉路口,至多被一个环包含。
  • Subtask 8(points: \(8\)):\(n \leq 1000\)\(m \leq 2000\)
  • Subtask 9(points: \(10\)):\(n \leq 100000\)\(m \leq 200000\)

先建圆方树

image
n^2
换种统计方法,对于树上的任意一条路径,都是一条合法路径,且此时的方案数为该点两边点数的积再乘上这个点代表的点双联通分量的大小。

当然这样割点被我们重复统计了,在最后算答案的时候我们要将经过割点的答案扣掉

---上午刚写的晚上连题解都看不懂了,一定是午饭的问题(bushi)

[SDOI2018] 战略游戏

省选临近,放飞自我的小 Q 无心刷题,于是怂恿小 C 和他一起颓废,玩起了一款战略游戏。

这款战略游戏的地图由 \(n\) 个城市以及 \(m\) 条连接这些城市的双向道路构成,并且从任意一个城市出发总能沿着道路走到任意其他城市。

现在小 C 已经占领了其中至少两个城市,小 Q 可以摧毁一个小 C 没占领的城市,同时摧毁所有连接这个城市的道路。只要在摧毁这个城市之后能够找到某两个小 C 占领的城市 \(u\)\(v\),使得从 \(u\) 出发沿着道路无论如何都不能走到 \(v\),那么小 Q 就能赢下这一局游戏。

小 Q 和小 C 一共进行了 \(q\) 局游戏,每一局游戏会给出小 C 占领的城市集合 \(S\),你需要帮小 Q 数出有多少个城市在他摧毁之后能够让他赢下这一局游戏。

先建出圆方树。
发现对于两个在s中的点,他们的树上路径中有几个圆点,答案就是几。但是这样枚举复杂度会爆

所以考虑能否一次统计所有路径的并。

我们,不妨把给出的点按照他们在圆方树中的 dfs 序重新排序

然后画一个图,发现,如果由 dfs 序从小到大,以此走过所有的点,然后再从第 s 个点走回第 1 个点.在走过路径中,如果不考虑每相邻两个点的 LCA(此时我们走的是树上最短路径,显然会经过 LCA,这里说的不考虑就是不把它计入在内),每个点恰好被走了两次,而这些被走过的点恰好就是我们要求的联通块
image

这图大概只有我能看的懂了

在路径上求圆点的个数,倍增求树上路径点权和就好了。
最后还要特判一下第一个点和最后一个点的LCA

贴一下题解的代码,问就是挂了调不出来

#include<cstdio>
#include<algorithm>
#include<iostream>
#include<cmath>
#include<map>
#include<iomanip>
#include<cstring>
#define reg register
#define EN std::puts("")
#define LL long long
inline int read(){
	register int x=0;register int y=1;
	register char c=std::getchar();
	while(c<'0'||c>'9'){if(c=='-') y=0;c=std::getchar();}
	while(c>='0'&&c<='9'){x=x*10+(c^48);c=std::getchar();}
	return y?x:-x;
}
#define N 400007
#define M 400006
struct graph{
	int tot;
	int fir[N],nex[M],to[M];
	inline void add(int u,int v){
		to[++tot]=v;
		nex[tot]=fir[u];fir[u]=tot;
	}
	inline void clear(){
		std::memset(fir,0,sizeof fir);std::memset(to,0,sizeof to);
		std::memset(nex,0,sizeof nex);tot=0;
	}
}T,G;
int n,m;
int dfn[N],deep[N],size[N],dfscnt;
int fa[22][N],val[N],S[N];
inline int cmp(int x,int y){return dfn[x]<dfn[y];}
struct get_bcc{
	int dfn[N],low[N],stack[N],top;
	int bcccnt,dfscnt;
	void tarjan(int u){
		dfn[u]=low[u]=++dfscnt;stack[top++]=u;
		for(reg int v,i=G.fir[u];i;i=G.nex[i]){
			v=G.to[i];
			if(!dfn[v]){
				tarjan(v);
				low[u]=std::min(low[u],low[v]);
				if(low[v]>=dfn[u]){
					bcccnt++;
					do{
						T.add(bcccnt,stack[--top]);T.add(stack[top],bcccnt);
					}while(stack[top]^v);
					T.add(bcccnt,u);T.add(u,bcccnt);
				}
			}
			else low[u]=std::min(low[u],dfn[v]);
		}
	}
}BCC;
inline void clear(){
	T.clear();G.clear();
	std::memset(BCC.dfn,0,sizeof BCC.dfn);std::memset(BCC.low,0,sizeof BCC.low);
	BCC.top=0;BCC.dfscnt=0;
	dfscnt=0;
	std::memset(dfn,0,sizeof dfn);
	std::memset(fa,0,sizeof fa);std::memset(val,0,sizeof val);
	std::memset(deep,0,sizeof deep);std::memset(size,0,sizeof size);
}
void dfs(int u,int fat){
	size[u]=1;deep[u]=deep[fat]+1;
	dfn[u]=++dfscnt;fa[0][u]=fat;
	val[u]=val[fat]+(u<=n);
	for(reg int v,i=T.fir[u];i;i=T.nex[i]){
		v=T.to[i];
		if(v==fat) continue;
		dfs(v,u);size[u]+=size[v];
	}
}
inline void pre(){
	for(reg int i=1;i<=20;i++){
		for(reg int j=1;j<=BCC.bcccnt;j++) fa[i][j]=fa[i-1][fa[i-1][j]];
	}
}
inline int get(int x,int y){
	if(deep[x]<deep[y]) std::swap(x,y);
	for(reg int i=20;~i;i--)
		if(deep[fa[i][x]]>=deep[y])x=fa[i][x];
	if(x==y) return x;
	for(reg int i=20;~i;i--)
		if(fa[i][x]!=fa[i][y]) x=fa[i][x],y=fa[i][y];
	return fa[0][x];
}
int main(){int cases=read();while(cases--){
	BCC.bcccnt=n=read();m=read();
	for(reg int u,v,i=1;i<=m;i++){
		u=read();v=read();
		G.add(u,v);G.add(v,u);
	}
	BCC.tarjan(1);
	dfs(1,0);
	pre();
//	
//		EN;EN;
//		std::printf("n : %d  m : %d  bcccnt : %d\n",n,m,BCC.bcccnt);
//		for(reg int i=1;i<=BCC.bcccnt;i++){
//			std::printf("now : %d  size : %d  deep : %d  dfn : %d  val : %d\n",i,size[i],deep[i],dfn[i],val[0][i]);
//			for(reg int j=T.fir[i];j;j=T.nex[j]) std::printf("%d ",T.to[j]);
//			EN;
//		}
//	
	reg int s,ans,lca;
	int q=read();while(q--){
 		s=read();ans=0;
		for(reg int i=1;i<=s;i++) S[i]=read();
		std::sort(S+1,S+1+s,cmp);S[s+1]=S[1];
		for(reg int i=1;i<=s;i++){
			lca=get(S[i],S[i+1]);
			ans+=val[S[i]]+val[S[i+1]]-val[lca]*2;
		}
		ans>>=1;ans-=s;
		ans+=(get(S[1],S[s])<=n);
		std::printf("%d\n",ans);
	}
	if(cases) clear();
}
	return 0;
}

结束
image