圆方树学习笔记

lindongli2004

2019-08-22 01:01:49

Personal

#### 一 . 分类 分为圆方树与广义圆方树,前者处理仙人掌,后者处理无向图,本文只介绍广义圆方树。 #### 二 . 构建 1. Tarjan求点双联通分量。 2. 对于每个点双新建一个方点,向每个圆点(点双中的点)连边,并保持原图中的边不变。 如下图: ![](https://images.cnblogs.com/cnblogs_com/cjoieryl/1143095/o_a.png) 这样就可以用圆点维护点上信息,方点维护点双信息了。 #### 三 . 性质 - 一条简单路径不可能有两个方点相邻,比如:圆方圆圆方,方圆方圆方。 - 原图中的割点即为度数>1的圆点。 更多的性质请大家补充。 #### 四 . 应用 ##### [ [ APIO2018 ] Duathlon 铁人两项](https://www.luogu.org/problem/P4630) **题意:** 给定一张无向图,统计合法三元组 (s,c,f) 个数,一个三元组是合法的,当且仅当存在一条 s->f 的简单路径经过 c 。 **题解:** 1. 固定 s,f ,合法的 c 的数量即为 s,f 之间点双的点数和-2(s,f两点不算在内) 2. 建出圆方树,令val[x](圆点点权)=-1,val[y](方点点权)=cnt[y](点双点数)。 3. f[x](一个点的贡献)=val[x](点权)*road[x](经过 x 的边数)*2((s,c,f)有序) 4. 所以 ans(答案)=$\sum_{x=1}^{sum(点双个数)+n}f[x]$ 5. 求 road[x],f[x] 用 树形 DP , val[x] 用 Tarjan 即可。 6. 备注:详细的求法在代码注释中。 **代码** ```cpp #include<iostream> #include<cstdio> using namespace std; const int N=502019; typedef long long LL; int n,sum,m,tot,tot1,top,siz; LL ans; int val[N],stk[N],low[N],dfn[N],v[N],v1[N],sz[N]; struct Edge{int to,next;}e[N<<2],e1[N<<1]; // sum 是方点的编号,siz 是当前连通块的点数,e 是新图,e1 是原图 . inline void add1(int x,int y){ e1[++tot1].to=y; e1[tot1].next=v1[x]; v1[x]=tot1; } inline void add2(int x,int y){ e[++tot].to=y; e[tot].next=v[x]; v[x]=tot; } inline int read(){ // 快读 int x(0); char ch=getchar(); while(ch<'0' || ch>'9')ch=getchar(); while(ch>='0' && ch<='9')x=x*10+ch-'0',ch=getchar(); return x; } void tarjan(int x){ // Tarjan 求点双 stk[++top]=x; low[x]=dfn[x]=++siz; for(int p=v1[x];p;p=e1[p].next){ int kp=e1[p].to; if(!dfn[kp]){ tarjan(kp); low[x]=min(low[x],low[kp]); if(low[kp]>=dfn[x]){ // 连边 val[++sum]=1; int th; do{ th=stk[top--]; ++val[sum]; add2(sum,th); add2(th,sum); }while(th!=kp); add2(sum,x); add2(x,sum); } } else low[x]=min(low[x],dfn[kp]); } } void dfs(int x,int fa=0){ sz[x]=(x<=n); for(int p=v[x];p;p=e[p].next){ int kp=e[p].to; if(kp==fa)continue; dfs(kp,x); // 此处可以自己手推一下,统计答案 . // 具体来说经过 x 的路径条数(road[x])分为两部分 . // 一部分是 儿子->儿子 (Part1) , 另一部分是 父亲->儿子 Part2) . ans+=2ll*val[x]*sz[x]*sz[kp]; sz[x]+=sz[kp]; // Part1 // 举个例子,点 x 有四个儿子, size 分别为 a,b,c,d 则 // road[x]=a*b+a*c+a*d+b*c+b*d+c*d+(Part2)=a*(b+c+d)+b*(c+d)*c*d+(Part2) . } ans+=2ll*val[x]*sz[x]*(siz-sz[x]); // Part2 } int main() { sum=n=read(); m=read(); for(int x,y,i=1;i<=m;i++) x=read(),y=read(),add1(x,y),add1(y,x); for(int i=1;i<=n;i++)val[i]=-1; for(int i=1;i<=n;i++) // 核心代码 if(!dfn[i])siz=0,tarjan(i),dfs(i); printf("%lld\n",ans); return 0; } ``` ##### [[ CF Round #278 ] Tourists](https://www.luogu.org/problem/CF487E) **题意** 给定一张带权无向图,两个操作 - C a w 将点 a 的点权改为 w 。 - A a b 询问 a->b 所有合法简单路径上点权的最小值。 **题解** 1. 不考虑 C 操作。 2. A 操作可以把圆方树建出来,方点的权值为其所对应点双内点权最小值,这样就转化为一个链上最值,树剖套线段树即可。 3. 考虑 C 操作。 4. 每个方点维护一个可删堆,修改一个圆点时,把对应的方点 pop 原数,push 新数,在线段树上单点修改即可。 5. 上面的做法是对的,但是会 TLE 。因为一个圆点可能对应多个方点,比如最上面的那张图。 6. 我们可以规定只修改圆点的父亲(一定是个方点),为保证正确性,最后在 LCA 处特判一下即可。 7. 备注:细节在代码注释中。 **代码** ```cpp #include<iostream> #include<cstdio> #include<queue> using namespace std; const int N=302018,INF=2e9; int n,m,aq,sum,cnt,size,top1,tot1,tot; int v[N],v1[N],stk[N],dfn[N],low[N],fa[N],id[N],son[N],top[N],dep[N],siz[N],wt[N]; struct Edge{int to,next;}e[N<<2],e1[N<<1]; void add1(int x,int y){ e1[++tot1].to=y; e1[tot1].next=v1[x]; v1[x]=tot1; } void add(int x,int y){ e[++tot].to=y; e[tot].next=v[x]; v[x]=tot; } struct My_Heap{// 可删堆 // 思路就是维护一个加入堆(q1),一个删除堆(q2)。 priority_queue<int,vector<int>,greater<int> > q1,q2; inline void push(int x){q1.push(x);}// push 时加入到 q1 中 inline void pop(int x){q2.push(x);}// pop 时加入到 q2 中 inline int get(){ while(!q1.empty() && !q2.empty() && q1.top()==q2.top())q1.pop(),q2.pop();// 查找堆顶时 check 如果当前 q1.top()==q2.top() 就说明这个元素已经应当被删除了,所以 q1,q2 同时 pop 掉。 return q1.top(); } }w[N]; struct Tr{int l,r,dt;}tr[N<<3]; void tarjan(int x){// Tarjan 求点双 stk[++top1]=x; dfn[x]=low[x]=++size; for(int p=v1[x];p;p=e1[p].next){ int kp=e1[p].to; if(!dfn[kp]){ tarjan(kp); low[x]=min(low[x],low[kp]); if(low[kp]>=dfn[x]){ ++sum; int th;// 连边 do{ th=stk[top1--]; add(th,sum); add(sum,th); }while(th!=kp); add(x,sum); add(sum,x); } } else low[x]=min(low[x],dfn[kp]); } } // dfs1,dfs2 都是树剖的预处理 void dfs1(int x,int ff,int d){ dep[x]=d; fa[x]=ff; siz[x]=1; int mxson=-1; for(int p=v[x];p;p=e[p].next){ int kp=e[p].to; if(kp!=ff){ if(x>n)w[x].push(w[kp].get());// 方点预处理 dfs1(kp,x,d+1); if(siz[kp]>mxson) mxson=siz[kp],son[x]=kp; siz[x]+=siz[kp]; } } } void dfs2(int x,int tp){ top[x]=tp; id[x]=++cnt; wt[cnt]=w[x].get(); if(son[x])dfs2(son[x],tp); else return; for(int p=v[x];p;p=e[p].next){ int kp=e[p].to; if(kp!=fa[x] && kp!=son[x]) dfs2(kp,kp); } } // 以下为线段树求区间最小值 void updata(int k){ tr[k].dt=min(tr[k<<1].dt,tr[k<<1|1].dt); } void build(int k,int l,int r){ tr[k].l=l; tr[k].r=r; if(l==r)return tr[k].dt=wt[l],void(); int mid=(l+r)>>1; build(k<<1,l,mid); build(k<<1|1,mid+1,r); updata(k); } void change(int k,int lc,int vl){ if(tr[k].l==tr[k].r)return tr[k].dt=vl,void(); int mid=(tr[k].l+tr[k].r)>>1; if(lc<=mid)change(k<<1,lc,vl); else change(k<<1|1,lc,vl); updata(k); } int query(int k,int ql,int qr){ if(ql<=tr[k].l && qr>=tr[k].r)return tr[k].dt; int mid=(tr[k].l+tr[k].r)>>1,ans=INF; if(ql<=mid)ans=min(ans,query(k<<1,ql,qr)); if(qr>mid)ans=min(ans,query(k<<1|1,ql,qr)); return ans; } void newit(int x,int dl,int ad){// 对于一个点,点权的插入与删除 w[x].pop(dl); w[x].push(ad); change(1,id[x],w[x].get()); } void Trchange(int x,int y){// 修改点权 // for(int i=0;i<bl[x].size();i++) // newit(bl[x][i],w[x].get(),y); // 上面是 TLE 的代码,下面是 AC 的代码 newit(fa[x],w[x].get(),y); newit(x,w[x].get(),y); } int Trquery(int x,int y){ int res=INF; while(top[x]!=top[y]){ if(dep[top[x]]<dep[top[y]])swap(x,y); res=min(res,query(1,id[top[x]],id[x])); x=fa[top[x]]; } if(dep[x]>dep[y])swap(x,y); if(x>n)res=min(res,w[fa[x]].get());// 特判 LCA 是方点的情况。 // 如果 LCA 是方点,需要将 fa[LCA] 这个圆点计入答案。 // 因为方点只维护了儿子的信息,父亲属于同一个点双但是没有维护,所以这里计入答案。 return min(res,query(1,id[x],id[y])); } inline int read(){// 快读 int x(0); char ch=getchar(); while(ch<'0' || ch>'9')ch=getchar(); while(ch>='0' && ch<='9')x=x*10+ch-'0',ch=getchar(); return x; } void pr(int x){if(x>9)pr(x/10);putchar(x%10+'0');}// 快输 int main() { sum=n=read(); m=read(); aq=read(); for(int i=1;i<=n;i++)w[i].push(read()); for(int x,y,i=1;i<=m;i++) x=read(),y=read(),add1(x,y),add1(y,x); tarjan(1); dfs1(1,0,0); dfs2(1,1); build(1,1,sum); //核心代码 while(aq--){ char c; scanf("%c",&c); int x=read(),y=read(); if(c=='C')Trchange(x,y); else pr(Trquery(x,y)),putchar('\n'); } return 0; } ``` ##### 六 . 总结 圆方树的核心在于圆点和方点,只要想清楚这两者的用途,大部分题目就可以迎刃而解了。 欢迎大家指正本 blog 的错误之处。