• 圆方树学习笔记


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

    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 的就没准备学,所有就放了这么两道例题供参考。

  • 相关阅读:
    Tomcat windows服务器配置多个Tomcat
    Sharepoint开发实用技巧(1)
    戏说MOSS关于EventHander编程
    协作应用程序标记语言 CAML 点滴(一)
    MOSS开发手记(3)
    协作应用程序标记语言 CAML点滴(二)
    MOSS项目开发(1) 项目计划,重点及文档
    MOSS项目开发(4) 开发文档的规范
    MOSS开发手记(2)
    Asp.Net页面执行流程分析
  • 原文地址:https://www.cnblogs.com/ET2006/p/yfs.html
Copyright © 2020-2023  润新知