Tarjan算法的应用---缩点与割点

news2024/12/24 9:11:22

   图论中有时候会涉及到一些连通性问题,主要是针对于点来说,在有向图中有时候需要计算强连通分量,这时候代表分量的的点就非常重要;在无向图中有时候会需要知道割点,用到的算法都是Tarjan,这个算法还是有难理解(我是这么觉得)。

简单介绍

Tarjan主要基于深搜,其中有两个非常关键的标记数组,分别是dfn和low,同时引入概念时间戳tt,也就是到达这个点的时间,实际上就是搜索到的次序,dfn记录每个点的时间戳,即第一次访问到的号次,low也就是能到达的最早的时间戳,下面分析。

缩点

题目来源:【模板】缩点 - 洛谷

    这一道题题意比较明显,就是你去走出一条路,点权之和最大,可以随便走,但是重复的点只算一次。

    思考了就知道,如果有那么一个环(环就是1->2->3->1这样的)在,那么一定是要把环上面的点全部拿来的,因为你可以走一圈然后回到原来的地方,那么何乐而不为呢?于是我们可以把一个环的权值全部加到一点,然后重新建一张图,此时的图就是有向无环图,后面利用拓扑排序和dp,可以确定最大的权值。

    如何缩点是这一题的重头戏,Tarjan算法基于深搜,每次会一股脑一直向下走,可以料想如果走到一个点发现走过了,那么再走上去是不是就成了一个环。

    引入dfn数组存储时间戳,low存储这个点可以到达的点的最早时间戳,栈st存储点的情况,用于统计哪些点是在环上的,vis用于当前这一轮找点的情况。

    得到一个点x,首先先入栈,然后将点的dfn和low均初始化为时间戳tt,然后开始查看与这个点相连的点,假设是to,如果这个点还没被访问过,也就是时间戳还是0,那么继续深搜,然后更新这个点的low值,也就是low[x]=min(low[x],low[to]);如果时间戳不是0,那么就看vis看是不是访问过,然后还是执行上面句子,为什么是这样呢,因为第一种是没有访问过,那么这个点的low值你是不知道的,所以无法更新,而访问过的可以直接得到值。在更新完所有与这个点连接的点之后就得到了这个点最终的low值。

    在一轮更新之后,此时有着相同low值的就构成了一个强联通图,其中任意两点都可以互相达到。为什么?因为我们在更新时遇到访问过的点会停止搜索,那么遇到访问过的那么这个点的时间戳一定早于我走来到达这个点的这一条环(深搜是一条边走到黑)任意一点的时间戳,在回溯进行low[x]=min(low[to],low[x])的操作之后,这一条环上的所有low值都被置为了访问到的那个点的,一条环一定是强连通图,因为更新的过程中有许多交叉,其实都会被置为最小的那个,所以最终一个low相同的图可能由多个环组成,环组成的还是强连通图。

    因为相同low值的是一个强连通图,而这个low值其实就是这个连通图最早被搜索到的值,我们就可以把这个点,也就是low[x]=dfn[x]的点作为这个强连通图的代表点,把这个连通图缩到这个点,至于如何找到这个代表点所代表的的块的所有节点就利用栈,一直弹到这个代表点出栈为止(因为这个点的low最小,所以最早进入栈)。

程序如下:

void tarjan(int x)
{
	vis[x] = 1;
	st.push(x);
	low[x] = dfn[x] = ++tt; // 时间戳初始化
	for (int i = last1[x]; i; i = e1[i].next)
	{
		int to = e1[i].to;
		//双层次判断,dfn是全局标记,vis是当前轮标记
		//dfn=0表示这个点还没有被纳入任何环,也没走过,这时候需要继续往下走,找完更新(回溯更新)
		//vis!=0表示当前轮走过这个点然后又走到了,说明走出了一个环,那么直接更新,不用再找 
		if (!dfn[to]) //这个点还没有时间戳,走下去
		{
			tarjan(to);
			low[x] = min(low[to], low[x]); //回溯的时候的更新
		}
		else if(vis[to])
			low[x] = min(low[to], low[x]); //走到了一个点这一轮已经被访问过了,这就说明走出一个圈了
	}
	if (low[x] == dfn[x])//说明是关键点,关键点权重就是整个环的权重(把换上其他点权重加上来)
	{
		int tmp;
		while (!st.empty())
		{
			tmp = st.top();
			st.pop();
			//printf("x=%d tmp=%d\n", x, tmp);
			vis[tmp] = 0;	    //清除标记,因为是每一轮的标记(每次找环要清除标记)
			squ[tmp] = x;
			if (x == tmp)break;//表示到了环的根节点 
			p[x] += p[tmp];    //缩点
		}
	}
}

