题目链接

算法:Tarjan+dfs(最短路的都行,判连通而已)

先了解一下什么是Tarjan

Tarjan算法用于求出图中所有的强连通分量。

转自NOCOW:点击打开链接

============================================================================

在有向图G中,如果两个顶点间至少存在一条路径,称两个顶点强连通(strongly connected)。如果有向图G的每两个顶点都强连通,称G是一个强连通图。非强连通图有向图的极大强连通子图,称为强连通分量(strongly connected components)。

Tarjan算法是基于对图深度优先搜索的算法,每个强连通分量为搜索树中的一棵子树。搜索时,把当前搜索树中未处理的节点加入一个堆栈,回溯时可以判断栈顶到栈中的节点是否为一个强连通分量。

定义DFN(u)为节点u搜索的次序编号(时间戳),Low(u)为u或u的子树能够追溯到的最早的栈中节点的次序号。由定义可以得出,

Low(u)=Min
{
DFN(u),
Low(v),(u,v)为树枝边,u为v的父节点
DFN(v),(u,v)为指向栈中节点的后向边(非横叉边)
}

当DFN(u)=Low(u)时,以u为根的搜索子树上所有节点是一个强连通分量。

算法伪代码如下

tarjan(u)
{
DFN[u]=Low[u]=++Index // 为节点u设定次序编号和Low初值
Stack.push(u) // 将节点u压入栈中
for each (u, v) in E // 枚举每一条边
if (v is not visted) // 如果节点v未被访问过
tarjan(v) // 继续向下找
Low[u] = min(Low[u], Low[v])
else if (v in S) // 如果节点v还在栈内
Low[u] = min(Low[u], DFN[v])
if (DFN[u] == Low[u]) // 如果节点u是强连通分量的根
repeat
v = S.pop // 将v退栈,为该强连通分量中一个顶点
print v
until (u== v)
}

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

下图中,子图{1,2,3,4}为一个强连通分量,因为顶点1,2,3,4两两可达。{5},{6}也分别是两个强连通分量。

从节点1开始DFS,把遍历到的节点加入栈中。搜索到节点u=6时,DFN[6]=LOW[6],找到了一个强连通分量。退栈到u=v为止,{6}为一个强连通分量。

返回节点5,发现DFN[5]=LOW[5],退栈后{5}为一个强连通分量。

返回节点3,继续搜索到节点4,把4加入堆栈。发现节点4向节点1有后向边,节点1还在栈中,所以LOW[4]=1。节点6已经出栈,(4,6)是横叉边,返回3,(3,4)为树枝边,所以LOW[3]=LOW[4]=1。

继续回到节点1,最后访问节点2。访问边(2,4),4还在栈中,所以LOW[2]=DFN[4]=5。返回1后,发现DFN[1]=LOW[1],把栈中节点全部取出,组成一个连通分量{1,3,4,2}。

至此,算法结束。经过该算法,求出了图中全部的三个强连通分量{1,3,4,2},{5},{6}。

可以发现,运行Tarjan算法的过程中,每个顶点都被访问了一次,且只进出了一次堆栈,每条边也只被访问了一次,所以该算法的时间复杂度为O(N+M)。

求有向图的强连通分量还有一个强有力的算法,为Kosaraju算法。Kosaraju是基于对有向图及其逆图两次DFS的方法,其时间复杂度也是 O(N+M)。与Trajan算法相比,Kosaraju算法可能会稍微更直观一些。但是Tarjan只用对原图进行一次DFS,不用建立逆图,更简洁。在实际的测试中,Tarjan算法的运行效率也比Kosaraju算法高30%左右。此外,该Tarjan算法与求无向图的双连通分量(割点、桥)的Tarjan算法也有着很深的联系。学习该Tarjan算法,也有助于深入理解求双连通分量的Tarjan算法,两者可以类比、组合理解。

求有向图的强连通分量的Tarjan算法是以其发明者Robert Tarjan命名的。Robert Tarjan还发明了求双连通分量的Tarjan算法,以及求最近公共祖先的离线Tarjan算法,在此对Tarjan表示崇高的敬意。

