Application of Tarjan Algorithm---Shrinkage Point and Cut Point

   Graph theory sometimes involves some connectivity issues, mainly for points. Sometimes it is necessary to calculate strongly connected components in directed graphs. At this time, the points representing components are very important; in undirected graphs Sometimes you need to know the cut point, the algorithm used is Tarjan, this algorithm is still difficult to understand (I think so).

a brief introdction

Tarjan is mainly based on deep search, which has two very critical tag arrays, namely dfn and low , and introduces the concept time stamp tt at the same time, that is, the time to reach this point, which is actually the searched order, and dfn records each point The timestamp is the number of the first visit, and low is the earliest timestamp that can be reached. The following analysis.

shrink point

Topic source: [Template] Shrink point - Luogu

    The meaning of this question is more obvious, that is, you go out a way, and the sum of the points is the largest, you can go as you like, but the repeated points are only counted once.

    After thinking about it, you will know that if there is such a ring (the ring is 1->2->3->1) , then you must take all the points on the ring, because you can walk around and go back. To the original place, so why not do it? So we can add all the weights of a ring to one point, and then rebuild a graph. The graph at this time is a directed acyclic graph. Later, topological sorting and dp can be used to determine the maximum weight.

    How to shrink the point is the highlight of this question. The Tarjan algorithm is based on deep search, and it will go down all the time every time. It can be expected that if you go to a point and find that you have passed it, will it become a circle if you go up again.

    Introduce the dfn array to store timestamps, low to store the earliest timestamp of the point that can be reached by this point, and stack st to store the situation of the point, which is used to count which points are on the ring, and vis is used to find the point in the current round.

    To get a point x, first put it on the stack, then initialize the dfn and low of the point to the timestamp tt, and then start to check the point connected to this point, assuming it is to, if this point has not been visited, it is the timestamp It is still 0, then continue to search deeply, and then update the low value of this point, that is, low[x]=min(low[x],low[to]); if the timestamp is not 0, then look at vis to see if it is accessed After that, the above sentence is still executed. Why is this? Because the first type has not been visited, then you do not know the low value of this point, so you cannot update it, and you can directly get the value if you have visited. After updating all the points connected to this point, the final low value of this point is obtained.

    After a round of updates, those with the same low value at this time constitute a strongly connected graph , in which any two points can reach each other. Why? Because we will stop searching when we encounter a visited point during the update, then the time stamp of this point must be earlier than the one I walked to reach this point (deep search is a side to black) For the timestamp at any point, after backtracking and performing the low[x]=min(low[to],low[x]) operation, all the low values ​​on this ring are set to the visited point, a The ring must be a strongly connected graph, because there are many intersections during the update process, which will actually be set to the smallest one, so in the end a graph with the same low value may be composed of multiple rings, and the ring is still a strongly connected graph.

    Because the same low value is a strongly connected graph, and this low value is actually the first searched value of this connected graph, we can use this point, that is, the point of low[x]=dfn[x] as the strongly connected graph The representative point of the connected graph, shrink the connected graph to this point, as for how to find all the nodes of the block represented by the representative point, use the stack until the representative point is popped out of the stack (because the low of this point is the smallest, So the earliest entry into the stack).

The procedure is as follows:

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];    //缩点
		}
	}
}

This process is relatively abstract. For example, for simplicity, it is shown in the following figure:

Enter the program from 1:

1 is pushed onto the stack, dfn[1]=low[1]=1, 1->2, because dfn[2]=0, you can search for 2;

2 into the stack, dfn[2]=low[2]=2, 2->1, 3, go to 1 first, find dfn[1]!=0, already searched, then directly update low[2]=min( low[2],low[1])=1

Look at 3 again, because dfn[3]=0, you can search for 3;

3 is pushed onto the stack, dfn[3]=low[3]=3, 3->None, cannot continue searching;

Judgment found that dfn[3]=low[3], so it can be used as a strongly connected graph, start popping points from the stack, pop 3, end with 3=3, and find the first component 3;

Backtracking, back to 2, update low[2]=min(low[2],low[3])=1, unchanged, cannot continue searching;

Judgment found that dfn[2]=2, low[2]=1, not equal;

Backtracking, back to 1,

It is found that dfn[1]=low[1]=1 can be used as a strongly connected graph, starting to pop points from the stack, popping 2,1,1=1 to end, and finding the second connected component 1,2. ,

When popping the stack, the weight can be added to the representative point, and the shrink point operation can be completed.

    Next, because we need to calculate the largest weight, we rebuild the graph after shrinking the point, and we can get a new acyclic directed graph, using dis[x]=min(dis[x],dis[to]+p [x]) plus topological sorting can calculate the answer.

Full code:

#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;
}

score

Topic Source: [Template] Cut Point (Cut Top) - Luogu 

    A cut point is in an undirected graph, if a point is removed, the graph is no longer connected, then this point is a cut point.

    So how to find the cut point is actually using the tarjan algorithm. Various definitions are similar to the above question, but because in an undirected graph, any connected block is always a strongly connected graph, which makes the definition of low in the above question meaningless. Because as long as it is connected, the low values ​​of all nodes are equal in the end, so the low update method here is slightly changed, that is, low[x] = min(low[x], dfn[to]) in the program.

    There are two situations for cutting points, assuming the following picture:

    The first one is for the root node (at the very beginning, there is only one connected block, which can be set casually), because deep search will search for a block connected to the root node every time, if the root node is connected to two or more blocks, then This root node is the cut point. Assuming that 1 is the root node, then after searching 2 and 3 for the first time, and 4 and 5 after the second search, there are two blocks, so 1 is the cutting point.

   The second case is that 2 is not the root node. In this case, it is necessary to judge whether the point connected to it can reach an earlier point without passing through this point. For example, 3 can only reach 1 through 2, and 5 can pass through 4. Can pass 6.

    Set the root node root, enter at point x, first initialize low and dfn as timestamps, and start checking all connected points to, if dfn! =0 means that it has already been visited, then directly update low[x] = min(low[x], dfn[to]), why is it dfn[to] instead of low[to], first of all, undirected graphs are bidirectional edges , if it is low[to], then all are the same. After the update, low stores the minimum timestamp value of all directly connected edges, which is also for the following judgment; if dfn=0 means that it has not been visited, then continue to search and calculate Update low[x]=min(low[x],low[to]) after low[to], if low[to]>=dfn[x], it means that the next point can’t find the ratio The point where x occurs earlier, so x is the cut point.

Part of the code:

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;
}

    Combining the judgment condition low[to]>=dfn[x], and the searched low[x]=min(low[x],low[to]) to understand why it is dfn[to].

    First of all, because every time a connected point is encountered, if to has not been visited, then to will be searched deeply. Due to the existence of low[x]=min(low[x],low[to]), then if to If you can reach a position earlier than x through other paths, then you can inherit its low value so that low[to]<dfn[x]. For example, x=4, to=5, 5 can inherit the low value of 1 through 6, which is smaller than the dfn of 4, so low[5]<dfn[4], 4 is not a cut point.

If there is no other way to go to an earlier position, then in the end low[to]>=dfn[x], then this point is the cut point. for example

2->3, 3 has no way to go in front of 2, so low[3]=dfn[2], 2 is used as the cutting point. 

   The rest of the details will not be repeated.

Full code:

#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;
}

It feels like my mind has gone blank, so be it

Guess you like

Origin blog.csdn.net/weixin_60360239/article/details/128778914