这个过程比较抽象,举个例子,简单起见如下图所示:

从1进入程序:

1入栈,dfn[1]=low[1]=1,1->2,因为dfn[2]=0,可以搜索2;

2入栈,dfn[2]=low[2]=2,2->1,3,先去1,发现dfn[1]!=0,已经搜过,那么直接更新low[2]=min(low[2],low[1])=1

再看3,因为dfn[3]=0,可以搜索3;

3入栈,dfn[3]=low[3]=3,3->无,不能继续搜索;

判断发现dfn[3]=low[3],所以可以作为一个强连通图,开始从栈弹出点,弹出3,3=3结束,找到第一个分量3;

回溯,回到2,更新low[2]=min(low[2],low[3])=1,不变,不能继续搜索;

判断发现dfn[2]=2,low[2]=1,不相等;

回溯,回到1,

发现dfn[1]=low[1]=1,可以作为一个强连通图,开始从栈弹出点,弹出2,1,1=1结束,找到第二个连通分量1,2。、

在弹栈的时候可以把权重都加到代表点上,就可以完成缩点操作。

    接下去因为要计算权值最大,我们在缩点之后重新建图,就可以得到一张新的无环有向图,利用dis[x]=min(dis[x],dis[to]+p[x])加上拓扑排序就可以计算出答案。

完整代码:

#include<stdio.h>
#include<algorithm>
#include<stack>
#include<queue>
#define Inf 0x3f3f3f3f
#define N 11000
#define M  110000
using namespace std;
int n, m, p[N];
bool vis[N];
int low[N], dfn[N],tt;//最小时间戳,当前时间戳,时间戳
int squ[N];//存储每个点所属的连通块的关键点
int du[N];//存储每个点的入度
int dis[N];//存储每个点的大小
stack<int>st;//存储暂时的答案序列(一个环的)
queue<int>q;//topo排序会用到
struct Edge
{
	int next, from, to;
}e1[M*5],e2[M*5];
int last1[N], last2[N], cnt1,cnt2;

void add(int from, int to, Edge e[],int last[],int &cnt)
{
	e[++cnt].to = to;
	e[cnt].from = from;
	e[cnt].next = last[from];
	last[from] = cnt;
}

 //tarjan算法本质就是找出一个个圈,因为只要一走到走过的点就形成一个环,此时是强连通图,可以缩成一个点
void tarjan(int x)
{
	vis[x] = 1;
	st.push(x);
	low[x] = dfn[x] = ++tt; // 时间戳初始化
	for (int i = last1[x]; i; i = e1[i].next)
	{
		int to = e1[i].to;
		//printf("x=%d to=%d last[x]=%d\n", x, to,last1[x]);
		//双层次判断,dfn是全局标记,vis是当前轮标记
		//dfn=0表示这个点还没有被纳入任何环,也没走过,这时候需要继续往下走,找完更新(回溯更新)
		//vis!=0表示当前轮走过这个点然后又走到了,说明走出了一个环,那么直接更新,不用再找 
		if (!dfn[to]) //这个点还没有时间戳,走下去
		{
			tarjan(to);
			low[x] = min(low[to], low[x]); //回溯的时候的更新
		}
		else if(vis[to])
			low[x] = min(low[to], low[x]); //走到了一个点这一轮已经被访问过了,这就说明走出一个圈了
	}
	if (low[x] == dfn[x])//说明是关键点,关键点权重就是整个环的权重(把换上其他点权重加上来)
	{
		int tmp;
		while (!st.empty())
		{
			tmp = st.top();
			st.pop();
			//printf("x=%d tmp=%d\n", x, tmp);
			vis[tmp] = 0;	    //清除标记,因为是每一轮的标记(每次找环要清除标记)
			squ[tmp] = x;
			if (x == tmp)break;//表示到了环的根节点 
			p[x] += p[tmp];    //缩点
		}
	}
}

 //topo排序+dp
