强连通分量-缩点

初学只需要背代码就好了,而复习的时候要考虑的就多了(

(打牛客的时候用到,发现忘得差不多了)

概念解读

  • 连通:在无向图中,从任意点 A 都可以到达任意点 B
  • 强连通:在有向图中,从任意点 A 都可以到达任意点 B
  • 弱连通:将有向图看作无向图后,从任意点 A 都可以到达任意点 B

特殊地,单独的点也可以看作强连通分量。

说白了,强连通分量就是环。

而用于求强连通分量的算法,就是大名鼎鼎的 tarjan!

image

而强连通分量最大的应用,则是求出环之后进行缩点。

模板

首先分析模板为什么能用缩点来做

这不显然吗题目就叫缩点

由于每个点的点权只计算一次,所以一个点重复走对于答案的贡献没有影响。

那么,如果走到了环上的一点,则走完环上的所有点一定会是最有答案的一部分。

所以就可以找到所有的强连通分量,然后把强连通分量缩成点,然后构造新图。

而构造出的新图,绝对是一个 DAG(有向无环图)!

然后就可以用拓扑去解决这个 DAG

tarjan

那么,求强连通分量的 tarjan 到底是怎样实现的呢。

众所周知,对树进行 dfs 的时候,有一个名为时间戳的神奇东西,他可以记录 dfs 时的顺序。

而对图进行 dfs 时,则会出现一下几个概念:

  • 树边:属于 dfs 生成树的边。
  • 非树边:不属于生成树的边,分为:
    • 返祖边:在 dfs 时指向祖先节点的边。
    • 前向边:在 dfs 时指向子树中节点的边。
    • 横叉边:在 dfs 时指向已经访问过的点,但不是祖先节点。

而我们求强连通分量,实际上就是找环。

dfs 中,我们用栈来储存目前搜到但没有构成强连通分量的。

tarjan 除了时间戳,还引入了另一个变量:回溯值 low

\(low_u\):在 \(u\) 的子树中能够回溯到的最早的已经在栈中的结点。

显然,环的组成是部分树边和一条返祖边,前向边和横叉边则不会成环。

同样在 dfs 时,只有树边和返祖边会影响 low

low 的初始值均为当前节点的时间戳。

  1. 分析树边:

    当前节点向下发出的树边连接的即为子节点。首先继续 dfs,到回溯时通过与子节点的 low 值取较小值更新当前节点的 low。此时当前节点的 low 满足定义。

  2. 分析回溯边:

    当前节点若发出回溯边,则必定指向自己的祖先,根据 low 的定义,当前节点的 low 即可根据回溯的祖先节点的时间戳更新。

而当分析完所有发出的边后,若时间戳与 low 相等,则说明子树中没有节点能到达更早的在栈中的节点,当前节点即可作为强连通分量的根,将栈中的节点弹出,作为一个强连通分量储存。

代码:

void Tarjan(int now)
{
	in[now]=1;h.push(now);
	dfn[now]=low[now]=++dfns;
	for(int i=fir[now];i;i=nex[i])
	{
		int p=poi[i];
		if(!dfn[p])
		{//没有 dfs 过,属于树边
			Tarjan(p);
			low[now]=min(low[now],low[p]);
		}
		else if(in[p])//dfs 过,但是还在栈中,属于回溯边
			low[now]=min(low[now],dfn[p]);
	}
	if(dfn[now]==low[now])
	{//强连通分量的根,弹出
		int ls;ks++;
		do{
			ls=h.top();h.pop();in[ls]=0;
			bel[ls]=ks;sum[ks]+=a[ls];
		}while(ls!=now);
	}
}

找完强连通分量之后,便要重建图,即缩点。

我们遍历所有的边,若边的两端不在同一强连通分量,则说明重建图中有这个边。

为不与原图冲突,重建图采用 vector 存图(原图采用邻接表存图)。

for(int i=1;i<=m;i++)
{
	int ru=bel[u[i]],rv=bel[v[i]];
	if(ru==rv)continue;
	e[ru].push_back(rv);
	deg[rv]++;
}

由于缩点之后的图绝对是 DAG,所以直接通过拓扑解决后续问题。

for(int i=1;i<=ks;i++)
	if(!deg[i])q.push(i),val[i]=sum[i];
while(q.size())
{
	int now=q.front();q.pop();
	int len=e[now].size();
	for(int i=0;i<len;i++)
	{
		int p=e[now][i];deg[p]--;
		val[p]=max(val[p],val[now]+sum[p]);
		if(deg[p]==0)q.push(p);
	}
}

完整代码

const int inf=1e5+7;
int n,m,ans,a[inf];
int u[inf],v[inf];
int fir[inf],nex[inf],poi[inf],cnt;
void ins(int x,int y)
{
	nex[++cnt]=fir[x];
	poi[cnt]=y;
	fir[x]=cnt;
}
int dfn[inf],low[inf],dfns;
bool in[inf];
stack<int>h;
int ks,bel[inf],sum[inf];
void Tarjan(int now)
{
	in[now]=1;h.push(now);
	dfn[now]=low[now]=++dfns;
	for(int i=fir[now];i;i=nex[i])
	{
		int p=poi[i];
		if(!dfn[p])
		{
			Tarjan(p);
			low[now]=min(low[now],low[p]);
		}
		else if(in[p])
			low[now]=min(low[now],dfn[p]);
	}
	if(dfn[now]==low[now])
	{
		int ls;ks++;
		do{
			ls=h.top();h.pop();in[ls]=0;
			bel[ls]=ks;sum[ks]+=a[ls];
		}while(ls!=now);
	}
}
vector<int>e[inf];
int deg[inf],val[inf];
queue<int>q;
int main()
{
	n=re();m=re();
	for(int i=1;i<=n;i++)
		a[i]=re();
	for(int i=1;i<=m;i++)
	{
		u[i]=re(),v[i]=re();
		ins(u[i],v[i]);
	}
	for(int i=1;i<=n;i++)
		if(!dfn[i])Tarjan(i);
	for(int i=1;i<=m;i++)
	{
		int ru=bel[u[i]],rv=bel[v[i]];
		if(ru==rv)continue;
		e[ru].push_back(rv);
		deg[rv]++;
	}
	for(int i=1;i<=ks;i++)
		if(!deg[i])q.push(i),val[i]=sum[i];
	while(q.size())
	{
		int now=q.front();q.pop();
		int len=e[now].size();
		for(int i=0;i<len;i++)
		{
			int p=e[now][i];deg[p]--;
			val[p]=max(val[p],val[now]+sum[p]);
			if(deg[p]==0)q.push(p);
		}
	}
	for(int i=1;i<=ks;i++)
		ans=max(ans,val[i]);
	wr(ans,'\n');
	return 0;
}

习题

posted @ 2024-07-28 16:38  Zvelig1205  阅读(29)  评论(0编辑  收藏  举报