圆方树学习笔记

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

Codeforces 487E Tourists

题意:

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

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

\(1\leq n,m,q\leq 10^5\)

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

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

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

回到此题来。先 tarjan 求出点双连通分量。圆点上的点权为对应点的 \(w_i\),方点上的点权为与其相连的圆点的点权的最小值。
那么答案即为 \(x,y\) 之间点权值的最小值。
为什么?设 \(P\)\(x\to y\) 的所有路径经过的点的集合的并集,那么答案显然为 \(\min\limits_{u\in 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\) 就是 \(\min\limits_{u\in P}w_u\)
用个树剖维护一下就行了。
但这样还是会被叉掉——考虑一张菊花图,修改菊花图上度数为 \(n-1\) 的点就要修改 \(n-1\) 个方点的权值,复杂度最坏为 \(n^2\log 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\n",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\n",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\n",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\n",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\neq c,f\neq 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\n",ans);
	return 0;
}

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

posted @ 2020-12-27 22:16  tzc_wk  阅读(104)  评论(0编辑  收藏  举报