数据结构5——圆方树

文章目录

    • 1. 建树原理
        • 1.1 缩点
        • 1.2 建点
        • 1.3 重建
        • 1.4 总结
    • 2. 性质
    • 3. 实现
    • 4. 例题
        • Solution
    • 5. 总结

我们知道很多树上的算法,但是在图上却难以实现,这个时候是不是就会想把图变成树呢?
这里介绍一个把无向图转化成树的方法,就是 圆方树

1. 建树原理

(Tips : 若无需“导读”可以直接往下翻至1.4节)

1.1 缩点

我们不妨先回顾一下,我们有什么方法把一张任意的有向图变成有向无环图(DAG)的?
比较熟悉的方法就是tarjan缩点。
所以我们对于无向图,我们也先缩点——把点双连通分量缩在一起。
特别注意,我们要把只有一条边,只连接 2 2 2个点的“伪点双”也要算进去。

1.2 建点

即使把点双缩起来了,我们得到的依然是一张无向图——那怎么把它统领成树呢?很容易想到就是对于这个缩好之后的点新建一个点,我们称之为方点
那么对应的缩点前原图的点就称之为圆点

1.3 重建

这时图就显得很杂乱了,为了保证这个树建出来有用,我们需要整理一下,具体方案如下:

  1. 拆掉每个点双内部的所有边。
  2. 将这个点双对应的方点向这个点双内的每一个圆点连一条边。

1.4 总结

简而言之,记原图点为圆点,建立圆方树就是找出每一个点双,然后建一个对应这个点双的方点,方点连向每一个点双内的圆点,删去每个点双内部圆点原有的连边。这样每一个点双就成为了一个树的形态,这样所有的都连起来也就是一颗树了。
(这里特别解释一下怎么“全部连起来”。实际上并不需要额外的操作。就是因为我们把只有 2 2 2个点 1 1 1条边的也算进去,因此相邻点双之间都会有公共点。然后两个点双就会是以“方点 1 → _1 \to 1 公共点 → \to 方点 2 _2 2 ”的顺序自然连接。)

2. 性质

这个圆方树肯定有一些特别的性质嘛,不然要了干什么……

  1. 对于任意无向图 G = { V , E } G=\{V , E\} G={V,E}均满足建出来的是森林;
  2. 对于任意连通无向图 G G G均满足建出来的是一颗无根树;
  3. 对于每一个圆点,都对应着有且仅有一个方点;
  4. 对于任意一条路径,路径上的圆点和方点都是相间出现,或者说同种形状的点不相邻。
  5. 原图的割点是圆方树中度数 > 1 >1 >1的圆点。

这些性质都是很显然的,无需过多证明。第 4 4 4个性质是之前多次强调的 2 2 2个点 1 1 1条边的特殊点双保证的。

3. 实现

以上为理论部分,实现上还有一些细节需要注意的。

  1. 点权 w i w_i wi,一般在题目中我们会给每一个点赋予一个点权,然后根据圆方树的性质,操作这些点权。
  2. 子树大小 s z i sz_i szi。成为树以后,就有了子树的概念,这样我们需要加入一个表示子树大小的量方便计算。当然, s z i sz_i szi w i w_i wi也不是必需的,视题目而定。
  3. 上文中曾经提到,相邻点双之间存在公共点,那么维护的时候岂不是会重复?为了解决这种情况,我们一般不维护点双中 d f s dfs dfs序最小的点,方点中只维护剩下的点。(如果你把图画出来之后,指定一个节点为根,你就会发现实际上 d f n dfn dfn最小的圆点是方点的父亲……这样你就理解为什么不用在方点中维护那个点了。最后那个点单独处理即可)。然后我们修改后的tarjan求点双+维护圆方树代码如下:
void dfs(int u,int fa){//tarjan
	dfn[u]=low[u]=++ind;
	w[u]=-1;sz[u]=1;
	q.push(u);
	for(edge *i=head[u];i!=NULL;i=i->nxt)
		if(i->v!=fa){
			int v=i->v;
			if(!dfn[v]){
				dfs(v,u);
				low[u]=min(low[u],low[v]);
				if(low[v]>=dfn[u]){
					w[++col]=1;
					int k;
					do{
						k=q.top();q.pop();
						c[col].push_back(k);
						sz[col]+=sz[k];
						w[col]++;
					}while(k!=v);
					sz[u]+=sz[col];
					c[u].push_back(col);
				}
			}
			else
				low[u]=min(low[u],dfn[v]);
		}
}

其中vectorc[]存的是每个点的子节点(包括圆点和方点), c o l col col存的是点双的编号。方便起见,我们从 n + 1 n+1 n+1开始计算,这样就不会和圆点产生混淆,又可以统一地维护(即初始时col=n)。

4. 例题

讲了这么多,我们再来看一看例题——[APIO2018] Duathlon 铁人两项.

Solution

先变成圆方树,然后尝试赋点权。
我们发现,如果说把圆点点权赋为 − 1 -1 1,方点为点双大小,那么就转化成(圆方树上)统计所有路径上的点权和。
然后令 c n t i cnt_i cnti表示每个点可以被多少条路径覆盖,答案
a n s = ∑ i n w i × c n t i ans=\displaystyle\sum_{i}^{n} w_i\times cnt_i ans=inwi×cnti
然后 d f s + d p dfs+dp dfs+dp即可。代码如下:

#include
#include
#include
#include
using std::vector;
using std::stack;
const int MAXN=200010;

struct edge{
	int v;
	edge *nxt;
	edge(){
		v=0;
		nxt=NULL;
	}
}*head[MAXN];
void adde(int u,int v){
	edge *p=new edge;
	p->v=v;
	p->nxt=head[u];
	head[u]=p;
}

vector<int>c[MAXN];
stack<int>q;

int n,m;
int dfn[MAXN],low[MAXN],ind=0;
int sz[MAXN],w[MAXN],col=0;

inline int min(int x,int y){
	return x<y?x:y;
}
inline int max(int x,int y){
	return x>y?x:y;
}
void dfs(int u,int fa){
	dfn[u]=low[u]=++ind;
	w[u]=-1;sz[u]=1;
	q.push(u);
	for(edge *i=head[u];i!=NULL;i=i->nxt)
		if(i->v!=fa){
			int v=i->v;
			if(!dfn[v]){
				dfs(v,u);
				low[u]=min(low[u],low[v]);
				if(low[v]>=dfn[u]){
					w[++col]=1;
					int k;
					do{
						k=q.top();q.pop();
						c[col].push_back(k);
						sz[col]+=sz[k];
						w[col]++;
					}while(k!=v);
					sz[u]+=sz[col];
					c[u].push_back(col);
				}
			}
			else
				low[u]=min(low[u],dfn[v]);
		}
}

long long ans=0,sum=0;
void treedp(int u){
	int tmp=(u<=n);
	for(int i=0;i<c[u].size();i++){
		int v=c[u][i];treedp(v);
		ans+=tmp*(long long)sz[v]*w[u];
		tmp+=sz[v];
	}
	ans+=sz[u]*(long long)(sum-sz[u])*w[u];//roate
}

int main(){
	scanf("%d%d",&n,&m);col=n;
	for(int i=1;i<=m;i++){
		int u,v;scanf("%d%d",&u,&v);
		adde(u,v);adde(v,u);
	}
	for(int i=1;i<=n;i++)
		if(!dfn[i]){
			dfs(i,0);
			sum=sz[i];
			treedp(i);
		}
	printf("%lld\n",ans<<1);
	return 0;
}

5. 总结

图论新技能:圆方树实现图转树。
注意细节吧。

你可能感兴趣的:(图论,数据结构)