============================================================================

伪码详解:

Tarjan(u)
{
Dfn[u]=Low[u]=++Time // 为节点u设定次序编号(发现时间)和Low初值
Stack.push(u) // 将节点u压入栈中
for each (u, v) in E // 枚举每一条边
if (!Dfn[v]) // 如果节点v未被访问过,因为是按时间戳来的,就直接用时间戳判断即可
tarjan(v) // 继续向下找
Low[u] = min(Low[u], Low[v]) // 找到栈最底下的可以是强连通分量的根(因为可能有多个)
else if (v in S) // 如果节点v还在栈内
Low[u] = min(Low[u], DFN[v]) // 如果在栈中找到S,说明栈内v到u一定是强连通分量,但要取最大的(栈最底下能满足的)
if (DFN[u] == Low[u]) // 如果节点u是强连通分量的根
repeat
v = S.pop // 将v退栈,为该强连通分量中一个顶点
print v
until (u== v)
}

我自己的代码(栈用STL):

stack<int> s;
int num = 0;
bool vis[MAXN];
void Tarjan(int r)
{
FF[r] = LL[r] = ++FT; //FF是Dfn,LL是Low
s.push(r); //压入
vis[r] = 1; //标志
for(int i = head[r]; i; i = next[i]) //我是用链式前向星的。具体实现看我另一篇博文
{
//v[i]表示以r为起点的边的v值,边(r,v)
if(!FF[v[i]]) //没有访问过
{
Tarjan(v[i]); //访问
LL[r] = min(LL[r], LL[v[i]]); //找到栈最底下的可以是强连通分量的根(因为可能有多个)
}
else if(vis[v[i]])
LL[r] = min(LL[r], FF[v[i]]); //如果在栈中找到S,说明栈内v到u一定是强连通分量,但要取最大的(栈最底下能满足的)
}
if(FF[r] == LL[r]) //如果是强连通的根,就输出了
{
int t;
num++;
do
{
t = s.top(); s.pop();
vis[t] = 0; //标记
}while(r != t); //一直循环下去
}
}

调用:

for(int i = 1; i <= N; i++)
if(!FF[i])
Tarjan(i);

缩点版(将强连通分量缩成一个点):

stack<int> s;
int p[MAXN]; //p[i]表示第i个节点所在的强连通分量
int num = 0;
bool vis[MAXN];
void Tarjan(int r)
{
FF[r] = LL[r] = ++FT; //FF是Dfn,LL是Low
s.push(r); //压入
vis[r] = 1; //标志
for(int i = head[r]; i; i = next[i]) //我是用链式前向星的。具体实现看我另一篇博文
{
//v[i]表示以r为起点的边的v值,边(r,v)
if(!FF[v[i]]) //没有访问过
{
Tarjan(v[i]); //访问
LL[r] = min(LL[r], LL[v[i]]); //找到栈最底下的可以是强连通分量的根(因为可能有多个)
}
else if(vis[v[i]])
LL[r] = min(LL[r], FF[v[i]]); //如果在栈中找到S,说明栈内v到u一定是强连通分量,但要取最大的(栈最底下能满足的)
}
if(FF[r] == LL[r]) //如果是强连通的根,就输出了
{
int t;
num++;
do
{
t = s.top(); s.pop();
p[t] = num; //将t点绑入第num个强连通分量中
vis[t] = 0; //标记
}while(r != t); //一直循环下去
}
}

调用是一样的。建新图的话,这样:

这只是建边而已,如果要权值什么的,也可以在弄一个链式前向星(自己习惯)。因为题目不需要,所以我只建边。

m[MAXN][MAXN]; //表示新图的边
for(i = 1; i <= M; i++)
m[p[u[i]]][p[v[i]]] = 1;

回归正题,本题的全部代码:

