圆方树总结

算法

圆方树是一种化图为树的方法,它能很好的维护点双的信息并用一些在树上的数据结构来进一步优化,其构建方法如下:

考虑到树的本质就是一个无环连通图,换句话说,没有点双连通分量。那么我们可以直接对每一个点双建一个新点,并将其向这个点双内所有点连边(为了保证性质,我们把有连边的两个割点也看成点双),这显然会得到一颗树,这就是圆方树。

为什么叫圆方树呢?因为我们通常称树上原有的点为圆点,新建的点为方点。我们只要在方点上记录它所对应的点双上的信息,就可以在树上快速处理了。

显然圆方树上每一条边都是连接一个圆点和一个方点。

代码也非常好写,直接用 T a r j a n \rm Tarjan Tarjan 求点双并在过程中加边即可,下面给出核心代码:

void Tarjan(int u,int fa){
	dfn[u] = low[u] = ++cnt,in[u] = 1,stk[++top] = u;
	for(int i = G0.last[u]; i; i = G0.e[i].nxt){//G0为原图,G为圆方树
		int v = G0.e[i].v;
		if(v == fa) continue;
		if(!dfn[v]){
			Tarjan(v,u);
			low[u] = min(low[u],low[v]);
			if(low[v] >= dfn[u]){
				m ++;
				while(stk[top + 1] != v) G.add(m,stk[top]),in[stk[top]] = 0,top --;//add就是加边
				G.add(m,u);
			}
		}else if(in[v]) low[u] = min(low[u],dfn[v]);
	}
}

圆方树这个东西没啥可说的,主要看题。

例题

[SDOI2018]战略游戏
分析

我们考虑对原图建出圆方树,显然合法点就是树上任意两关键点路径中的圆点个数。
那么总的答案就是所有关键点路径并中的圆点个数减去 ∣ S ∣ |S| S,不难发现这个路径并就是所有关键点对应的虚树。我们令方点的权值为 0 0 0,圆点的权值为 1 1 1,那么问题转化为 S S S 对应虚树的权值和。

这种问题有一个经典的做法:将每个点按照 d f s \rm dfs dfs 序排序,然后从第一个点出发按最短路径一次走遍所有的点,最后再从最后一个点走回第一个点,那么点集对应虚树中的每条边都会被走到恰好两次。

我们再考虑回原问题,如果我们将每个点的点权转换成它与父亲连边的边权,那么最后的答案不就是虚树总边权加上根的点权吗?而总边权我们可以直接用上面的方法求,那么这题就做完了,复杂 O ( ∣ S ∣ log ⁡ ∣ S ∣ ) \Omicron(|S| \log |S|) O(SlogS)

