ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

缩点—DAG,拓扑排序与Tarjan

2022-07-27 21:33:02  阅读:153  来源: 互联网

标签:缩点 Tarjan DAG visit 连通 dfn low sct ssc


模板题Luogu-P3387


1.DAG

说缩点,就必须要先说DAG

有向无环图(DAG),是一种特殊的有向图,它没有有向环;

这就是个DAG

这个就是不是DAG,那你觉得里面有几个环呢?

事实上只有一个,2-3-4-5是一个环

你可能觉得5-9-8-7也是,但其实它不能算环,因为它们不是一个强连通分量

强连通分量就是若存在点集A,且对于任意的两点X∈A,都相互可达,这就是一个强连通分量。特别的,一个点也被看作是一个强连通分量。

其实就是环

那DAG有什么好处?DAG上可以跑很多的算法,而且不用有环的顾虑,处理起来有时比普通的有向图更加方便

要是题目给的不是DAG怎么办?这就引出了今天的主角——


2.缩点

所谓缩点,就是把有向图里的环看做一个点,然后把整个图变成DAG的过程

还是刚才的图

把2345看做一个点会怎样呢?

变成了DAG!

其实很河里,因为把所有的环都变成点了,所以没有环了,就成了有向无环图了

思想有了,那如何用代码实现呢?

想要把强连通分量(环)缩成点,首先就要能找强连通分量

这就是Tarjan算法

2.1 Tarjan

我们定义数组dfn[i]表示节点i在遍历中访问到的次序,low[i]表示从节点i开始所能访问到的最早的次序

这么说也许有些抽象,还是那个图

如果我们从1开始dfs,dfn[1]=1,dfn[2]=2,dfn[3]=3.....以此类推

但low就不一样了,low[1]=2

这是为什么?回看low[i]的定义,我们来模拟,从1出发可以1-2-3-4-5-2回到2,在这之后就回不到更早的地方了

同样的,2,3,4,5的low都等于2

而如果有一个点u,dfn[u]=low[u],那肯定出现了强连通分量(也可能是一个点构成的)

为什么呢?u的时间访问次序是dfn[u],而根据low的定义,从u出发能到的最早次序竟然就是dfn[u]自己,说明要么是都没法走下去,要么是走下去又通过环绕回来了

噫!好!现在我知道有强连通分量了!(该死的畜生!你发现了甚么?)

那我咋知道有哪些呢?

用一个简单的图看一下

如果我们要找到2345这个强连通分量,怎么记录这些点呢?

反正我们知道肯定是在访问节点2的时候揪出这个强连通分量来,那首先我们来决定到底是在递归3之前揪还是在回溯回来之后揪

这个问题看起来很蠢,因为你还没往下递归怎么可能知道后面有什么

那我们只要记录2后面有什么,然后全都是强连通分量是不是就行了?

但看图,还有个小尾巴6在后面呢

那么Tarjan是怎么办的呢?开栈,然后每到一个点压栈

有人就要说了:哎呀开栈有什么鸟用,6不还是最后在栈顶带着呢!

但不要忘了6自己也是个强连通分量,按照我们的规则,在6即将推出dfs的时候,因为dfn[6]=low[6](此时属于走不下去的情况),6自己作为一个强连通分量已经被揪出来了

所以,栈里2以上的所有点都是2这个强连通分量的点

我们只需要开个栈,每访问一个点就压栈,然后接着往下dfs。在回溯回这个点的时候,退出前看下dfn[i]是不是等于low[i],如果是的话就把栈里i及以上的所有点全都yue地吐出来作为一个强连通分量

 

至此,Tarjan求强连通分量的基本思想已经明确了,接下来就是一些细枝末节的小细节如何处理

首先,main函数里只用tarjan一次吗?

还是简单的图

虽然这里第一个点是1,但如果我交换一下1和2的序号呢?

如果我们还是tarjan(1),这样对于这个图可以成功地揪出来{1,3,4,5},{6}这两个强连通分量,但2还没有

所以我们要有循环,遍历每个点,且如果这个点的dfn不为零(没有被遍历过)就从这个点再来一波Tarjan

具体代码

foru(i,1,n)	if(!dfn[i])	tarjan(i);

但这样不会炸掉复杂度吗?对于这个图,从2再遍历不会再把下面跑一边吗?不会重复揪出来吗?

其实,在Tarjan时有一个重要的条件,只有当下一个点的dfn为0时才会过去遍历

为了方便理解,这里先给出Tarjan函数的代码,下面我们结合代码再说

