圆方树学习笔记

大家好,在颓了一周思维题后,我们来扯一扯圆方树。
首先我们先看道例题。

Codeforces 487E Tourists

题意:

有一张 (n) 个点 (m) 条边的无向图,点上有点权,(q) 次操作,每次操作有以下两种类型:

  • "C (x y)",将 (x) 点的点权改为 (y)
  • "A (x y)",求所以 (x o y) 的简单路径上点权最小值的最小值。

(1leq n,m,qleq 10^5)

首先把握住关键信息。本题的题眼显然在这个“简单路径”上。简单路径意味着不能经过同一个点。
很自然地可以想到点双连通分量。显然,根据点双连通分量的定义,在同一点双连通分量中,我们可以走到其中点权最小的点并走到相邻的点双连通分量中,并且不会经过重复的点。
考虑缩点。不过直接缩点有一个问题,之前我们遇到的连通分量都是“强连通分量”或“边双连通分量”,对于这一类连通分量都有一个特点,那就是每个点恰好属于一个强连通分量或边双连通分量。而有可能出现一个点属于多个点双连通分量的情况,故不能直接缩点。
那么怎么办呢?就要先从点双连通分量的性质开始说起了。
点双连通分量,指不含割点的极大连通子图。特别地,两个点之间有一条边的子图也是点双连通分量。
点双连通分量有以下性质:

  1. 点双连通分量以割点连接
  2. 每条边必须恰好属于一个点双连通分量。
  3. 任意两个点双连通分量至多有一个公共点
  4. 同一点双连通分量中任意两点 (u,v) 之间简单路径的并集恰好等于整个点双。

性质 1,2,3 都比较显然,性质 4 粉兔神仙给出了严格证明,然鹅我看了半天一个字也没看懂,有兴趣自己去翻他的 blog。
那么什么是圆方树呢?如果我们将原图中的每一个点看作一个“圆点”,对每个点双连通分量新建一个“方点”。对于每一个点双连通分量,在其对应的方点与点双当中每个”圆点“之间连边,那么得到的就是圆方树。
比如下图:

回到此题来。先 tarjan 求出点双连通分量。圆点上的点权为对应点的 (w_i),方点上的点权为与其相连的圆点的点权的最小值。
那么答案即为 (x,y) 之间点权值的最小值。
为什么?设 (P)(x o y) 的所有路径经过的点的集合的并集,那么答案显然为 (minlimits_{uin P}w_u)
那么 (P) 究竟是个什么东西呢?随便找一条 (x,y) 之间的路径 (T),假设其经过的边为 (e_1,e_2,dots,e_k)
根据点双连通分量的性质 2,这些边可以被划分到一个个点双连通分量中。假设这 (k) 条边总共属于 (m) 个点双连通分量,其中边 (e_{i_{j,1}},e_{i_{j,2}},dots,e_{i_{j,c_j}}) 属于点双连通分量 (j)
根据点双连通分量的性质 4,这 (c_j) 条边可以包含整个点双连通分量 (j),也就是说,这个点双连通分量的所有点都应当属于 (P)
也就是说这 (m) 个点双连通分量点集的并 (subseteq P)
而不在这 (m) 个点双连通分量中的点显然不可能被访问到,不然就违反了点双连通分量的定义了。
故我们得到了一个很重要的性质:这 (m) 个点双连通分量点集的并 (=P)

回到圆方树上来,(x,y) 路径上的方点显然就对应这 (m) 个点双连通分量,而每个方点上的权值是这个点双中所有点权值的 (min),故这 (m) 个方点权值的 (min) 就是 (minlimits_{uin P}w_u)
用个树剖维护一下就行了。
但这样还是会被叉掉——考虑一张菊花图,修改菊花图上度数为 (n-1) 的点就要修改 (n-1) 个方点的权值,复杂度最坏为 (n^2log n)
那么有什么办法呢?
我们可以在每个方点开一个 multiset,储存它所有儿子的权值。则方点的权值即为 multiset 中的最小值。这样,修改一个圆点时,就只需要改动它父亲的multiset即可。
然后在查询的时候,若两点之间的 LCA 是个方点,则将答案与 LCA 的父亲(必定是个圆点)的权值取 (min) 即可。