代码
#include <iostream>
#include <cstdio>
#include <cmath>
#include <algorithm>
using namespace std;
const int maxn = 1e5 + 50,maxm = 2e5 + 50;
int T,n,m,q,x,y,t,cnt,sum,a[maxm],st[maxm],s[maxm],dfn[2 * maxm],ST[2 * maxm][20];
struct Edge{
	int v,nxt;
};
struct Graph{//由于要建两张图,直接写了结构体,后面几题也一样
	int cnt,last[maxm];
	Edge e[2 * maxm];
	inline void insert(int u,int v){
		e[++cnt] = {v,last[u]},last[u] = cnt;
	}
	inline void add(int u,int v){
//		cout << "adde : " << u << ' ' << v << endl;
		insert(u,v);
		insert(v,u);
	}
	inline void init(){
		cnt = 0;
		for(int i = 2 * n; i >= 1; i --) last[i] = 0;
	}
}G0,G;
int read(){
	int x = 0;
	char c = getchar();
	while(c < '0' || c > '9') c = getchar();
	while(c >= '0' && c <= '9') x = x * 10 + (c ^ 48),c = getchar();
	return x;
}
namespace Tarjan{
	int cnt,top,dfn[maxn],low[maxn],in[maxn],stk[maxn];
	inline void init(){
		cnt = 0;
		for(int i = 1; i <= n; i ++) dfn[i] = 0;
	}
	void dfs(int u,int fa){
		dfn[u] = low[u] = ++cnt,in[u] = 1,stk[++top] = u;
		for(int i = G0.last[u]; i; i = G0.e[i].nxt){
			int v = G0.e[i].v;
			if(v == fa) continue;
			if(!dfn[v]){
				dfs(v,u);
				low[u] = min(low[u],low[v]);
				if(low[v] >= dfn[u]){
					m ++;
					while(stk[top + 1] != v) G.add(m,stk[top]),in[stk[top]] = 0,top --;
					G.add(m,u);
				}
			}else if(in[v]) low[u] = min(low[u],dfn[v]);
		}
	}
}
void dfs(int u,int fa){
	dfn[++cnt] = u,st[u] = cnt,s[u] = s[fa] + (u <= n);//欧拉序求LCA
	for(int i = G.last[u]; i; i = G.e[i].nxt){
		int v = G.e[i].v;
		if(v == fa) continue;
		dfn[++cnt] = u;
		dfs(v,u);
	}
}
void ST_init(){
	for(int i = 2 * m - 1; i >= 1; i --){
		ST[i][0] = dfn[i];
		for(int j = 1; i + (1 << j) <= 2 * m; j ++){
			if(st[ST[i][j - 1]] < st[ST[i + (1 << (j - 1))][j - 1]]) ST[i][j] = ST[i][j - 1];
			else ST[i][j] = ST[i + (1 << (j - 1))][j - 1];
		}
	}
}
int Lca(int u,int v){
	int l = st[u],r = st[v];
	if(l > r) swap(l,r);
	int k = log2(r - l + 1);
	if(st[ST[l][k]] < st[ST[r - (1 << k) + 1][k]]) return ST[l][k];
	else return ST[r - (1 << k) + 1][k];
}
bool cmp(int x,int y){
	return st[x] < st[y];
}
void INIT(){
	cnt = 0,m = n;
	G0.init(),G.init();
	Tarjan :: init();
}
int main(){
	T = read();
	while(T --){
		n = read(),t = read();
		INIT();
		for(int i = 1; i <= t; i ++){
			x = read(),y = read();
			G0.add(x,y);
		}
		Tarjan :: dfs(1,0);
		dfs(1,0);
		ST_init();
		q = read();
		while(q --){
			t = read(),sum = 0;
			for(int i = 1; i <= t; i ++) a[i] = read();
			sort(a + 1,a + t + 1,cmp);
			a[t + 1] = a[1];
			for(int i = 1; i <= t; i ++){
				int c = Lca(a[i],a[i + 1]);
				sum += s[a[i]] + s[a[i + 1]] - 2 * s[c];
			}
			sum = (sum >> 1) + (Lca(a[1],a[t]) <= n);
			printf("%d\n",sum - t);
		}
	}
	return 0;
}
[APIO2018] 铁人两项
分析

首先,给出一个引理:

在一个点双中,指定两点 A , B A,B A,B 后点双中其它每一点 C C C 都存在一条 A − > C − > B A->C->B A>C>B 的不经过重复点的路径。

证明应该非常显然。


回到原题,我们考虑当指定两端点(在同一连通块内)后会发生什么:

  1. 两端点 “之间” 的割点显然是要经过且能经过的
  2. 1 1 1 与引理,不同的路径显然可以分别经过 端点间的相邻两割点 “之间” 的点双中的所有点。
  3. 同样根据引理,两端点所在点双内的所有点我们也能经过。
  4. 显然不能经过原图中除 1 1 1 2 2 2 3 3 3 外的其他点,否则无法互达。

也就是说,指定两点之后的贡献就是 两点所在点双 与 两点之间的割点间点双减去两点本身后的点数和。

那么不难想到建一颗圆方树,指定两端点(显然只能是圆点)后的贡献就是 两点树上路径对应的所有点减去两端点的点数和。

进一步考虑,发现这个东西就将树上每个圆点点权赋为 − 1 -1 1,方点点权赋为其所代表的点双的点数后的路径权值和。原因是路径中除端点外每个圆点(即割点)都会会被相邻的两点双算两遍,而两端点虽然只被算一遍但本身并不能被统计,故每个点都需要减一。