#include <iostream>
#include <stack>
#include <algorithm>
using namespace std;
//如果某个爱心天使被其他所有人或爱心天使所爱则请输出这个爱心天使是由哪些人构成的
//注意,还可以是某个爱心天使所爱(即找爱心天使是否为所有人的都可达,如果没有就输出-1,不是就不用输出)
const int MAXN = 1010, MAXE = 10010;
int ans = 0, nn = 0;
int p[MAXN], sum[MAXN], FF[MAXN], LL[MAXN], FT = 0;
int head[MAXN], u[MAXN], v[MAXE], next[MAXE];
bool m[MAXN][MAXN];
int N, M; stack<int> s;
bool vis[MAXN];
void Tarjan(int r)
{
FF[r] = LL[r] = ++FT;
s.push(r); //压入
vis[r] = 1;
for(int i = head[r]; i; i = next[i])
{
if(!FF[v[i]])
{
Tarjan(v[i]);
LL[r] = min(LL[r], LL[v[i]]);
}
else if(vis[v[i]])
LL[r] = min(LL[r], FF[v[i]]);
}
if(FF[r] == LL[r])
{
int t = s.top(); s.pop(); //先取第一个,因为只要2个以上节点的强连通图
p[t] = ++nn; //为后面构图准备
sum[nn] = 1; //新图每个节点代表节点数
vis[t] = 0; //出栈标志
if(t != r) //不是本节点,即有连通的
{
ans++; //建立一个爱心天使
//将这个节点绑定建立起的爱心天使
while(r != t) //一直循环下去
{
t = s.top(); s.pop();
sum[nn]++;
p[t] = nn; //缩点
vis[t] = 0; //标记
}
}
}
} //缩点后再判断是否为连通图 int main()
{
cin >> N >> M;
int i, j, k;
for(i = 1; i <= M; i++)
{
cin >> u[i] >> v[i];
next[i]=head[u[i]];
head[u[i]]=i;
}
for(i = 1; i <= N; i++)
if(!FF[i])
Tarjan(i);
cout << ans << endl; //输出天使数目
//建边
for(i = 1; i <= M; i++)
m[p[u[i]]][p[v[i]]] = 1;
//Floyd,判断连通图(数据小,能过)
for(k = 1; k <= nn; k++)
for(i = 1; i <= nn; i++)
for(j = 1; j <= nn; j++)
if(i!=j && m[i][k] && m[k][j]) //此处必须2条路都通,即i->k && k->j
m[i][j] = 1;
int ok = 1, no = 1;
for(i = 1; i <= nn; i++)
{
if(sum[i] == 1) continue;
ok = 1;
for(j = 1; j <= nn; j++) //看看每一个节点是否通i,因为前面初始化边时有本身,所以不用判断是否一条边
if(!m[j][i])
ok = 0;
if(ok)
{
no = 0;
for(j = 1; j <= N; j++)
if(p[j] == i)
cout << j << ' ';
cout << endl;
}
}
if(no) cout << "-1\n"; return 0;
}