void tarjan(int x){
	dfn[x]=low[x]=++times;
	sct.push(x);
	visit[x]=1;
	foru(i,0,(int)e[x].size()-1){
		int v=e[x][i];
		if(!dfn[v]){
			tarjan(v);
			low[x]=min(low[x],low[v]);
		}else{
			if(visit[v]){
				low[x]=min(low[x],dfn[v]);
			}
		}
	}
	if(dfn[x]==low[x]){
		sid++;
		while(!sct.empty()){
			ssc[sct.top()]=sid;
			wei[sid]+=a[sct.top()];
			visit[sct.top()]=0;
			if(sct.top()==x){
				sct.pop();
				break;
			}
			sct.pop();
		}
	}
}

注意到,这里还有一个visit数组,布尔型,明显是在记录这个点有没有被走过。

当一个点被访问(入栈)时,visit=1

当一个点作为强连通分量的一部分被yue出来时,visit=0

但为什么中间遍历的时候不用!visit[v]而是!dfn[v]?

注意到,dfn无论怎样只会被赋值一次,从此以后不再变化

而一个点的visit是会变化的

当遍历到的点visit=0的时候,并不代表这个点可以走

我们浅改一下刚才的图

加入我们从1开始跑了一遍tarjan,到这一趟结束以后发生了什么呢?

1,3,4,5,6的dfn和low都已经被计算确定,{1,3,4,5}和{6}两个ssc(强连通分量)已经被揪出来了

而visit已全部是0了

当我们再从2开始跑tarjan,visit[1]=0,而dfn[1]是不等于0的,如果以visit为标准,岂不是又白白向右侧跑了一趟?

 

此外,好像我们一直没有明确过dfn和low的计算方式

dfn似乎很简单,我们可以用一个times变量,每访问一个节点就dfn[x]=++times,这样即可按时间顺序依次赋予不同的dfn

事实上low的初值也应该和dfn同时初始化为++times。尽管它后面有可能通过环走到更早的地方,但谁知道呢?先赋值成自己的dfn,要是确实没法走回更早的地方就不用了再想着给low赋值了

怎么知道l会不会走回更早的地方?问问后面的点呗!

low[当前点]=min(low[当前点],low[后面的点])

是不是这样就完事大吉了?对每一个v我们都取low?我们再浅改一下图

多了一条从7到5的单向边,有什么区别?

一样的套路,一样的口味,先从1开始跑一边tarjan,拿出1345和6俩ssc

好戏上演!从2开始遍历,一开始没什么问题,2->7,到7就有问题了

从7遍历的时候,会尝试对5的low取min,而且能取过来!但5的low是因为5可以走到1所以才会形成环,7走过去,但走不回来呀!

所以要把对low[v]取min的操作 放到if(!dfn[v])里面

 

现在呢?彻底解决Low的问题了?

并没有!

在刚才,我们限制只有当能走过去的时候才尝试用low[v]更新low[当前点]

看图,那5怎么办?根据我们的限制条件,如果5尝试遍历1是过不去的,因为!dfn[1]==false,可明明low[5]=1就是用从5到1的单向边取过来的啊!怎么不能取了?

特判就行了,既然5已经走过了(visit[5]==1),我们就可以取一次,而且这样不会影响7->5时的不取,因为visit数组在yue ssc的时候会变成0,所以枚举7的时候5这边已经全都visit=0了

 

在之后就是存储的问题

对于缩点后的新图,因为原图中所有的环和剩下的点都各自构成了强连通分量被储存,所以我们新图里的节点就是所有的强连通分量

到现在要引入新的编号,原题给的点编号一般是1~n,但在tarjan过后,我们用ssc[i]表示节点i所属的强连通分量的ssc编号(变量sid),为了不让ssc编号(变量sid)重复,在每次yue之前先++sid

点不光有编号,还有点权。分类讨论易得,如果一个ssc只有一个点,其新点权明显=旧点权;如果ssc是一个环,那新点权=旧点权之和

这个好说,用wei[i]表示 sid=i的ssc 的点权,在yue的时候,一yue出来一个就把这个点的点权加上到wei[ssc[这个点]]里

点解决,还要在ssc之间建边

枚举所有的边,如果这个边的两端点不属于同一个ssc,那么把它加到新的vector里,作为新图的边

注意到这样缩出来的DAG是可能有重边的,在无边权图中问题不大,有边权图中就要根据方向注意考虑合并边权了


3.拓扑排序

虽然取着个排序的名,但似乎和我们熟悉的排序算法作用不大一样,在我看来,对DAG拓扑排序是找到一种执行算法的顺序,比如为DP找到顺序

什么是拓扑排序呢?用一张图来轻松解释

这张图拓扑排序完的序列是:1,2,4,3,5

开个队列,每次取出队首,依次遍历出边,把v的入度减一,如果v减完以后入度为0,把v入队

有个很形象的解释:假如有向边代表前置条件,例如完成5需要同时完成3和4,所有的点被遍历的顺序是如何的?