那么问题又进一步转化成了求树上所有路径的权值和,显然只要分别计算每个点的贡献即可,就是一个简单的 D P \rm DP DP

注意统计大小时只能统计圆点(端点只能为圆点)。

代码
#include <iostream>
#include <cstdio>
using namespace std;
const int maxn = 1e5 + 50,maxm = 2 * maxn;
int n,m,t,x,y,cnt,top,dfn[maxn],low[maxn],in[maxn],stk[maxn],size[maxm],d[maxm];
long long ans;
struct Edge{
	int v,nxt;
};
struct Graph{
	int cnt,last[maxm];
	Edge e[2 * maxm];
	inline void insert(int u,int v){
		e[++cnt] = {v,last[u]},last[u] = cnt;
		if(u > n) d[u] ++;
	}
	inline void add(int u,int v){
		insert(u,v);
		insert(v,u);
	}
}G0,G;
int read(){
	int x = 0;
	char c = getchar();
	while(c < '0' || c > '9') c = getchar();
	while(c >= '0' && c <= '9') x = x * 10 + (c ^ 48),c = getchar();
	return x;
}
void Tarjan(int u,int fa){
	dfn[u] = low[u] = ++cnt,in[u] = 1,stk[++top] = u;
	for(int i = G0.last[u]; i; i = G0.e[i].nxt){
		int v = G0.e[i].v;
		if(v == fa) continue;
		if(!dfn[v]){
			Tarjan(v,u);
			low[u] = min(low[u],low[v]);
			if(low[v] >= dfn[u]){
				m ++;
				while(stk[top + 1] != v) G.add(m,stk[top]),in[stk[top]] = 0,top --;
				G.add(m,u);
			}
		}else if(in[v]) low[u] = min(low[u],dfn[v]);
	}
}
void dfs(int u,int fa){//只统计了有序的情况
	size[u] = u <= n;
	for(int i = G.last[u]; i; i = G.e[i].nxt){
		int v = G.e[i].v;
		if(v == fa) continue;
		dfs(v,u);
		ans += 1ll * size[v] * size[u] * d[u],size[u] += size[v];
	}
	ans += 1ll * size[u] * (cnt - size[u]) * d[u];
}
int main(){
	n = read(),t = read(),m = n;
	while(t --){
		x = read(),y = read();
		G0.add(x,y);
	}
	for(int i = 1; i <= n; i ++) d[i] = -1;
	for(int i = 1; i <= n; i ++) if(!dfn[i]) cnt = 0,Tarjan(i,0),dfs(i,0);//cnt为当前连通块大小
	printf("%lld\n",2 * ans);//答案是无序的所以要乘2
	return 0;
}
CF487E Tourists
分析

看懂前面两题这个应该很简单了吧,这题虽然码量较大但是思维很简单。

显然每个点双内的物品是一样的,建圆方树后每个方点开个 s e t \rm set set 然后直接统计树上路径最小值就行了,这个可以用树剖+线段树实现 当然你闲的话也可以写 L C T \sout{\rm LCT} LCT

但是实现时有点小问题,如果每个方点记录整个连通块信息的话修改就很慢(要修改所有与其相邻的方点)。所以可以改一下变成每个方点记录圆方树内所有儿子节点的最小值,这样修改就只用修改目标圆点的父节点。但查询的时候两点的 L C A \rm LCA LCA 会漏掉,最后再判一下就行。