时间复杂度线性对数方。

#include <bits/stdc++.h>
using namespace std;
#define fi first
#define se second
#define fz(i,a,b) for(int i=a;i<=b;i++)
#define fd(i,a,b) for(int i=a;i>=b;i--)
#define ffe(it,v) for(__typeof(v.begin()) it=v.begin();it!=v.end();it++)
#define fill0(a) memset(a,0,sizeof(a))
#define fill1(a) memset(a,-1,sizeof(a))
#define fillbig(a) memset(a,63,sizeof(a))
#define pb push_back
#define ppb pop_back
#define mp make_pair
template<typename T1,typename T2> void chkmin(T1 &x,T2 y){if(x>y) x=y;}
template<typename T1,typename T2> void chkmax(T1 &x,T2 y){if(x<y) x=y;}
typedef pair<int,int> pii;
typedef long long ll;
template<typename T> void read(T &x){
	x=0;char c=getchar();T neg=1;
	while(!isdigit(c)){if(c=='-') neg=-1;c=getchar();}
	while(isdigit(c)) x=x*10+c-'0',c=getchar();
	x*=neg;
}
const int MAXN=1e5;
int n,m,qu,w[MAXN*2+5],cnt;
namespace segtree{
	struct node{int l,r,val;} s[MAXN*8+5];
	void build(int k,int l,int r){
		s[k].l=l;s[k].r=r;s[k].val=0x3f3f3f3f;if(l==r) return;
		int mid=(l+r)>>1;build(k<<1,l,mid);build(k<<1|1,mid+1,r);
	}
	void modify(int k,int x,int v){
		if(s[k].l==s[k].r){s[k].val=v;return;}
		int mid=(s[k].l+s[k].r)>>1;
		if(x<=mid) modify(k<<1,x,v);
		else modify(k<<1|1,x,v);
		s[k].val=min(s[k<<1].val,s[k<<1|1].val);
	}
	int query(int k,int l,int r){
		if(l<=s[k].l&&s[k].r<=r) return s[k].val;
		int mid=(s[k].l+s[k].r)>>1;
		if(r<=mid) return query(k<<1,l,r);
		else if(l>mid) return query(k<<1|1,l,r);
		else return min(query(k<<1,l,mid),query(k<<1|1,mid+1,r));
	}
}
namespace tree{
	int hd[MAXN*4+5],nxt[MAXN*4+5],to[MAXN*4+5],ec=0;
	void adde(int u,int v){to[++ec]=v;nxt[ec]=hd[u];hd[u]=ec;}
	int siz[MAXN*2+5],fa[MAXN*2+5],wson[MAXN*2+5],dep[MAXN*2+5];
	int top[MAXN*2+5],dfn[MAXN*2+5],tim=0;
	void dfs1(int x,int f){
		fa[x]=f;siz[x]=1;
		for(int e=hd[x];e;e=nxt[e]){
			int y=to[e];if(y==f) continue;
			dep[y]=dep[x]+1;dfs1(y,x);siz[x]+=siz[y];
			if(siz[y]>siz[wson[x]]) wson[x]=y;
		}
	}
	void dfs2(int x,int tp){
		dfn[x]=++tim;top[x]=tp;
		if(wson[x]) dfs2(wson[x],tp);
		for(int e=hd[x];e;e=nxt[e]){
			int y=to[e];if(y==fa[x]||y==wson[x]) continue;
			dfs2(y,y);
		}
	}
	multiset<int> st[MAXN+5];
	void prework(){
		dfs1(1,0);dfs2(1,1);segtree::build(1,1,cnt);
		for(int i=2;i<=n;i++) w[fa[i]]=min(w[fa[i]],w[i]),st[fa[i]-n].insert(w[i]);
		for(int i=1;i<=n-cnt;i++) st[i].insert(0x3f3f3f3f);
//		for(int i=1;i<=cnt;i++) printf("%d %d %d %d %d %d
",fa[i],siz[i],dep[i],wson[i],top[i],dfn[i]);
		for(int i=1;i<=cnt;i++) segtree::modify(1,dfn[i],w[i]);
	}
	void change(int x,int v){
		if(x!=1){
			st[fa[x]-n].erase(st[fa[x]-n].find(w[x]));st[fa[x]-n].insert(v);
			w[fa[x]]=*st[fa[x]-n].begin();segtree::modify(1,dfn[fa[x]],w[fa[x]]);
		} w[x]=v;segtree::modify(1,dfn[x],w[x]);
	}
	int query(int x,int y){
		if(dep[x]<dep[y]) swap(x,y);
		int ret=0x3f3f3f3f;
		while(top[x]!=top[y]){
			if(dep[top[x]]<dep[top[y]]) swap(x,y);
			chkmin(ret,segtree::query(1,dfn[top[x]],dfn[x]));
			x=fa[top[x]];
		}
		if(dep[x]<dep[y]) swap(x,y);
		chkmin(ret,segtree::query(1,dfn[y],dfn[x]));
//		printf("%d
",y);
		if(y>n) chkmin(ret,w[fa[y]]);
		return ret;
	}
}
namespace graph{
	int hd[MAXN*2+5],nxt[MAXN*2+5],to[MAXN*2+5],ec=0;
	void adde(int u,int v){to[++ec]=v;nxt[ec]=hd[u];hd[u]=ec;}
	int dfn[MAXN+5],low[MAXN+5],stk[MAXN+5],top=0,tim=0;
	void tarjan(int x){
		dfn[x]=low[x]=++tim;stk[++top]=x;
		for(int e=hd[x];e;e=nxt[e]){
			int y=to[e];if(!dfn[y]){
				tarjan(y);low[x]=min(low[x],low[y]);
				if(low[y]>=dfn[x]){
					cnt++;w[cnt]=0x3f3f3f3f;int z;
					do {
						//printf("%d ",stk[top]);
						z=stk[top];tree::adde(cnt,z);tree::adde(z,cnt);top--;
					} while(z!=y);
					tree::adde(cnt,x);tree::adde(x,cnt);//printf("%d
",x);
				}
			} else low[x]=min(low[x],dfn[y]);
		}
	}
}
int main(){
	scanf("%d%d%d",&n,&m,&qu);cnt=n;
	for(int i=1;i<=n;i++) scanf("%d",&w[i]);
	for(int i=1;i<=m;i++){
		int u,v;scanf("%d%d",&u,&v);
		graph::adde(u,v);graph::adde(v,u);
	} graph::tarjan(1);tree::prework();
	while(qu--){
		char opt[3];int x,y;scanf("%s%d%d",opt+1,&x,&y);
		if(opt[1]=='C') tree::change(x,y);
		else printf("%d
",tree::query(x,y));
	}
	return 0;
}
/*
9 9 1
2
4
8
7
7
6
7
8
10
2 1
1 7
3 2
4 3
5 4
5 6
6 7
4 8
2 9
A 4 3
*/

另一道例题:

洛谷 P4630 [APIO2018] Duathlon 铁人两项

容易想到固定住 (s,f),计算有多少个符合要求的 (c)
(c) 的个数就是 (s)(f) 简单路径经过的点的并集的大小 (-2)(s eq c,f eq c))。
那么这个并集大小怎么计算呢?
根据之前的推论,这个并集就是 (s)(f) 之间所有 (m) 个点双点集的并集,故其大小为这 (m) 个点集的大小之和 (sum)
欸等等……好像有什么问题。有的点双之间有公共点,而这个公共点在两个点双中都会被算一次。故还需减掉公共点的个数。而只有相邻经过的点双之间才会有公共点,故公共点的个数为 (m-1)。所以并集的大小就是 (sum-(m-1))
如果我们把方点的权值赋为这个点双中点的个数,圆点的权值赋为 (-1)。考虑 (s)(f) 之间路径上所有点的权值和。方点贡献的权值之和为 (sum),圆点共 (m+1) 个,每圆点贡献 (-1) 的权值,故总权值为 (sum-(m+1)),刚好就等于 (s)(f) 简单路径经过的点的并集的大小 (-2)
故题目转化为:求圆点之间两两路径上的权值之和。这个随便乱搞搞就行了。

