強連通分量
目錄
-
基本概念
-
\(Kosaraju\)演算法
-
\(Tarjan\)演算法
-
例題講解
-
題目推薦
-
學習資源
基本概念
- 連通圖
在無向圖中,從任意點\(i\)可以到達任意點\(j\)
- 強連通圖
在有向圖中,從任意點\(i\)可以到達任意點\(j\)
- 弱連通圖(瞭解即可)
人為地將有向圖看做無向圖後,從任意點\(i\)可以到達任意點\(j\)
- 極大強連通子圖
\(G\)是一個極大強連通子圖,當且僅當\(G\)是一個強連通子圖且不存在另一個強連通子圖\(G'\),使得\(G\)是\(G'\)的真子集
- 強連通分量
有向非強連通圖的極大強連通子圖
因為來現實生活中有意義的強連通圖很少,所以一般討論的都是強連通分量
若將有向圖中的強連通分量都縮為一個點,則原圖就會變成一個DAG(有向無環圖),如下圖(1)-圖(2)所示:
圖(1)
圖(2)
來講(囉嗦 )一下,因為強連通分量相當於環啊,將環縮為點之後那就是無環圖咯,這個很好想,證明的話反證法即可
- 強連通分量的應用
-
有向圖的縮點:見上圖示
-
解決\(2-SAT\)問題(還沒學....之後更新啊qwq)
\(Kosaraju\)演算法
基於兩次\(DFS\)的有向圖強連通分量演算法,時間複雜度為\(O(n+m)\)
- 演算法框架
-
對原圖\(G\)進行\(DFS\),記錄每個節點訪問完的順序\(dfn[i]\)並將點壓入棧中
-
選擇最晚訪問完的節點對\(G\)的反向圖進行第二次\(DFS\),刪除能夠遍歷到的節點,每次遍歷到的一坨(或一個)節點構成一個強連通分量
-
一直執行\((2)\)操作,直到所有節點都二次遍歷完
- 例子圖示
第一次\(DFS\)順序:\(3->2->1->4\) (棧!)
第二次\(DFS\)順序:\(4,2->1,3\) (一個逗號前為一個強連通分量)
- 程式碼函式段
個人認為比接下來的\(Tarjan\)好理解,但是使用的更多的還是擴充套件性更強的\(Tarjan\)
inline void dfs1(int x) {
vis[x]=1;
for(register int i=1;i<=n;i++) {
if(!vis[i]&&map[x][i]) dfs1(i);
dfn[++t]=x;
}
}
inline void dfs2(int x) {
vis[x]=t;
for(register int i=1;i<=n;i++) {
if(!vis[x]&&map[i][x]) dfs2(i);
}
}
inline void ko() {
t=0;
for(register int i=1;i<=n;i++) {
if(!vis[i]) dfs1(i);
}
memset(vis,0,sizeof(vis));
t=0;
for(register int i=n;i>=1;i--) {
if(!vis[dfn[i]]) {
t++;
dfs2(dfn[i]);
}
}
}
\(Tarjan\)演算法
基於一次\(DFS\)的演算法,時間複雜度也是\(O(n+m)\)
和\(kosaraju\)演算法的\(DFS\)不同,\(Tarjan\)的\(DFS\)更類似於樹的後序遍歷
上圖理解吧:
(圖片截圖自我的學習視訊,在文章最後會貼上,侵刪!)
至於很多部落格講的四種邊(樹枝邊、前向邊、後向邊、橫叉邊),我個人認為是沒有必要掌握的,瞭解一下就可以了
- 所需變數
-
\(dfn[i]\):表示節點\(i\)的遍歷順序(同\(Kosaraju\)演算法)
-
\(low[i]\):表示節點\(i\)可回溯到的最早遍歷時間(初始與\(dfn[i]\)一致)
-
\(fir[i]=x\):表示節點\(i\)和節點\(x\)同屬於一個強連通分量
-
\(q[top]\):手寫棧qwq
5. 以及一啪啦的變數(可麻煩了)
- 演算法框架
設當前點為\(x\)
-
初始\(dfn[x]\)=\(low[x]\)=\(++ti\)(時間戳)
-
入棧當前點並標記為訪問過
-
遍歷與\(x\)相連的點,進行下一層的\(DFS\),然後更新\(low[x]\)
-
遍歷完後,如果當前\(x\)的\(dfn\)==\(low\),則可以彈出一個強連通分量了
可能有點抽象,如果不好理解,可以先跳到文末點選視訊連結,裡面講得敲詳細qwq
- 程式碼實現
以下程式碼是根據洛谷P3387 【模板】縮點 這道題編的,大家注意區分啊
另外,下面這份程式碼涉及到的拓撲排序,有興趣的可以看我的另一篇部落格
(PS:變數申請那塊奇醜...輕噴)
#include <bits/stdc++.h>
using namespace std;
int n,m,u,v,tot,ans,a[520010],in[520010],fir[520010],head[520010];
int ti,top,num,q[520010],vis[520010],dis[520010],sum[520010],dfn[520010],low[520010];
struct node {
int to,net,fro;
} e[520010],es[520010];
inline void add(int u,int v) {
e[++tot].to=v;
e[tot].fro=u;
e[tot].net=head[u];
head[u]=tot;
}
inline void tarjan(int x) {
dfn[x]=low[x]=++ti;
q[++top]=x;
vis[x]=1;
for(register int i=head[x];i;i=e[i].net) {
int v=e[i].to;
if(!dfn[v]) {
tarjan(v);
low[x]=min(low[x],low[v]);
}
else {
if(vis[v]) low[x]=min(low[x],dfn[v]);
}
}
if(low[x]==dfn[x]) {
int v=q[top];
while(top) {
fir[v]=x;
vis[v]=0;
if(v==x) {
top--;
break;
}
a[x]+=a[v];
v=q[--top];
}
}
}
inline void topo() { //拓撲排序求最長路
queue<int> q;
for(register int i=1;i<=n;i++) {
dis[i]=a[i];
if(!in[i]&&fir[i]==i) q.push(i);
}
while(!q.empty()) {
int x=q.front();
q.pop();
for(register int i=head[x];i;i=es[i].net) {
int v=es[i].to;
dis[v]=max(dis[v],dis[x]+a[v]);
if(--in[v]==0) q.push(v);
}
}
}
int main() {
scanf("%d%d",&n,&m);
for(register int i=1;i<=n;i++) scanf("%d",&a[i]);
for(register int i=1;i<=m;i++) {
scanf("%d%d",&u,&v);
add(u,v);
}
for(register int i=1;i<=n;i++) {
if(!dfn[i]) tarjan(i);
}
tot=0;
memset(vis,0,sizeof(vis));
memset(head,0,sizeof(head));
for(register int i=1;i<=m;i++) {
u=fir[e[i].fro];
v=fir[e[i].to];
if(u!=v) {
in[v]++;
es[++tot].to=v;
es[tot].net=head[u];
es[tot].fro=u;
head[u]=tot;
}
}
topo();
for(register int i=1;i<=n;i++) ans=max(ans,dis[i]);
printf("%d",ans);
return 0;
}
例題講解
洛谷P2341 [USACO03FALL][HAOI2006]受歡迎的牛 G
個人認為很模板的題,思路稍微轉換一下就出來了,寫個小題解當做例題講解qwq
- 分析
奶牛們之間的喜愛是單向的、可傳遞的,那麼將文字描述抽象一下,即:
奶牛們是點,喜愛關係是單向邊,整個關係則構成了有向圖
相互喜愛的一群牛組成一個集合,則\(N\)頭牛可以劃分為\(S\)個集合(縮點的思想,縮環為點)
集合與集合之間也存在喜愛關係,則原圖就轉化為了DAG(有向無環圖)
這個時候我們就需要思考一下:到底什麼樣的牛是所有牛喜歡的那個?
顯然:是出度為\(0\)的集合中的牛
為什麼?因為出度為\(0\)則說明這個集合不喜愛其他的牛,則滿足所有牛都喜歡這頭牛(有點繞,畫一下圖會好一點,作者懶不想畫了QAQ)
由此,我們就將問題轉換為了:縮點,然後求出度為\(0\)的點
現在給出以上思路的\(AC\)程式:
#include <bits/stdc++.h>
using namespace std;
int n,m,u,v,tot,num,out[520010],sum[520010],head[520010];
int ti,top,q[520010],val[520010],fir[520010],vis[520010],dfn[520010],low[520010];
struct node {
int to,net,fro;
} e[520010];
inline void add(int u,int v) {
e[++tot].to=v;
e[tot].fro=u;
e[tot].net=head[u];
head[u]=tot;
}
inline void tarjan(int x) {
dfn[x]=low[x]=++ti;
q[++top]=x;
vis[x]=1;
for(register int i=head[x];i;i=e[i].net) {
int v=e[i].to;
if(!dfn[v]) {
tarjan(v);
low[x]=min(low[x],low[v]);
}
else {
if(vis[v]) low[x]=min(low[x],dfn[v]);
}
}
if(low[x]==dfn[x]) {
int v=q[top];
while(top) {
fir[v]=x;
vis[v]=0;
if(v==x) {
top--;
break;
}
val[x]+=val[v];
v=q[--top];
}
}
}
int main() {
scanf("%d%d",&n,&m);
for(register int i=1;i<=n;i++) val[i]=1;
for(register int i=1;i<=m;i++) {
scanf("%d%d",&u,&v);
add(u,v);
}
for(register int i=1;i<=n;i++) {
if(!dfn[i]) tarjan(i);
}
for(register int i=1;i<=m;i++) {
u=fir[e[i].fro];
v=fir[e[i].to];
if(u!=v) out[u]++;
}
for(register int i=1;i<=n;i++) {
if(!out[i]&&fir[i]==i) {
sum[++num]=val[i];
}
}
if(num==1) printf("%d",sum[num]);
else if(num>=2) puts("0");
return 0;
}
PS:以下內容為我的無腦暴力騙分程式碼,可以跳過(52\(pts\)真香)
#include <bits/stdc++.h>
using namespace std;
int n,m,u,v,sum,flag;
int in[520010],out[520010];
int main() {
scanf("%d%d",&n,&m);
for(register int i=1;i<=m;i++) {
scanf("%d%d",&u,&v);
out[u]++;
in[v]++;
}
for(register int i=1;i<=n;i++) {
if(out[i]==0) sum++;
}
if(sum==1) printf("1");
else if(sum>=2) printf("0");
else if(sum==0){
for(register int i=1;i<=n;i++) {
if(out[i]!=in[i]) {
flag=true;
break;
}
}
if(flag==false) printf("%d",n);
}
return 0;
}
例題講解(正文)完畢~~