tarjan求(强连通分量)缩点与拓扑DP求DAG最长路

18 篇文章 0 订阅
16 篇文章 2 订阅

tarjan算法

定义

tarjan算法是一种求强连图分量的算法。

步骤

准备两个数组dfn[n]与low[n]

  • dfn[i]:i 点访问到时候的时间戳 (可以通过是否为0判断该点有没有被访问过)
  • low[i]:i 点所能访问到的最早的时间戳(初始化时和dfn[i]相同)
  • 栈s:用于dfs时存放点
  • in_stack[i] :判断i点是否在栈中

步骤:

  • 外层循环遍历所有点,如果该点dfn[i]==0(没有被访问过),则dfs(u)

dfs(u)

  • u点入栈。
  • 遍历所有u能到达的边,对于u->v的边,有如下三种情况。见下图。
  1. 如果v没有被访问过则dfs(v),然后使得low[u] = min(low[u] , low[v])(因为,u能到达v,所有v能到达的最早时间戳,u也能到达)。(比如下图的边2->3)
  2. 如果v已经被访问过,则判断v是否在栈中,如果v在栈中,则low[u] = min(low[u],low[v])此时是一条回路。(比如下图的边4->1)
  3. 如果v已经被访问过,并且v不在栈中,则说明u->v是一条横叉边。什么都不做。 (比如下图的边4->3)
  • 在判断完u的所有边后,如果dfn[u] == low[u],说明u不能到达更早的时间戳,则弹栈一直到把u弹出,这一轮弹出的点全都属于一个强连通分量。
    在这里插入图片描述

例题

hdoj1269迷宫城堡
该题为判断所给的有向图是否为一个强连通图。因为此题不卡常,因此可以判断一共有几个强连通子图,如果不是1个则返回"No",否则返回"Yes"。
另外,这题数据很弱,之前写了一个板子中间有一步判断是否访问过(dfn[u]==0)时写成了v也AC了。

#include<cstdio>
#include<stack>
#include<algorithm>
using namespace std;

//此题不卡常,直接计算一共有几个强连通分量,然后判断是否等于1即可 
const int maxn = 10004;
const int maxm = 100005;
struct{
	int t;
	int next;
}edge[maxm];
int cnt = 1;
int head[maxn];
void addedge(int u,int v){
	edge[cnt].t = v;
	edge[cnt].next = head[u];
	head[u] = cnt++;
	return;
}

stack<int> s;
bool in_stack[maxn];
int dfn[maxn];	//每个点访问到时的时间 
int low[maxn];	//每个点所能到达的最早的时间戳
//值为0说明还没有被访问过 
int n,m;
int time = 1;
int res = 0;

void init(){
	cnt = 1;	
	for(int i=1;i<=n;i++)
		dfn[i]=low[i]=head[i]=0;
	//初始化时间戳与链式前向星 
	time = 1;//初始化起始时间 
	res = 0;
	return;
}

void dfs(int u){	//当前访问u点 
	s.push(u);
	dfn[u]=low[u] = time++;
	in_stack[u] = true;
	for(int i=head[u];i!=0;i=edge[i].next){
		int v = edge[i].t;
		if(dfn[v]==0){	//还没有被访问过了 
			dfs(v);
			low[u] = min(low[u],low[v]);//v能到达u之前的时间戳(有回路) 
		}else if(in_stack[v]){	//换句话说,v也能到达u 
			low[u] = min(low[u],low[v]);
		}//剩余的情况是横叉点,忽略 
	}
	if(dfn[u]==low[u]){	//没能够回溯到更早的时间点
		while(s.top()!=u){
			in_stack[s.top()] = false;
			s.pop();
		}
		in_stack[u] = false;
		s.pop();
		res++;
	}
	return;
}

int main(){
	while(1){
		scanf("%d%d",&n,&m);
		if(n==0&&m==0) break;
		init();
		int u,v;
		for(int i =0;i<m;i++){
			scanf("%d%d",&u,&v);
			addedge(u,v);
		}
		for(int i=1;i<=n;i++)
			if(dfn[i]==0)//还没被访问过 
				dfs(i);
		if(res==1)
			printf("Yes\n");
		else
			printf("No\n");
	}
	return 0;
} 