【wikioi】2822 爱在心中的更多相关文章

  1. codevs 2822 爱在心中

    codevs 2822 爱在心中  时间限制: 1 s  空间限制: 128000 KB  题目等级 : 钻石 Diamond 题目描述 Description “每个人都拥有一个梦,即使彼此不相同, ...

  2. codevs——2822 爱在心中

    2822 爱在心中  时间限制: 1 s  空间限制: 128000 KB  题目等级 : 钻石 Diamond 题解       题目描述 Description “每个人都拥有一个梦,即使彼此不相 ...

  3. codevs 2822爱在心中

    不想吐槽题目.... /* K bulabula 算法(好像用哪个T bulabula更简单 然而我并不会 - -) 丑陋的处理cnt: Printf时 cnt中 ans[i][0]==1 的删掉 然 ...

  4. codevs2822 爱在心中

      2822 爱在心中 时间限制: 1 s 空间限制: 128000 KB 题目等级 : 钻石 Diamond 题目描述 Description “每个人都拥有一个梦,即使彼此不相同,能够与你分享,无 ...

  5. 【CodeVS】2822 爱在心中 [2017年6月计划 强连通分量03]

    2822 爱在心中 时间限制: 1 s 空间限制: 128000 KB 题目等级 : 钻石 Diamond         题目描述 Description “每个人都拥有一个梦,即使彼此不相同,能够 ...

  6. 【codevs2822】爱在心中 tarjan 缩点+理解

    [codevs2822]爱在心中 2014年1月26日5580 题目描述 Description “每个人都拥有一个梦,即使彼此不相同,能够与你分享,无论失败成功都会感动.爱因为在心中,平凡而不平庸, ...

  7. 【CodeVS 2822】爱在心中

    “每个人都拥有一个梦,即使彼此不相同,能够与你分享,无论失败成功都会感动.爱因为在心中,平凡而不平庸,世界就像迷宫,却又让我们此刻相逢Our Home.” 在爱的国度里有N个人,在他们的心中都有着一个 ...

  8. 爱在心中(codevs 2822)

    题目描述 Description “每个人都拥有一个梦,即使彼此不相同,能够与你分享,无论失败成功都会感动.爱因为在心中,平凡而不平庸,世界就像迷宫,却又让我们此刻相逢Our Home.” 在爱的国度 ...

  9. 习题:codevs 2822 爱在心中 解题报告

    这次的解题报告是有关tarjan算法的一道思维量比较大的题目(真的是原创文章,希望管理员不要再把文章移出首页). 这道题蒟蒻以前做过,但是今天由于要复习tarjan算法,于是就看到codevs分类强联 ...

随机推荐

  1. Sexagenary Cycle(天干地支法表示农历年份)

    Sexagenary Cycle Time Limit: 2 Seconds      Memory Limit: 65536 KB 题目链接:zoj 4669 The Chinese sexagen ...

  2. 讲解JS的promise,这篇是专业认真的!

    http://www.zhangxinxu.com/wordpress/2014/02/es6-javascript-promise-%E6%84%9F%E6%80%A7%E8%AE%A4%E7%9F ...

  3. 攻城狮在路上(壹) Hibernate(三)--- 属性访问、命名策略、派生属性、指定包名等

    一.hibernate访问持久化类属性的策略: 在<property>元素中的access属性用于指定Hibernate访问持久化类属性的方式. 常见的方式如下: 1.property:默 ...

  4. ASP.NET 自定义URL重写 分类: ASP.NET 2014-10-31 16:05 174人阅读 评论(0) 收藏

    一.功能说明: 可以解决类似 http://****/news 情形,Url路径支持正则匹配. 二.操作步骤: 1.增加URL重写模块: using System; using System.IO; ...

  5. Parallel.js初探

    今天闲着看了一下Parallel.js.这个库暂时貌似还没有什么中文的介绍(可能暂时用的人都不多吧).所以就只能上github找它得源码和介绍看看了.貌似它的代码也不多,以后可以深入研究一下. 先简单 ...

  6. [Spring] 事务级别定义

    记录下来,以后备用 //事务传播属性 @Transactional(propagation=Propagation.REQUIRED)//如果有事务,那么加入事务,没有的话新创建一个 @Transac ...

  7. Debian下安装vim

    问题描述:安装完系统以后,刚要打算开始写程序,发现,vim还没有装,用su -切换到root后 直接运行apt-get install vim,提示插入disc源,然后回车,陷入无法解决的状态. 上网 ...

  8. windows phone SDK 8.0 模拟器异常 0x89721800解决办法

    删除 APPDATA\LOCAL\Microsoft\Phone Tools\CoreCon\10.0 从新启动即可!

  9. Java8中的default方法

    default方法 Java 8中引入了一个新的概念,叫做default方法,也可以称为Defender方法,或者虚拟扩展方法(Virtual extension methods). Default方 ...

  10. Android LayoutInflater详解 (转)

    在实际开发中LayoutInflater这个类还是非常有用的,它的作用类似于findViewById().不同点是LayoutInflater是用来找res/layout/下的xml布局文件,并且实例 ...