0
点赞
收藏
分享

微信扫一扫

强连通分量较难例题【受欢迎的牛 + 最大半联通子图】

秦瑟读书 2022-03-18 阅读 50
图论算法

这几道题都是 Tarjan 和其他算法的综合,难度较大,含金量较高,特别是第二题。

我的博客:关于 Tarjan 算法的入门博客。

传送门 - 板子题 B3609 题解

开始——


例题一 受欢迎的牛

传送门 - P2314 受欢迎的牛 G

思路

概述: Tarjan + 缩点 DAG + 出入度

1. Tarjan

在一个强连通分量中,很明显,所有奶牛都是互相喜欢的,所以我们分别求出每一个强连通分量,并记录下他们的 size 。

2. 缩点 DAG

我们把强连通分量看成一个超级点,并在相互之间建边。

(图片附在第 3 点中。)

缩点是什么?

如果你没接触过缩点,没有关系,缩点算法博客 + 很好的阅读体验 。

如何建边?

我们输入时存下每一条边的 u 和 v ,然后现在判断他们是否属于同一强连通分量中,如果不属于,就在他们之间建边。注意,在这之前要先清空结构体 e ,数组 hd 以及变量 cnt 。

这样,缩完点之后它就是一个 DAG ——有向无环图,我们运用的就是它不成环的特质。

但是!!!这道题不用这么麻烦!

我们在思想上把它们重新建边了,但在实现过程中,我们只需要判断两点是否在同一强连通分量里,如果没有,就进行建边之后的操作(具体见第 3 点或代码);如果有,那可以直接忽略。

缩点有什么用?

缩点之后我们可以大大简化这道题了。对于每一个超级点(也就是一个强连通分量),我们有进一步关于出度的操作了。

3. 出度的运用

接下来我们把图看成一个只有 3 个点的 DAG 。

首先放几张做了二十分钟的百万图片 (wtcl)

ps:关于图片,看不清请打开一个新标签页,放大看。由于本人第一次画图,所以丑了点…图 1 中有一条画错的边,已改正,正确边为从 E 到 G 。

原始建边

从上图不难看出,有 3 个强连通分量,我们的第一步操作为反向建边,操作之后图为这样。

图2

出度有什么作用?

它就是我们用来找出符合条件的超级点。

因为每一个超级点要被所有的点(也就是牛)所喜欢,那么它必须满足出度为 0 。

假设我们有一条边为 ( u , v ) (u, v) (u,v) ,表示从 u 到 v ,此时 u 的出度为 1 , v 的出度为 0 。

只有一个超级点出度为 0 ,也就是它没有喜欢的奶牛了,才可能是受欢迎的牛。因为此图没有环,所以它的喜欢不会回来,它也就没有受到所有牛的喜欢。

同时,若该图有两个或两个以上的出度为 0 的点,此题答案依旧为 0 。为什么?还是那句话,因为现在的 DAG 不成环。相信不用我再说,这点已经很好理解了。

为什么要反向建边?

若是正常地建边,我们在计算出度的时候会较为麻烦,所以反向建边,这样一来在统计出度的时候就可以变成“统计入度” 了,相对来说简单很多。

最终,我们的图会变成这样。

图3

这题说到这里,相信已经不用我再说什么了。(好累啊,逃)

数据范围也没有什么特别的,正常设就行。

最后,代码附上。

#include<bits/stdc++.h>
using namespace std;

const int maxn = 50005;
int n, m;
int cnt, hd[maxn];
struct node{
	int to, nxt;
}e[maxn * 2];
int dfn[maxn], low[maxn];
int top, st[maxn], de[maxn], si[maxn];
int col, co[maxn];
int tmp;

void add (int u, int v)
{
	e[++cnt].to = v;
	e[cnt].nxt = hd[u];
	hd[u] = cnt;
}

