有向图的强联通分量

总起

目前做到的有向图的强联通分量大多是模板题,按照基本套路可以完成。对于该类题目,基本解决方法如下:

  • tarjan算法
  • 缩点、去重边、建图
  • 在新建的拓扑图中找出问题答案
  • 基础结论

知识讲解

基本概念

image

算法原理

image

基础结论

  • tarjan缩点后的图为拓扑图
  • 按照scc_cnt编号,倒序为拓扑序
  • 将一个有向图变为强连通图最少需要添加max(p,q)条边,pdin0的强联通分量的个数,qdout0的强连通分量的个数

例题

AcWing 1174. 受欢迎的牛

  • 题目描述:给定一个有向图,问有多少个点可以被除自己外的其他所有点到达。

  • 解题思路:

    • tarjan算法缩点
    • 本题无需建图,只需计算所有强连通分量的出度
    • 若出度为0的强连通分量只有一个,输出该强连通分量的Size;若出度为0的强连通分量个数2,输出0
  • 代码:

    #include <iostream>
    #include <cstdio>
    #include <cstring>
    #include <algorithm>
    
    using namespace std;
    
    const int N = 1e4 + 10, M = 5e4 + 10;
    
    int n, m;
    int h[N], e[M], ne[M], idx;
    int dfn[N], low[N], timestamp;
    int stk[N], top;
    bool in_stk[N];
    int id[N], scc_cnt, Size[N];
    int dout[N];
    
    void add(int a, int b) {
    	e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
    }
    
    void tarjan(int u)
    {
    	dfn[u] = low[u] = ++ timestamp;
    	stk[++ top] = u, in_stk[u] = true;
    	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(in_stk[j]) low[u] = min(low[u], dfn[j]);
    	}
    
    	if(dfn[u] == low[u])
    	{
    		++ scc_cnt;
    		int y;
    		do {
    			y = stk[top --];
    			in_stk[y] = false;
    			id[y] = scc_cnt;
    			Size[scc_cnt] ++;
    		} while(y != u);
    	}
    }
    
    int main()
    {
    	scanf("%d%d", &n, &m);
    	memset(h, -1, sizeof h);
    	while(m --){
    		int a, b;
    		scanf("%d%d", &a, &b);
    		add(a, b);
    	}
    
    	for(int i = 1; i <= n; i ++)
    		if(!dfn[i]) tarjan(i);
    
    	for(int i = 1; i <= n; i ++){
    		for(int j = h[i]; j != -1; j = ne[j]){
    			int k = e[j];
    			int a = id[i], b = id[k];
    			if(a != b) dout[a] ++;
    		}
    	}
    
    	int zeros = 0, sum = 0;
    	for(int i = 1; i <= scc_cnt; i ++){
    		if(!dout[i]){
    			zeros ++;
    			sum += Size[i];
    			if(zeros > 1){
    				sum = 0;
    				break;
    			}
    		}
    	}
    	printf("%d\n", sum);
    
    	return 0;
    }
    
  • 题目链接:https://www.acwing.com/problem/content/1176/

