算法:强连通分量缩点

有时对于一个有向图我们及其渴望将其变为一个有向无环图,这样我们就要用到强连通分量缩点了。

例题

洛谷3387 缩点

题目背景
缩点+DP。

题目描述
给定一个 n个点 m条边有向图,每个点有一个权值,求一条路径,使路径经过的点权值之和最大。你只需要求出这个权值和。

允许多次经过一条边或者一个点,但是,重复经过的点,权值只计算一次。

输入格式
第一行两个正整数n,m。
第二行n个整数,依次代表点权。
第三至(m + 2)行,每行两个整数u,v,表示一条u -> v的有向边。

输出格式
共一行,最大的点权之和。

输入输出样例

输入

2 2
1 1
1 2
2 1

输出

2

说明/提示
对于 100%的数据,1 <= n <= 10^4,1 <= m <= 10^5,0 <= 点权 <= 10^3。

强连通分量缩点

对于一道图论题,有时我们会发现如果说这是一个有向无环图会很好解决,但题目中却并没有说无环,这时我们希望将这个有向图变成一个有向无环图,这就要用到强连通分量缩点了。

在一个强连通分量中,我们知道任意两个点可以互相到达,那么其实我们就可以利用这个特点去对其进行缩点,将原图变成一个有向无环图。

在做tarjan算法时,我们已经对每个点进行了染色,所以这样缩点就很简单了,如果一条边起点u和终点v的颜色不一样,就以u的颜色color[u]为起点、v的颜色color[v]为终点建一条边。

竞赛中,我们为了不把原来的图和新的图搞混,往往不会建两个图,而是先以边表的形式储存一下原图,之后等跑完tarjan后清空邻接表再重新按原图的边表加边,这样就不容易把原来的图和新的图搞混了。还有记住原图和新图中点的数目是不一样的,务必不要忘记。

最后再说一下这道题,先缩点,之后有三种方式解决:

  1. 拓扑排序:按照拓扑序进行dp。

  2. 记忆化搜索:直接暴力深搜,再加个记忆化。

  3. 最长路:spfa跑一个最长路。

这里我就用思路最简单的拓扑排序来写了。

最后算一下算法时间复杂度:我们发现其中就是一个tarjan和一个拓扑排序,综合一下也就O(n + m)级别了。

代码

# include 
# include 
# include 
# include 
# include 
# include 
# include 

using namespace std;

const int N_MAX = 10000, M_MAX = 100000;

int n, m;
int u[M_MAX + 10], v[M_MAX + 10];
int a[N_MAX + 10]; // a[i]表示小点i的权值
vector <int> g[N_MAX + 10];

// 一大堆tarjan算法所用到的变量
int now, dfn[N_MAX + 10], low[N_MAX + 10];
bool ins[N_MAX + 10];
stack <int> s;
int color[N_MAX + 10], cnt[N_MAX + 10];

int in[N_MAX + 10];
queue <int> q;
int w[N_MAX + 10], dp[N_MAX + 10]; // w[i]表示大点i的权值

void addEdge(int x, int y)
{
	g[x].push_back(y);
}

void tarjan(int x)
{
	dfn[x] = low[x] = ++now;
	ins[x] = true;
	s.push(x);
	for (int i = 0; i < (int) g[x].size(); i++) {
		int y = g[x][i];
		if (dfn[y] == 0) tarjan(y), low[x] = min(low[x], low[y]);
		else if (ins[y]) low[x] = min(low[x], dfn[y]);
	}
	if (low[x] != dfn[x]) return;
	color[x] = ++color[0];
	cnt[color[0]]++;
	while (s.top() != x) {
		int y = s.top();
		color[y] = color[0];
		ins[y] = false;
		s.pop();
		cnt[color[0]]++;
	}
	ins[x] = false;
	s.pop();
}

void resetPoint()
{
	for (int i = 1; i <= n; i++)
		if (dfn[i] == 0) tarjan(i);
	memset(g, 0, sizeof(g)); // 重置所有的边
	for (int i = 1; i <= m; i++) {
		int x = color[u[i]], y = color[v[i]];
		if (x != y) addEdge(x, y);
	}
}

int topSort()
{
	for (int x = 1; x <= color[0]; x++)
		for (int i = 0; i < (int) g[x].size(); i++)
			in[g[x][i]]++;
	for (int i = 1; i <= color[0]; i++)
		if (in[i] == 0) dp[i] = w[i], q.push(i);
	int ans = 0;
	while (!q.empty()) {
		int x = q.front();
		q.pop();
		ans = max(ans, dp[x]);
		for (int i = 0; i < (int) g[x].size(); i++) {
			int y = g[x][i];
			dp[y] = max(dp[y], dp[x] + w[y]);
			if (--in[y] == 0) q.push(y);
		}
	}
	return ans;
}

int main()
{
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= n; i++)
		scanf("%d", &a[i]);
	for (int i = 1; i <= m; i++) {
		scanf("%d%d", &u[i], &v[i]);
		addEdge(u[i], v[i]);
	}
	resetPoint();
	for (int i = 1; i <= n; i++)
		w[color[i]] += a[i];
	printf("%d\n", topSort());
	return 0;
}

你可能感兴趣的:(算法)