Tarjan Tarjan算法是一种非常实用的图论算法,可以解决连通块、割点、缩点、桥等问题。
建图 建图采用链式前向星方式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 int head[maxn]; int nxt[maxn]; int edge[maxn]; int weight[maxn]; int tot; void addEdge (int u,int v,int w) { edge[++tot] = v; weight[tot] = w; nxt[tot] = head[u]; head[u] = tot; } void traverse () { for (int i=1 ;i<=n;i++) for (int j=head[i];j;j=nxt[j]) cout<<i<<" " <<edge[j]<<endl; }
强连通分量 强连通分量,即图的一个子集。如果两个顶点可以相互通达,则称这两个点强连通(strongly connected).如果有向图的每两个顶点都强连通,则称图G为强连通图。有向图的强连通子图,称为强连通分量。
弄懂Tarjan算法需要明白最重要的两个数组:$dfn、low$.
$dfn[x]$记录每个点最早被遍历的时间,即在dfs过程中$x$的时间戳。
$low[x]$表示从$x$处搜索,能够回溯到的最早被遍历的点的时间戳 。
用$vis$数组来记录每个点的分块编号,或者换个写法表示每个点所在的连通块的根的编号也可。
使用dfs遍历图,每次访问到新节点$u$时,向栈中压入$u$,记录$dfn[u]==low[u]$,并依次访问该节点的每一个相邻节点$v$.会出现三种情况:
$dfn[v]==0$:此时$v$还没有搜索到,直接对$v$进行递归,会得到$low[v]$,故更新$low[u] = min(low[u],low[v])$
$vis[v]==0$:此时$v$早在$u$之前就已经访问过了,但是$u$的邻接点还未遍历完全,更新$low[u]=min(low[u],dfn[v])$.
$vis[v]!=0$:此时$v$已经操作完全,略过。
对于第二种情况的$v$,我们用其$dfn$值更新$u$的$low$值的原因是,从$u$处能搜到曾经搜过的$v$,那么从$v$开始就有可能存在一个强联通块包含了$u$,因此我们将$u$的low值更新,再通过回溯时对low值的更新,一步步更新回$v$点,若$low[v]$与$dfn[v]$相等,就说明从$v$开始绕了一圈又找到了$v$,也就是说找到了强连通分量。 那么如何对应地找到其中所有的节点呢?我们通过栈来实现,在dfs过程中,若遇到没搜过的点,则将其入栈,最后在$low[u]=dfn[u]$处,因为从$u$处最终能走回$u$,回溯到$u$时,途中的所有节点都在栈中,而中途可能遇到的分支都会在相同的过程中全部出栈(不在环中的自成一个强连通分量),因此从栈顶到栈中$u$的位置,中间的节点正好在一个强连通分量中,所以我们只需要不断弹栈,直到将$u$弹出栈,所有弹出的元素都归为一个强联通块中。可以证明,这样求得的强联通块是最大强联通块,将强联通块抽象成点,这便是缩点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 void Tarjan (int u) { dfn[u] = low[u] = ++tit; st.push (u); for (int i=head[u];i;i=nxt[i]) { int v = edge[i]; if (!dfn[v]){ Tarjan (v); low[u] = min (low[u],low[v]); } else if (!vis[v]) low[u] = min (low[u],dfn[v]); } if (dfn[u]==low[u]) { vis[u] = ++cnt; while (st.top ()!=u) { vis[st.top ()] = cnt; st.pop (); } st.pop (); } }
上题目校园网 缩点
割点 割点是指若将连通图的某个点及其连接的边删去后,图中的连通分量增加,则称这个点为割点。
对于点$u$,假如其从$fa$搜索而来,连接着某点$v$,若$low[v] \geq dfn[u]$,则$u$是割点。
可以画出图来理解,如果我们将$u$和其连接的边删掉,$v$和$fa$必然不属于同一个连通块。若删掉后$v$和$fa$属于同一个连通块,那么$low[v]$必然会小于$dfn[u]$。通过这一点我们可以对每个走过的点都用其$dfn$值更新$u$的$low$值,就是为了保证求出的割点一定保证$fa$和$v$不在任何同一个连通块中。
那么这时我们会考虑一个特殊的点:第一个被搜索的点,这个点没有$fa$。该如何确定这个点是不是割点呢?我们直接记录其子树数量,如果其子树数量大于1,那么就是割点,因为把它去掉,其子树不能相互到达。
于是算法模板如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 void tarjan (int u,int root) { dfn[u] = low[u] = ++tit; int childtree = 0 ; for (int i=head[u];i;i=e[i].nxt) { int v = e[i].to; if (!dfn[v]){ tarjan (v,root); low[u] = min (low[u],low[v]); if (low[v]>=dfn[u]&&u!=root){ cut[u] = true ; } if (u==root) childtree++; } else low[u] = min (low[u],dfn[v]); } if (childtree>=2 &&u==root) cut[u] = true ; }
根据上述求法,我们还可以统计出删掉割点$u$之后连通块增加的个数
对于第一个搜索的点,连通块增加的个数是$childtree-1$。
对于其他点,连通块增加的个数是$u$被判为割点的次数
1 2 3 4 5 6 7 8 9 10 if (low[v]>=dfn[u]&&u!=root) cut[u] = true ; 改为 if (low[v]>=dfn[u]&&u!=root) ++delta[u]; if (childtree>=2 &&u==root) cut[u] = true ; 改为 if (childtree>=2 &&u==root) delta[root] = childtree-1 ;
上例题 割点 嗅探器
桥 在无向图中,删去一条边,使得图的连通块数量增加,则称这条边为桥。
在$tarjan$的过程中,若$u$连接着$v$,$low[v]>dfn[u]$,则连接$u、v$的边是桥,可画图理解。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void Tarjan (int u, int fa) { dfn[u] = low[u] = ++cnt; for (int i = head[u]; i != -1 ; i = nxt[i]) { int v = edge[i]; if (dfn[v] == 0 ) { Tarjan (v, u); low[u] = min (low[u], low[v]); if (low[v] > dfn[u]) bridge[i] = bridge[i ^ 1 ] = true ; } else if (fa != v) low[u] = min (low[u], dfn[v]); } }
上例题 炸铁路
拓扑排序 在图论中,拓扑排序(Topological Sorting)是一个有向无环图(DAG,Directed Acyclic Graph)的所有顶点的线性序列。且该序列必须满足以下两个条件:
每个顶点出现且仅出现一次。
若存在一条从顶点A到顶点B的路径,则在序列中顶点A在顶点B的前面出现。
有向无环图(DAG)才有拓扑排序,非DAG图没有拓扑排序一说。根据定义可知,一个DAG图的拓扑排序也许不止一个。
直接上题 摄像头
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 #include <iostream> #include <queue> using namespace std;const int maxn = 510 ;int edge[maxn],head[maxn],nxt[maxn];int ind[maxn],outd[maxn];int has[maxn],a[maxn];int n,tot,ans;queue<int > q; void addEdge (int u,int v) { edge[++tot] = v; nxt[tot] = head[u]; head[u] = tot; } int main () { ios::sync_with_stdio (false ); cin.tie (0 ),cout.tie (0 ); cin>>n; for (int i=1 ;i<=n;i++) { int m,y; cin>>a[i]>>m; has[a[i]] = 1 ; while (m--) { cin>>y; addEdge (a[i],y); outd[a[i]]++,ind[y]++; } } for (int i=1 ;i<=n;i++) { if (ind[a[i]]==0 ) q.push (a[i]); } while (!q.empty ()) { int u=q.front (); q.pop (); if (has[u]) ans++; for (int i=head[u];i;i=nxt[i]) { int v=edge[i]; ind[v]--; if (ind[v]==0 &&has[v]==1 ) q.push (v); } } if (ans==n) cout<<"YES" <<endl; else cout<<n-ans<<endl; return 0 ; }
旅行计划