void tarjan (int u)
{
	dfn[u] = low[u] = ++tmp;
	st[++top] = u;
	for (int i = hd[u]; i; i = e[i].nxt)
	{
		int v = e[i].to;
		if (!dfn[v])
		{
			tarjan (v);
			low[u] = min (low[u], low[v]);
		}
		else if (!co[v]) low[u] = min (low[u], dfn[v]);
	}
	if (dfn[u] == low[u])
	{
		co[u] = ++col;
		++si[col];
		while (st[top] != u)
		{
			++si[col];
			co[st[top]] = col;
			--top;
		}
		--top;
	}
}

int main ()
{
	scanf ("%d %d", &n, &m);
	for (int i = 1; i <= m; i++)
	{
		int u, v;
		scanf ("%d %d", &u, &v);
		add (v, u);
	}
	for (int i = 1; i <= n; i++)
	{
		if (!dfn[i]) tarjan (i);
	}
	for (int i = 1; i <= n; i++)
	{
		for (int j = hd[i]; j; j = e[j].nxt)
		{
			int v = e[j].to;
			if (co[i] != co[v]) de[co[v]]++;
		}
	}
	int ans, u;
	ans = u = 0;
	for (int i = 1; i <= col; i++)
	{
		if (!de[i]) ans = si[col], u++;
	}
	if (u == 1) printf ("%d\n", ans);
	else printf ("0\n");
	return 0;
}

例题二 最大半联通子图

P2272 [ZJOI2007]最大半连通子图 - 传送门

题目大意

强连通分量我们都知道,这题问的是“半连通分量”——跟强连通分量差不多,半连通子图是指在有向图 G 中,在任意两点 u,v 之间有一条从 u 到 v 的路径,从 v 到 u 的路径。

这道题要我们求的就是最大半连通子图的节点数以及数量。(是的,别被那么复杂的题面骗了。)

求解最大半连通子图

PRE 概述

这道题难度较大,综合性较强,除此之外也没啥了。

主要运用三个算法:Tarjan、拓扑排序、DP。


STEP 1 Tarjan 缩点

毋庸置疑,每一个强连通分量都是一个半连通子图。所以,我们可以在求出所有强连通分量的个数以及节点数后,缩点,让图变成一个 DAG。( Tarjan 代码操作这里不说了。)

在缩点前我们还有很重要的一步——去重边

(注意:“重”念作 c h o ˊ n g ch \acute{o} ng choˊng,读错音容易影响理解。)

  • 为什么要去重边?

    为了后续使用拓扑排序,我们就需要统计入度(啊不用反向建边),那么重边就容易让入度值偏大,导致错误。

  • 怎么去重边?

    具体我们有两步预操作,实际操作是在缩点的时候一起判:

    1. 重新遍历每条边,让初始点、终点的点编号变成它所在的强连通分量的编号—— x i ← c o ( x i ) x_i \gets co(x_i) xico(xi);

    2. 给所有边排序(这样才能在缩点时判重边),排序时按初始点的编号大小,相等则按终点的标号大小,小的在前。

    代码操作如下:

     	inline bool cmp (int a, int b)
     	{
     		if (x[a] != x[b]) return x[a] < x[b];
     		return y[a] < y[b];
     	}
       
         int n[maxn];
    
     	inline void remove ()
     	{
     		for (int i = 1; i <= m; i++)
     		{
     			nu[i] = i;
     			x[i] = co[x[i]];
     			y[i] = co[y[i]];
     		}
     		sort (nu + 1, nu + m + 1, cmp);
     	}
    
    

然后就是缩点操作了。

记得清空数组变量结构体。

我们缩点前判重,然后边缩点边统计入度。

  1. 遍历每条边(已排序);

  2. 若起始点和初始点不相同(不在同一个强连通分量)且不和上一条边相同,就:

    1. 统计入度—— d e ( y ( n u i ) ) ← d e ( y ( n u i ) ) + 1 de(y(nu_i)) \gets de(y(nu_i))+1 de(y(nui))de(y(nui))+1( nu 的作用从上面代码可以看出);

    2. 建边。