代码
#include <iostream>
#include <cstdio>
#include <set>
#define ls (x << 1)
#define rs (x << 1 | 1)
using namespace std;
const int maxn = 1e5 + 50,maxm = 2 * maxn,inf = 2e9;
int n,m,q,t,x,y,cnt,w[maxn],fa[maxm],size[maxm],d[maxm],top[maxm],dfn[maxm],re[maxm],son[maxm],tr[4 * maxm];
char c;
multiset <int> s[maxn];
struct Edge{
	int v,nxt;
};
struct Graph{
	int cnt,last[maxm];
	Edge e[2 * maxm];
	inline void insert(int u,int v){
		e[++cnt] = {v,last[u]},last[u] = cnt;
	}
	inline void add(int u,int v){
//		cout << "nmsl " << u << ' ' << v << endl;
		insert(u,v);
		insert(v,u);
	}
}G0,G;
int read(){
	int x = 0;
	char c = getchar();
	while(c < '0' || c > '9') c = getchar();
	while(c >= '0' && c <= '9') x = x * 10 + (c ^ 48),c = getchar();
	return x;
}
namespace Tarjan{
	int cnt,top,dfn[maxn],low[maxn],in[maxn],stk[maxn];
	void dfs(int u,int fa){
		dfn[u] = low[u] = ++cnt,in[u] = 1,stk[++top] = u;
		for(int i = G0.last[u]; i; i = G0.e[i].nxt){
			int v = G0.e[i].v;
			if(v == fa) continue;
			if(!dfn[v]){
				dfs(v,u);
				low[u] = min(low[u],low[v]);
				if(low[v] >= dfn[u]){
					m ++;
					while(stk[top + 1] != v) G.add(m,stk[top]),in[stk[top]] = 0,top --;
					G.add(m,u);
				}
			}else if(in[v]) low[u] = min(low[u],dfn[v]);
		}
	}
}
void build(int x = 1,int l = 1,int r = m){
	if(l == r){
		if(re[l] > n) tr[x] = *s[re[l] - n].begin();//线段树中只记录方点的值
		else tr[x] = inf;
		return;
	}
	int mid = (l + r) / 2;
	build(ls,l,mid),build(rs,mid + 1,r);
	tr[x] = min(tr[ls],tr[rs]);
}
void update(int p,int k,int x = 1,int l = 1,int r = m){
	if(l == r){
		tr[x] = k;
		return;
	}
	int mid = (l + r) / 2;
	if(p <= mid) update(p,k,ls,l,mid);
	else update(p,k,rs,mid + 1,r);
	tr[x] = min(tr[ls],tr[rs]);
}
int query(int L,int R,int x = 1,int l = 1,int r = m){
	if(L <= l && R >= r) return tr[x];
	int mid = (l + r) / 2,ret = inf;
	if(L <= mid) ret = min(ret,query(L,R,ls,l,mid));
	if(R > mid) ret = min(ret,query(L,R,rs,mid + 1,r));
	return ret;
}
void dfs1(int u,int f){
	fa[u] = f,size[u] = 1,d[u] = d[f] + 1;
	for(int i = G.last[u]; i; i = G.e[i].nxt){
		int v = G.e[i].v;
		if(v == f) continue;
		dfs1(v,u);
		size[u] += size[v];
		if(u > n) s[u - n].insert(w[v]);
		if(size[v] > size[son[u]]) son[u] = v;
	}
}
void dfs2(int u,int tp){
	dfn[u] = ++cnt,re[cnt] = u,top[u] = tp;
	if(son[u]) dfs2(son[u],tp);
	for(int i = G.last[u]; i; i = G.e[i].nxt){
		int v = G.e[i].v;
		if(v == fa[u] || v == son[u]) continue;
		dfs2(v,v);
	}
}
void solve(int u,int v){
	int ret = inf;
	while(top[u] != top[v]){
		if(d[top[u]] < d[top[v]]) swap(u,v);
		ret = min(ret,query(dfn[top[u]],dfn[u]));
		u = fa[top[u]];
	}
//	cout << ret << endl;
	if(d[u] < d[v]) swap(u,v);
	ret = min(ret,query(dfn[v],dfn[u]));
//	cout << u << ' ' << v << endl;
	if(v <= n) ret = min(ret,w[v]);//LCA是圆点时,只与其权值比较
	else ret = min(ret,min(w[fa[v]],*s[v - n].begin()));
	//方点时,因为线段树未记录圆点信息,所以还要与其所有儿子比较
	printf("%d\n",ret);
}
int main(){
	n = read(),t = read(),q = read(),m = n;
	w[0] = inf;
	for(int i = 1; i <= n; i ++) w[i] = read();
	for(int i = 1; i <= t; i ++){
		x = read(),y = read();
		G0.add(x,y);
	}
//	cout << "nmsl " << endl;
	Tarjan :: dfs(1,0);
	dfs1(1,0);
	dfs2(1,1);
//	for(int i = 1; i <= 9; i ++) cout << fa[i] << ' ';
//	cout << endl;
	build();
	while(q --){
//		getchar();
		c = getchar(),x = read(),y = read();
		if(c == 'A') solve(x,y);
		else{
			if(fa[x]){
				s[fa[x] - n].erase(s[fa[x] - n].find(w[x]));
				s[fa[x] - n].insert(y);
				update(dfn[fa[x]],*s[fa[x] - n].begin());
			}
			w[x] = y;
		}
	}
	return 0;
}
PUSHFLOW
分析