应用(缩点)

通过生成强连通子图,可以把对应的强连通分量缩成一个点,从而转换成一个DAG图。

拓扑DP求DAG对最长路

DAG图

DAG图是有向无环图。因此可以通过求出拓扑顺序。就不过多解释,给出一道例题。
P1137 旅行计划,此题可以通过拓扑顺序,依次判断该点u有连边v(u->v)的的最长距离。也可以反向建图记忆化dfs搜索求解。

例题:缩点+DAG

P3387 【模板】缩点
最后这题结合了上述提到的两种算法,可以拿来练习。
AC代码

#include<cstdio>
#include<stack>
#include<queue>
#include<algorithm>
using namespace std;

const int maxn = 10004;
const int maxm = 100005;
struct{
	int t;
	int next;
}edge[maxm],ed[maxm];
int cnt = 1;
int head[maxn],h[maxn];	//h,ed为缩点的链式前向星 
void addedge(int u,int v){
	edge[cnt].t = v;
	edge[cnt].next = head[u];
	head[u] = cnt++;
	return;
}

int ff[maxn];	//所在强连通分量的父亲
int w[maxn];	//每个点的权值 
stack<int> s;
bool in_stack[maxn];
int dfn[maxn];	//每个点访问到时的时间 
int low[maxn];	//每个点所能到达的最早的时间戳
int time = 1;
int n,m;
int in[maxn];	//每个点的入度 
queue<int> que;	//用来拓扑排序
int dp[maxn];	//每个点为终点的最大路径 

void tarjan(int u){	//当前访问u点 
	s.push(u);
	dfn[u] = low[u] = time++;
	in_stack[u] = true;
	for(int i=head[u];i!=0;i=edge[i].next){
		int v = edge[i].t;
		if(dfn[v]==0){	//还没有被访问过了 
			tarjan(v);
			low[u] = min(low[u],low[v]);//v能到达u之前的时间戳(有回路) 
		}else if(in_stack[v]){	//换句话说,v也能到达u 
			low[u] = min(low[u],low[v]);
		}//剩余的情况是横叉点,忽略 
	}
	if(dfn[u]==low[u]){	//没能够回溯到更早的时间点
        while (1){
        	int y = s.top();
        	s.pop();
            ff[y]=u;
            in_stack[y]=false;
            if (u==y) break;
            w[u]+=w[y];
		}
	}
	return;
}

void suotu(){	//建立缩图
	int cnt2 = 1; 
	for(int u=1;u<=n;u++){
		for(int i=head[u]; i!=0; i=edge[i].next){	//遍历老图 
			int x = ff[u],y = ff[edge[i].t];
			if(x!=y){	//u和v不在一个强连通分量内 
				ed[cnt2].t = y;
				ed[cnt2].next = h[x];
				h[x] = cnt2++;
				in[y]++;	//入度 
			}
		}
	}
	return;
}

int topo(){
	for(int i=1;i<=n;i++)
		if(ff[i]==i&&in[i]==0){
			que.push(i);
			dp[i] = w[i];
		}
	while(!que.empty()){
		int u = que.front();
		que.pop();
		for(int i=h[u]; i!=0; i=ed[i].next){
			int v = ed[i].t;
			dp[v] = max(dp[v],dp[u]+w[v]);
			in[v]--;
			if(in[v]==0) que.push(v);
		}
	}
	int res = 0;
	for(int i=1;i<=n;i++)
		res = max(res,dp[i]);
	return res;
}

int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++) scanf("%d",&w[i]);
	for(int i =0;i<m;i++){
		int u,v;
		scanf("%d%d",&u,&v);
		addedge(u,v);
	}
	for(int i=1;i<=n;i++)
		if(dfn[i]==0)//该点还没被访问过 
			tarjan(i);//得到强连通分量 
	suotu();//得到缩图
	printf("%d",topo());//拓扑求解 
	return 0;
} 
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值