开篇废话
关于本蒟蒻五一假期一直在研究tarjan算法,感觉弄懂了,发篇博客庆祝一下。
前置知识(懂的可以跳过)
图
都已经发两篇博客了,还没讲一下什么是图呢。
以下内容来自百度百科:
图论〔Graph Theory〕是数学的一个分支。它以图为研究对象。图论中的图是由若干给定的点及连接两点的线所构成的图形,这种图形通常用来描述某些事物之间的某种特定关系,用点代表事物,用连接两点的线表示相应两个事物间具有这种关系。
图G=(V,E)是一个二元组(V,E)使得E⊆[V]的平方,所以E的元素是V的2-元子集。
说人话就是,图是一个数学里研究的东西,数学里不是数,就是图,图由点和边构成且点数边数都是正整数。
联通
联通图:百度给的解释太复杂(我也没看懂),简单来说,在一个无向图里,任意两个点都可以相互到达,则称这个图是联通的。
强连通图:强连通是指一个有向图中任意两点v1、v2间存在v1到v2的路径及v2到v1的路径。一个点也是一个强连通图。
弱连通分量(不常考):如果一个非强连通分量图变成无向图后是一个连通图,则这个图是一个弱连通分量图。
强连通分量
定义
不是所有有向图都是强连通图,但一定存在这个图的子图中一定有强连通图,则这个字图就是原图的一个强连通分量。强连通图的强连通分量只有一个,任意有向图的强连通分量最多有点数那么多个。
作用
我们知道,在图论里,最让人厌烦的一样东西莫过于环,最短路的克星就是负环,最小生成树也是要生成一个没有环的图,在编程中处理环是很费劲的,所以SCC(强连通分量)就有用武之地了。
缩点
众所周知,强连通分量里的点都是彼此相互连通的(定义告诉我们的),所以,只要其中一个点具有某种性质(能被遍历……),该强连通分量里的所有点都一定能具有这一性质,所以,一个强连通分量和一个点是没有区别的,我们就可以将一个有向图看作很多个点组成的拓扑图(没有环的图),对这个图的操作就容易多了。
Tarjan
概述
tarjan算法的本质就是深搜,在深搜中维护某些值。
算法思路
我们首先引入一个叫做时间戳的概念,不要没这么高大尚的名字吓到,它的本质就是在这个图中第几个被访问的点。我们需要两个数组,一个叫dfn,一个叫low,分别表示这个点的时间戳和所有它能到达的点的时间戳的最小值。
我们从一个点开始搜索,把这个点压入栈中,修改low和dfn的值为时间戳。
从这个点开始遍历,如果他的dfn还是0,说明他还没被遍历过,就在tarjan这个点,用他的low值来更新自己的low值。
如果这个点还在队列里,就用他的dfn值来更新自己的low值
循环结束后,只要这个点的dfn值等于low值,说明这个点是一个强连通分量的起点,那就不断弹出栈顶元素,直到弹出的元素就是这个点,更新维护强连通分量的数组。
过程模拟
这样,所有点都被遍历过,开始深搜回溯
5的low值和dfn值相等,本身就是一个强连通分量
4还有一条连边指向2,2被遍历过,所以用2的dfn值来更新4的low值
回溯到3,用4来更新3
2能走到1,用1的dfn值来更新2
1没有边可以遍历了,dfn值等于low值
强连通分量全部被找到了
代码
void tarjan(int u)
{
dfn[u] = low[u] = ++ timestamp;//赋值
stk[ ++ top] = u, in_stk[u] = true;//频繁操作的时候习惯用手写栈
for (int i = h[u]; i != -1; i = ne[i])//用链式前向星存图
{
int j = e[i];
if (!dfn[j])
{
tarjan(j);//深搜
low[u] = min(low[u], low[j]);//更新
}
else if (in_stk[j]) low[u] = min(low[u], dfn[j]);//还在栈里就更新
}
if (dfn[u] == low[u])//判断是不是一个强连通分量
{
++ scc_cnt;//强连通分量计数器
int y;
do {
y = stk[top -- ];//出栈
in_stk[y] = false;
id[y] = scc_cnt;//保存结果
Size[scc_cnt] ++ ;
} while (y != u);
}
}
例题
受欢迎的牛
[题目描述]
每一头牛的愿望就是变成一头最受欢迎的牛。现在有N头牛,给你M对整数(A,B),表示牛A认为牛B受欢迎。 这种关系是具有传递性的,如果A认为B受欢迎,B认为C受欢迎,那么牛A也认为牛C受欢迎。你的任务是求出有多少头牛被所有的牛认为是受欢迎的。
输入
第一行两个数N,M。 接下来M行,每行两个数A,B,意思是A认为B是受欢迎的(给出的信息有可能重复,即有可能出现多个A,B)
输出
一个数,即有多少头牛被所有的牛认为是受欢迎的。
样例输入
3 3 1 2 2 1 2 3
样例输出
1
提示
10%的数据N<=20, M<=50
30%的数据N<=1000,M<=20000
70%的数据N<=5000,M<=50000
100%的数据N<=10000,M<=50000
思路分析
1.缩点,求强连通分量
2.统计拓扑图内每一个点(缩点后的点)的出度
3.用计数器记录出度不为零的点(强连通分量)的元素个数
4.如果有两个及以上的点出度等于零,计数器归零,跳出循环
AC代码
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 10010, M = 50010;
int n, m;
int h[N], e[M], ne[M], idx;
int dfn[N], low[N], timestamp;
int stk[N], top;
bool in_stk[N];
int id[N], scc_cnt, Size[N];
int dout[N];
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
void tarjan(int u)
{
dfn[u] = low[u] = ++ timestamp;
stk[ ++ top] = u, in_stk[u] = true;
for (int i = h[u]; i != -1; i = ne[i])
{
int j = e[i];
if (!dfn[j])
{
tarjan(j);
low[u] = min(low[u], low[j]);
}
else if (in_stk[j]) low[u] = min(low[u], dfn[j]);
}
if (dfn[u] == low[u])
{
++ scc_cnt;
int y;
do {
y = stk[top -- ];
in_stk[y] = false;
id[y] = scc_cnt;
Size[scc_cnt] ++ ;
} while (y != u);
}
}
int main()
{
scanf("%d%d", &n, &m);
memset(h, -1, sizeof h);
while (m -- )
{
int a, b;
scanf("%d%d", &a, &b);
add(a, b);
}
for (int i = 1; i <= n; i ++ )
if (!dfn[i])
tarjan(i);
for (int i = 1; i <= n; i ++ )
for (int j = h[i]; ~j; j = ne[j])
{
int k = e[j];
int a = id[i], b = id[k];
if (a != b) dout[a] ++ ;
}
int zeros = 0, sum = 0;
for (int i = 1; i <= scc_cnt; i ++ )
if (!dout[i])
{
zeros ++ ;
sum += Size[i];
if (zeros > 1)
{
sum = 0;
break;
}
}
printf("%d\n", sum);
return 0;
}
网络协议/学校网络
[题目描述]
一些学校连接在一个计算机网络上。学校之间存在软件支援协议。每个学校都有它应支援的学校名单(学校 a 支援学校 b,并不表示学校 b 一定支援学校 a)。当某校获得一个新软件时,无论是直接得到还是网络得到,该校都应立即将这个软件通过网络传送给它应支援的学校。因此,一个新软件若想让所有连接在网络上的学校都能使用,只需将其提供给一些学校即可。
任务
请编一个程序,根据学校间支援协议(各个学校的支援名单),计算最少需要将一个新软件直接提供给多少个学校,才能使软件通过网络被传送到所有学校;
如果允许在原有支援协议上添加新的支援关系。则总可以形成一个新的协议,使得此时只需将一个新软件提供给任何一个学校,其他所有学校就都可以通过网络获得该软件。编程计算最少需要添加几条新的支援关系。
输入
第一行是一个正整数 n,表示与网络连接的学校总数。 随后 n 行分别表示每个学校要支援的学校,即:i+1 行表示第 i 号学校要支援的所有学校代号,最后 0 结束。
如果一个学校不支援任何其他学校,相应行则会有一个 0。一行中若有多个数字,数字之间以一个空格分隔。
输出
包含两行,第一行是一个正整数,表示任务 a 的解,第二行也是一个正整数,表示任务 b 的解。
样例输入
5 2 4 3 0 4 5 0 0 0 1 0
样例输出
1 2
提示
2≤n≤100
思路分析
1.缩点,求强连通分量
2.统计拓扑图内每一个点(缩点后的点)的出度和入度
3.输出的第一行即为入度不为零的个数
4.如果只有一个强连通分量,第二行输出0
5.输出的第二行即为出度入度的最大值
AC代码
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 110, M = 10010;
int n;
int h[N], e[M], ne[M], idx;
int dfn[N], low[N], timestamp;
int stk[N], top;
bool in_stk[N];
int id[N], scc_cnt;
int din[N], dout[N];
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
void tarjan(int u)
{
dfn[u] = low[u] = ++ timestamp;
stk[ ++ top] = u, in_stk[u] = true;
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
if (!dfn[j])
{
tarjan(j);
low[u] = min(low[u], low[j]);
}
else if (in_stk[j])
low[u] = min(low[u], dfn[j]);
}
if (dfn[u] == low[u])
{
++ scc_cnt;
int y;
do {
y = stk[top -- ];
in_stk[y] = false;
id[y] = scc_cnt;
} while (y != u);
}
}
int main()
{
cin >> n;
memset(h, -1, sizeof h);
for (int i = 1; i <= n; i ++ )
{
int t;
while (cin >> t, t) add(i, t);
}
for (int i = 1; i <= n; i ++ )
if (!dfn[i])
tarjan(i);
for (int i = 1; i <= n; i ++ )
for (int j = h[i]; j != -1; j = ne[j])
{
int k = e[j];
int a = id[i], b = id[k];
if (a != b)
{
dout[a] ++ ;
din[b] ++ ;
}
}
int a = 0, b = 0;
for (int i = 1; i <= scc_cnt; i ++ )
{
if (!din[i]) a ++ ;
if (!dout[i]) b ++ ;
}
printf("%d\n", a);
if (scc_cnt == 1) puts("0");
else printf("%d\n", max(a, b));
return 0;
}
这是我的第三篇文章,如有纰漏也请各位大佬指正
辛苦创作不易,还望看官点赞收藏打赏,后续还会更新新的内容。