Tarjan算法(强连通分量)与缩点

简介

Tarjan算法可以求出一个有向图中的强连通分量的个数,同时还可以将强连通分量改造为一个点,也就是所谓的“缩点”。

前置知识

强连通分量

强连通分量(或者极大连通子图),指的是在一个子图中,所有点能够两两互达,且再加入新的点时将破坏连通性。特别要注意的是,一个点可以被认为是一个强连通分量。

dfs过程产生的边

在tarjan算法的dfs过程中,会产生以下四种边(默认x->y):

  • 树枝边:x是y的父节点
  • 前向边:x是y的祖先结点
  • 后向边:y是x的孩子节点
  • 横叉边:连接到其他连通分量的边

算法

时间戳

Tarjan算法引入了“时间戳”的概念。具体来讲,在dfs每一个点时,将会赋予每一个点一个“时间点”,也就是维护dfn。其次,维护一个low,表示当前这个点和它的子树返祖边和横插边能连到的还没出栈的dfn最小的点。

过程

在dfs的过程中维护一个栈,每个点第一次访问时就将其加入栈中。当这个点的所有后继节点遍历完毕时,如果这个点的dfn==low,则这个点是这个强连通分量里面dfs树的最高的点。于是,可以将栈中的点一个一个出栈,直到当前这个点也出栈为止。

在遍历一个点的子树过程中,也需要判断后继顶点是否遍历过。简单来说,当后继顶点遍历过(而且在栈里面时),则将这个点的low和后继顶点的low取最小,否则直接dfs下去,再更新low

代码实现

vector<int> edge[N];
int low[N], dfn[N], timestamp = 0, scc_cnt = 0, id[N] = 0;
bool inst[N];
stack<int> st;
void tarjin(int u) {
    dfn[u] = low[u] = ++ timestamp;
    st.push(u);
    inst[u] = true;
    for (auto v : edge[u]) {
        if (!dfn[v]) {
            trajin(v);
            low[u] = min(low[u], low[v]);
        } else if (inst[v]) {
            low[u] = min(low[u], low[v]);
        }
    }
    if (dfn[u] == low[u]) {
        int v = 0;
        ++ scc_cnt;
        do {
            v = st.top();
            st.pop();
            inst[v] = false;
            id[v] = scc_cnt;
            /*
            * 这里是还有一些其他条件的时候
            */
        } while (v != u);
    }
}
void solve() {
	/*
	* 其他的代码
	*/
    // 考虑到图其实不一定是一个连通图
    for (int i = 1; i <= n; ++ i)
        if (!dfn[i])
            trajan(i);
}

缩点

在上面的代码中,实际上tarjan算法也创建了一个新的图,这个图中每一个顶点都是原来图中的强连通分量。也就是说,现在这个新的图不再存在回路,是一个DAG。因此,这个新的图可以执行原先不能的操作,比如拓扑排序等等。这样的操作就可以叫做缩点。

缩点的应用

例题 P2746 [USACO5.3]校园网Network of Schools

[题目链接]([P2746 USACO5.3]校园网Network of Schools - 洛谷 | 计算机科学教育新生态 (luogu.com.cn))

一个一个问题分解看待。

  • 对于A问题,求这个网络中至少需要接受几个新的软件副本。那么如果根据拓扑序的定义的话,遍历完一张图只需要从其入度为0的点开始遍历即可。
  • 对于B问题,求至少连多少边,只需要一个副本就可以完成任务。还是考虑拓扑排序。如果能够把入度为0的点和出度为0的点相连的话,那么是不是意味着这两个图是连通的。当入度(或者出度)为0的点在之前的步骤之后还有多余的话,那么是不是意味着这几个点相连的话就可以实现。因此,这个问题的答案就是入度为0的点的个数和出度为0的点的个数中的最大值。

从上面的分析可以得到,我们需要的的是一个DAG图。但是这个图并不是一个DAG图。因此需要使用tarjan算法求缩点,将图改造为一个DAG图。

#include <bits/stdc++.h>
using namespace std;
#define endl '\n'

const int N = 110;
vector<int> edge[N];
void add(int from, int to) {
	edge[from].push_back(to);
}
int n;

int low[N], dfn[N], step, scc, id[N];
bool inst[N];
stack<int> st;
int ind[N], outd[N];

void tarjan(int u) {
	dfn[u] = low[u] = ++ step;
	st.push(u);
	inst[u] = true;
	for (auto v : edge[u]) {
		if (!dfn[v]) {
			tarjan(v);
			low[u] = min(low[v], low[u]);
		} else if (inst[v]) {
			low[u] = min(low[v], low[u]);
		}
	}
	if (dfn[u] == low[u]) {
		int v = 0;
		++ scc;
		do {
			v = st.top();
			st.pop();
			inst[v] = false;
			id[v] = scc;
		} while (v != u);
	}
}

int main() {
	std::cin.tie(nullptr)->sync_with_stdio(false);
	cin >> n;
	for (int i = 1; i <= n; ++ i) {
		int v;
		while (cin >> v && v) {
			add(i, v);
		}
	}
	for (int i = 1; i <= n; ++ i) {
		if (!dfn[i]) {
			tarjan(i);
		}
	}
	for (int i = 1; i <= n; ++ i) {
		for (auto j : edge[i]) {
			if (id[j] != id[i]) {
				ind[id[j]] ++;
				outd[id[i]] ++;
			}
		}
	}

	if (scc == 1) {
		cout << 1 << endl;
		cout << 0 << endl;
		return 0;
	}

	int in = 0, out = 0;
	for (int i = 1; i <= scc; ++ i) {
		in += ind[i] == 0;
		out += outd[i] == 0;
	}
	cout << in << endl;
	cout << max(in, out) << endl;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值