代码操作如下:

inline void new_map ()
{
	cnt = 0;
	memset (hd, 0, sizeof hd);
	memset (e, 0, sizeof e);
	for (int i = 1; i <= m; i++)
	{
		int op = nu[i];
		if ((x[op] != y[op]) and (x[op] != x[nu[i - 1]] or y[op] != y[nu[i - 1]]))
		{
			de[y[op]]++;
			add (x[op], y[op]);
		}
	}
}

STEP 2 拓扑排序 + DP

对于一幅新图(还是 DAG),我们易得:

  • 求最大半连通子图的节点数:求最长链长度。

    为什么是链?

    因为它不能有分支。证明:不难知道,对于一条为半连通子图的链:

    1. 有边所连接起来的两点(此时已缩点)的边有且仅有一个方向;

    2. 整条链的各个边的方向一致(抽象)。

    既然已知这两点,那么无论分支与链的连接边的方向如何,它都不能满足与链上所有点之间都有连通路径。

    所以,我们要求的最大半连通子图,就是在 DAG 上的一条链。

  • 求最大半连通子图的个数 —— 最长链个数。

  1. 拓扑排序初始入队:

    我们分别开两个数组,dis 和 e:dis 统计长度,类似(就是) spfa 中的 dis,松弛操作更新它;ec 统计长度相等的链的个数。

    我们开一个变量 ans 表示当前最长链链顶的编号。

    初始操作和拓扑模板一样,找到入度为 0 的压入栈即可。

    代码:

	inline void init ()
	{
		for (int i = 1; i <= col; i++)
		{
			if (!de[i])
			{
				ue[++w] = i;
				dis[i] = si[i];
				ec[i] = 1;
				if (dis[ans] < dis[i]) ans = i;
			}
		}
	}
  1. 拓扑排序及递推 dis:

    主要的流程跟拓扑模板没有什么太大出入,重点是在松弛时的操作。

    松弛的时候有先后顺序:

    1. d i s v < d i s u + s i z e v dis_v < dis_u+size_v disv<disu+sizev (临时最长距离被更新,重新统计方案数):用后者松弛前者,那么就 e c v ← 0 ec_v \gets 0 ecv0 (清空以前以 v 结尾的链的个数);

    这时候你肯定会问:为什么 e c v ← 0 ec_v \gets 0 ecv0 呢,怎么会没有个数呢?

    1. 第二步,我们就更新了 e c v ec_v ecv d i s v = d i s u + s i z e v dis_v=dis_u+size_v disv=disu+sizev 统计方案数即可。

      • 若以前已经统计过以 v 结尾且长度和现在从 e 到 v 的链相等,直接在 v 的数量上加上 e 的数量;

      • 如果这是目前以 v 为结尾的链最长的一条且以前没有统计过这个长度的链呢?这时候要注意,我们的两步是并行的,所以,此时 v (简写了)的数量就等于以 e 结尾的数量。

      总的来说:两步是并行的。

    最后别忘了每次入度变为 0 的点要压入栈。

    代码:

inline void topo ()
{
  while (t < w)
  {
  	int u = ue[++t];
  	for (int i = hd[u]; i; i = e[i].nxt)
  	{
  		int v = e[i].to;
  		de[v]--;
  		if (dis[v] < dis[u] + si[v])
  		{
  			dis[v] = dis[u] + si[v];
  			ec[v] = 0;
  			if (dis[ans] < dis[v]) ans = v;
  		}
  		if (dis[v] == dis[u] + si[v]) ec[v] = (ec[v] + ec[u]) % mod;
  		if (!de[v]) ue[++w] = v;
  	} 
  }
}

最后的最后,统计一下答案总数即可。

完整代码:

#include<bits/stdc++.h>
using namespace std;

