在有向图G中,如果两个顶点vi,vj间(vi>vj)有一条从vi到vj的有向路径,同时还有一条从vj到vi的有向路径,则称两个顶点强连通(strongly connected)。如果有向图G的每两个顶点都强连通,称G是一个强连通图。有向图的极大强连通子图,称为强连通分量(strongly connected components)。
Tarjan算法基于有向图的DFS算法,每个强连通分量为搜索树中的一颗子树。连通分量中的任一点都可以作为该连通分量的根,若这一点在搜索时首先被扩展。
若u为连通分量的根节点,则u满足两个性质:
(1)u不存在路径返回它的祖先
(2)u的子树也不存在路径可以返回到u的祖先。
证明:
设u的祖先为v,若u有路径返回v,则连通分量中(搜索子树)的点都有路径到达v,则u及u的子树可以与v构成一个更大的连通分量,则u不为根。
若u的子树有路径返回v,则u沿着子树亦存在路径返回v,同上情况,所以u不为根。
故连通分量的根节点满足以上两种性质。
学习Tarjan算法的过程中搜寻了很多资料,但觉得都只是罗列了性质,而没有对这个算法分析得特别透彻,最后看了维基百科的介绍,从循环不变式的角度证明了正确性。以下翻译自维基百科,跟大家分享一下。
栈中不变式:
节点以它们被访问的次序放入栈中。当DFS递归地扩展节点v和它的后代节点时,在递归调用结束之前,这些节点不需要从栈中pop出来。不变特性为:节点扩展完成后仍保留在栈中,当且仅当该节点有一条路径可以到达栈中更早的节点。
节点v和它的后代节点扩展完毕返回时,我们知道v自身是否有一条路径到达栈中更早的节点。如果有,则返回,让v留在栈中,以保持不变式。如果没有路径,那么v必为它和栈中比它晚的节点所构成的连通分量的根节点。这些节点都有路径到达v,但不能到达比v更早的节点,因为如果存在这样的路径,那么v同样也有路径能到达更早的节点。整个分量从栈中pop出来,函数返回,继续保持栈中不变式。
记录:
每个节点v有两个值:dfn和low。dfn表示节点在dfs中被访问的次序。low表示从v可到达的最早的节点(包括v自身)的dfn。因此若v.low < v.dfn,则v一定要留在栈中,否则若v.low == v.dfn,则v一定是一个连通分量的根节点,需要从栈中移除。v.low是在从v开始的dfs过程中计算出来的。
算法描述如下:
Tarjan(G(V, E)) index := 0 S := empty for each v in V : if v.dfn not defined dfs(v)
dfs(u) u.low := index u.dfn := index index := index + 1 S.push(u) u.onStack := true for each (u, v) in E if v.dfn not defined dfs(v) u.low := min(u.low, v.low) else if(v.onStack) u.low := min(u.low, v.dfn) if u.low = u.dfn start a new strongly connected component repeat v := S.pop() v.onStack := false; add v to current strongly connected component until u == v output current strongly connected component
DFN(u)为节点u搜索的次序编号(时间戳),Low(u)为从u可达的最早的栈中节点的次序号。栈中存储的是已扩展但还未划分到强连通分量的节点。
外层循环搜索所有没访问过的节点,保证从第一个节点不可达的那些节点仍被遍历到。函数dfs对图进行深度优先搜索,找到所有从v可达的后继,输出子图的所有强连通分量。
当一个节点完成递归之后,如果它的low值仍与dfn值相等,那么它是由从栈顶到它自身所构成的强连通分量的根节点。算法将这些节点连同当前节点一起pop出来,将它们作为一盒强连通分量。
由于每个节点只访问一次进栈一次,每条边最多被考虑两次(for each语句),故算法的复杂度为O(V + |E|)。
题目链接:http://codeforces.com/contest/467/problem/D
每个单词可以经过转换到达另一个单词,使得r最少,将同义词关系转换为有向边,构建一个有向图。由于存在有向环,所以跑tarjan算法将强连通分量进行缩点。一个强连通分量中点两两之间可以互相转换,都可以用其中r最少len最小的单词代替。处理时先将单词进行离散化,抽取出其中有用的信息:r的数量和len,离散化使用map保存单词的标号。
代码如下:
#include <cstdio> #include <cstring> #include <string> #include <map> using namespace std; #define N 100005 #define min(a, b) (a) < (b) ? (a) : (b) map<string, int> str_idx; char word[5 * N], word2[5 * N]; int r_cnt[N], len[N], head[N], word_num, essay[N], edge_cnt; int stack[N], dfn[N], low[N], timestamp, top, component[N], component_number; long long ans_cnt, ans_len; struct edge{ int to, next; }E[N]; void add(int from, int to){ ++ edge_cnt; E[edge_cnt].to = to; E[edge_cnt].next = head[from]; head[from] = edge_cnt; } int read(char a[N]){ //离散化 int r = 0, l = strlen(a); for(int i = 0; i < l; i ++){ if(a[i] < 'a') a[i] += 32; if(a[i] == 'r') r ++; } map<string, int>::iterator it = str_idx.find(a); if(it != str_idx.end()) return it->second; else{ ++ word_num; len[word_num] = l; r_cnt[word_num] = r; str_idx[a] = word_num; return word_num; } } void scc(int u){ dfn[u] = low[u] = ++ timestamp; stack[++ top] = u; for(int i = head[u]; i != 0; i = E[i].next){ int v = E[i].to; if(!dfn[v]){ scc(v); low[u] = min(low[u], low[v]); } else if(!component[v]) low[u] = min(low[u], dfn[v]); if(r_cnt[u] > r_cnt[v] || (r_cnt[u] == r_cnt[v] && len[u] > len[v])) //用后代节点更新最优值 r_cnt[u] = r_cnt[v], len[u] = len[v]; } if(low[u] == dfn[u]){ component[u] = ++ component_number; do{ int idx = stack[top]; component[idx] = component_number; r_cnt[idx] = r_cnt[u], len[idx] = len[u]; //缩点,一个强连通中的点是等价的,均可取到最优值 }while(stack[top --] != u); } } int main(){ int n, m; scanf("%d", &m); for(int i = 1; i <= m; i ++){ getchar(); scanf("%s", word); essay[i] = read(word); } scanf("%d", &n); for(int i = 0; i < n; i ++){ getchar(); scanf("%s %s", word, word2); int u = read(word), v = read(word2); add(u, v); } for(int i = 1; i <= word_num; i ++) if(!dfn[i]) scc(i); for(int i = 1; i <= m; i ++) ans_cnt += r_cnt[essay[i]], ans_len += len[essay[i]]; //直接取每个单词的最优值 printf("%I64d %I64d\n", ans_cnt, ans_len); return 0; }
小结:
不存在有向环时,直接用dfs过程中的子树最优值来更新自身,因为扩展后代节点时,子树中的点对该节点来说是可达的,所以可以用子树中的最优值来更新自身,该值一定是自身能取到的最优值.
而存在环时,由于子树已经求解完毕,子树存在路径到达祖先节点,意味着可以取到祖先节点的最优值,故应该求解强连通分量来更新所有点的最优值。具体做法为:根据题意连边构造有向图后,若途中可能存在有向环,则可以先求一遍强连通,将强连通分量缩成一个点,缩点后在新图上求解问题。