点双连通分量与圆方树(大坑)
点双连通分量
割点
删去该点会使得原图不连通的点叫做割点。
一般地,删去该点集后能使得原图不连通的点集叫做割点集。
在这篇博文,我们主要讨论前者的性质。
点双连通子图和点双连通分量
不存在割点的子图叫做点双连通子图。
极大的点双连通子图叫做点双连通分量(没有特殊说明的情况下,一般简称点双)。
点双连通分量一定是点双连通子图,点双连通子图不一定是点双连通分量。
点双连通分量的性质
先说几个很简单的性质。其它参见下一个部分圆方树。
内部性质(点双连通分量的性质)
首先,点双连通分量继承了点双连通子图的性质。
- 点双连通分量中任意一对点间存在两条简单路径,使得它们经过的点并集为空集。
这很容易证明,因为如果不存在这样两条简单路径,考虑两条简单路径必须共同经过的点,这个点显然也在点双中,将它删去,则就会使这个子图不连通,与原命题矛盾。
外部性质(点双连通分量、割点与无向图的关系)
点双的外部性质则是点双连通分量特有的性质,比如(不作详细证明):
- 一个有割点的无向图必然可以由若干个点双连通分量组成。
- 点双连通分量之间的公共点一定是唯一的,且一定是割点。
- 一个割点一定连接着至少两个点双连通分量。
这些结论的应用和详细证明下面再讲。
点双连通分量的最简单应用——图的连通性问题
你有一张无向图,你可以在上面设定若干个关键点,使得任意一个点爆炸以后,每个连通块有至少一个关键点。你需要使得关键点数量尽量小,输出这个最小值和符合这个最小值的设置方案数。
source:lp3225 矿场搭建
首先,显然联通块之间不会相互影响。
先考虑没有割点的连通块。显然其中每个点爆炸了都不会对图的连通块数产生影响。所以我们在其中任意放两个关键点即可,除了只有一个点的连通块放一个点外。
然后,有割点的连通块必然由若干个点双组成,它们由割点互相连接。显然,一个点双中,不是割点的点爆炸对这个连通块没有任何影响。
如果这个点双只有一个割点连向外界,那么割点爆炸后它就会变成一个孤立的连通块,必须要挑一个非割点放置关键点。
如果这个点双有两个割点连接到外界,那么它不需要放关键点,因为它必然在爆炸后会和有关键点的点双相连(如果顺着一个割点走出去,都从来时不同的割点走出去的话,最后必然会无路可走,即走到了一个只有一个割点的点双,这也保证了原图的每个连通块内至少有一个关键点,下面讲圆方树的时候会证明)。
圆方树
引入:圆方树
我们发现,割点是点双的交界点,而且有一些结论:
- 两个相邻的点双连通分量之间有且仅有一个公共割点。
这个结论可以用反证法来证明。假若两个点双连通分量中有两个公共割点,那么,即使删去其中一个割点,也不会导致这两个点双的连通性发生改变,如果这样,那么将这两个点双合并形成的子图也是点双连通子图,不符合点双连通分量的定义,原命题得证。
- 连接一个点双连通分量内部两个点的简单路径不可能经过这个点双外部的点。
这个结论也很好证明。如果连接点双内部两个点的简单路径经过了这个点双外部的点,那么显然可以把这个点也加入点双。原命题得证。
这个结论告诉我们从一个割点出去点双就必须从这个割点回来。
由此还可以衍生出一个结论:
- 对于点双中的任意一对点,连接它们的简单路径所经过点的并集一定就是这个点双连通分量本身。
证明:首先由第一个结论,这个并集一定包含于这个点双连通分量的点集。然后证明它包含这个点双连通分量的点集。假设点双中的一对点 \((s,f)\) 不能以一条简单路径经过一个点 \(c\) ,那么考虑这条路上必须重复经过的点,显然这些点也应该在点双中,可是由于点双的性质,如果删去这个点,那就不会使点双连通分量不连通,那么这一点就不必重复经过,与原命题矛盾,所以得证。
这个结论告诉我们,假若一条路在圆方树上横穿了一个方点,那么它也可以在原图上以一条简单路径经过方点所属的所有点。
根据第二个结论,我们也许会猜想:
- 如果把每个点双当成一个点,那么把这些点按照相邻关系连接起来以后,形成的无向图会是一棵树。
但是这是不对的,因为虽然从一个割点出去一个点双就必须从这个割点回来,但是由于这个割点可能连接着第三个点双,我们可以经由这个割点到达第三个点双再重复经过这个割点回到最初的点双,由于图上删去了割点,所以它还是一条简单路径。这样就形成了一个环,因而这个图不是一棵树。 然而,根据第一个结论(两个点双间有且仅有一个公共点),我们可以考虑保留割点,这样就不可能找到一条简单路径,使得它的起点和终点相同(即成环)。
于是,一种能把无向图缩成一棵树的算法——圆方树应运而生。
圆方树是什么
圆方树是根据点双连通分量把无向图缩点所形成的树。
我们把每一个点双连通分量缩成一个“方点”,把原有的点称作“圆点”。然后我们把原图的边全部删除,让每个点向其所属的点双连通分量的“方点”连边。显然,一个割点会向多个方点连边,而由此,除了根节点以外,所有的非割点都是叶子节点。我们也知道,圆点和圆点之间不会互相连边,方点和方点之间也不会互相连边。
圆方树能干什么
作为一个将原图点双之间的关系准确表现出来的数据结构,由于点双关于简单路径、连通性的一些优秀性质,圆方树可以处理一些有关简单路径、连通性的问题。
它好就好在,既集树的性质和点双的性质于一体,又较为完整的保留了我们需要的原图的信息。下面我们就圆方树的应用讲一讲,顺便提一下圆方树的各种其他性质。
圆方树的经典应用——统计简单路径
例题一
你有一张无向图,每个点上都有一个权值,有修改和查询:修改任意一个点的权值和或统计两点间所有简单路径经过的点的并集中点权最小的那个点。
source:Codeforces 487E tourists
考虑到两点间的简单路径可能会有很多种,而他们经过的点却大多是重复的。还记得吗?我们有这样一个结论:
- 对于点双中的任意一对点,连接它们的简单路径所经过点的并集一定就是这个点双连通分量本身。
所以我们考虑以一个点双作为单位考虑。用圆方树建图以后,在图中的简单路径即在树上的简单路径,由于在树上,两点之间的简单路径是唯一的。所以我们可以在每个方点上维护其所属的点双的最小值,对于每次修改我们先暴力处理,然后对于询问直接树上跑lca即可。
但是!事情没有这么简单。
我们知道,一个圆点可以连向多个方点,那么如果暴力修改,时间复杂度是$ O(度数*\log n) $ 的,那么考虑一张类似菊花的图,一直戳修改度数巨大的菊花,就会获得TLE的好成绩。
所以我们可以考虑只修改一个圆点的父亲方点,这样可以有效减少重复统计次数,在统计的时候如果lca是个方点,那么记得统计上它的父亲圆点。这样,复杂度就被降到了 \(O(n \log n)\)。
例题二
你有一张无向图,求三元组 \((s,c,f)\) 使得 \(s,c,f\) 两两不等且存在一条简单路径满足起点是 \(s\) ,终点是 \(f\) ,且经过 \(c\) 的数量。
source:APIO2018 铁人两项
首先我们考虑对于一个固定的 \((s,f)\) ,有多少个 \(c\) 满足题目的条件。
显然,建出圆方树以后,\(s,c\) 在树上的简单路径上所经过的所有方点所属的点双上的所有圆点都对答案有贡献。然而,我们的起点,终点和割点都会被多算一次。
我们观察发现,所有的起点,终点,割点都是路径上的圆点。所以我们给点设置权值,方点的权值设为点双的 \(size\) ,圆点的权值设为 \(-1\) 。那么问题就转化为求一颗树上所有简单路径(起点和终点必须都是圆点,且起点和终点可以对调)的权值和。
我们完成了问题和圆方树的转化。但是,这个统计问题还是太复杂了(时间上)。所以我们转而统计每个点被简单路径经过的次数(即经过该点的上述简单路径的条数)\(*\) 该点的权值。
于是我们的复杂度就降到了 \(O(n)\) ,本题解决了。
同时,要注意统计路径的细节(以及注意处理森林的问题)。
圆方树的经典应用——图的连通性问题(暂咕)
我们先来考虑圆方树中叶子节点的性质。还记得之前的题吗?
代码实现
矿场搭建:
#include<cstdio>
#include<vector>
#include<cstring>
const int MaN=20005;
const int MaE=100005;
struct edge{
int to,ne;
}e[2*MaE];
int h[MaN],cnt;
void add(int a,int b){
e[++cnt]=(edge){b,h[a]};
h[a]=cnt; return ;
}
std::vector <int> bcc[MaN];
int num;
int low[MaN],dfn[MaN];
int st[MaN],con,dfnCnt;
bool iscut[MaN];
void tjw(int x,int fa){
int child=0;
dfn[x]=low[x]=++dfnCnt;
st[con++]=x;
for(int i=h[x];i;i=e[i].ne){
int v=e[i].to;
if(!dfn[v]){
++child;
tjw(v,x);
low[x]=std::min(low[v],low[x]);
if(low[v]>=dfn[x]){
iscut[x]=true;
++num;
while(st[con]!=v)
bcc[num].push_back(st[--con]);
bcc[num].push_back(x);
}
}
else if(v!=fa) low[x]=std::min(low[x],dfn[v]);
}
if(!fa&&child<=1) iscut[x]=false;
return ;
}
void init(){
memset(h,0,sizeof h);
// memset(e,0,sizeof e);
memset(st,0,sizeof st);
memset(dfn,0,sizeof dfn);
// memset(low,0,sizeof low);
memset(iscut,false,sizeof iscut);
for(int i=1;i<=num;i++) bcc[i].clear();
cnt=num=con=0;
}
int n,m;
int main(){
scanf("%d",&m);
int T=0;
do{
init();
++T;
long long ans=1;
n=0;
for(int i=1;i<=m;i++){
int a,b;
scanf("%d%d",&a,&b);
add(a,b);add(b,a);
n=std::max(n,std::max(a,b));
}
for(int i=1;i<=n;i++) if(!dfn[i]) tjw(i,0);
int anss=0;
for(int i=1;i<=num;i++){
int lscnt=0;
for(int j=0;j<bcc[i].size();j++)
if(iscut[bcc[i][j]]) ++lscnt;
if(!lscnt) ans*=bcc[i].size()*(bcc[i].size()-1ll)/2,anss+=2;
if(lscnt==1) ans*=(bcc[i].size()-1),anss+=1;
}
for(int i=1;i<=n;i++) if(!h[i]) ++anss;
printf("Case %d: %d %lld\n",T,anss,ans);
scanf("%d",&m);
}while(m!=0);
return 0;
}
[Tourists](#例题一 “Tourists”)
#include<cstdio>
#include<queue>
#include<set>
#include<cstdlib>
#include<cstring>
const int MaN=2e5,MaE=5e5;
class edgeSet{
public:
edgeSet(){memset(this,0,sizeof *this);}
struct edge{int to,ne;}e[MaE];
int h[MaN],cnt;
void add(int a,int b){
e[++cnt]=(edge){b,h[a]};
h[a]=cnt; return ;
}
}e1,e2;
int w[MaN];
std::multiset<int> s[MaN];
int n,m,q;
int st[MaN],dfn[MaN],low[MaN];
//std::priority_queue <int,vector<int>,greater<int> > q[MaN];
int dfncnt,bcccnt,con;
void tjw(int x,int fa){
st[con++]=x;
low[x]=dfn[x]=++dfncnt;
#define e e1.e
#define h e1.h
for(int i=h[x];i;i=e[i].ne){
if(e[i].to==fa) continue;
int v=e[i].to;
if(!dfn[v]){
tjw(v,x);
low[x]=std::min(low[v],low[x]);
if(low[v]>=dfn[x]){
++bcccnt;
while(st[con]!=v){
int now=st[--con];
e2.add(now,bcccnt);
e2.add(bcccnt,now);
s[bcccnt].insert(w[now]);
}
e2.add(x,bcccnt);
e2.add(bcccnt,x);
s[bcccnt].insert(w[x]);
}
}
else low[x]=std::min(low[x],dfn[v]);
}
#undef e
#undef h
return ;
}
int son[MaN],zh[MaN],top[MaN],fat[MaN],depth[MaN];
void dfs(int x,int fa){
// dfn[x]=++dfncnt;
fat[x]=fa;
if(x>n) s[x].erase(s[x].find(w[fa]));
#define e e2.e
#define h e2.h
for(int i=h[x];i;i=e[i].ne){
if(e[i].to==fa) continue;
depth[e[i].to]=depth[x]+1;
dfs(e[i].to,x);
son[x]+=son[e[i].to]+1;
zh[x]=son[zh[x]]>son[e[i].to]?zh[x]:e[i].to;
}
#undef e
#undef h
return ;
}
void rdfs(int x,int fa){
dfn[x]=++dfncnt;
#define e e2.e
#define h e2.h
top[zh[x]]=top[x];
if(zh[x]) rdfs(zh[x],x);
for(int i=h[x];i;i=e[i].ne){
if(e[i].to==fa||e[i].to==zh[x]) continue;
top[e[i].to]=e[i].to; rdfs(e[i].to,x);
}
#undef e
#undef h
}
int vals[MaN<<2];
#define mid ((l+r)>>1)
void ins(int k,int l,int r,int pos,int val){
if(l==r){
vals[k]=val;
return ;
}
if(pos<=mid) ins(k<<1,l,mid,pos,val);
else ins(k<<1|1,mid+1,r,pos,val);
vals[k]=std::min(vals[k<<1],vals[k<<1|1]);
return ;
}
int que(int k,int l,int r,int ql,int qr){
// printf("%d %d\n",ql,qr);
if(l==ql&&r==qr) return vals[k];
int ans=0x3f3f3f3f;
if(ql<=mid) ans=std::min(que(k<<1,l,mid,ql,std::min(mid,qr)),ans);
if(qr>mid) ans=std::min(que(k<<1|1,mid+1,r,std::max(ql,mid+1),qr),ans);
return ans;
}
#undef mid
void update(int x,int y){
ins(1,1,bcccnt,dfn[x],y);
if(fat[x]!=x){
s[fat[x]].erase(s[fat[x]].find(w[x]));
// printf("%d\n",*(s[fat[x]].begin()));
s[fat[x]].insert(y);
// printf("%d\n",*(s[fat[x]].begin()));
ins(1,1,bcccnt,dfn[fat[x]],*(s[fat[x]].begin()));
}
w[x]=y;
return ;
}
int query(int a,int b){
int ans=0x3f3f3f3f;
while(true){
int anca=top[a],ancb=top[b];
if(anca==ancb){
ans=std::min(ans,dfn[a]>dfn[b]?que(1,1,bcccnt,dfn[b],dfn[a]):que(1,1,bcccnt,dfn[a],dfn[b]));
a=b=(dfn[a]>dfn[b]?b:a);
break;
}
if(depth[anca]<=depth[ancb]){
ans=std::min(ans,que(1,1,bcccnt,dfn[ancb],dfn[b]));
b=fat[ancb];
}
else{
ans=std::min(ans,que(1,1,bcccnt,dfn[anca],dfn[a]));
a=fat[anca];
}
}
if(a>n) ans=std::min(ans,w[fat[a]]);
return ans;
}
bool flag=true;
void init(){
scanf("%d%d%d",&n,&m,&q);
bcccnt=n;
for(int i=1;i<=n;i++){
scanf("%d",&w[i]);
if(w[i]!=1) flag=false;
}
for(int i=1;i<=m;i++){
int a,b;
scanf("%d%d",&a,&b);
e1.add(a,b),e1.add(b,a);
}
top[1]=1;
}
int main(){
init();
tjw(1,0);
memset(dfn,0,sizeof dfn);
dfncnt=0;
dfs(1,1);
rdfs(1,1);
for(int i=1;i<=n;i++) s[i].insert(w[i]),ins(1,1,bcccnt,dfn[i],w[i]);
for(int i=1;i<=bcccnt;i++){
int now=*(s[i].begin());
ins(1,1,bcccnt,dfn[i],now);
}
char opt;
int a,b;
for(int i=1;i<=q;i++){
opt=getchar();
while(opt!='C'&&opt!='A') opt=getchar();
if(opt=='A'){
scanf("%d%d",&a,&b);
printf("%d\n",query(a,b));
}
else{
scanf("%d%d",&a,&b);
update(a,b);
}
}
return 0;
}
铁人两项
#include<cstdio>
#include<algorithm>
#include<cstring>
class edgeSet{
struct edge{int to,ne;}e[500005];
int h[200005],cnt,now;
public:
edgeSet(){memset(this,0,sizeof(edgeSet));}
void add(int a,int b){
e[++cnt]=(edge){b,h[a]};
h[a]=cnt; return ;
}
int getV(){return e[now].to;}
int setBeg(int x){return now=h[x];}
int setNxt(int i){return now=e[i].ne;}
}e1,e2;
int bcccnt,dfncnt;
int dfn[200005],low[200005];
int st[200005],con;
int val[200005];
int siz[200005],cols[200005];
void tjw(int x,int fa,int col){
cols[x]=col; ++siz[col];
dfn[x]=low[x]=++dfncnt;
st[con++]=x;
for(int i=e1.setBeg(x);i;i=e1.setNxt(i)){
if(e1.getV()==fa) continue;
int v=e1.getV();
if(!dfn[v]){
tjw(v,x,col);
low[x]=std::min(low[x],low[v]);
if(low[v]>=dfn[x]){
++bcccnt;
cols[bcccnt]=col;
while(st[con]!=v){
int now=st[--con];
e2.add(bcccnt,now);
e2.add(now,bcccnt);
++val[bcccnt];
}
e2.add(x,bcccnt);
e2.add(bcccnt,x);
++val[bcccnt];
}
}
else low[x]=std::min(low[x],dfn[v]);
}
return ;
}
int n,m;
int son[200005];
long long ans[200005];
void dfs(int x,int fa){
dfn[x]=1;
ans[x]=(siz[cols[x]])*1ll*(siz[cols[x]]-1ll)/2ll;
// printf("%d :\n",x);
for(int i=e2.setBeg(x);i;i=e2.setNxt(i)){
if(e2.getV()==fa) continue;
int v=e2.getV();
// printf("%d\n",v);
dfs(v,x);
son[x]+=son[v]+(v<=n);
ans[x]-=(son[v]*1ll+(long long)(v<=n))*(son[v]*1ll-(long long)(v>n))/2ll;
}
ans[x]-=(siz[cols[x]]-son[x]-1ll+(long long)(x>n))*(siz[cols[x]]-son[x]-2ll+(long long)(x>n))/2ll;
ans[x]*=val[x];
// printf("%d,%d\n",son[x],fa);
return ;
}
//勿把圆方树点数和原图点数搞混。
int main(){
int colcnt=0;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) val[i]=-1;
bcccnt=n;
for(int i=1;i<=m;i++){
int a,b;
scanf("%d%d",&a,&b);
e1.add(a,b); e1.add(b,a);
}
for(int i=1;i<=n;i++)
if(!dfn[i]) tjw(i,0,++colcnt);
memset(dfn,0,sizeof dfn);
for(int i=1;i<=n;i++)
if(!dfn[i]) dfs(i,0);
long long nowans=0;
for(int i=1;i<=bcccnt;i++) nowans+=ans[i];
printf("%lld\n",nowans*2ll);
return 0;
}