tarjan算法,桥判断,割点判断

算法背景

Tarjan算法是由Robert Tarjan(罗伯特·塔扬,不知有几位大神读对过这个名字) 发明的求有向图中强连通分量的算法。

预备知识点

有向图强连通分量:在有向图G中,如果两个顶点vi,vj间(vi>vj)有一条从vi到vj的有向路径,同时还有一条从vj到vi的有向路径,则称两个顶点强连通(strongly connected)。如果有向图G的每两个顶点都强连通,称G是一个强连通图。有向图的极大强连通子图,称为强连通分量(strongly connected components)。

搜索树:将每个顶点只访问一次形成的树,如果图不连通就形成森林。
eg:
在这里插入图片描述
画成搜索树后
在这里插入图片描述
DFN[]数组,查找到当前节点的时间戳,这个数值是不能变的。
LOW[]数组,能够通过不在搜索树上的边回到最早的父节点的时间戳。不断的更新,使其一直是最小的。
visited[]数组,标记数组,如果标记代表这个节点在当前连通分量里面。后面提到的标记都是使用这个数组。

算法过程

通过dfs来递归查找子节点标记并压入栈s中,直到无法找到新的节点,然后判断LOW[v]是不是等于DFN[v]。为什么呢?上面说了LOW[v]是当前节点能够连通的最早的父节点,而DFN[v]是时间戳,也就是被深搜到的时间,那二者相等说明了这个点是一个强连通分量的根节点。
上面说到这条路已经走不下去了,那么就在v!=s.top()的条件下不断地pop,并且把标记取消,表明这个节点不在栈里面了。然后再回到上一层继续深搜。不断地进行下去
来一发图解

在这里插入图片描述
初始:ans = 0;
从1节点开始深搜 DFN[1] = LOW[1] = ++ans = 1
1搜索到2 DFN[2] = LOW[2] = ++ans = 2,如图
在这里插入图片描述
然后2节点深搜到3
所以DFN[3] = LOW[3] = ++ans = 3, 如图
在这里插入图片描述
3查找到6节点,所以
DFN[6] = LOW[6] = ++ans = 4, 如图
在这里插入图片描述
然后发现6节点是死路了,这时候判断发现DFN[6] == LOW[6], 所以直接输出6. 回退到3,判断LOW[3] 和LOW[6]的大小,发现LOW[3]小,所以LOW[3]不变,搜索发现也没有点了,再输出3。

回退到2,判断LOW[2]和LOW[3],LOW[2]小,所以LOW[2]值不变。
从2再次搜索到5
DFN[5] = LOW[5] = ++ans = 5, 如图
在这里插入图片描述
5先搜索到6,发现6的DFN值不为0,也就是已经搜索过了,然后又发现6并没有被标记过,那就说明这个节点属于其他连通分量,不再理它。
然后查找到了1,同样1的DFN值也不为0,但是1还在被标记的状态中,说明1属于当前这个连通分量。所以直接进行判断DFN[1]和LOW[5]的值谁的小。然后LOW[5]变为1,说明它现在不经过搜索树的父子边能够向上连通的最早的分量是1。
如图所示
在这里插入图片描述
这时候回退到2,然后判断LOW[2]和LOW[5]的值,更新LOW[2]值为1。2已经没可以搜索的点了,就判断LOW[2]和DFN[2]相不相等,不相等,证明2不是当前连通分量根节点,退回到1。

1又搜索到了4,DFN[4] = LOW[4] = ++ans = 6,
然后4搜索到了5,5的DFN值不为零并且5还在标记中。所以判断DFN[5]和LOW[4]的大小,修改LOW[4]为5。
回退到1,判断LOW[1]和LOW[4]的大小关系。然后判断LOW[1]和DFN[1]相不相等,发现相等,执行出栈操作。

整个过程搜索完毕。

代码实现

测试数据:
输入:

6 8
1 2
2 3
3 6
2 5
5 6
5 1
1 4
4 5
输出:
6
3
4 5 2 1