inline int read ()
{
	int x = 1, s = 0;
	char ch = getchar ();
	while (ch < '0' or ch > '9'){if (ch == '-') x = -1; ch = getchar ();}
	while (ch >= '0' and ch <= '9'){s = s * 10 + ch - '0'; ch = getchar ();}
	return x * s;
}

const int maxn = 1500005;
int n, m, mod;
int cnt, hd[maxn];
struct node{
	int nxt, to;
}e[maxn * 2];
int x[maxn], y[maxn];

inline void add (int u, int v)
{
	e[++cnt].nxt = hd[u];
	e[cnt].to = v;
	hd[u] = cnt;
}

int dfn[maxn], low[maxn], si[maxn];
int de[maxn], ue[maxn], st[maxn], co[maxn];
int col, top, tmp;

inline void tarjan (int u)
{
	dfn[u] = low[u] = ++tmp;
	st[++top] = u;
	for (int i = hd[u]; i; i = e[i].nxt)
	{
		int v = e[i].to;
		if (!dfn[v]) {tarjan (v); low[u] = min (low[u], low[v]);}
		else if (!co[v]) low[u] = min (low[u], dfn[v]);
	}
	if (dfn[u] == low[u])
	{
		co[u] = ++col, si[col]++;
		while (st[top] != u)
		{
			si[col]++;
			co[st[top]] = col;
			top--;
		}
		top--;
	}
}

int t, w;
int ans;
int ec[maxn], dis[maxn];

inline bool cmp (int a, int b)
{
	if (x[a] != x[b]) return x[a] < x[b];
	return y[a] < y[b];
}

int nu[maxn];

inline void remove ()
{
	for (int i = 1; i <= m; i++)
	{
		nu[i] = i;
		x[i] = co[x[i]];
		y[i] = co[y[i]];
	}
	sort (nu + 1, nu + m + 1, cmp);
}

inline void new_map ()
{
	cnt = 0;
	memset (hd, 0, sizeof hd);
	memset (e, 0, sizeof e);
	for (int i = 1; i <= m; i++)
	{
		int op = nu[i];
		if ((x[op] != y[op]) and (x[op] != x[nu[i - 1]] or y[op] != y[nu[i - 1]]))
		{
			de[y[op]]++;
			add (x[op], y[op]);
		}
	}
}

inline void init ()
{
	for (int i = 1; i <= col; i++)
	{
		if (!de[i])
		{
			ue[++w] = i;
			dis[i] = si[i];
			ec[i] = 1;
			if (dis[ans] < dis[i]) ans = i;
		}
	}
}

inline void topo ()
{
	while (t < w)
	{
		int u = ue[++t];
		for (int i = hd[u]; i; i = e[i].nxt)
		{
			int v = e[i].to;
			de[v]--;
			if (dis[v] < dis[u] + si[v])
			{
				dis[v] = dis[u] + si[v];
				ec[v] = 0;
				if (dis[ans] < dis[v]) ans = v;
			}
			if (dis[v] == dis[u] + si[v]) ec[v] = (ec[v] + ec[u]) % mod;
			if (!de[v]) ue[++w] = v;
		} 
	}
}

int anss;
inline void ask ()
{
	for (int i = 1; i <= n; i++)
	{
		if (dis[i] == dis[ans]) 
		{
			anss = (anss + ec[i]) % mod;
//			cout << anss << " " << ec[i] << endl;
		}
	}
}

int main ()
{
	n = read (), m = read (), mod = read ();
	for (int i = 1; i <= m; i++)
	{
		x[i] = read (), y[i] = read ();
		add (x[i], y[i]);
	}
	for (int i = 1; i <= n; i++)
	{
		if (!dfn[i]) tarjan (i);
	}
	remove ();
	new_map ();
	init ();
	topo ();
	ask ();
	printf ("%d\n%d", dis[ans], anss);
	return 0;
}

终于写完了…

—— E n d End End——

举报

相关推荐

0 条评论