肯定要先找没有前置条件的点1,完成它,之后对于2和4来说,它们的前置条件量减一,再看看现在谁没有前置条件,用它循环即可

void Topu(){
	foru(i,1,sid){
		if(rd[i]==0)	q.push(i);
	}
	while(!q.empty()){
		int p=q.front();
		// cout<<p<<endl;
		q.pop();
		dor.push_back(p);
		foru(i,0,(int)de[p].size()-1){
			int v=de[p][i];
			rd[v]--;
			if(rd[v]==0)	q.push(v);
		}
	}
}

在这里dor存储的是拓扑排序的输出序列


至此,缩点这个题的模板已经记录完毕了,后面的dp因为没有任何含金量就不写了

总体思路:要求最大点权和路径->发现是有向有环图->想转换成DAG跑DP->缩点->用tarjan找ssc

完整代码

// Problem: P3387 【模板】缩点
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P3387
// Memory Limit: 125 MB
// Time Limit: 1000 ms
// 
// Powered by CP Editor (https://cpeditor.org)

#include <bits/stdc++.h>
#define INF 0x7fffffff
#define MAXN 10050
#define MAXM 100050
#define foru(a,b,c)	for(int a=b;a<=c;a++)
#define ford(a,b,c)	for(int a=b;a>=c;a--)
#define RT return 0;
#define db(x)	cout<<endl<<x<<endl;
#define LL long long
#define LXF int
#define RIN rin()
#define HH printf("\n")
using namespace std;
inline LXF rin() {
    LXF a=0;char c=getchar();
    while(c<'0'||c>'9') c=getchar();
    while(c>='0'&&c<='9') a=(a<<1)+(a<<3)+c-'0',c=getchar();
    return a;
}
inline void out(LXF n){
    if(n==0) return;
    out(n/10);
    putchar(n%10+'0');
}
//Main
int n,m,a[MAXN];
vector<int> e[MAXN];
void SaveOriginalG(){
	n=RIN,m=RIN;
	foru(i,1,n)	a[i]=RIN;
	foru(i,1,m){
		int x=RIN,y=RIN;
		e[x].push_back(y);
	}
}
//Tarjan
int dfn[MAXN],low[MAXN],ssc[MAXN],wei[MAXN],sid,times;
bool visit[MAXN];
stack<int> sct;
void tarjan(int x){
	dfn[x]=low[x]=++times;
	sct.push(x);
	visit[x]=1;
	foru(i,0,(int)e[x].size()-1){
		int v=e[x][i];
		if(!dfn[v]){
			tarjan(v);
			low[x]=min(low[x],low[v]);
		}else{
			if(visit[v]){
				low[x]=min(low[x],dfn[v]);
			}
		}
	}
	if(dfn[x]==low[x]){
		sid++;
		while(!sct.empty()){
			ssc[sct.top()]=sid;
			wei[sid]+=a[sct.top()];
			visit[sct.top()]=0;
			if(sct.top()==x){
				sct.pop();
				break;
			}
			sct.pop();
		}
	}
}
//DAG
int rd[MAXN];
vector<int> de[MAXN],rdp[MAXN];
void SaveDAG(){
	foru(i,1,n){
		foru(j,0,(int)e[i].size()-1){
			int v=e[i][j];
			if(ssc[i]!=ssc[v]){
				de[ssc[i]].push_back(ssc[v]);
				rd[ssc[v]]++;
				rdp[ssc[v]].push_back(ssc[i]);
			}
		}
	}
}
//Topu
queue<int>	q;
vector<int> dor;
void Topu(){
	foru(i,1,sid){
		if(rd[i]==0)	q.push(i);
	}
	while(!q.empty()){
		int p=q.front();
		// cout<<p<<endl;
		q.pop();
		dor.push_back(p);
		foru(i,0,(int)de[p].size()-1){
			int v=de[p][i];
			rd[v]--;
			if(rd[v]==0)	q.push(v);
		}
	}
}
//DP
int dp[MAXN],ans;
void DP(){
	foru(i,0,(int)dor.size()-1){
		int x=dor[i];
		dp[x]=wei[x];
		foru(j,0,(int)rdp[x].size()-1){
			dp[x]=max(dp[x],dp[rdp[x][j]]+wei[x]);
		}
		ans=max(ans,dp[x]);
	}
}
int main(){
	SaveOriginalG();
	foru(i,1,n)	if(!dfn[i])	tarjan(i);
	SaveDAG();
	Topu();
	DP();
	cout<<ans;
	return 0;
}

 

标签:缩点,Tarjan,DAG,visit,连通,dfn,low,sct,ssc
来源: https://www.cnblogs.com/XHZS-XY/p/16526540.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有