根据题目定义,这显然是一颗仙人掌,也就是没有环套环。

那么两点之间的流量显然就是每个环的贡献(即它的流量)的最小值。

而每个环中的最小边一定会被算进这个环的贡献,然后就是环上另一边的最小值。

显然我们可以建一个圆方树,然后每个环维护一颗线段树,在修改时求另一边的最小值并更新环的贡献,最后再用一个树剖+线段树求树上路径最小值。

然后可以发现这玩意根本就不是人写的,而且复杂度是 O ( n log ⁡ 2 n ) \Omicron (n\log^2 n) O(nlog2n)

于是我们考虑把圆方树扔在一边 我为什么要放这道题 ,可以发现如果将每个环上的最小边去掉,则我们需要统计的边就只有剩下的树中两点路径上的边了 (其它边与所在环的最小值在同一边,不能选)。

那么能不能通过某种方法直接变成两点间路径最小值呢?
看上去不行,因为每个环还要加上最小边的贡献。
那我们直接将每个环上每条边加上最小边的贡献再把最小边去掉,不就是树上路径最小值了吗?

仔细想一下可以发现 L C T \rm LCT LCT 就可以直接 O ( n log ⁡ n ) \Omicron(n\log n) O(nlogn) 实现上面的过程。

不对啊, L C T \rm LCT LCT 怎么维护边的最小值?
你把每条边看做一个点,向它相连的两点连边,然后新点权设为边权,原点权设为 + ∞ +\infty + 不就行了。

这题权的范围特别大, + ∞ +\infty + 设为 i n t \rm int int 的话修改时容易爆,所以要开 l o n g    l o n g \rm long\;long longlong

注意修改时还要特判一下边在不在环上。