int topo()
{
	for (int i = 1; i <= n; i++)
		if (squ[i] == i && du[i] == 0) //关键点入队
		{
			q.push(i);
			dis[i] = p[i];
		}
	while (!q.empty())
	{
		int x = q.front();
		q.pop();
		for (int i = last2[x]; i; i = e2[i].next)
		{
			int to = e2[i].to;
			du[to]--;
			dis[to] = max(dis[to], dis[x] + p[to]);
			if (du[to] == 0)q.push(to); //度为0入队
		}
	}
	int ans = 0;
	for (int i = 1; i <= n; i++)
		ans = max(ans, dis[i]);
	return ans;
}

int main()
{
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= n; i++)
		scanf("%d", p + i);
	int x, y;
	for (int i = 1; i <= m; i++)
	{
		scanf("%d%d", &x, &y);
		add(x, y, e1, last1, cnt1); // 全局变量传进去也只是形参
	}
	for (int i = 1; i <= n; i++)
		if (!dfn[i]) //没有时间戳代表是没访问过
			tarjan(i);
	for (int i = 1; i <= m; i++)
	{
		int x = squ[e1[i].from];
		int y = squ[e1[i].to];
		if (x != y) // 去除自环
		{
			add(x, y, e2, last2, cnt2);
			du[y]++;
		}
	}
	int ans = topo();
	printf("%d\n", ans);
	return 0;
}

割点

题目来源:【模板】割点(割顶) - 洛谷 

    割点就是在无向图中,一个点去掉了,图就不再连通了,那么这个点就是割点。

    那么如何寻找割点,实际上也是利用tarjan算法, 各种定义类似于上题,不过因为在无向图中,任意连通块总是强连通图,这就使得上题定义的low失去了意义,因为只要连通,那么最终所有节点的low值均相等,所以在这里的low的更新方式稍稍改变,也就是程序中的low[x] = min(low[x], dfn[to])。

    割点有两种情况,假设有下面这么一张图:

    第一种是对于根节点(最开始的点,一个连通块仅一个,随便设置),因为深搜每次会搜索完一块与根节点相连的,要是根节点连接了两块或更多,那么这个根节点就是割点。假设1是根节点,那么第一次搜索完2、3,第二次搜索完4、5,有两块,所以1是割点。

    第二种情况是像2不是根节点的,这种就需要判断与他相连的点能不能不通过这个点到达更早的点,比如3只能通过2到达1,而5除了通过4,还可以通过6。

    设定根节点root,x点进入,先初始化low和dfn为时间戳,开始检查所有相连点to,如果dfn!=0也就是已经访问过,那么直接更新low[x] = min(low[x], dfn[to]),为什么是dfn[to]而不是low[to],首先无向图都是双向边,要是low[to]那么全都是一样了,这样更新之后low存储了所有直接相连边中的时间戳最小值,也是为了下面的判断;如果dfn=0也就是没访问过,那么继续搜索,计算出low[to]后更新low[x]=min(low[x],low[to]),要是low[to]>=dfn[x]的话,就说明下一个点找遍也无法找出比x更早出现的点,所以x为割点。

部分代码:

