您的位置:首页 > Web前端

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

2015-07-16 13:06 441 查看
在有向图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
, len
, head
, word_num, essay
, edge_cnt;
int stack
, dfn
, low
, timestamp, top, component
, component_number;
long long ans_cnt, ans_len;
struct edge{
int to, next;
}E
;

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
){    //离散化
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过程中的子树最优值来更新自身,因为扩展后代节点时,子树中的点对该节点来说是可达的,所以可以用子树中的最优值来更新自身,该值一定是自身能取到的最优值.
而存在环时,由于子树已经求解完毕,子树存在路径到达祖先节点,意味着可以取到祖先节点的最优值,故应该求解强连通分量来更新所有点的最优值。具体做法为:根据题意连边构造有向图后,若途中可能存在有向环,则可以先求一遍强连通,将强连通分量缩成一个点,缩点后在新图上求解问题。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: