割邊 + 縮點(得到邊連通分量) + 樸素LCA

用到的算法

割邊 + 縮點(得到邊連通分量) + 樸素LCA

算法解析

  • 無向圖區分重邊與同一條邊的反方向: 對每一條邊都用一個變量id來標識,一條無向邊的兩個方向用同一個id表示。

  • 割邊: c++ if(low[v] > dfn[u]) ,即以點v爲根的子樹不能到達點u及以上,所以邊uv爲一條割邊。

  • 縮點(得到邊雙連通分量): 去掉橋後,圖就變成了若干個隔離的邊雙連通分量,因爲將這些分量縮點。

    • 實現方法:直接對整個圖進行dfs,如果有些點屬於同一個連通分量,則可以被相同的變量值標記,代表同一個點。這樣以每一個沒有被標記過的點爲起點進行dfs,則完成縮點。
  • 樸素LCA:離線查詢,時間複雜度是兩點距離最近公共祖先的距離和,

    • 思想:若兩點的深度不一樣,則深度大的結點向上跳;若兩點多的深度一樣但不是同一個結點,則兩點同時向上跳。
  • 其他注意的點

    • 求得割邊後,如何找到它同一條邊反方向的邊?利用疑惑操作的性質,若a是奇數,則 a ^ 1 = a - 1; 若a是偶數,則 a ^ 1 = a + 1.

    • 由於LCA是對樹的操作, 所以新圖必須要找到一個根節點,來確定其它結點的深度。可以讓1號結點作爲根節點,因爲縮點時,新形成點的序號是從1開始的。

    • 添加邊後,需要對途徑的邊進行標記,防止下次再減一遍,但LCA是對點的操作,如何標記邊?判斷邊的下端點,若點需要向上跳且該點還未標記過,則割邊數--。

//poj 3694
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cmath>
#include <algorithm>
#include <queue>
#include <stack>
#include <vector>
#include <deque>
#include <map>
#include <iostream>
using namespace std;
typedef long long LL;
const double pi = acos(-1.0);
const double e = exp(1);
//const int MAXN =2e5+10;
const LL N = 1000000007;

struct edge
{
	int id;
	int flag;
	int from;
	int to;
	int next;
} edge[200009], edge3[200009];
int head[200009];
int head3[200009]; //描述新圖中結點之間的關係

int dfn[100009];
int low[100009];
int cnt = 1;
int cnt3 = 0; //用於構建縮點後新圖的鏈式前向星

int New[100009];	 //第i個雙聯通分量所含有的結點個數
int captain[100009]; //點i所在的邊雙連通分量

int father[100009];
int vis[100009];
int deep[100009];
int sum;

void tarjan(int u, int id)  //需要考慮去重邊的問題
{
	int i;
	low[u] = dfn[u] = cnt++;
	for (i = head[u]; i != -1; i = edge[i].next)
	{
		int v = edge[i].to;
		if(id == edge[i].id)     //判斷是不是同一條邊,若id相同則爲同一條
			continue;
		if (!dfn[v])
		{
			tarjan(v, edge[i].id);
			low[u] = min(low[u], low[v]);
			if (low[v] > dfn[u])
			{
				edge[i].flag = edge[i ^ 1].flag = 1; // 標記割邊,在尋找邊雙連通分量時忽略掉該邊

			//	printf("%d ---> %d\n", edge[i].from, edge[i].to);
			}
		}
		else
		{
			low[u] = min(low[u], dfn[v]);
		}
	}
}

void seek_doubleEdge(int u, int cnt2)  //縮點
{
	int i;
	dfn[u] = cnt2;
	New[cnt2]++;
	captain[u] = cnt2;
	for (i = head[u]; i != -1; i = edge[i].next)
	{
		int v = edge[i].to;
		if (edge[i].flag)       //連接新形成的點
		{

			if (captain[v])         //如果所連接的點還沒有縮點,則跳過。由於是條無向邊且當前端點已經縮點,當對無向邊的另一個端點縮點時可以建立一條邊。
			{
				int a = captain[u];
				int b = captain[v];

				edge3[cnt3].to = b;
				edge3[cnt3].next = head3[a];
				head3[a] = cnt3++;

				edge3[cnt3].to = a;
				edge3[cnt3].next = head3[b];
				head3[b] = cnt3++;

				//	cout << u << " " << v << " " << a << " "<< b << " ** " << endl;
			}
			continue;
		}
		else
		{
			if (!dfn[v])
			{
				seek_doubleEdge(v, cnt2);
			}
		}
	}
}