void tarjan(int x,int root)
{
	//printf("x=%d\n", x);
	int child = 0;
	low[x] = dfn[x] = ++tt;
	for (int i = last[x]; i; i = e[i].next)
	{
		int to = e[i].to;
		if (!dfn[to]) //这个点还没有被找过,那么继续找,可能会找到更早的
		{
			tarjan(to, root);
			low[x] = min(low[to], low[x]);//更新当前值,如果能找到更早的,传给x
			//printf("low[%d]=%d dfn[%d]=%d\n", to,low[to],x,dfn[x]);
			if (low[to] >= dfn[x] && x != root)
				//相当于说把下一个点to找遍了相连的,找不到一个直接与to相连的点比x更早出现
				flag[x] = 1;
			if (x == root)child++;//child就是一个个块           
			//根节点遍历一次找一个块,这个块与其他与根节点相连的块只能通过根节点相连(因为一旦dfn!=0就不tarjan了)
			/* example: 
			2  1  3  5  6 
			      4
			*/
			//假设3是根节点,3第一次找把21找了,child=1,第二次找了56,child=2,,第三次找了4,child=3>2,所以3是割点
		}
		low[x] = min(low[x], dfn[to]);
		//这个地方一定要注意!如果把dfn写做low的话那么一个连通块的low实际上都是根节点low值
		//low[x]始终存储与x相连的最早出现的时间戳
	}
	if (x == root && child >= 2)
		flag[root] = 1;
}

    结合判断条件low[to]>=dfn[x],还有搜索后的low[x]=min(low[x],low[to])再来理解一下为什么是dfn[to]。

    首先因为每次遇到一个相连的点,如果to 没有访问过,那么就会对to进行深搜,由于low[x]=min(low[x],low[to])的存在那么如果to能够通过其他路径到达了比x更早的位置,那么就可以继承其low值从而使得low[to]<dfn[x]。比如x=4,to=5,5可以通过6继承到1的low值,这个值比4的dfn要小,所以low[5]<dfn[4],4不是割点。

要是没有其他路可以走到更早的位置,那么最终low[to]>=dfn[x],那么这个点就是割点。比如

2->3,3没有路可以走到2的前面,所以low[3]=dfn[2],2作为割点。 

   其余细节不再赘述。

完整代码:

#include<stdio.h>
#include<algorithm>
using namespace std;
#define Inf 0x3f3f3f
#define M 500000
#define N 50000
bool flag[N];//存储点是否为割点
int num;	 //存储割点个数
int n, m;
int dfn[N], low[N], tt;
//low存储可以连到的最早出现的时间戳
struct Edge
{
	int to, next;
}e[M*5];
int last[N], cnt;

void tarjan(int x,int root)
{
	//printf("x=%d\n", x);
	int child = 0;
	low[x] = dfn[x] = ++tt;
	for (int i = last[x]; i; i = e[i].next)
	{
		int to = e[i].to;
		if (!dfn[to]) //这个点还没有被找过,那么继续找,可能会找到更早的
		{
			tarjan(to, root);
			low[x] = min(low[to], low[x]);//更新当前值,如果能找到更早的,传给x
			//printf("low[%d]=%d dfn[%d]=%d\n", to,low[to],x,dfn[x]);
			if (low[to] >= dfn[x] && x != root)
				//相当于说把下一个点to找遍了相连的,找不到一个直接与to相连的点比x更早出现
				flag[x] = 1;
			if (x == root)child++;//child就是一个个块           
			//根节点遍历一次找一个块,这个块与其他与根节点相连的块只能通过根节点相连(因为一旦dfn!=0就不tarjan了)
			/* example: 
			2  1  3  5  6 
			      4
			*/
			//假设3是根节点,3第一次找把21找了,child=1,第二次找了56,child=2,,第三次找了4,child=3>2,所以3是割点
		}
		low[x] = min(low[x], dfn[to]);
		//这个地方一定要注意!如果把dfn写做low的话那么一个连通块的low实际上都是根节点low值
		//low[x]始终存储与x相连的最早出现的时间戳
	}
	if (x == root && child >= 2)
		flag[root] = 1;
}

void add(int from, int to)
{
	e[++cnt].to = to;
	e[cnt].next = last[from];
	last[from] = cnt;
}

int main()
{
	scanf("%d%d", &n, &m);
	int x, y;
	for (int i = 1; i <= m; i++)
	{
		scanf("%d%d", &x, &y);
		add(x, y);
		add(y, x);//无向图双向建边
	}
	for (int i = 1; i <= n; i++)
		if (!dfn[i])
			tarjan(i,i);//把i作为根节点,寻找割点
	for (int i = 1; i <= n; i++)
		printf("i=%d low=%d dfn=%d\n", i, low[i], dfn[i]);
	for (int i = 1; i <= n; i++)
		if (flag[i])
			num++;
	printf("%d\n", num);
	for (int i = 1; i <= n; i++)
		if (flag[i])
			printf("%d ", i);
	printf("\n");
	return 0;
}