#include <iostream>
#include <vector>
#include <stack>
#include <cstring>

using namespace std;

const int MAX_V = 500001;

int DFN[MAX_V], visited[MAX_V], LOW[MAX_V];
int n, m;

struct edge
{
	int to;
};

vector<edge>G[MAX_V];

void add_edge(int from, int to)
{
	G[from].push_back((edge){to});
}

stack<int> s;
int ans = 0;

bool tarjan(int v)
{
	s.push(v);
	visited[v] = 1;
	DFN[v] = LOW[v] = ++ans;
	
	for (int i=0; i<G[v].size(); i++)
	{
		edge &e = G[v][i];
		
		if (!DFN[e.to])
		{
			tarjan(e.to);
			LOW[v] = min(LOW[v], LOW[e.to]);
		}
		else if (visited[e.to])
		{
			LOW[v] = min(LOW[v], DFN[e.to]);
		}
	}
	
	if (DFN[v] == LOW[v])
	{
		int num = 0, temp;
		
		do
		{
			num++;
			temp = s.top();
			cout << temp << " ";
			s.pop();
			visited[temp] = 0;
		}while (v != temp);
		
		cout << endl;
	}
}

int main()
{
	while (scanf("%d %d", &n, &m) && (n+m))
	{
		memset(visited, 0, sizeof(visited));
		memset(DFN, 0, sizeof(DFN));
		memset(LOW, 0, sizeof(LOW));
		for (int i=1; i<=n; i++)
		{
			G[i].clear();
		}
		
		for (int i=0; i<m; i++)
		{
			int x, y;
			
			scanf("%d %d", &x, &y);
			add_edge(x, y);
		}
		
		for (int i=1; i<=n; i++)
		{
			if (!DFN[i])
			{
				tarjan(i);
			}
		} 
		
	} 
		
	return 0;
}

桥的判断

下面就到了我们的桥的判断,什么是桥呢?
如果一张无向图图去掉一条边后分成两个图,那这个边就是一个桥。
Eg:
在这里插入图片描述
显而易见边<4, 5>就是一个桥。
从图上我们能够直观的判断出一个边是不是桥,那怎么用tarjan算法去判断它是不是一个桥呢?
很简单,对于边<v, u>。只要满足 DFN[v] < LOW[u]就证明这条边是一个桥。为什么呢?根据两个数组的定义,LOW[u] > DFN[v]说明了u点在不通过<v, u>这条边的情况下,是无论如何也无法到达v点的。所以这两个点一定在两个连通分量里面,因此他们俩的边一定是一个桥。
由于这是一个无向图,我们一定会搜索到一个节点的父节点,我们又不能通过这条边来更新LOW值,这要怎么解决呢?
存图的时候把双向边存放在一起,也就是偶数如2,4,6,等位置放正向边,而3,5,7等位置放反向边, 并且2和3是互为反向边。在tarjan函数参数列表里多传一个参数,为父节点和当前节点边的值fa,当子节点搜索到一个点后,如果这个点被搜索过了,也就是DFN不为零了,进行判断i和fa的关系,如果i==fa^1的话,fa就是i的父节点,不理他。如果不是就更新LOW[v]的值。
对于^运算,能够快捷的判断这两条边是不是一对反向边。
如果n是偶数,那么n ^ 1的结果为n+1,。
如果n是奇数,那么n^1就是n-1。正好是一对一对的关系。

下面还是说一说存图的技巧吧,如果你看了上一个算法你会发现,我根本没有用到网上的head数组。就我个人理解而言,如果这个算法不需要边的作用,就使用上面的方法就行,简单易懂。
这个桥的判定就不一样了,总是用到边的关系,就需要我们使用head数组进行存图的边值,也就是给每条边一个编号。具体head数组怎么用呢?看代码。
在add_edge函数中是这样写的

void add_edge(int from, int to)
{
	G[++len].to = to;
	G[len].next = head[from];
	Head[from] = len;
}

在具体的查找图的算法中是这样使用的

for (int i=head[v]; i; i=G[i].next)

