无向连通图小结
无向图:桥和割点
桥的概念:无向图删去边e后分裂成两个不相连的子图
割点概念:无向图删去点v以及和v相连的所有边后分裂成两个及以上的子图
一些概念:
搜索树:在无向图中任意选择一点作为起点进行dfs,每个点访问一次,每次发生递归的边(x,y),即访问到之前没有访问到的点所经过的边,组成的搜索树
时间戳:在dfs过程中无向图中的结点被访问到的时间,用dfn数组来表示
追溯值:用low数组表示追溯值,low[x]定义为以下结点的时间戳的最小值:
1.x 子孙结点的时间戳
2.x子孙能通过非搜索树上的边所达到的点的时间戳
桥的判定法则:无向边(x,y)是桥 <==> 搜索树上存在x的一个子节点y,有dfn[x]<low[y]
推论:桥一定是搜索树中的边,一个简单环中的边一定都不是桥
割点的判定法则:如果x是root,那么如果x有两个及以上儿子,x就是割点
如果x非root,那么如果搜索树上存在x的一个子节点y,有dfs[x]<=low[y],那么x就是割点
求桥时要特别注意一下重边和父边!
下面是求桥和割点的代码
void Tarjan(int u,int pre){ low[u]=dfn[u]=++index; int son=0,pre_cnt=0;//处理重边和回边 for(int i=head[u];i!=-1;i=edge[i].nxt){ int v=edge[i].to; if(v==pre&&pre_cnt==0){ pre_cnt++;continue; } if(!dfn[v]){//如果v未被访问过 son++; Tarjan(v,u); low[u]=min(low[u],low[v]); if(dfn[u]<low[v]){//桥 bridge++; edge[i].cut=true; edge[i^1].cut=true; } if(u!=pre && dfn[u]<=low[v]){//非树根的割点 cut[u]=true; add_block[u]++; } } else low[u]=min(low[u],dfn[v]);//v被访问过了 } if(u==pre && son>1)cut[u]=true;//树根割点 if(u==pre)add_block[u]=son-1; }
无向图:双联通分量
点双联通图:无向连通图不存在割点
边双联通图:无向连通图不存在桥
点双联通分量:无向图的极大点双联通子图v-DCC
边双联通分量:无向图的极大边双联通子图e-DCC
定理
点双联通图的判定条件:
1.图的顶点数不超过2
2.图中任意两点至少包含在一个简单环中
边双联通图的判定条件:
1.图中任意一条边都包含在一个简单环中
e-DCC的求法:只要求出图中所有桥,把桥删掉后图分裂成的联通块就是e-DCC
先把桥都求出来,然后一次dfs进行染色,数组c[x]表示x所在的联通块的编号
int c[maxn],dcc; void dfs(int u){ c[u]=dcc; for(int i=head[u];i!=-1;i=edge[i].nxt){ int v=edge[i].to; if(c[v] || bridge[i])continue;//碰到桥就不往下dfs dfs(v); } } int main(){ //... dcc=0; for(int i=1;i<=n;i++) if(!c[i]){ ++dcc; dfs(i); } //... }
e-DCC的缩点法,把每个边双联通分量缩成一个点,将原图变成一棵树或森林,存在新的邻接表中
这个过程中同样可以求出每个缩点的度数
struct Edge{ int to,nxt; }edge_c[maxn<<1]; int head_c[maxn],tot_c; void init(){ memset(head_c,-1,sizeof head_c); tot_c=0; } void add_c(int u,int v){ edge_c[tot_c].to=v; edge_c[tot_c].nxt=head_c[u]; head_c[u]=tot_c++; } int main(){ //... for(int i=0;i<tot;i+=2){ int v=edge[i].to,u=edge[i^1].to;//无向图的两条边一定连在一起 if(c[u]==c[v])continue; add_c(c[u],c[v]); } //... }
v-DCC的求法:需要在Tarjan算法中维护一个栈
1.当结点x第一次被访问时,把该节点入栈
2.当dfn[x]<=low[y]条件成立时,无论x是否为根都要
a.从栈顶不断弹出结点,直到结点 y 被弹出!!!(注意是y,不用担心同一个联通分量的点会被分成两个)
b.刚才弹出的所有结点与结点x一起构成一个v-DCC
开一个vector数组dcc,dcc[i]保存编号为i的v-DCC中的所有结点
vector<int>dcc[maxn]; int cnt; void tarjan(int x){ dfn[x]=low[x]=++index; stack[++top]=x; if(x==root && head[x]==-1){//x是个孤立点 dcc[++cnt].push_back(x); return; } int flag=0; for(int i=head[x];i!=-1;i=edge[i].nxt){ int y=edge[i].to; if(!dfn[y]){ tarjan(y); low[x]=min(low[x],low[y]); if(low[y]>=dfn[x]){ flag++; if(x!=root || flag>1)cut[x]=true;//独立判断一次x是割点 cnt++; int z; do{//退栈 z=stack[top--]; dcc[cnt].push_back(z); }while(z!=y); dcc[cnt].push_back(x);//x也要进栈,但是x可能作为割点会被两个及以上联通分量包含,暂时不出栈 } } else low[x]=min(low[x],dfn[y]); } }
v-DCC的缩点
设图中有p个割点,t个v-DCC,缩点后的图有p+t个点
把每个DCC和每个割点都作为新图中的结点,并在每个割点与包含它的所有v-DCC之间连边
先给所有的割点新编号
int main(){ //... num=cnt; for(int i=1;i<=n;i++) if(cut[x])new_id[x]=++num;//给割点新编号 tot_c=0; for(int i=1;i<=cnt;i++)//枚举每个联通分量 for(int j=0;j<dcc[i].size();i++){ int x=dcc[i][j]; if(cut[x]){//割点和联通分量连边 add_c(i,new_id[x]); add_c(new_id[x],i); } else c[x]=i;//染色 } //... }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】