有向图强连通分量Tarjan算法+ Codeforces Round #267 (Div. 2) D.Fedor and Essay

来源:互联网 发布:电子元器件数据库ic 编辑:程序博客网 时间:2024/06/05 09:13

在有向图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 := 0S := emptyfor each v in V :if v.dfn not defineddfs(v)

dfs(u)u.low := indexu.dfn := indexindex := index + 1S.push(u)u.onStack := truefor each (u, v) in Eif v.dfn not defineddfs(v)u.low := min(u.low, v.low)else if(v.onStack)u.low := min(u.low, v.dfn)if u.low = u.dfnstart a new strongly connected componentrepeatv := S.pop()v.onStack := false;add v to current strongly connected componentuntil u == voutput 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过程中的子树最优值来更新自身,因为扩展后代节点时,子树中的点对该节点来说是可达的,所以可以用子树中的最优值来更新自身,该值一定是自身能取到的最优值.

而存在环时,由于子树已经求解完毕,子树存在路径到达祖先节点,意味着可以取到祖先节点的最优值,故应该求解强连通分量来更新所有点的最优值。具体做法为:根据题意连边构造有向图后,若途中可能存在有向环,则可以先求一遍强连通,将强连通分量缩成一个点,缩点后在新图上求解问题。

0 0
原创粉丝点击