P3916 图的遍历

原题

最开始简单的认为这题是一个dp问题,的确,如果题目附加条件为DAG,那么用记忆化搜索没有任何问题,但是本题没有这个条件,就是说测试点存在有环图的情况,那自然不能用记忆化。

记忆化连WA三发,我还在纳闷为什么会不对,我忘了使用dp的最基本的条件是无后效性,有环图明显不满足无后效性,他是有后效性的,就是当前状态算出来的值并不一定是最优的,也就是算出来的值并不是就在这个状态下结束了,还可能受到后面还没算的状态的影响。对于在这一题中有向有环图存在的后效性为什么会影响答案的正确型困扰了我好久,下面举一个例子:

记忆化的过程:

int dfs(int u){
	if(f[u]) return f[u];
	f[u] = u;
	
	for(int i = h[u]; i != -1; i = ne[i]){
		int j = e[i];
		f[u] = max(f[u], dfs(j));
	}
	
	return f[u];
}

对于这个图,在这个记忆化过程下从1开始的dfs顺序为:1->2->3->4->5->6...,关键问题就出在6这个位置,6希望去找到能够到达的最大序号,6只能去找2,而此时f[2]已经被赋值为2了,所以dfs(2)会直接返回f[2] ( = 2), 那自然f[6]就无法搜索到它能到达的最大值,f[6]被错误更新成了6,并不会再被修改。

这个时候考虑修改dfs过程:

int dfs(int u){
	if(f[u]) return f[u];
	
	for(int i = h[u]; i != -1; i = ne[i]){
		int j = e[i];
		f[u] = max(f[u], dfs(j));
	}
	
	f[u] = max(f[u], u);
	
	return f[u];
}

希望能通过这中搜索方式得到正确答案,但不幸的是,在遍历到6的时候,6去找2,此时f[2] = 0, 那么会从2重新开始搜索,陷入无限递归,MLE。

所以说对于有环图用记忆化,并不能保证答案的正确性,就算对了,也只是碰对了,因为对于不同的搜索方式答案就不一样了。

正解:tarjan算法求SCC,并重建图为DAG,然后用记忆化。

思路:在tarjan求SCC的过程中,每求出一个SCC,就存下这个SCC中的最大结点编号,求完所有SCC后,对于所有的SCC去重建一张图,这个图是DAG,最后在DAG上用记忆化。

复杂度:(O(N + E))

代码

#include<iostream>
#include<cstring>

using namespace std;

const int N = 100010;

int h[N], e[N], ne[N], idx;
int dfn[N], low[N], nidx;
int stk[N], top;
int instk[N];
int SCC[N], cnt;
int maxv[N];

int f[N];

int n, m;

struct edge{
    int a, b;
}edges[N];

void add(int a, int b){
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}

/*
    void init(){
        nidx = top = 0;
        memset(instk, 0, sizeof instk);
        memset(dfn, 0, sizeof dfn);
        memset(low, 0, sizeof low);
    }
*/

void tarjan(int u){
    dfn[u] = low[u] = ++ nidx;
    stk[top ++] = u;
    instk[u] = 1;
    
    for(int i = h[u]; i != -1; i = ne[i]){
        int j = e[i];
        if(!dfn[j]){
            tarjan(j);
            low[u] = min(low[u], low[j]);
        }else if(instk[j] && dfn[j] < low[u])
            low[u] = dfn[j];
    }
    
    if(low[u] == dfn[u]){
        cnt ++;
        do{
            top --;
            maxv[cnt] = max(maxv[cnt], stk[top]); // 求这个SCC的最大编号
            SCC[stk[top]] = cnt;
            instk[stk[top]] = 0;
        }while(stk[top] != u);
    }
    
}

int dfs(int u){
    if(f[u]) return f[u];
    
    f[u] = maxv[u];
    
    for(int i = h[u]; i != -1; i = ne[i]){
        int j = e[i];
        
        f[u] = max(f[u], dfs(j));
    }
    
    return f[u];
}

int main(){
    memset(h, -1, sizeof h);
    
    cin >> n >> m;
    
    for(int i = 1; i <= m; i ++){
        int a, b;
        
        cin >> a >> b;
        
        edges[i] = {a, b};
        
        add(a, b);
    }
    
    for(int i = 1; i <= n; i ++)
        if(!dfn[i]) tarjan(i); 
    
    idx = 0;
    memset(h, -1, sizeof h);
    
    // 重建图
    for(int i = 1; i <= m; i ++){
        int a = edges[i].a, b = edges[i].b;
        
        if(SCC[a] != SCC[b]) add(SCC[a], SCC[b]); // DAG有重边是没有关系的
    }
    
    for(int i = 1; i <= cnt; i ++) dfs(i);
    
    for(int i = 1; i <= n; i ++) cout << f[SCC[i]] << ' ';
    
    return 0;
}

解释一下求SCC的代码:

 for(int i = 1; i <= n; i ++)
        if(!dfn[i]) tarjan(i); // if(!SCC[i]) tarjan(i); 也可

这样写是没有问题的,因为对于一个结点,如果它已经被访问过,那么要么它在一个环中,要么它不再一个环中,这两种情况都能够确定它对应的SCC,所以只要一个结点被访问过,那么它的SCC一定已经求过了,所以只需要对没有访问到的结点求SCC就可以了。

另外,最开始在循环中我添加一个init() ,显然不对

void init(){
    nidx = top = 0; // 栈和nidx不需要清0 (1)
    memset(instk, 0, sizeof instk);// instk没有必要清0 (2)
    memset(dfn, 0, sizeof dfn); // dfn不可清0 (3)
    memset(low, 0, sizeof low);// low没有必要清0 (4)
}

nidx标识dfs序,结点数<=100000, 并且也不应该清零,虽然清零没有关系。另外每一次tarjan过后,栈必为空,也没必要,instk同理,dfn作为每一个结点是否找到包含它的SCC的标识,不可清零,low数组不用清0的原因:每次dfs时都会先覆盖low的值,其次没有找到SCC的结点它的low一定没有被赋过值。
第一次提交WA+RE是因为把dfn清零了(tarjan中要用到dfn)

综上,init是不对也是不必要的。

原文地址:https://www.cnblogs.com/tomori/p/14255602.html