接下来就是查找桥的算法了。

#include <iostream>
#include <cstring>

using namespace std;

const int MAX_V = 1005;
int DFN[MAX_V], LOW[MAX_V], head[MAX_V*MAX_V], bridge[MAX_V*MAX_V];
int len = 1, ans = 0;

struct edge
{
	int y, next, cost;
}G[MAX_V*MAX_V];

void add_edge(int from, int to)
{
	G[++len].y = to;
	G[len].next = head[from];
	head[from] = len;
}

void tarjan(int v, int v_bridge)
{
	DFN[v] = LOW[v] = ++ans;
	
	for (int i=head[v]; i; i=G[i].next)
	{
		int to = G[i].y;
		
		if (!DFN[to])
		{
			tarjan(to, i);
			LOW[v] = min(LOW[v], LOW[to]);
			
			if (DFN[v] < LOW[to])
			{
				bridge[i] = bridge[i^1] = 1;
			}	
		}
		else if (i != (v_bridge^1))
		{
			LOW[v] = min(LOW[v], DFN[to]);
		}
	}
}

int main()
{
	int n, m;

	cin >> n >> m;

	for (int i=0; i<m; i++)
	{
		int x, y;

		cin >> x >> y;

		add_edge(x, y);
		add_edge(y, x);
	}

	for (int i=1; i<=n; i++)
	{
		if (!DFN[i])
		{
			tarjan(i, 0);
		}
	}

	for (int i=2; i<len; i+=2)
	{
		if (bridge[i])
		{
			cout << G[i^1].y << " " << G[i].y << endl;
		}
	}

	return 0;
}

对于上图例子

输入:
8 9
1 2
2 3
3 4
4 1
4 5
5 6
6 7
7 8
8 5
输出:
4 5

例题

一道桥判断的题。

地址

hei,ha

题目大意

在赤壁战役中,曹操被诸葛亮和周瑜击败。但是他不会放弃。曹操的部队仍不擅长水战,因此他想出了另一个主意。他在长江上建了许多岛屿,在这些岛屿的基础上,曹操的军队可以轻松地攻击周瑜的部队。曹操还修建了连接各岛的桥梁。如果所有岛屿都通过桥梁连接起来,那么曹操的军队可以很方便地在这些岛屿之间部署。周瑜不能忍受,所以他想摧毁一些草桥,以使一个或多个岛屿与其他岛屿分开。但是周瑜只有诸葛亮留下的一颗炸弹,所以他只能摧毁一座桥。周瑜必须派人携带炸弹摧毁这座桥。桥梁上可能会有警卫。轰炸队的士兵人数不得少于桥梁的守卫人数,否则任务将失败。请至少弄清楚周瑜必须派多少士兵来完成离岛任务。

解题思路

很明显就是用桥判断来进行做题,但需要注意的是,如果给出的图不是一个完整的图,直接输出0,如果桥上一个敌人也没有,输出1,因为至少有一个人要去炸桥。不要去掉重边,没说两座岛之间只允许有一座桥。

AC代码

#include <iostream>
#include <cstring>

#define mem(a) memset(a, 0, sizeof(a))

using namespace std;

const int MAX_V = 1005;
int DFN[MAX_V], LOW[MAX_V], head[MAX_V*MAX_V], bridge[MAX_V*MAX_V];
int len = 1, ans = 0;

struct edge
{
	int y, next, cost;
}G[MAX_V*MAX_V];

void add_edge(int from, int to, int cost)
{
	G[++len].y = to;
	G[len].next = head[from];
	G[len].cost = cost;
	head[from] = len;
}

void tarjan(int v, int v_bridge)
{
	DFN[v] = LOW[v] = ++ans;
	
	for (int i=head[v]; i; i=G[i].next)
	{
		int to = G[i].y;
		
		if (!DFN[to])
		{
			tarjan(to, i);
			LOW[v] = min(LOW[v], LOW[to]);
			
			if (DFN[v] < LOW[to])
			{
				bridge[i] = bridge[i^1] = 1;
			}	
		}
		else if (i != (v_bridge^1))
		{
			LOW[v] = min(LOW[v], DFN[to]);
		}
	}
}