感觉脑子已经一片空白了,就这样吧

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/183264.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

了解多线程与并发

文章目录前言继承Thread类实现Runnable接口实现Callable和Future接口线程生命周期线程优先级线程加入操作线程休眠操作中断线程线程安全问题线程同步机制1. 同步代码块2. 同步方法线程暂停与恢复知识拓展死锁前言 &#x1f4cb;前言&#x1f4cb; &#x1f49d;博客&#xff1a…

用Devc++与easyx一步一步做游戏[启动界面部分]-之按钮制作

前面我们介绍了如何为dev c配置好easyx&#xff0c;至于用easyx能够做一些什么呢&#xff1f;大用处我不敢说&#xff0c;用来学习了解消息机制还是不错的。这我们来实现一个简单的游戏启动界面的设计&#xff0c;主要是按钮的设计。总体设计好的效果如下&#xff1a; GIF截图…

(免费分享)springboot音乐网站

开发工具&#xff1a;eclipse&#xff0c;数据库mysql5.7 jdk1.8技术&#xff1a;springbootmybatis/** * * * */package com.bjpowernode.music.ss.service.impl;import javax.annotation.Resource;import com.bjpowernode.music.common.AbstractService; import com.bjpowe…

基于 ROS 机器人和 RTAB-MAP 算法实现室内三维重建

本文叙如何利用RTAB-Map算法和Turtlebot3机器人在自己构建的室内场景中建图 文章目录1、安装依赖2、创建工作空间3、安装rtabmap和rtabmap_ros4、建立gazebo场景功能包5、建立机器人功能包6、为机器人添加kinect相机参考7、编译工作空间8、建立环境地图9、建图1、安装依赖 必要…

数据结构第五周 :(进制转换问题 + 迷宫自动行走问题 + 杨辉三角形 + 队列元素逆置 + 银行排队 + 整数划分问题 + 卡特兰数)

目录进制转换问题迷宫自动行走问题杨辉三角形队列元素逆置银行排队——队列整数划分问题买票问题——卡特兰数小兔的棋盘——卡特兰数进制转换问题 【问题描述】根据课堂讲授&#xff0c;请用“顺序栈”解决进制转换问题&#xff0c;不采用顺序栈&#xff0c;不给分。 【输入形…

前端屏幕录制工具 + 录制<video>标签内容

一、录制的实现思路 1.开始录制、停止录制、下载视频 2.Blob介绍 3.概念 var mediaRecord //用于录制视频 var mediaStream //视频流 var videoBuffer [] //保存的视频数据二、屏幕录制工具 下载地址&#xff1a; https://chrome.google.com/webstore/detail/tampermonkey…

Linux- 系统随你玩之--文本处理三剑客--grep继任者awk

文章目录1、sed概述1.1、 与vim等编辑器的区别&#xff1a;1.2、sed工作原理1.3 、sed数据处理原理1.4 、正则表达式概念2、 sed语法和常用选项2.1、语法&#xff1a;2.2、sed常用内部命令2.3、参数&#xff1a;3、 sed 正则表达式&#xff08;定位&#xff09;3.1 、数字定址…

管理机密(RH294)

在ansible中有一个命令行工具ansible-vault可用于创建 编辑 加密 解密 查看文件举个栗子ansible-vaultcreate filenameNew Vault password&#xff1a; #输入密码Confirm New Vault password&#xff1a; #确认密码也可以使用别的方法 比如创建一个密码文件ansible-vaultcreate…

互联网开发必读Git工具利器-《30天精通Git版本控管》中文版免费分享

本书介绍在软体开发领域&#xff0c;对原始码进行版本控管是非常重要的一件事&#xff0c;有别于Subversion或TFS这类集中式版本控管系统&#xff0c;Git是一套分散式版本控管系统&#xff0c;并带来许多版本控管上的各种优势与解决传统集中式版本控管的缺失&#xff0c;例如支…

Spring Cloud_Hystrix断路器

目录一、概述1.分布式系统面临的问题2.是什么3.能干嘛4.官网资料5.Hystrix官宣&#xff0c;停更进维二、Hystrix重要概念1.服务降级Fallback2.服务熔断Breaker3.服务限流Flowlimit三、hystrix案例1.构建2.高并发测试3.故障现象和导致原因4.上诉结论5.如何解决&#xff1f;解决的…

