Aplicación del Algoritmo de Tarjan---Punto de Contracción y Punto de Corte

   La teoría de grafos a veces implica algunos problemas de conectividad, principalmente para los puntos. A veces es necesario calcular componentes fuertemente conectados en gráficos dirigidos. En este momento, los puntos que representan componentes son muy importantes; en gráficos no dirigidos. A veces es necesario saber el punto de corte, el El algoritmo utilizado es Tarjan, este algoritmo aún es difícil de entender (creo que sí).

una breve introducción

Tarjan se basa principalmente en la búsqueda profunda, que tiene dos matrices de etiquetas muy críticas, a saber, dfn y low , e introduce el concepto de marca de tiempo tt al mismo tiempo, es decir, el tiempo para llegar a este punto, que en realidad es el orden buscado. y dfn registra cada punto La marca de tiempo es el número de la primera visita, y bajo es la marca de tiempo más temprana que se puede alcanzar El siguiente análisis.

punto de contracción

Fuente del tema: [Plantilla] Punto de contracción - Luogu

    El significado de esta pregunta es más obvio, es decir, sales por un camino y la suma de los puntos es la mayor, puedes ir como quieras, pero los puntos repetidos solo se cuentan una vez.

    Después de pensarlo, sabrás que si existe tal anillo (el anillo es 1->2->3->1) , entonces debes tomar todos los puntos del anillo, porque puedes caminar y luego ir. Volver al lugar original, así que ¿por qué no hacerlo? Entonces, podemos agregar todos los pesos de un anillo a un punto y luego reconstruir un gráfico. El gráfico en este momento es un gráfico acíclico dirigido. Más tarde, la clasificación topológica y dp se pueden usar para determinar el peso máximo.

    Cómo reducir el punto es lo más destacado de esta pregunta. El algoritmo de Tarjan se basa en la búsqueda profunda, y bajará todo el tiempo cada vez. Se puede esperar que si va a un punto y descubre que lo ha pasado , se convertirá en un círculo si vuelves a subir.

    Introduzca la matriz dfn para almacenar marcas de tiempo, low para almacenar la marca de tiempo más antigua del punto que se puede alcanzar en este punto, y stack st para almacenar la situación del punto, que se usa para contar qué puntos están en el anillo, y vis se utiliza para encontrar el punto en la ronda actual.

    Para obtener un punto x, primero colóquelo en la pila, luego inicialice dfn y low del punto en la marca de tiempo tt, y luego comience a verificar el punto conectado a este punto, asumiendo que es to, si este punto no ha sido visitado, es la marca de tiempo Todavía es 0, luego continúe buscando profundamente y luego actualice el valor bajo de este punto, es decir, bajo [x] = min (bajo [x], bajo [a]); si el la marca de tiempo no es 0, luego mire vis para ver si se accede Después de eso, la oración anterior aún se ejecuta. ¿ Por qué sucede esto? Debido a que el primer tipo no ha sido visitado, entonces no sabe el valor bajo de este punto, por lo que no puede actualizarlo y puede obtener directamente el valor si lo ha visitado. Después de actualizar todos los puntos conectados a este punto, se obtiene el valor mínimo final de este punto.

    Después de una ronda de actualizaciones, aquellos con el mismo valor bajo en este momento constituyen un gráfico fuertemente conectado , en el que dos puntos cualesquiera pueden alcanzarse. ¿Por qué? Debido a que dejaremos de buscar cuando encontremos un punto visitado durante la actualización, entonces la marca de tiempo de este punto debe ser anterior a la que caminé para llegar a este punto (la búsqueda profunda es un lado en negro) Para la marca de tiempo en cualquier punto, después de retroceder y realizar la operación low[x]=min(low[to],low[x]), todos los valores bajos en este anillo se establecen en el punto visitado, a El anillo debe ser un gráfico fuertemente conectado, porque hay muchas intersecciones durante el proceso de actualización, que en realidad se establecerán en la más pequeña, por lo que al final, un gráfico con el mismo valor bajo puede estar compuesto por múltiples anillos, y el anillo sigue siendo un gráfico fuertemente conectado.

    Debido a que el mismo valor bajo es un gráfico fuertemente conectado, y este valor bajo es en realidad el primer valor buscado de este gráfico conectado, podemos usar este punto, es decir, el punto de bajo[x]=dfn[x] como el punto fuertemente gráfico conectado El punto representativo del gráfico conectado, reduzca el gráfico conectado a este punto, en cuanto a cómo encontrar todos los nodos del bloque representado por el punto representativo, use la pila hasta que el punto representativo salga de la pila (porque el mínimo de este punto es el más pequeño, por lo que la primera entrada en la pila).

El procedimiento es el siguiente:

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

Este proceso es relativamente abstracto, por ejemplo, por simplicidad, se muestra en la siguiente figura:

Ingrese al programa desde 1:

1 se coloca en la pila, dfn[1]=low[1]=1, 1->2, porque dfn[2]=0, puede buscar 2;

2 en la pila, dfn[2]=low[2]=2, 2->1, 3, vaya a 1 primero, encuentre dfn[1]!=0, ya buscado, luego actualice directamente low[2]=min (bajo[2],bajo[1])=1

