题目链接

算法: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. Delphi的枚举类型

    参考:http://blog.csdn.net/kissdeath/article/details/2060573 Delphi程序不仅可以用于数值处理,还更广泛的用于处理非数值的数据.例如:性别.月 ...

  2. at org.apache.jsp.index_jsp._jspInit(index_jsp.java:22) 报空指针

    原来发布到weblogic 的项目,想改动发布到tomcat上.启动发布一切都正常.出入项目请求路径却包错: java.lang.NullPointerException at org.apache. ...

  3. poj 1195:Mobile phones(二维线段树,矩阵求和)

    Mobile phones Time Limit: 5000MS   Memory Limit: 65536K Total Submissions: 14391   Accepted: 6685 De ...

  4. 【openGL】画直线

    #include "stdafx.h" #include <GL/glut.h> #include <stdlib.h> #include <math ...

  5. 解决mysql无法插入中文数据及插入后显示乱码的问题

    (1)废话不多说就是使用mysql数据库的时候无法输入中文,可以输入中文后显示的又是乱码!! (2开始解决问题: 第一步:找到安装mysql的目录找到 my.ini 文件: 第二步:使用记事本打开my ...

  6. phpcms v9 黄页实现手机访问手机版,电脑访问电脑版(双模板)

    第一步.模板文件夹下,yp复制一份,改名字 ypwap 第二步.修改phpcms/modules/yp/index.php和phpcms/modules/ypwap/index.php //判断客户端 ...

  7. WPF QuickStart系列之样式和模板(Style and Template)

    在WPF桌面程序中,当我们想构建一个统一的UI表现时(在不同操作系统下,显示效果一致),此时我们就需要使用到WPF中的样式和模板技术.简单来说,如果我们需要简单的给一个Button设置宽,高,Marg ...

  8. C语言里面捕获错误机制

    在C语言中异常处理一般有这么几种方式: 1.使用标准C库提供了abort()和exit()两个函数,它们可以强行终止程序的运行,其声明处于<stdlib.h>头文件中. 2.使用asser ...

  9. java杂记——数组拷贝

    这里介绍两种java提供的数组拷贝方法: (1)Arrays提供的copyOf(T src, T desLength)和copyOfRange(T src, int from, int to) (2) ...

  10. 小甲鱼PE详解之IMAGE_OPTIONAL_HEADER32 结构定义即各个属性的作用(PE详解03)

    咱接着往下讲解IMAGE_OPTIONAL_HEADER32 结构定义即各个属性的作用! (视频教程:http://fishc.com/a/shipin/jiemixilie/) 接着我们来谈谈 IM ...