AcWing 367. 学校网络

  • 题目描述:给定一个有向图,按照图中边的方向进行信息传递,问:

    • 至少需要给多少个点发送信息才能使得所有点收到信息
    • 至少需要添加多少条边使得该图变成强连通图
  • 解题思路:

    • tarjan算法缩点
    • 本题无需建图,只需计算所有强连通分量的出度与入度
    • 问题一的答案是入度为0的强连通分量中点的个数
    • 问题二的答案是max(p,q)pdin0的强联通分量的个数,qdout0的强连通分量的个数
  • 代码:

    #include <iostream>
    #include <cstdio>
    #include <cstring>
    #include <algorithm>
    
    using namespace std;
    
    const int N = 110, M = 1e4 + 10;
    
    int n, m;
    int h[N], e[M], ne[M], idx;
    int dfn[N], low[N], timestamp;
    int stk[N], top;
    bool in_stk[N];
    int id[N], scc_cnt, Size[N];
    int dout[N], din[N];
    
    void add(int a, int b) {
    	e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
    }
    
    void tarjan(int u)
    {
    	dfn[u] = low[u] = ++ timestamp;
    	stk[++ top] = u, in_stk[u] = true;
    	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(in_stk[j]) low[u] = min(low[u], dfn[j]);
    	}
    
    	if(dfn[u] == low[u])
    	{
    		++ scc_cnt;
    		int y;
    		do {
    			y = stk[top --];
    			in_stk[y] = false;
    			id[y] = scc_cnt;
    			Size[scc_cnt] ++;
    		} while(y != u);
    	}
    }
    
    int main()
    {
    	scanf("%d", &n);
    	memset(h, -1, sizeof h);
    	for(int i = 1; i <= n; i ++){
    		int t;
    		while(scanf("%d", &t), t){
    			add(i, t);
    		}
    	}
    
    	for(int i = 1; i <= n; i ++)
    		if(!dfn[i]) tarjan(i);
    
    	for(int i = 1; i <= n; i ++){
    		for(int j = h[i]; j != -1; j = ne[j]){
    			int k = e[j];
    			int a = id[i], b = id[k];
    			if(a != b) din[b] ++, dout[a] ++;
    		}
    	}
    
    	int a = 0, b = 0;
    	for(int i = 1; i <= scc_cnt; i ++){
    		if(!din[i]) a ++;
    		if(!dout[i]) b ++;
    	}
    
    	printf("%d\n", a);
    	if(scc_cnt == 1) puts("0");
    	else printf("%d\n", max(a, b));
    
    	return 0;
    }
    
  • 题目链接:https://www.acwing.com/problem/content/369/

AcWing 1175. 最大半连通子图

  • 题目描述:给出半连通图定义,给定一个有向图G,问:

    • G的最大半连通子图拥有的节点数K
    • 不同的最大半连通子图的数目C
  • 解题思路:

    • tarjan算法缩点
    • 去重边并建图
    • 求最大半连通子图拥有的节点数等价于求最长链上的节点数,不同最大半连通子图的数目等价于求方案数
    • 在缩点后的拓扑图上进行dp,记录答案及方案数
  • 代码:

    #include <iostream>
    #include <cstdio>
    #include <cstring>
    #include <algorithm>
    #include <unordered_set>
    
    using namespace std;
    
    typedef long long ll;
    
    const int N = 1e5 + 10, M = 2e6 + 10;    //M要开二倍,因为新图最坏与原图边数相同
    
    int n, m, mod;
    int h[N], hs[N], e[M], ne[M], idx;
    int dfn[N], low[N], timestamp;
    int stk[N], top;
    bool in_stk[N];
    int id[N], scc_cnt, Size[N];
    int f[N], g[N];
    
    void add(int h[], int a, int b) {
    	e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
    }
    
    void tarjan(int u)
    {
    	dfn[u] = low[u] = ++ timestamp;
    	stk[++ top] = u, in_stk[u] = true;
    	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(in_stk[j]) low[u] = min(low[u], dfn[j]);
    	}
    
    	if(dfn[u] == low[u])
    	{
    		++ scc_cnt;
    		int y;
    		do {
    			y = stk[top --];
    			in_stk[y] = false;
    			id[y] = scc_cnt;
    			Size[scc_cnt] ++;
    		} while(y != u);
    	}
    }
    
    int main()
    {
    	scanf("%d%d%d", &n, &m, &mod);
    	memset(h, -1, sizeof h);
    	memset(hs, -1, sizeof hs);
    	while(m --){
    		int a, b;
    		scanf("%d%d", &a, &b);
    		add(h, a, b);
    	}
    
    	for(int i = 1; i <= n; i ++)
    		if(!dfn[i]) tarjan(i);
    
    	unordered_set<ll> S;
    	for(int i = 1; i <= n; i ++){
    		for(int j = h[i]; j != -1; j = ne[j]){
    			int k = e[j];
    			int a = id[i], b = id[k];
    			ll hash = a * 1000000ll + b;
    			if(a != b && !S.count(hash)){
    				add(hs, a, b);
    				S.insert(hash);
    			}
    		}
    	}
    
    	for(int i = scc_cnt; i; i --){           //scc_cnt倒序即为拓扑序
    		if(!f[i]){
    			f[i] = Size[i];
    			g[i] = 1;
    		}
    		for(int j = hs[i]; j != -1; j = ne[j]){
    			int k = e[j];
    			if(f[k] < f[i] + Size[k]){
    				f[k] = f[i] + Size[k];
    				g[k] = g[i];
    			}
    			else if(f[k] == f[i] + Size[k]) g[k] = (g[k] + g[i]) % mod;
    		}
    	}
    
    	int maxf = 0, sum = 0;
    	for(int i = 1; i <= scc_cnt; i ++){
    		if(f[i] > maxf){
    			maxf = f[i];
    			sum = g[i];
    		}
    		else if(f[i] == maxf) sum = (sum + g[i]) % mod;
    	}
    
    	printf("%d\n%d\n", maxf, sum);
    
    	return 0;
    }
    
  • 题目链接:https://www.acwing.com/problem/content/1177/

