题目链接

算法: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. Easy UI 面板

    驾考园 http://www.jiakaoyuan.com 驾考园信息网 下载(源码)

  2. OCJP(1Z0-851) 模拟题分析(四)over

    Exam : 1Z0-851 Java Standard Edition 6 Programmer Certified Professional Exam 以下分析全都是我自己分析或者参考网上的,定有 ...

  3. [荐]javascript Date format(js日期格式化)

    cnblog:http://www.cnblogs.com/zhangpengshou/archive/2012/07/19/2599053.html 方法一: // 对Date的扩展,将 Date  ...

  4. hdu 5833 Zhu and 772002 高斯消元

    Zhu and 772002 Problem Description Zhu and 772002 are both good at math. One day, Zhu wants to test ...

  5. loj 1036(dp)

    题目链接:http://acm.hust.edu.cn/vjudge/problem/viewProblem.action?id=25913 思路:易证存在一条从左上角到右下角的折线,沿着格子边缘的. ...

  6. 智能车学习(一)—— 硬件准备

    一.硬件环境准备 1.烧写器焊接 (1)原理图: (2)焊接成品图 2.电源模块: 3.屏幕+键盘 4.单片机 二.软件环境准备 1.安装air6.7 2.安装JLINK驱动 3.安装软件包寄存在GI ...

  7. 【项目经验】之——Controller向View传值

    我们的ITOO进行了一大部分了,整体上来说还是比较顺利的.昨天进行了一次验收,大体上来说,我们新生这块还是可以的.不仅仅进行了学术上的交流,还进行了需求上的更新.也正是由于这一次,我有了解到了一个新的 ...

  8. Jmeter 检查点

    Jmeter的检查点就是插入个断言,但用下来不好用,没LR好用,先放放.

  9. 浅谈C++多态性

    本文转载至http://blog.csdn.net/hackbuteer1/article/details/7475622 总结: (1)区分概念:    重载----同一个类中,相同的函数名字,不同 ...

  10. [转载]void及void*的深度剖析

    void的含义 void即"无类型",void *则为"无类型指针",可以指向任何数据类型. void指针使用规范 ①void指针可以指向任意类型的数据,亦即可 ...