zoukankan      html  css  js  c++  java
  • 关于有向图强连通分量的一点想法

    前言

    在 20210410 宏帆机房,gm讲强连通分量和缩点的时候,因为大家对于这个知识点的理解和想法不同,产生了激烈的争吵(诡辩)。

    本来就很蒻的zyq在听了各位巨佬的讨论后,更加不知所云,以至于露出了这样的表情
    c7e89108d8252f3fb3a938b08fd49629_75.jpg
    (还被gm看见了就很淦)
    于是 zyq 在第二天,又打开了强连通分量的 PPT , 然后把代码又打了打,在草稿纸上进行了推算,感觉加深了理解,也有了一些自己的看法和解释,如果又巨佬发现这篇 blog 有什么问题,或者有自己的看法和见解,欢迎评论鸭~

    前置芝士

    DFS,割点,割边,点双连通块,边双连通块

    点双连通块 DFS 代码

    //谢谢lym(062)大巨佬帮忙改代码
    void dfs(int u, int fa){
    	low[u] = dfn[u] = ++ cnt;
    	for(int i = head[u]; i; i = Next[i]){
    		int v = tail[i];
    		if(v == fa)continue;
    		if(!dfn[v]){
    			q.push(make_pair(u, v));
    			dfs(v, u);
    			low[u] = min(low[u], low[v]);
    			if(dfn[u] <= low[v]){
    				cntcnt ++;
    				do{
    					flagf = q.top();
    					if(flagf.first > flagf.second)
    						swap(flagf.first, flagf.second);
    					q.pop();
    					gb[cntcnt].push_back(flagf);
    				}
    				while(flagf.first != u || flagf.second != v);
    			}
    		}
    		else{
    			low[u] = min(low[u], dfn[v]); 
    			if(dfn[v] < dfn[u]){
    				q.push(make_pair(u, v));
    			}
    		}
    	}
    }
    

    边双连通块 DFS 代码

    void dfs(int u, int fa){
    	low[u] = dfn[u] = ++ cnt;
    	s.push(u);
    	for(int i = head[u]; i; i = Next[i]){
    		int v = tail[i];
    		if(v == fa)continue;
    		if(!dfn[v]){
    			dfs(v, u);
    			low[u] = min(low[u], low[v]);
    			if(dfn[u] < low[v]){
    				bridge[i] = bridge[i ^ 1] = 1;
    			}
    		}
    		else{
    			low[u] = min(low[u], dfn[v]);
    		}
    	}
    	if(dfn[u] == low[u]){
    		cntans ++;
    		++ cntnum; 
    		int flag;
    		do{
    			flag = s.top();
    			s.pop();
    			block[flag] = cntnum;
    		}
    		while(flag != u);
    	} 
    }
    

    强连通分量

    关于边

    在 gm 的 PPT 开头放了这样一张图,其中将遍历 (e(u, v)) (表示从 (u)(v) 的一条有向边)时,(dfn(v) e 0) 的情况分类讨论并下了定义

    图片1.png

    • 那么,在这三种情况当中有且只有回退边会形成环

    2.png

    • 而向前边,重复访问了 (u) 结点的某个子孙,更新值没有意义,对于该结点的讨论已经在之前完成。

      但是在后文的代码中,在dfn[v] != 0的时候,直接对于 block[v] != 0 进行了 low[u] = min(low[u], dfn[v]);这又是为何呢? 因为子孙的 dfn 值是不可能大于(u),故无需分类讨论,直接比 min 即可。

    • 对于横插边
      4.png

      (u) 结点和 (v) 结点,两结点互无关系,两节点不可能在同一环内,也无法从 (v) 遍历到 (u)

      证明:如果可以从 (v) 遍历到 (u) , 那么 (v) 结点一定是 u 结点的祖先,与两结点互无关系相违背。

      所以两个结点并不处于同一强连通子图。

    关于 low 值的定义和解释

    在听完各位巨佬的争辩后,蒟蒻 zyq 对于 low 值的定义一头雾水,虽然现在基本忘记了各位巨佬在争什么了

    在 gm 的 ppt 上对于 low 值的定义是这样的

    从本质上讲,low参数是用来告诉我们从u出发,能形成环的“最高辈份”祖先结点是谁。

    个人觉得:即使在有向图中,加入了上面的三种边的概念后依旧是没有问题的。

    • 对于回退边 (v) 本来就是 (u) 的祖先 'low[u] = min(low[u], dfn[v]);' 没有问题
    • 对于向前边,前文已经解释了 low[u] = min(low[u], dfn[v]); 并不会更新准确的值,只是一个简化的写法。
    • 对于横插边,因为'v' 与 'u' 没有关系,所以只需要在特判排除。又因为两个点不在同一棵子树中,两点不可能有环,也不可能在同一强连通子图中,只需要if(!block[v])判断即可。

    所以 zyq 上课的时候为什么会对 low值产生疑问,可能跟 zyq 是个大SB 有关

    最后贴上一道 “很板” 的强连通分量 + 缩点的代码

    「USACO 2003 Fall」受欢迎的牛

    #include <stack>
    #include <cmath>
    #include <cstdio>
    #include <cstring>
    #include <algorithm>
    using namespace std;
    const int maxn = 1e4 + 5;
    const int maxm = 1e5 + 5;
    
    int n, m;
    int head[maxn], tail[maxm], Next[maxm];
    int tot = 1;
    int low[maxn], dfn[maxn];
    int cnt;
    bool vis[maxn];
    int cntnum;
    int block[maxn];
    int head2[maxn], tail2[maxm], Next2[maxm];
    bool ib[maxn];   //is begin 
    bool vvis[maxn][maxn];
    
    stack<int> s;
    
    void add(int x, int y){
    	tot ++;
    	tail[tot] = y;
    	Next[tot] = head[x];
    	head[x] = tot;
    }
    
    void add2(int x, int y){
    	tot ++;
    	tail2[tot] = y;
    	Next2[tot] = head2[x];
    	head2[x] = tot;
    }
    void dfs(int u, int fa){
    	low[u] = dfn[u] = ++cnt;   
    	s.push(u);
    	vis[u] = 1;
    	for(int i = head[u]; i; i = Next[i]){
    		int v = tail[i];
    		if(!dfn[v]){   //这个子节点没有访问 
    			dfs(v, u);
    			low[u] = min(low[u], low[v]);  //回溯 
    		}
    		else if(!block[v]){   //这个结点没有被分入任意一个强连通子图 
    			low[u] = min(low[u], dfn[v]);
    		}
    	}
    	if(low[u] == dfn[u]){
    		cntnum ++;
    		int x;
    		do{
    			x = s.top();
    			s.pop();
    			block[x] = cntnum;
    		}
    		while(x != u); 
    	}
    }
    int main() {
    	scanf("%d %d", &n, &m);
    	int x, y;
    	for(int i = 1; i <= m; i ++){
    		scanf("%d %d", &x, &y);
    		ib[x] = 1;   //可以从这个点出发至其他点 
    		add(x, y); 
    	}
    	for(int i = 1; i <= n; i ++){
    		if(!dfn[i] && ib[i]){
    			dfs(i, 0);   
    		}
    	}
    	tot = 0;
    	for(int i = 1; i <= n; i ++){
    		for(int j = head[i]; j; j = Next[j]){
    			int v = tail[j];
    			if(block[v] == block[i] || vvis[block[i]][block[v]])continue;
    			add2(block[i], block[v]);
    			vvis[block[i]][block[v]] = 1;
    		}
    	}
    	for(int i = 1; i <= n; i ++){
    		if(!dfn[i]){  //在所有点都遍历完成后,依旧没有遍历到,说明这个点与其它任何点都没有连接 
    			printf("0");
    			return 0;
    		} 
    	}
    	int sum = 0;
    	int side = 0;
    	for(int i = 1; i <= cntnum; i ++){
    		if(!head2[i]){
    			sum ++;
    			side = i;
    		}
    	}
    	int ans = 0;
    	if(sum != 1){
    		printf("0");
    		return 0;
    	}
    	for(int i = 1; i <= n; i ++){
    		if(block[i] == side){
    			ans ++;
    		}
    	}
    	printf("%d", ans);
    	return 0;
    }
    

    结语

    笔者已经家长叫笔者搞 whk 的声音了,可能明天会做一些没有那么板的题,886~

    夜空中最亮的星,请照亮我前行
  • 相关阅读:
    新年来了,却似乎没有新年的迹象和心情
    XHTC最新进展
    有件事很是头疼
    完善中的HTML版ProcessBuilder
    20041114 Bindows
    想不到这么快就申请成功了,多年未曾体会的激动仿佛又重返我身了
    有人需要这样的HTML Menu组件吗?
    告知所有需要XHTC和ProcessBuilder的朋友
    醉里插花花莫笑,可怜人似春将老
    Flex创建一个UI component的全过程
  • 原文地址:https://www.cnblogs.com/Nefelibata/p/14643313.html
Copyright © 2011-2022 走看看