缩点—DAG,拓扑排序与Tarjan
作者:互联网
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