void init_lca(int u, int fa, int d)
{
	int i;

	deep[u] = d;
	father[u] = fa;
	//	printf("%d  %d  ?? \n", u, deep[u]);
	for (i = head3[u]; i != -1; i = edge3[i].next)
	{
		int v = edge3[i].to;
		if (!deep[v])
		{
			//printf("%d %d %d  (^_^)\n", v, u, d);
			init_lca(v, u, d + 1);
		}
	}
}

int lca(int a, int b)
{
	while (deep[a] < deep[b])
	{
		if (vis[b] == 0 && b != 1)
		{
			sum--;
			vis[b] = 1;
		}
		b = father[b];
		
	}
	while (deep[a] > deep[b])
	{
		if (vis[a] == 0 && a != 1)
		{
			sum--;
			vis[a] = 1;
		}
		a = father[a];
		
	}

	while (deep[a] == deep[b] && a != b)
	{
		if (vis[a] == 0 && a != 1)
		{
			sum--;
			vis[a] = 1;
		}

		if (vis[b] == 0 && b != 1)
		{
			sum--;
			vis[b] = 1;
		}

		a = father[a];
		b = father[b];
	}

	return sum;
}

int main()
{
	int n, m, i;
	int cnt1, a, b, q, nn = 0;
	while (scanf("%d%d", &n, &m) != EOF)
	{
		if (n == 0 && m == 0)
			break;
		nn++;
		cnt1 = 0;
		cnt = 1;
		memset(head, -1, sizeof(head));
		memset(dfn, 0, sizeof(dfn));
		memset(low, 0, sizeof(low));
		memset(captain, 0, sizeof(captain));
		while (m--)
		{
			scanf("%d%d", &a, &b);

			edge[cnt1].id = cnt1;          //[1]
			edge[cnt1].flag = 0;
			edge[cnt1].from = a;
			edge[cnt1].to = b;
			edge[cnt1].next = head[a];
			head[a] = cnt1++;

			edge[cnt1].id = cnt1 - 1;      //[2],同[1]處註釋一起爲一條無向邊的來回方向都標記同一個id.
			edge[cnt1].flag = 0;
			edge[cnt1].from = b;
			edge[cnt1].to = a;
			edge[cnt1].next = head[b];
			head[b] = cnt1++;
		}
		for (i = 1; i <= n; i++) //雙連通分量去割邊
		{
			if (!dfn[i])
			{
				tarjan(i, -1);
			}
		}

		memset(dfn, 0, sizeof(dfn));
		memset(head3, -1, sizeof(head3));
		memset(vis, 0, sizeof(vis));
		memset(deep, 0, sizeof(deep));

		int cnt2 = 0;
		for (int i = 1; i <= n; i++) //標記雙連通分量(縮點)
		{
			if (!dfn[i])
			{
				cnt2++;
				seek_doubleEdge(i, cnt2);
			}
		}
/*
		cout << " ** " << cnt2 << endl; // 邊雙連通分量的個數
		for (int i = 1; i <= n; i++)
		{
			printf("* %d   %d\n", i, captain[i]);
		}

		
		for (int i = 1; i <= cnt2; i++) //輸出縮點後的新圖
		{
			for (int j = head3[i]; j != -1; j = edge3[j].next)
			{
				cout << i << " " << edge3[j].to << endl;
			}
		}
*/
		sum = cnt2 - 1;
		init_lca(1, -1, 1);

/*		for (int i = 1; i <= cnt2; i++)
		{
			cout << i << " " << deep[i] << "  ?? " << endl;
		}
*/
		printf("Case %d:\n", nn);
		scanf("%d", &q);
		while (q--)
		{

			scanf("%d%d", &a, &b);

			if (captain[a] != captain[b])
			{
				lca(captain[a], captain[b]);
			}
			printf("%d\n", sum);
		}
		printf("\n");
	}
	return 0;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章