Mire 3 nuevamente, porque dfn[3]=0, puede buscar 3;

3 se coloca en la pila, dfn[3]=low[3]=3, 3->Ninguno, no se puede seguir buscando;

Judgement encontró que dfn[3]=low[3], por lo que puede usarse como un gráfico fuertemente conectado, comenzar a sacar puntos de la pila, sacar 3, terminar con 3=3 y encontrar el primer componente 3;

Retrocediendo, de vuelta a 2, actualice low[2]=min(low[2],low[3])=1, sin cambios, no puede continuar buscando;

El juicio encontró que dfn[2]=2, low[2]=1, no igual;

Retrocediendo, de vuelta a 1,

Se encuentra que dfn[1]=low[1]=1 se puede usar como un gráfico fuertemente conectado, comenzando a sacar puntos de la pila, sacando 2,1,1=1 hasta el final y encontrando el segundo componente conectado 1 ,2. ,

Al hacer estallar la pila, el peso se puede agregar al punto representativo y se puede completar la operación del punto de contracción.

    Luego, debido a que necesitamos calcular el peso más grande, reconstruimos el gráfico después de reducir el punto, y podemos obtener un nuevo gráfico dirigido acíclico, usando dis[x]=min(dis[x],dis[to]+p [ x]) más la clasificación topológica puede calcular la respuesta.

Código completo:

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

puntaje

Fuente del tema: [Plantilla] Punto de corte (Cut Top) - Luogu 

    Un punto de corte está en un gráfico no dirigido, si se elimina un punto, el gráfico ya no está conectado, entonces este punto es un punto de corte.

    Entonces, cómo encontrar el punto de corte es en realidad usando el algoritmo tarjan Varias definiciones son similares a la pregunta anterior, pero debido a que en un gráfico no dirigido, cualquier bloque conectado es siempre un gráfico fuertemente conectado, lo que hace que el bajo definido en la pregunta anterior no tenga sentido. Porque mientras esté conectado, los valores bajos de todos los nodos son iguales al final, por lo que el método de actualización bajo aquí cambia ligeramente, es decir, bajo[x] = min(bajo[x], dfn[ a]) en el programa.

    Hay dos situaciones para los puntos de corte, asumiendo la siguiente imagen:

    El primero es para el nodo raíz (al principio, solo hay un bloque conectado, que se puede configurar casualmente), porque la búsqueda profunda buscará un bloque conectado al nodo raíz cada vez, si el nodo raíz está conectado. a dos o más bloques, entonces este nodo raíz es el punto de corte. Suponiendo que 1 es el nodo raíz, luego de buscar 2 y 3 por primera vez, y 4 y 5 después de la segunda búsqueda, hay dos bloques, por lo que 1 es el punto de corte.

   El segundo caso es que 2 no es el nodo raíz. En este caso, es necesario juzgar si el punto conectado a él puede llegar a un punto anterior sin pasar por este punto. Por ejemplo, 3 solo puede llegar a 1 a 2, y 5 puede pasar por 4. Puede pasar por 6.

    Establezca la raíz del nodo raíz, ingrese en el punto x, primero inicialice low y dfn como marcas de tiempo, y comience a verificar todos los puntos conectados, si dfn! =0 significa que ha sido visitado, luego actualice directamente low[x] = min(low[x], dfn[to]), ¿ por qué es dfn[to] en lugar de low[to], en primer lugar, gráficos no dirigidos son bordes bidireccionales, si es low[to], entonces todos son iguales. Después de la actualización, low almacena el valor de marca de tiempo mínimo de todos los bordes conectados directamente, que también es para el siguiente juicio; si dfn=0 significa que tiene no ha sido visitado, luego continúe buscando y calcule Actualice low[x]=min(low[x],low[to]) after low[to], si low[to]>=dfn[x], significa que el el siguiente punto no puede encontrar la razón El punto donde x ocurre antes, por lo que x es el punto de corte.

Parte del código:

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

    Combinando la condición de juicio low[to]>=dfn[x], y la búsqueda low[x]=min(low[x],low[to]) para entender por qué es dfn[to].

    En primer lugar, porque cada vez que se encuentre un punto conectado, si no se ha visitado, entonces se buscará profundamente.Debido a la existencia de low[x]=min(low[x],low[to]), entonces si a Si puede llegar a una posición anterior a x a través de otras rutas, puede heredar su valor bajo para que low[to]<dfn[x]. Por ejemplo, x=4, to=5, 5 puede heredar el valor bajo de 1 a 6, que es más pequeño que el dfn de 4, por lo que low[5]<dfn[4], 4 no es un punto de corte.

Si no hay otra forma de ir a una posición anterior, entonces al final low[to]>=dfn[x], entonces este punto es el punto de corte. Por ejemplo

2->3, 3 no tiene forma de pasar por delante de 2, por lo que low[3]=dfn[2], 2 se usa como punto de corte. 

   El resto de los detalles no se repetirán.

Código completo:

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

Se siente como si mi mente se hubiera quedado en blanco, que así sea.

Supongo que te gusta

Origin blog.csdn.net/weixin_60360239/article/details/128778914
Recomendado
Clasificación