tarjan算法求scc & 缩点
前置知识
图的遍历(dfs)
强连通&强连通分量
对于有向图G中的任意两个顶点u和v存在u->v的一条路径,同时也存在v->u的路径,我们则称这两个顶点强连通。以此类推,强连通分量就是某一个分量内各个顶点之间互相连通。
简单来说,就是有向图内的一个分量,其中的任意两个点之家可以互相到达。
求有向图内部强连通分量的方法大概有2种:tarjan算法,korasaju算法。这里我们只对tarjan算法进行讨论。
tarjan算法
tarjan算法是tarjan神仙提出的基于dfs时间戳和堆栈的算法,这里我们可以先来看一下什么是dfs时间戳
dfs时间戳
dfs时间戳就是dfs的先后顺序,详细来讲,比如我们dfs最先访问到的节点是A,于是A的时间戳就是1,第二个访问到的节点是E,那么E的时间戳就是2,我们用\(dfn[u]\)来表示u节点的时间戳,应该算是比较简单的
算法步骤
首先,除了dfn以外我们还需要一个low数组,这个数组记录了某个点通过图上的边能回溯到的dfn值最小的节点。这句话相信在大多数博客里面都有提到,这里我们来看一个简单的例子:
首先,我们有一个图G:
假设我们从a点出发开始dfs,我们可以画出一个dfs树:
为什么我们画出来的dfs树和原来的图不一样呢?因为我们在dfs的过程中实际上是会忽略某一些连接到已访问节点的边的,这些边我们暂且称之为回边。对于点u来说,\(low[u]\)保存的就是点u通过某一条(或者是几条)回边能到达的dfn值最小的节点(也就是被最先访问的节点)。假设这个dfn值最小的节点是u',我们可以知道,因为u和u'都是在一棵dfs树上的,并且u'可以到达u,同时u可以通过一条或多条回边到达u',也就是说u'->u路径上的任意节点都可以通过这一条回边来互相到达,也就是说他们会形成一个强连通分量。
更加详细的例子
我们有一个新图G:
假设我们从A点出发开始dfs,一路跑到D点,那么我们为这个图上的每一个点加上dfn数组和low数组的值(dfn,low),整个图就会长成这个样子:
此时我们会遇到一条D->A的回边,也就是说点D能访问到的dfn值最小的节点从点D本身变化到了A点,所以点D的low值就会发生相应的变化,\(low[D]=min(low[D],dfn[A])\)。
紧接着,dfs发生回溯,我们沿着之前的路径逐步更新路径上节点的low值,于是就有\(low[C]=min(low[C],low[D])\),知道更新到某一个dfn值和low值相同的节点。因为这个节点能访问到的最小dfn的节点就是其本身,也就是说这个节点是整个scc最先被访问到的节点。
全部搞完大概会变成这个样子:
我们用一个辅助栈来保存dfs的路径,这样就可以在找到一个强连通分量里面最早被访问到的节点的时候可以输出路径。同时因为dfs访问是一条路走到黑的,所以可以保证栈内在节点u(low[u]==dfn[u])之前的的节点都是属于同一个scc的。
还是上面这幅图,我们顺便把E点给更新了:
跑完E点之后就会发现,E点本身的low就是和dfn相等的,所以此时栈内也只有E这一个节点。
于是上面这个图的scc有以下几个:
[E]
[A,B,C,D]
代码实现
首先我们要发现,在dfs的初期我们每一个节点的low和dfn都是相同的,也就是说有dfn[u]=low[u]=++cnt(cnt为计数变量),并且在回溯的过程中要用后访问节点的low值来更新先访问节点的low值,也就是说有\(low[u]=min(low[u],low[v])\),当访问到某一个在栈中的节点的时候,我们要用这个节点的dfn值来更新其他节点,所以有\(low[u]=min(low[u],dfn[v])\)。
那么我们一个简单的代码就可以写出来了:
void tarjan(int u){
dfn[u]=low[u]=++cnt;
s.push(u);
ins[u]=1;
for(int i=0;i<gpe[u].size();i++){
int v=gpe[u][i].to;
if(!dfn[v]){//如果节点未访问,则访问之
tarjan(v);
low[u]=min(low[u],low[v]);
}else if(ins[v]){//ins是为栈中节点做的一个标记
low[u]=min(low[u],dfn[v]);
}
}
}
当更新完毕之后,我们需要找出一个完整的scc,因为我们提前已经用辅助栈来记录节点了,剩下的工作就只剩下从栈中不停地pop就完事了
if(low[u]==dfn[u]){
ins[u]=0;
scc[u]=++sccn;//sccn是强连通分量的编号
size[sccn]=1;//size记录了强连通分量的大小
//找到某一个low[u]==dfn[u]的节点的时候就要立即处理,因为这个节点也属于一个新的scc
while(s.top()!=u){
scc[s.top()]=sccn;//scc[u]记录了u点属于哪一个scc
ins[s.top()]=0;
size[sccn]+=1;
s.pop();
}
s.pop();
//这里pop掉的就是一开始的那个low[u]==dfn[u]的节点。因为相关信息已经维护完毕,所以这里直接pop也没问题
}
把这两部分结合在一起,就是tarjan求scc的完整代码了:
void tarjan(int u){
dfn[u]=low[u]=++cnt;
s.push(u);
ins[u]=1;
for(int i=0;i<gpe[u].size();i++){
int v=gpe[u][i].to;
if(!dfn[v]){
tarjan(v);
low[u]=min(low[u],low[v]);
}else if(ins[v]){
low[u]=min(low[u],dfn[v]);
}
}
if(low[u]==dfn[u]){
ins[u]=0;
scc[u]=++sccn;
size[sccn]=1;
printf("%d ",u);
while(s.top()!=u){
scc[s.top()]=sccn;
printf("%d ",s.top());
ins[s.top()]=0;
size[sccn]+=1;
s.pop();
}
s.pop();
printf("\n");
}
return;
}
tarjan与缩点
tarjan算法最有用的地方就是缩点了。缩点,顾名思义,就是把图上的某一块的信息整合成一个点,从而使得后续处理的速度加快(个人的简单总结,可能会有遗漏之类的)。
先来一个模板题吧:
emmm......题目大意就是对于一条边u->v代表了u喜欢v ,然后给出了一个奶牛和奶牛之间的关系网(不要问我为什么是奶牛,这不是usaco题目的传统艺能吗),要你求出这群奶牛之中的明星奶牛。明星奶牛就是那些被所有奶牛所喜欢的奶牛。这里要注意,喜欢是可以传递的,也就是说a->b,b->c,那么a->c。(更多题目细节可以去连接里面看看)
首先最朴素的dfs方法就是对于每一个点来检查喜欢它的节点的数量,但是这样的效率肯定是太低了,所以我们考虑缩点。如果在这个关系网内部存在某一个强连通分量,也就是说这个分量里面的每一个奶牛都是互相喜欢着的,并且任何喜欢这个分量的奶牛都会喜欢到这个分量内部的每一个奶牛,于是我们可以把这个分量当成一个点来看待。
缩点结束之后的新图肯定是一个DAG(有向无环图),又因为缩点本身对题目是没有影响的,所以我们可以基于这个DAG来分析题目,比之前算是简单许多了。
很明显,一个DAG里面只能有一个明星牛(或者是由明星牛组成的SCC),因为当存在两个的时候他们是无法互相喜欢的(如果互相喜欢的话就会被缩成一个点)
答案就很明显了,我们只需要维护每一个SCC的出度(出度为0则证明这就是一个明星),如果存在两个或两个以上的明星则证明这个图里面没有明星。如果只有一个的话我们就在tarjan里面顺手维护每一个scc的大小,最后统计一下输出就完事了
AC代码:
#include <bits/stdc++.h>
using namespace std;
const int maxn=10010;
struct edge{
int to;
edge(int to_){
to=to_;
}
};
vector<edge> gpe[maxn];
int dfn[maxn],low[maxn],ins[maxn],scc[maxn],size[maxn],cnt=0,sccn=0;
stack<int> s;
void tarjan(int u){
dfn[u]=low[u]=++cnt;
s.push(u);
ins[u]=1;
for(int i=0;i<gpe[u].size();i++){
int v=gpe[u][i].to;
if(!dfn[v]){
tarjan(v);
low[u]=min(low[u],low[v]);
}else if(ins[v]){
low[u]=min(low[u],dfn[v]);
}
}
if(low[u]==dfn[u]){
ins[u]=0;
scc[u]=++sccn;
size[sccn]=1;
while(s.top()!=u){
scc[s.top()]=sccn;
ins[s.top()]=0;
size[sccn]+=1;
s.pop();
}
s.pop();
}
return;
}
int n,m,oud[maxn];
int main(void){
scanf("%d %d",&n,&m);
memset(low,0x3f,sizeof(low));
memset(ins,0,sizeof(ins));
for(int i=1;i<=m;i++){
int u,v;
scanf("%d %d",&u,&v);
gpe[u].push_back(edge(v));
}
for(int i=1;i<=n;i++){
if(!dfn[i]){
cnt=0;
tarjan(i);
}
}
for(int u=1;u<=n;u++){
for(int i=0;i<gpe[u].size();i++){
int v=gpe[u][i].to;
if(scc[u]!=scc[v]) oud[scc[u]]++;
}
}
int cont=0,ans=0;
for(int i=1;i<=sccn;i++){
if(oud[i]==0){
cont++;
ans+=size[i];
}
}
if(cont==1){
printf("%d",ans);
}else{
printf("0");
}
return 0;
}
代码以前写的,略冗长,见谅
题目推荐:
真·模板题: P2863 [USACO06JAN]The Cow Prom S
P2746 [USACO5.3]校园网Network of Schools
tarjan算法求scc & 缩点的更多相关文章
- 转载 - Tarjan算法(求SCC)
出处:http://blog.csdn.net/xinghongduo/article/details/6195337 说到以Tarjan命名的算法,我们经常提到的有3个,其中就包括本文所介绍的求强连 ...
- Tarjan算法求有向图强连通分量并缩点
// Tarjan算法求有向图强连通分量并缩点 #include<iostream> #include<cstdio> #include<cstring> #inc ...
- tarjan算法求无向图的桥、边双连通分量并缩点
// tarjan算法求无向图的桥.边双连通分量并缩点 #include<iostream> #include<cstdio> #include<cstring> ...
- Tarjan算法初探(2):缩点
接上一节 Tarjan算法初探(1):Tarjan如何求有向图的强连通分量 Tarjan算法一个非常重要的应用就是 在一张题目性质在点上性质能够合并的普通有向图中将整个强连通分量视作一个点来把整张图变 ...
- [Tarjan系列] Tarjan算法求无向图的双连通分量
这篇介绍如何用Tarjan算法求Double Connected Component,即双连通分量. 双联通分量包括点双连通分量v-DCC和边连通分量e-DCC. 若一张无向连通图不存在割点,则称它为 ...
- Tarjan算法求割点
(声明:以下图片来源于网络) Tarjan算法求出割点个数 首先来了解什么是连通图 在图论中,连通图基于连通的概念.在一个无向图 G 中,若从顶点i到顶点j有路径相连(当然从j到i也一定有路径),则称 ...
- Tarjan算法 求 有向图的强连通分量
百度百科 https://baike.baidu.com/item/tarjan%E7%AE%97%E6%B3%95/10687825?fr=aladdin 参考博文 http://blog.csdn ...
- ZOJ Problem - 2588 Burning Bridges tarjan算法求割边
题意:求无向图的割边. 思路:tarjan算法求割边,访问到一个点,如果这个点的low值比它的dfn值大,它就是割边,直接ans++(之所以可以直接ans++,是因为他与割点不同,每条边只访问了一遍) ...
- HDU 1269 迷宫城堡 tarjan算法求强连通分量
基础模板题,应用tarjan算法求有向图的强连通分量,tarjan在此处的实现方法为:使用栈储存已经访问过的点,当访问的点离开dfs的时候,判断这个点的low值是否等于它的出生日期dfn值,如果相等, ...
随机推荐
- css变量的使用
css变量的使用 1.介绍:我们也可以在css中定义变量,和less.sass一样,通过--来定义变量 div { /* 开始定义变量 */ --color: red; /* 通过var()函数来使用 ...
- OpenStack的Trove组件详解
一:简介 一.背景 1. 对于公有云计算平台来说,只有计算.网络与存储这三大服务往往是不太够的,在目前互联网应用百花齐放的背景下,几乎所有应用都使用到数据库,而数据库承载的往往是应用最核心的数 ...
- C#基础之方法的重载
在C#语言中,方法的重载作用非常大,但是使用重载需要注意方法的签名,必须有一种要不一样,具体指的是: 1.方法的返回值类型 2.方法的形参类型 3.形参类型的顺序 4.形参的个数 4.泛型的类型< ...
- 【C++】常见易犯错误之数值类型取值溢出与截断(2)
本节内容紧接上节,解决红色字体遗留问题.本节所有例子运行环境: win10 + VS2015 + X64 + debug 在上节例子中,查看变量 c .d .d+1 的类型. //// Console ...
- [Python进阶]002.装饰器(1)
装饰器(1) 介绍 HelloWorld 需求 使用函数式编程 加入装饰器 解析 介绍 Python的装饰器叫Decorator,就是对一个模块做装饰. 作用: 为已存在的对象添加额外功能. 与Jav ...
- StackOverflow 创始人关于如何高效编程的清单.md
这是 StackOverflow 联合创始人 Jeff Atwood 注释的十戒.程序员普遍有很强的自尊心,都应该看看本文,打印下来时刻提醒自己. "无我编程"发生在开发阶段,表现 ...
- kali下安装beef并联合Metasploit
安装beef 在kali中安装beef比较容易,一条命令就可以安装了,打开终端,输入apt-get install beef-xss ,安装前可以先更新一下软件apt-get update 修改bee ...
- Java实现 LeetCode 720 词典中最长的单词(字典树)
720. 词典中最长的单词 给出一个字符串数组words组成的一本英语词典.从中找出最长的一个单词,该单词是由words词典中其他单词逐步添加一个字母组成.若其中有多个可行的答案,则返回答案中字典序最 ...
- Java实现 蓝桥杯VIP 算法训练 平方计算
问题描述 输入正整数a, m,输出a2%m,其中表示乘方,即a^2表示a的平方,%表示取余. 输入格式 输入包含两个整数a, m,a不超过10000. 输出格式 输出一个整数,即a^2%m的值. 样例 ...
- Java实现LeetCode 139 单词拆分
public boolean wordBreak(String s, List<String> wordDict) { if(s.length() == 0){ return false; ...