大家好,在颓了一周思维题后,我们来扯一扯圆方树。
首先我们先看道例题。
Codeforces 487E Tourists
题意:
有一张 (n) 个点 (m) 条边的无向图,点上有点权,(q) 次操作,每次操作有以下两种类型:
- "C (x y)",将 (x) 点的点权改为 (y)。
- "A (x y)",求所以 (x o y) 的简单路径上点权最小值的最小值。
(1leq n,m,qleq 10^5)
首先把握住关键信息。本题的题眼显然在这个“简单路径”上。简单路径意味着不能经过同一个点。
很自然地可以想到点双连通分量。显然,根据点双连通分量的定义,在同一点双连通分量中,我们可以走到其中点权最小的点并走到相邻的点双连通分量中,并且不会经过重复的点。
考虑缩点。不过直接缩点有一个问题,之前我们遇到的连通分量都是“强连通分量”或“边双连通分量”,对于这一类连通分量都有一个特点,那就是每个点恰好属于一个强连通分量或边双连通分量。而有可能出现一个点属于多个点双连通分量的情况,故不能直接缩点。
那么怎么办呢?就要先从点双连通分量的性质开始说起了。
点双连通分量,指不含割点的极大连通子图。特别地,两个点之间有一条边的子图也是点双连通分量。
点双连通分量有以下性质:
- 点双连通分量以割点连接
- 每条边必须恰好属于一个点双连通分量。
- 任意两个点双连通分量至多有一个公共点
- 同一点双连通分量中任意两点 (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 的就没准备学,所有就放了这么两道例题供参考。