ABC 245 | F - Endless Walk

  • 题目描述:给定一个有向图,问有所少点可以不停地走下去。
  • 解题思路:
    • 建反图
    • tarjan算法缩点
    • 去重边并建图
    • 答案为Size>1的强连通分量中的点以及这些点可以走到的点
    • 不可以将din0的点加入队列进行bfs,直至走到Size>1的强连通分量,然后取补集作为答案,有反例如下:
      image
      此时id = 1的节点会被误算进答案。
  • 代码:
    #include <iostream>
    #include <cstdio>
    #include <cstring>
    #include <algorithm>
    #include <queue>
    #include <unordered_set>
    
    using namespace std;
    
    typedef long long ll;
    
    const int N = 2e5 + 10, M = 4e5 + 10;
    
    int n, m;
    int h[N], hs[N], e[M], ne[M], idx;
    int dfn[N], low[N], timestamp;
    int stk[N], top;
    bool in_stk[N];
    int id[N], scc_cnt, Size[N];
    int din[N];
    bool st[N];
    
    void add(int h[], int a, int b) {
    	e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
    }
    
    void tarjan(int u)
    {
    	dfn[u] = low[u] = ++ timestamp;
    	stk[++ top] = u, in_stk[u] = true;
    	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(in_stk[j]) low[u] = min(low[u], dfn[j]);
    	}
    
    	if(dfn[u] == low[u])
    	{
    		++ scc_cnt;
    		int y;
    		do {
    			y = stk[top --];
    			in_stk[y] = false;
    			id[y] = scc_cnt;
    			Size[scc_cnt] ++;
    		} while(y != u);
    	}
    }
    
    int main()
    {
    	scanf("%d%d", &n, &m);
    	memset(h, -1, sizeof h);
    	memset(hs, -1, sizeof h);
    	while(m --){
    		int a, b;
    		scanf("%d%d", &a, &b);
    		add(h, b, a);
    	}
    
    	for(int i = 1; i <= n; i ++)
    		if(!dfn[i]) tarjan(i);
    
    	unordered_set<ll> S;
    	for(int i = 1; i <= n; i ++){
    		for(int j = h[i]; j != -1; j = ne[j]){
    			int k = e[j];
    			int a = id[i], b = id[k];
    			ll hash = a * 1000000ll + b;
    			if(a != b && !S.count(hash)){
    				add(hs, a, b);
    				din[b] ++;
    				S.insert(hash);
    			}
    		}
    	}
    
    	int ans = 0;
    	queue<int> que;
    	for(int i = 1; i <= scc_cnt; i ++)
    		if(Size[i] > 1) {
    			que.push(i);
    			st[i] = true;
    		}
    	while(que.size()) {
    		int t = que.front();
    		que.pop();
    		ans += Size[t];
    		for(int i = hs[t]; ~ i; i = ne[i]){
    			int j = e[i];
    			if(!st[j]) {
    				que.push(j);
    				st[j] = true;
    			}
    		}
    	}
    	printf("%d\n",ans);
    	return 0;
    }
    
    
  • 题目链接:https://atcoder.jp/contests/abc245/tasks/abc245_f
posted @   小菜珠的成长之路  阅读(61)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
点击右上角即可分享
微信分享提示