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

在有向图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过程中的子树最优值来更新自身,因为扩展后代节点时,子树中的点对该节点来说是可达的,所以可以用子树中的最优值来更新自身,该值一定是自身能取到的最优值.

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

你可能感兴趣的:(DFS,codeforces)