算法:Tarjan+dfs(最短路的都行,判连通而已)
先了解一下什么是Tarjan
Tarjan算法用于求出图中所有的强连通分量。
转自NOCOW:点击打开链接
============================================================================
在有向图G中,如果两个顶点间至少存在一条路径,称两个顶点强连通(strongly connected)。如果有向图G的每两个顶点都强连通,称G是一个强连通图。非强连通图有向图的极大强连通子图,称为强连通分量(strongly connected components)。
Tarjan算法是基于对图深度优先搜索的算法,每个强连通分量为搜索树中的一棵子树。搜索时,把当前搜索树中未处理的节点加入一个堆栈,回溯时可以判断栈顶到栈中的节点是否为一个强连通分量。
定义DFN(u)为节点u搜索的次序编号(时间戳),Low(u)为u或u的子树能够追溯到的最早的栈中节点的次序号。由定义可以得出,
Low(u)=Min { DFN(u), Low(v),(u,v)为树枝边,u为v的父节点 DFN(v),(u,v)为指向栈中节点的后向边(非横叉边) }
当DFN(u)=Low(u)时,以u为根的搜索子树上所有节点是一个强连通分量。
算法伪代码如下
tarjan(u) { DFN[u]=Low[u]=++Index // 为节点u设定次序编号和Low初值 Stack.push(u) // 将节点u压入栈中 for each (u, v) in E // 枚举每一条边 if (v is not visted) // 如果节点v未被访问过 tarjan(v) // 继续向下找 Low[u] = min(Low[u], Low[v]) else if (v in S) // 如果节点v还在栈内 Low[u] = min(Low[u], DFN[v]) if (DFN[u] == Low[u]) // 如果节点u是强连通分量的根 repeat v = S.pop // 将v退栈,为该强连通分量中一个顶点 print v until (u== v) }
接下来是对算法流程的演示。
下图中,子图{1,2,3,4}为一个强连通分量,因为顶点1,2,3,4两两可达。{5},{6}也分别是两个强连通分量。
从节点1开始DFS,把遍历到的节点加入栈中。搜索到节点u=6时,DFN[6]=LOW[6],找到了一个强连通分量。退栈到u=v为止,{6}为一个强连通分量。
返回节点5,发现DFN[5]=LOW[5],退栈后{5}为一个强连通分量。
返回节点3,继续搜索到节点4,把4加入堆栈。发现节点4向节点1有后向边,节点1还在栈中,所以LOW[4]=1。节点6已经出栈,(4,6)是横叉边,返回3,(3,4)为树枝边,所以LOW[3]=LOW[4]=1。
继续回到节点1,最后访问节点2。访问边(2,4),4还在栈中,所以LOW[2]=DFN[4]=5。返回1后,发现DFN[1]=LOW[1],把栈中节点全部取出,组成一个连通分量{1,3,4,2}。
至此,算法结束。经过该算法,求出了图中全部的三个强连通分量{1,3,4,2},{5},{6}。
可以发现,运行Tarjan算法的过程中,每个顶点都被访问了一次,且只进出了一次堆栈,每条边也只被访问了一次,所以该算法的时间复杂度为O(N+M)。
求有向图的强连通分量还有一个强有力的算法,为Kosaraju算法。Kosaraju是基于对有向图及其逆图两次DFS的方法,其时间复杂度也是 O(N+M)。与Trajan算法相比,Kosaraju算法可能会稍微更直观一些。但是Tarjan只用对原图进行一次DFS,不用建立逆图,更简洁。在实际的测试中,Tarjan算法的运行效率也比Kosaraju算法高30%左右。此外,该Tarjan算法与求无向图的双连通分量(割点、桥)的Tarjan算法也有着很深的联系。学习该Tarjan算法,也有助于深入理解求双连通分量的Tarjan算法,两者可以类比、组合理解。
求有向图的强连通分量的Tarjan算法是以其发明者Robert Tarjan命名的。Robert Tarjan还发明了求双连通分量的Tarjan算法,以及求最近公共祖先的离线Tarjan算法,在此对Tarjan表示崇高的敬意。
============================================================================
伪码详解:
Tarjan(u) { Dfn[u]=Low[u]=++Time // 为节点u设定次序编号(发现时间)和Low初值 Stack.push(u) // 将节点u压入栈中 for each (u, v) in E // 枚举每一条边 if (!Dfn[v]) // 如果节点v未被访问过,因为是按时间戳来的,就直接用时间戳判断即可 tarjan(v) // 继续向下找 Low[u] = min(Low[u], Low[v]) // 找到栈最底下的可以是强连通分量的根(因为可能有多个) else if (v in S) // 如果节点v还在栈内 Low[u] = min(Low[u], DFN[v]) // 如果在栈中找到S,说明栈内v到u一定是强连通分量,但要取最大的(栈最底下能满足的) if (DFN[u] == Low[u]) // 如果节点u是强连通分量的根 repeat v = S.pop // 将v退栈,为该强连通分量中一个顶点 print v until (u== v) }
我自己的代码(栈用STL):
stack<int> s; int num = 0; bool vis[MAXN]; void Tarjan(int r) { FF[r] = LL[r] = ++FT; //FF是Dfn,LL是Low s.push(r); //压入 vis[r] = 1; //标志 for(int i = head[r]; i; i = next[i]) //我是用链式前向星的。具体实现看我另一篇博文 { //v[i]表示以r为起点的边的v值,边(r,v) if(!FF[v[i]]) //没有访问过 { Tarjan(v[i]); //访问 LL[r] = min(LL[r], LL[v[i]]); //找到栈最底下的可以是强连通分量的根(因为可能有多个) } else if(vis[v[i]]) LL[r] = min(LL[r], FF[v[i]]); //如果在栈中找到S,说明栈内v到u一定是强连通分量,但要取最大的(栈最底下能满足的) } if(FF[r] == LL[r]) //如果是强连通的根,就输出了 { int t; num++; do { t = s.top(); s.pop(); vis[t] = 0; //标记 }while(r != t); //一直循环下去 } }
调用:
for(int i = 1; i <= N; i++) if(!FF[i]) Tarjan(i);
缩点版(将强连通分量缩成一个点):
stack<int> s; int p[MAXN]; //p[i]表示第i个节点所在的强连通分量 int num = 0; bool vis[MAXN]; void Tarjan(int r) { FF[r] = LL[r] = ++FT; //FF是Dfn,LL是Low s.push(r); //压入 vis[r] = 1; //标志 for(int i = head[r]; i; i = next[i]) //我是用链式前向星的。具体实现看我另一篇博文 { //v[i]表示以r为起点的边的v值,边(r,v) if(!FF[v[i]]) //没有访问过 { Tarjan(v[i]); //访问 LL[r] = min(LL[r], LL[v[i]]); //找到栈最底下的可以是强连通分量的根(因为可能有多个) } else if(vis[v[i]]) LL[r] = min(LL[r], FF[v[i]]); //如果在栈中找到S,说明栈内v到u一定是强连通分量,但要取最大的(栈最底下能满足的) } if(FF[r] == LL[r]) //如果是强连通的根,就输出了 { int t; num++; do { t = s.top(); s.pop(); p[t] = num; //将t点绑入第num个强连通分量中 vis[t] = 0; //标记 }while(r != t); //一直循环下去 } }
调用是一样的。建新图的话,这样:
这只是建边而已,如果要权值什么的,也可以在弄一个链式前向星(自己习惯)。因为题目不需要,所以我只建边。
m[MAXN][MAXN]; //表示新图的边 for(i = 1; i <= M; i++) m[p[u[i]]][p[v[i]]] = 1;
回归正题,本题的全部代码:
#include <iostream> #include <stack> #include <algorithm> using namespace std; //如果某个爱心天使被其他所有人或爱心天使所爱则请输出这个爱心天使是由哪些人构成的 //注意,还可以是某个爱心天使所爱(即找爱心天使是否为所有人的都可达,如果没有就输出-1,不是就不用输出) const int MAXN = 1010, MAXE = 10010; int ans = 0, nn = 0; int p[MAXN], sum[MAXN], FF[MAXN], LL[MAXN], FT = 0; int head[MAXN], u[MAXN], v[MAXE], next[MAXE]; bool m[MAXN][MAXN]; int N, M; stack<int> s; bool vis[MAXN]; void Tarjan(int r) { FF[r] = LL[r] = ++FT; s.push(r); //压入 vis[r] = 1; for(int i = head[r]; i; i = next[i]) { if(!FF[v[i]]) { Tarjan(v[i]); LL[r] = min(LL[r], LL[v[i]]); } else if(vis[v[i]]) LL[r] = min(LL[r], FF[v[i]]); } if(FF[r] == LL[r]) { int t = s.top(); s.pop(); //先取第一个,因为只要2个以上节点的强连通图 p[t] = ++nn; //为后面构图准备 sum[nn] = 1; //新图每个节点代表节点数 vis[t] = 0; //出栈标志 if(t != r) //不是本节点,即有连通的 { ans++; //建立一个爱心天使 //将这个节点绑定建立起的爱心天使 while(r != t) //一直循环下去 { t = s.top(); s.pop(); sum[nn]++; p[t] = nn; //缩点 vis[t] = 0; //标记 } } } } //缩点后再判断是否为连通图 int main() { cin >> N >> M; int i, j, k; for(i = 1; i <= M; i++) { cin >> u[i] >> v[i]; next[i]=head[u[i]]; head[u[i]]=i; } for(i = 1; i <= N; i++) if(!FF[i]) Tarjan(i); cout << ans << endl; //输出天使数目 //建边 for(i = 1; i <= M; i++) m[p[u[i]]][p[v[i]]] = 1; //Floyd,判断连通图(数据小,能过) for(k = 1; k <= nn; k++) for(i = 1; i <= nn; i++) for(j = 1; j <= nn; j++) if(i!=j && m[i][k] && m[k][j]) //此处必须2条路都通,即i->k && k->j m[i][j] = 1; int ok = 1, no = 1; for(i = 1; i <= nn; i++) { if(sum[i] == 1) continue; ok = 1; for(j = 1; j <= nn; j++) //看看每一个节点是否通i,因为前面初始化边时有本身,所以不用判断是否一条边 if(!m[j][i]) ok = 0; if(ok) { no = 0; for(j = 1; j <= N; j++) if(p[j] == i) cout << j << ' '; cout << endl; } } if(no) cout << "-1 "; return 0; }