有向圖的強連通分支及DAG
有向圖的強連通分支:在有向圖G中,如果任意兩個不同的頂點相互可達,則稱該圖是強連通的。有向圖G的極大連通子圖為G的強連通分支;
圖中每個節點只屬於一個強連通分支;
有關DAG(有向無環圖)的一些定理性質:(有時需要特殊考慮只有一個節點的時候)
1、DAG中唯一出度為0的點,一定可以由任意點出發可達。因為無環,所以從任何點出發,必然終止於一個出度為0的點;
2、DAG中,所有入度不為0的點一定可以由某個入度為0的點出發可達。因為無環,所以從入度不為0的點往回走,必然終止於一個入度為0的點;
3、在DAG中加max(n,m)(其中n為入度為0的點數,m為出度為0的點數)條邊,能使DAG變成強連通的;即為每個入度為0的點新增入邊,為每個出度為0的點添加出邊;
有向圖的強連通分支問題常通過求強連通分量,縮點,轉化為DAG問題;
求解有向圖強連通分量的Korasaju演算法:(G的轉置T和G具有相同的強連通分量)
1、深度優先遍歷G,按照dfs完成的先後順序將節點記錄下來;
2、深度優先遍歷G的轉置圖T,按照1中節點結束時間從大到小選擇為遍歷的起點,遍歷T。遍歷過程中給節點分類,每找到一個新的起點,分類就加1;
3、第二步產生的標記值相同的節點構成深度優先深林中的一棵樹,即一個強連通分量;
演算法時間複雜度為(V+E)
注意:Korasaju演算法求強連通分量得到標記點,根據原圖G對其縮點建DAG後,標記編號最大的點對應的一定是DAG中的出度為0的點,編號最小的點一定是DAG中的一個入度為0的點(特殊考慮原圖本身就是雙連通的,即縮點後只有一個點)
poj2186 Korasaju演算法;
#include<cstdio> #include<cstring> #include<cmath> #include<cstdlib> #include<climits> #include<cctype> #include<iostream> #include<algorithm> #include<queue> #include<vector> #include<map> #include<set> #include<stack> #include<string> #define ll long long #define MAX 10010 #define INF INT_MAX #define eps 1e-8 using namespace std; struct Edge{ int from,to; }; vector<Edge>edges; vector<int> G[MAX],T[MAX],A[MAX]; vector<int>S; int n; void init(){ for (int i=0; i<= n; i++){ G[i].clear(); T[i].clear(); A[i].clear(); } S.clear(); edges.clear(); } int vis[MAX],sccno[MAX],scc_cnt; void dfs1(int u){ if (vis[u]) return; vis[u] = 1; for (int i=0; i<G[u].size(); i++) dfs1(G[u][i]); S.push_back(u); //按照dfs完成的先後順序將節點儲存起來 } void dfs2(int u){ if (sccno[u]) return; sccno[u] = scc_cnt; for (int i=0; i<T[u].size(); i++) dfs2(T[u][i]); } void find_scc(){ scc_cnt = 0; memset(sccno,0,sizeof(sccno)); memset(vis,0,sizeof(vis)); for (int i=1; i<=n; i++) dfs1(i); //節點結束時間從大到小選擇為遍歷的起點,遍歷T for (int i = n-1; i>=0; i--){ if (!sccno[S[i]]) { scc_cnt++; dfs2(S[i]); } } } /*int mark[MAX]; void dfs(int s){ if (mark[s]) return; mark[s] = 1; for (int i=0; i<A[s].size(); i++){ dfs(A[s][i]); } }*/ int out[MAX]; int main(){ int m; while (scanf("%d%d",&n,&m) != EOF){ int u,v; init(); for (int i=0; i<m; i++){ scanf("%d%d",&u,&v); G[u].push_back(v); T[v].push_back(u); edges.push_back((Edge){u,v}); } find_scc(); memset(out,0,sizeof(out)); for (int i=0; i<edges.size(); i++){ //縮點:思考?如何縮點 int u = edges[i].from; int v = edges[i].to; if (sccno[u] == sccno[v]) continue; // A[sccno[v]].push_back(sccno[u]); out[sccno[u]] ++; } int k = 1,cnt = 0; for (int i=1; i<=scc_cnt; i++) if (!out[i]){ //DAG的性質1; k = i; cnt++; } // printf("k = %d\n",k); if (cnt > 1) printf("%d\n",0); else { int ans = 0; for (int i=1; i<=n; i++) if (sccno[i] == k) ans++; printf("%d\n",ans); } /* memset(mark,0,sizeof(mark)); dfs(scc_cnt); int ok = 1; for (int i=1; i<=scc_cnt; i++) if (!mark[i]) ok = 0; if (ok){ int res = 0; for (int i=1; i<=n; i++) if(sccno[i] == scc_cnt) res++; printf("%d\n",res); } else printf("0\n");*/ } return 0; }
求強連通分量的Tarjan演算法:
做一遍dfs,記錄每個節點在dfs中訪問的開始時間pre[i],顯然,pre值越小,就是越早訪問的;用一個數組low[i]記錄從i節點出發的dfs過程中,i下方的節點能達到的最早的節點開始時間。初始值為low[i] = pre[i];
dfs中,碰到哪個節點,就將那個節點入棧(無向圖的點雙連通分量是將邊入棧:因為無向圖的雙連通分量中,每個邊恰好屬於一個雙連通分量)。棧中的節點只有在其所屬的強連通分量已經全部求出來時,才會出棧。
對於u的子節點v,從v出發進行的dfs回到u時,使得low[u] = min(low[u],low[v]),因為u可達v,所以v可達的最早節點,也是u可達的;
如果一個節點u,從其出發的dfs完成時,low[u] == pre[u],則說明u可達的所有節點多不能達到比u更早的節點,此時,棧中u上方的節點,都不能達到比u更早的節點。將棧中節點彈出,一直彈到u(包括u)就構成了一個強連通分量;
注意:與Korasaju演算法不同,Tarjan演算法中如果按照彈出順序給強連通分兩編號,則根據原圖縮點建DAG後後,編號最小的節點出度一定為0,編號最大的節點入度一定為0(特殊考慮只有一個點的時候);
poj2186Tarjan演算法:
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<cmath>
#include<climits>
#include<cctype>
#include<iostream>
#include<algorithm>
#include<queue>
#include<vector>
#include<map>
#include<set>
#include<stack>
#include<string>
#define ll long long
#define MAX 10010
#define eps 1e-8
#define INF INT_MAX
using namespace std;
struct Edge{
int from, to;
};
vector<int> G[MAX],A[MAX];
vector<Edge>edges;
stack<int>S;
int n;
void init(){
for (int i=0; i<=n; i++){
G[i].clear();
A[i].clear();
}
edges.clear();
while (!S.empty()) S.pop();
}
int pre[MAX],lowlink[MAX],sccno[MAX],dfs_clock,scc_cnt;
void dfs(int u){
pre[u] = lowlink[u] = ++dfs_clock;
S.push(u);
for (int i=0; i<G[u].size(); i++){
int v =G[u][i];
if (!pre[v]){
dfs(v);
lowlink[u] = min(lowlink[u],lowlink[v]);
}
else if(!sccno[v]){
lowlink[u] = min(lowlink[u],pre[v]);
}
}
if (lowlink[u] == pre[u]){
scc_cnt++;
while (1){
int x = S.top();
S.pop();
sccno[x] = scc_cnt;
if (x == u) break;
}
}
}
void find_scc(){
dfs_clock = scc_cnt = 0;
memset(sccno,0,sizeof(sccno));
memset(pre,0,sizeof(pre));
for (int i=1; i<=n; i++){
if (!pre[i]) dfs(i);
}
}
int mark[MAX];
void judge(int u){
if (mark[u]) return;
mark[u] = 1;
for (int i=0; i<A[u].size(); i++){
judge(A[u][i]);
}
}
int main(){
int m;
while (scanf("%d%d",&n,&m) != EOF){
int u,v;
init();
for (int i=0; i<m; i++){
scanf("%d%d",&u,&v);
edges.push_back((Edge){u,v});
G[u].push_back(v);
}
find_scc();
int ans = 0;
for (int i=1; i<=n; i++){
if (sccno[i] == 1) ans++; //注意與Kosaraju演算法的不同
}
//printf("%d ",sccno[i]); printf("\n");
for (int i = 0; i<edges.size(); i++){ //此處時從入度為0得點沿又向邊逆向搜尋,看是否能到達所有點,也可以用DAG性質1直接判斷;
int x = edges[i].from;
int y = edges[i].to;
if (sccno[x] == sccno[y]) continue;
A[sccno[y]].push_back(sccno[x]); //注意這個DAG和原圖對應的DAG是相反方向的
}
memset(mark,0,sizeof(mark));
judge(1);
int ok = 1;
for (int i=1; i<=scc_cnt; i++) if (!mark[i]) ok = 0;
if (ok) printf("%d\n",ans);
else printf("%d\n",0);
}
return 0;
}
poj1236 Tarjan演算法以及DAG的一些性質;
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<cmath>
#include<climits>
#include<cctype>
#include<iostream>
#include<algorithm>
#include<queue>
#include<vector>
#include<map>
#include<set>
#include<stack>
#include<string>
#define ll long long
#define MAX 110
#define eps 1e-8
#define INF INT_MAX
using namespace std;
struct Edge{
int from, to;
};
vector<int> G[MAX],A[MAX];
vector<Edge>edges;
stack<int>S;
int n;
void init(){
for (int i=0; i<=n; i++){
G[i].clear();
A[i].clear();
}
edges.clear();
while (!S.empty()) S.pop();
}
int pre[MAX],lowlink[MAX],sccno[MAX],dfs_clock,scc_cnt;
void dfs(int u){
pre[u] = lowlink[u] = ++dfs_clock;
S.push(u);
for (int i=0; i<G[u].size(); i++){
int v =G[u][i];
if (!pre[v]){
dfs(v);
lowlink[u] = min(lowlink[u],lowlink[v]);
}
else if(!sccno[v]){
lowlink[u] = min(lowlink[u],pre[v]);
}
}
if (lowlink[u] == pre[u]){
scc_cnt++;
while (1){
int x = S.top();
S.pop();
sccno[x] = scc_cnt;
if (x == u) break;
}
}
}
void find_scc(){
dfs_clock = scc_cnt = 0;
memset(sccno,0,sizeof(sccno));
memset(pre,0,sizeof(pre));
for (int i=1; i<=n; i++){
if (!pre[i]) dfs(i);
}
}
int in[MAX],out[MAX];
int main(){
while (scanf("%d",&n) != EOF){
int t;
init();
for (int i=1; i<=n; i++){
while (scanf("%d",&t) && t != 0){
G[i].push_back(t);
edges.push_back((Edge){i,t});
}
}
find_scc();
if (scc_cnt == 1){ //注意只有一個節點是要特判一下
printf("%d\n%d\n",1,0);
continue;
}
memset(in,0,sizeof(in));
memset(out,0,sizeof(out));
for (int i=0; i<edges.size(); i++){
int x = edges[i].from;
int y = edges[i].to;
if (sccno[x] == sccno[y]) continue; //注意這個縮點、建圖將DAG只有一個節點的情況看作出度、入度都為0的
in[sccno[y]] ++;
out[sccno[x]]++;
}
int cnt1 = 0,cnt2 = 0;
for (int i=1; i<=scc_cnt; i++){
if(!in[i]) cnt1++;
if (!out[i]) cnt2++;
}
printf("%d\n%d\n",cnt1,max(cnt1,cnt2));
}
}