【Ybtoj 第15章例1】有向图缩点【强连通分量】【Tarjan缩点】

67 篇文章 0 订阅
9 篇文章 0 订阅

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述


知识点

强连通分量:是对于有向图来讲的。一个强连通分量定义为一个点集V与他们之间的边集E所组成的集合二元对(V,E),并满足若点x,y∈V,那么x,y可以互相到达。极大强连通分量定义为一个不被任何其它强连通分量所包含的强连通分量。

Tarjan算法:是基于对图深度优先搜索的算法,每个强连通分量为搜索树中的一棵子树。搜索时,把当前搜索树中未处理的节点加入一个堆栈,回溯时可以判断栈顶到栈中的节点是否为一个强连通分量。
下图中,子图{1,2,3,4}为一个强连通分量,因为顶点1,2,3,4两两可达。{5},{6}也分别是两个强连通分量。
在这里插入图片描述

接下来是对算法流程的演示:

定义:

  • D F N ( u ) DFN(u) DFN(u)为节点 u u u搜索的次序编号(时间戳)
  • L o w ( u ) Low(u) Low(u) u u u u u u的子树能够追溯到的最早的栈中节点的次序号。
  • D F N ( u ) = L o w ( u ) DFN(u)=Low(u) DFN(u)=Low(u)时,以u为根的搜索子树上所有节点是一个强连通分量。

从节点 1 1 1开始 D F S DFS DFS,把遍历到的节点加入栈中。搜索到节点 u = 6 u=6 u=6时, D F N [ 6 ] = L O W [ 6 ] DFN[6]=LOW[6] DFN[6]=LOW[6],找到了一个强连通分量 6 {6} 6,退栈。
返回节点5,发现 D F N [ 5 ] = L O W [ 5 ] DFN[5]=LOW[5] DFN[5]=LOW[5],退栈后 5 {5} 5为一个强连通分量。
在这里插入图片描述

返回节点 3 3 3,继续搜索到节点4,把4加入堆栈。节点1还在栈中,所以 L O W [ 4 ] = 1 LOW[4]=1 LOW[4]=1。节点6已经出栈,返回3, ( 3 , 4 ) (3,4) (3,4)为树枝边,所以 L O W [ 3 ] = L O W [ 4 ] = 1 LOW[3]=LOW[4]=1 LOW[3]=LOW[4]=1
在这里插入图片描述
继续回到节点1,最后访问节点2。访问边 ( 2 , 4 ) , 4 (2,4),4 (2,4)4还在栈中,所以 L O W [ 2 ] = D F N [ 4 ] = 5 LOW[2]=DFN[4]=5 LOW[2]=DFN[4]=5。返回1后,发现 D F N [ 1 ] = L O W [ 1 ] DFN[1]=LOW[1] DFN[1]=LOW[1],把栈中节点全部取出,组成一个连通分量 1 , 3 , 4 , 2 {1,3,4,2} 1,3,4,2
在这里插入图片描述
至此,算法结束。经过该算法,求出了图中全部的三个强连通分量{1,3,4,2},{5},{6}。可以发现,运行 T a r j a n Tarjan Tarjan算法的过程中,每个顶点都被访问了一次,且只进出了一次堆栈,每条边也只被访问了一次,所以该算法的时间复杂度为 O ( N + M ) O(N+M) O(N+M)
在这里插入图片描述

PS:图中的栈是从上而下的。。。见谅。。。

原代码

void Tarjan(int x)
{
	dfn[x]=low[x]=++t;//为节点u设定次序编号和Low初值
	st[++top]=x;//将节点u压入栈中
	for(int i=head[x];i;i=a[i].next)//枚举每一条边
	{
		int y=a[i].x;
		if(!dfn[y])//如果节点v未被访问过
		{
			Tarjan(y);//继续向下找
			low[x]=min(low[x],low[y]);
		}
		else if(!c[y])//如果节点v还在栈内
			low[x]=min(low[x],low[y]);
	}
	if(dfn[x]==low[x])//如果节点u是强连通分量的根
	{
		c[x]=++tot;
		sum[tot]+=lyx[x];
		while(st[top]!=x)
			sum[tot]+=lyx[st[top]],c[st[top--]]=tot;
		top--;//将v退栈,为该强连通分量中一个顶
	}

}

解题思路

d i s i dis_i disi表示点i的点权,用 f i f_i fi表示以点i为终点的路径所经过的最大点权和。则转移方程为 f v = m a x ( f u + d i s v , f v ) f_v=max(f_u+dis_v,f_v) fv=max(fu+disv,fv)

考虑优化DP的过程,如果点对(u,v)可以互相到达,就称他们是强连通的,我们把所有强连通的点进行捆绑,缩点,变成一个有向无环图,就可以用拓扑排序进行DP。


代码

#include<iostream>
#include<cstdio>
#include<iomanip>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<queue>
using namespace std;

int n,m,k,tot,top,t,ans;
int u[10010],v[10010],st[10010],sum[10010],head[100010],lyx[10010],dfn[10010],low[10010],c[10010],ru[10010],f[10010];

struct c{
	int x,next;
}a[100100];

void add(int x,int y){
	a[++k].x=y;
	a[k].next=head[x];
	head[x]=k;
}

void Tarjan(int x)
{
	dfn[x]=low[x]=++t;
	st[++top]=x;
	for(int i=head[x];i;i=a[i].next)
	{
		int y=a[i].x;
		if(!dfn[y])
		{
			Tarjan(y);
			low[x]=min(low[x],low[y]);
		}
		else if(!c[y])
			low[x]=min(low[x],low[y]);
	}
	if(dfn[x]==low[x])
	{
		c[x]=++tot;
		sum[tot]+=lyx[x];
		while(st[top]!=x)
			sum[tot]+=lyx[st[top]],c[st[top--]]=tot;
		top--;
	}

}

int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)
		scanf("%d",&lyx[i]);
	for(int i=1;i<=m;i++)
	{
		scanf("%d%d",&u[i],&v[i]);
		add(u[i],v[i]);
	}
	for(int i=1;i<=n;i++)
	{
		if(!dfn[i])
			Tarjan(i);
	}
	memset(head,0,sizeof(head));
	memset(a,0,sizeof(a));
	k=0;
	for(int i=1;i<=m;i++)
	{
		if(c[u[i]]!=c[v[i]])
		{
			add(c[u[i]],c[v[i]]);
			ru[c[v[i]]]++;
		}
	}
	queue<int>q;
	for(int i=1;i<=tot;i++)
	{
		f[i]=sum[i];
		if(!ru[i])q.push(i);
	}
	while(!q.empty())
	{
		int y=q.front();
		q.pop();
		for(int i=head[y];i;i=a[i].next)
		{
		
			int w=a[i].x;
			f[w]=max(f[w],f[y]+sum[w]);
			ru[w]--;
			if(!ru[w])q.push(w);
		}
	}
	for(int i=1;i<=tot;i++)
		ans=max(ans,f[i]);
	printf("%d",ans);
} 
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值