#include <bits/stdc++.h>
using namespace std;
#define fi first
#define se second
#define fz(i,a,b) for(int i=a;i<=b;i++)
#define fd(i,a,b) for(int i=a;i>=b;i--)
#define ffe(it,v) for(__typeof(v.begin()) it=v.begin();it!=v.end();it++)
#define fill0(a) memset(a,0,sizeof(a))
#define fill1(a) memset(a,-1,sizeof(a))
#define fillbig(a) memset(a,63,sizeof(a))
#define pb push_back
#define ppb pop_back
#define mp make_pair
template<typename T1,typename T2> void chkmin(T1 &x,T2 y){if(x>y) x=y;}
template<typename T1,typename T2> void chkmax(T1 &x,T2 y){if(x<y) x=y;}
typedef pair<int,int> pii;
typedef long long ll;
template<typename T> void read(T &x){
	x=0;char c=getchar();T neg=1;
	while(!isdigit(c)){if(c=='-') neg=-1;c=getchar();}
	while(isdigit(c)) x=x*10+c-'0',c=getchar();
	x*=neg;
}
const int MAXN=1e5;
const int MAXM=2e5;
int n,m,cnt,w[MAXN*2+5],pcnt=0;
struct graph{
	int hd[MAXN*2+5],nxt[MAXM*2+5],to[MAXM*2+5],ec;
	graph(){memset(hd,0,sizeof(hd));memset(nxt,0,sizeof(nxt));memset(to,0,sizeof(to));ec=0;}
	void adde(int u,int v){to[++ec]=v;nxt[ec]=hd[u];hd[u]=ec;}
} g,t;
int dfn[MAXN+5],low[MAXN+5],stk[MAXN+5],top=0,tim=0;
void tarjan(int x){
	dfn[x]=low[x]=++tim;stk[++top]=x;pcnt++;
	for(int e=g.hd[x];e;e=g.nxt[e]){
		int y=g.to[e];if(!dfn[y]){
			tarjan(y);low[x]=min(low[x],low[y]);
			if(low[y]>=dfn[x]){
				cnt++;int z=0;
				do {
					z=stk[top];t.adde(cnt,z);t.adde(z,cnt);w[cnt]++;top--;
				} while(z!=y);
				t.adde(cnt,x);t.adde(x,cnt);w[cnt]++;
			}
		} else low[x]=min(low[x],dfn[y]);
	}
}
int siz[MAXN*2+5];
ll ans=0;
void dfs(int x,int f){
	siz[x]=(x<=n);
	for(int e=t.hd[x];e;e=t.nxt[e]){
		int y=t.to[e];if(y==f) continue;
		dfs(y,x);siz[x]+=siz[y];
		ans+=1ll*siz[y]*(pcnt-siz[y])*w[x];
	} ans+=1ll*(pcnt-siz[x])*siz[x]*w[x];
	if(x<=n) ans+=1ll*(pcnt-1)*w[x];
}
int main(){
	scanf("%d%d",&n,&m);cnt=n;
	for(int i=1;i<=n;i++) w[i]=-1;
	for(int i=1;i<=m;i++){
		int u,v;scanf("%d%d",&u,&v);
		g.adde(u,v);g.adde(v,u);
	}
	for(int i=1;i<=n;i++) if(!dfn[i]){
		pcnt=0;tarjan(i);dfs(i,0);
	} printf("%lld
",ans);
	return 0;
}

由于圆方树应用没有那么广泛,而它原本应用的场景——仙人掌因为太 dl 的就没准备学,所有就放了这么两道例题供参考。

原文地址:https://www.cnblogs.com/ET2006/p/yfs.html