面试_Selenium常见问题

1.selenium 工作原理 1.对于每一条Selenium脚本&#xff0c;一个http请求会被创建并且发送给浏览器的驱动 2.浏览器驱动中包含了一个HTTP Server&#xff0c;用来接收这些http请求 3.HTTP Server接收到请求后根据请求来具体操控对应的浏览器 4.浏览器执行具体的测试步骤 5.浏览…

Smali语法小记

Smali语法小记 介绍 在执行 Android Java 层的代码时&#xff0c;其实就是 Dalvik(ART) 虚拟机&#xff08;使用 C 或 C 代码实现&#xff09;在解析 Dalvik 字节码&#xff0c;从而模拟程序的执行过程。 自然&#xff0c;Dalvik 字节码晦涩难懂&#xff0c;研究人员们给出了…

通过 eShopOnContainers 项目学习一下微服务

这里是项目地址 https://github.com/dotnet-architecture/eShopOnContainers, 这是微软创建的一个基于 .NET 平台的微服务架构的示例应用程序&#xff0c;里面基本上市面上主流的时髦的技术都用上了。 因为涉及的内容比较多&#xff0c;所以我们只简单查看一下微服务的代码实现…

信息抽取命名实体识别和关系抽取)

信息抽取的定义为&#xff1a;从自然语言文本中抽取指定类型的实体&#xff0c;关系、事件等事实信息。并形成结构化数据输出的文本处理技术。 信息抽取是从文本数据中抽取特定信息的一种技术&#xff0c;文本数据由医学具体的单位构成&#xff0c;例如&#xff0c;句子、段落、…

JavaWeb—Vue的简单介绍

1 Vue介绍 概述 Vue是一套构建用户界面的渐进式前端框架。只关注视图层&#xff0c;并且非常容易学习&#xff0c;还可以很方便的与其它库或已有项目整合。通过尽可能简单的API来实现响应数据的绑定和组合的视图组件。 数据渲染 数据库 --JDBC–> java程序 --http协议–>…

《MySQL实战45讲》——学习笔记23 “binlogredolog 的写入机制/组提交机制“

本篇主要介绍数据的可靠性有关的知识&#xff0c;包括binlog的写入机制和redolog的写入机制&#xff0c;通过了解这些机制从而可以在MySQL的IO性能瓶颈上做些优化&#xff1b;前文介绍了MySQL在可靠性、性能相关的概念&#xff0c;包括WAL技术、redolog与binlog、2阶段提交、ch…

阿里云图标使用 (symbol 引用方式)

阿里云图标网址: https://www.iconfont.cn/ 一、登录注册 这个简单&#xff0c;就不说了 二、给当前项目找图库 2.1、添加项目 2.2、寻找图标添加入库 添加入库 2.3、打开入库 的图标添加到指定项目 添加到当前项目 1 2 三、项目使用图标 ( symbol 引用方式) 3.1、下…

《HelloGitHub》第 82 期

兴趣是最好的老师&#xff0c;HelloGitHub 让你对编程感兴趣&#xff01;简介HelloGitHub 分享 GitHub 上有趣、入门级的开源项目。https://github.com/521xueweihan/HelloGitHub这里有实战项目、入门教程、黑科技、开源书籍、大厂开源项目等&#xff0c;涵盖多种编程语言 Pyth…

Studio One6有哪些新功能及系统配置要求介绍

Studio One6全新版本上线记录、生产、混合、掌握和执行所有操作。从工作室到舞台&#xff0c;Studio One6以易用为核心&#xff0c;是您的创意合作伙伴。当你准备好登上舞台时&#xff0c;Studio One就在那里。只有Studio One从最初的灵感到完整的制作&#xff0c;最终混音到精…

一个数码管显示0-F

数码管的一种是半导体发光器件&#xff0c;数码管可分为七段数码管和八段数码管&#xff0c;区别在于八段数码管比七段数码管多一个用于显示小数点的发光二极管单元DP&#xff08;decimal point&#xff09;&#xff0c;其基本单元是发光二极管。七段数码管是一类价格便宜使用简…