代码
#include <iostream>
#include <cstdio>
#include <set>
using namespace std;
const int maxn = 3e5 + 50,maxm = 2e5 + 50,maxk = 1e5 + 50;
const long long inf = 1e18;
int n,m,q,x,y,z,cnt = 1,tot,top,last[maxk],vis[maxk],ins[maxk],stk[maxn],id[maxm],U[maxm],V[maxm],w[maxm],fa[maxn],rev[maxn],son[maxn][2];
long long val[maxn],mn[maxn],tag[maxn];
set <pair<long long,int> > s[maxk];
struct Edge{
	int v,nxt;
}e[2 * maxm];
int read(){
	int x = 0;
	char c = getchar();
	while(c < '0' || c > '9') c = getchar();
	while(c >= '0' && c <= '9') x = x * 10 + (c ^ 48),c = getchar();
	return x;
}
inline void insert(int u,int v){
	e[++cnt] = {v,last[u]},last[u] = cnt;
}
inline void update(int u){
	mn[u] = min(val[u],min(mn[son[u][0]],mn[son[u][1]]));
}
inline bool nroot(int u){
	return son[fa[u]][0] == u || son[fa[u]][1] == u;
}
inline void reverse(int u){
	if(!u) return;
	rev[u] ^= 1;
	swap(son[u][0],son[u][1]);
}
void push_down(int u){
	if(rev[u]){
		reverse(son[u][0]);
		reverse(son[u][1]);
		rev[u] = 0;
	}
	if(tag[u]){
		val[son[u][0]] += tag[u],mn[son[u][0]] += tag[u],tag[son[u][0]] += tag[u];
		val[son[u][1]] += tag[u],mn[son[u][1]] += tag[u],tag[son[u][1]] += tag[u];
		tag[u] = 0;
	}
}
void rotate(int u){
	int v = fa[u],d = son[v][1] == u;
	if(nroot(v)) son[fa[v]][son[fa[v]][1] == v] = u;
	son[v][d] = son[u][d ^ 1],son[u][d ^ 1] = v;
	fa[u] = fa[v],fa[v] = u,fa[son[v][d]] = v;
	update(v),update(u);
}
void splay(int u){
	int t = u;
	stk[top = 1] = t;
	while(nroot(t)) t = fa[t],stk[++top] = t;
	while(top) push_down(stk[top --]);
	while(nroot(u)){
		int f = fa[u];
		if(!nroot(f)){
			rotate(u);
			break;
		}
		if((son[fa[f]][1] == f) == (son[f][1] == u)) rotate(f),rotate(u);
		else rotate(u),rotate(u);
	}
}
void access(int u){
	int v = 0;
	while(u){
		splay(u);
		son[u][1] = v;
		update(u);
		v = u,u = fa[u];
	}
}
void change_root(int u){
	access(u);
	splay(u);
	rev[u] ^= 1;
	swap(son[u][0],son[u][1]);
}
void split(int u,int v){
	change_root(u);
	access(v);
	splay(v);
}
void link(int u,int v){
	change_root(u);
	fa[u] = v;
}
void cut(int u,int v){
	split(u,v);
	fa[u] = son[v][0] = 0;
	update(v);
}
void add(int u,int v,int k){
	split(u,v);
	val[v] += k,mn[v] += k,tag[v] += k;
}
//上面基本都是LCT板子
void change(int k,int c){
	if(!id[k]){//不在环上
		access(k),splay(k);
		val[k] = w[k] = c;
		update(k);
		return;
	}
	int t = s[id[k]].begin()->second;
	add(U[t],V[t],-w[t]);
	s[id[k]].erase({w[k],k});
	s[id[k]].insert({c,k});
	access(k),splay(k);
	val[k] = w[k] = c;
	update(k);
	int p = s[id[k]].begin()->second;
	if(p != t){
		cut(p,U[p]),cut(p,V[p]);
		link(t,U[t]),link(t,V[t]);
	}
	add(U[p],V[p],w[p]);
}
inline void solve(int u,int v){
	split(u,v);
	printf("%d\n",mn[v]);
}
void dfs(int u,int fa){
	vis[u] = 1,ins[u] = 1;
	for(int i = last[u]; i; i = e[i].nxt){
		int v = e[i].v;
		if(v == fa) continue;
		if(!vis[v]){
			stk[++top] = i;
			dfs(v,u);
			top --;
		}else if(ins[v]){
			tot ++,id[i >> 1] = tot;
			for(int j = top; e[stk[j + 1] ^ 1].v != v; j --) id[stk[j] >> 1] = tot;
			//注意这里不能退栈,我因为这个调了很久
		}
	}
	ins[u] = 0;
}
int main(){
	n = read(),m = read(),mn[0] = inf;
	for(int i = 1; i <= n; i ++) val[i + m] = mn[i + m] = inf;
	for(int i = 1; i <= m; i ++){
		U[i] = read(),V[i] = read(),w[i] = read();
		insert(U[i],V[i]),insert(V[i],U[i]);
		U[i] += m,V[i] += m,val[i] = mn[i] = w[i];//将原点编号加上m与边区分
	}
	dfs(1,0);
	for(int i = 1; i <= m; i ++) if(id[i]) s[id[i]].insert({w[i],i});
	for(int i = 1; i <= m; i ++) if(!id[i] || i != s[id[i]].begin()->second) link(U[i],i),link(i,V[i]);
	//若不是最小边或不在换上就加边
	for(int i = 1; i <= m; i ++) if(id[i] && i == s[id[i]].begin()->second) add(U[i],V[i],w[i]);
	//给环上剩余边加上最小值的贡献
	q = read();
	while(q --){
		z = read(),x = read(),y = read();
		if(z == 0) solve(x + m,y + m);
		else change(x,y);
	}
	return 0;
}
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值