int main()
{
	ios::sync_with_stdio(false);
	
	int n, m;
	
	while (cin >> n >> m && (n+m))
	{
		ans = 0, len = 1;
		mem(DFN);
		mem(LOW);
		mem(head);
		mem(bridge);
		
		for (int i=0; i<m; i++)
		{
			int x, y, cost;
			
			cin >> x >> y >> cost;
			add_edge(x, y, cost);
			add_edge(y, x, cost);
		}
		
		int temp = 0;
		
		for (int i=1; i<=n; i++)
		{
			if (!DFN[i])
			{
				temp++;
				tarjan(i, 0);
			}
		}
		
		if (temp > 1)
		{
			cout << 0 << endl;
		}
		else
		{
			int minnum = 0x3f3f3f3f;
			bool flag = true;
			
			for (int i=2; i<len; i+=2)
			{
				if (bridge[i] && G[i].cost < minnum)
				{
					minnum = G[i].cost;
					flag = false;
				}
			}
			
			if (!flag)
			{
				if (minnum != 0)
					cout << minnum << endl;
				else 
					cout << 1 << endl;
			}
			else
				cout << "-1" << endl;
		}
	}
	
	return 0;
} 

割点判断

什么是割点呢?在一个无向图中,如果有一个顶点集合,删除这个顶点集合以及这个集合中所有顶点相关联的边以后,图的连通分量增多,就称这个点集为割点集合。如果某个割点集合只含有一个顶点X(也即{X}是一个割点集合),那么X称为一个割点。
割点是怎么判断的呢?上面我们知道了桥的判断方法是LOW[to] > DFN[from],根据割点的定义可以知道它是可以回到回到父节点的,所以就是LOW[to] >= DFN[from]。对于割点来说不用担心重边以及父节点问题,因为这并不影响它作为割点的性质,那么在DFN[to]不等于0时就可以直接更新LOW[from]值。需要注意的是,如果from节点是搜索树根节点的话,会至少有两个节点满足条件,所以需要判断一下
割点是不需要边的关系的,直接使用最开始的vector存图就可以了。

#include <iostream>
#include <vector>
#include <queue>

using namespace std;

const int MAX_V = 100005;

bool cut[MAX_V];
int DFN[MAX_V], LOW[MAX_V];
int len = 1;

struct edge
{
	int to;
};

vector<edge> G[MAX_V];

void add_edge(int from, int to)
{
	G[from].push_back((edge){to});
}

int ans = 0;
int root;

void tarjan(int v)
{
	DFN[v] = LOW[v] = ++ans;
	
	int flag = 0;
	
	for (int i=0; i<G[v].size(); i++)
	{
		int to = G[v][i].to;
		
		if (!DFN[to])
		{
			tarjan(to);
			LOW[v] = min(LOW[to], LOW[v]);
			
			if (LOW[to] >= DFN[v])
			{
				flag++;
				
				if (flag > 1 || v != root)
					cut[v] = true;
			}
		}
		else
		{
			LOW[v] = min(LOW[v], DFN[to]);
		}
	}
}

int main()
{
	int n, m;
	
	cin >> n >> m;
	
	for (int i=0; i<m; i++)
	{
		int x, y;
		
		cin >> x >> y;
		add_edge(x, y);
		add_edge(y, x);
	}
	
	for (int i=1; i<=n; i++)
	{
		if (!DFN[i])
		{
			root = i;
			tarjan(i);
		}
	}
	
	int num = 0;
	
	for (int i=1; i<=n; i++)
	{
		if (cut[i])
		{
			num++;
		}
	}
	
	cout << num << endl;
	
	for (int i=1; i<=n; i++)
	{
		if (cut[i])
		{
			cout << i << " ";
		}
	}
	
	return 0;
}

例题

洛谷3388
比较简单就不打ac代码了。

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值