无向图——双连通分量

    双连通图:在无向图连通图中,如果删除该图中的任意一点和依附它的边,不改变图的连通性,则称该图为双连通的无向图。

    由上述定义可知,双连通分量中,每两个结点之间至少有两条不同的路径可以相互到达。

    割点:在无向连通图中删去某个点a和依附a的边,图变为不连通,则该点称为割点,也叫关节点。

    割边:在无向连通图中删去某条边,图变为不连通,则该边称为割边,也叫桥。

    点双连通分支(块)与边双连通分支:

点双连通分支与边双连通分支是两个完全不同的概念。割点可以存在多个点连通分支中(相反,桥就不一样)。一个图可以有割点而没有割边,也可以有割边而没有割点。

 

点双连通分支的求法和边双连通分支的求法类似,不过在出栈的地方有些不同。下面介绍几个例子。

 

POJ3177(3352)

题目大意:最少需要加多少条边使得原图变为双连通图(原图连通)。

解:求桥(注意平行边),在求桥的过程中缩点,利用并查集,将每个连通分量用一个点代表。将这些代表用桥连接起来,就构成了一颗树。统计树中度数为1的点(即叶子结点)的个数count,将叶子结点两两相连,则添加边的数量为(count+1)/2。

#include <iostream>
const int MAX = 5002;

int p[MAX];
struct Graph
{
	int to;
	int next;
}e[MAX*4];
int index[MAX];
int edgeNum;
int seq;

int low[MAX];		//low[u]表示在树中从u点出发,经过一条其后代组成的路径和回退边,所能到达的最小深度的顶点标号
int dfn[MAX];		//dfn[u]表示结点u在树中的编号
int bridge[MAX][2],bridge_n;
int degree[MAX];
int n,m;

int min(int x, int y)
{
	return x < y ? x : y;
}

void makeSet()
{
	for(int i = 1; i <= n; i++)
		p[i] = i;
}

int findSet(int x)
{
	if(x != p[x])
		p[x] = findSet(p[x]);
	return p[x];
}

void Union(int x, int y)
{
	x = findSet(x);
	y = findSet(y);
	if(x == y)
		return;
	p[y] = x;
}

void addEdge(int from, int to)
{
	e[edgeNum].to = to;
	e[edgeNum].next = index[from];
	index[from] = edgeNum++;
	e[edgeNum].to = from;
	e[edgeNum].next = index[to];
	index[to] = edgeNum++;
}

//边连通分量,求桥
void bridge_dfs(int u, int v)
{
	int repeat = 0;					//有平行边
	low[u] = dfn[u] = seq++;
	for(int i = index[u]; i != -1; i = e[i].next)
	{
		int w = e[i].to;
		if(w == v)
			repeat++;
		if(dfn[w] < 0)
		{
			bridge_dfs(w,u);
			low[u] = min(low[u],low[w]);
			if(!(low[w] > dfn[u]))		//不是桥,缩点
			{
				Union(w,u);
			}
			else
			{
				bridge[++bridge_n][0] = u;
				bridge[bridge_n][1] = w;
			}
		}
		else if(v != w || repeat != 1)    //重要
			low[u] = min(low[u],dfn[w]);
	}
}

int solve()
{
	int i,j;
	int a,b;
	int count = 0;
	memset(degree,0,sizeof(degree));
	for(i = 1; i <= bridge_n; i++)
	{
		a = findSet(bridge[i][0]);
		b = findSet(bridge[i][1]);
		degree[a]++;
		degree[b]++;
	}
	for(i = 1; i <= n; i++)
		if(degree[i] == 1)
			count++;
	return (count+1)/2;
}

int main()
{
	int i,j;
	int a,b;
	edgeNum = 0;
	seq = 0;
	bridge_n = 0;
	memset(index,-1,sizeof(index));
	memset(dfn,-1,sizeof(dfn));
	scanf("%d %d",&n,&m);
	for(i = 0; i < m; i++)
	{
		scanf("%d %d",&a,&b);
		addEdge(a,b);
	}
	makeSet();
	bridge_dfs(1,-1);
	printf("%d\n",solve());
	return 0;
}

 

POJ2942

题目大意:有n个骑士,骑士一段时间要坐在圆桌上举行高级会议,但要满足条件:互相憎恨的骑士不能相邻,圆桌上的人数必须是大于1的奇数。现在给出骑士之间的憎恨关系,问至少有多少个骑士要被排除在外。

解:首先建补图,这样骑士a和骑士b之间有连线,说明a和b可以相邻。还可以想到奇环,不在任何奇环的骑士将被排除。问题是如何求图中的奇环?这里有一个定理:双连通分量中如果存在奇环,那么整个分量的点全部包含在奇环中(自己体会)。这样,可以先求点的双连通分量,判断每个双连通分量是否包含奇环(用染色,若相邻点颜色相同,存在奇环),若存在奇环,则该分量包含的骑士都不会被排除。

#include <iostream>
const int MAX = 1001;
int n,m;
//若某块(双连通分量)不可染色为二分图,则该块存在奇圈;若某块存在奇圈,那么该块中的所有点都存在与奇圈中;
//那么答案就是所有不在任何奇圈中的骑士的个数。
bool map[MAX][MAX];
int dfn[MAX],low[MAX],stack[MAX];
int top,seq,result;
bool b[MAX],used[MAX];
int color[MAX];

int min(int x, int y)
{
	return x < y ? x : y;
}

bool isOk(int v, int col)
{
	color[v] = col;
	for(int w = 1; w <= n; w++)
	{
		if(map[v][w])
		{
			if(b[w])
			{
				if(color[v] == color[w])	//相邻两点颜色相同,构不成二分图,含奇圈
					return true;
				if(color[w] == -1)
					isOk(w, col^1);
			}
		}
	}
	return false;
}

void dummy(int t, int *a)
{
	int i,j;
	memset(b,0,sizeof(b));	//b[i]=1表示结点i属于当前的双连通分量中
	for(i = 0; i < t; i++)
		b[a[i]] = true;
	for(i = 0; i < t; i++)
	{
		memset(color,-1,sizeof(color));
		if(isOk(a[i],1))
			break;
	}
	if(i < t)		//含奇圈
	{
		for(j = 0; j < t; j++)
		{
			if(!used[a[j]])
			{
				result++;
				used[a[j]] = true;
			}
		}
	}
}

void bicon(int u)
{
	int a[MAX];
	low[u] = dfn[u] = seq++;
	stack[top] = u;
	top++;
	for(int w = 1; w <= n; w++)
	{
		if(map[u][w])
		{
			if(dfn[w] < 0)					//第一种情况,w是新点
			{
				bicon(w);
				low[u] = min(low[u],low[w]);
				if(low[w] >= dfn[u])		//u割点(把割点留在栈中) 
				{
					int k = 1;
					a[0] = u;
					do
					{
						--top;
						a[k++] = stack[top];
					}while(stack[top] != w);
					dummy(k,a);
				}
			}
			else					//u,w是回边(w是u的祖先)
				low[u] = min(low[u],dfn[w]);
		}
	}
}

void block()
{
	for(int i = 1; i <= n; i++)
		if(dfn[i] < 0)
			bicon(i);
}

int main()
{
	int i,j;
	int a,b;
	while(true)
	{
		scanf("%d %d",&n,&m);
		if(n == 0 && m == 0)
			break;
		memset(map,1,sizeof(map));
		memset(dfn,-1,sizeof(dfn));
		memset(used,0,sizeof(used));
		seq = 0;
		top = 0;
		result = 0;
		for(i = 0; i < m; i++)
		{
			scanf("%d %d",&a,&b);
			map[a][b] = false;
			map[b][a] = false;
		}
		for(i = 1; i <= n; i++)
			map[i][i] = false;
		block();
		printf("%d\n",n - result);
	}
	return 0;
}

 

POJ3694

题目大意:给定一个初始的网络,每次(1000次)向网络里加一条边,问网络中桥的数量。(网络是动态的)

解:这题难就难在网络是动态的,如果是静态,可以用边的双连通分量来直接求解。简单的想法是每修改一次就重新计算一次,但是这样超时。联想:通过缩点,缩点之间用桥连接,形成一颗树,树边就是桥,桥的总数为sum。每次向网络里加一条边a,b,先用并查集找出a和b所属的树的结点,显然,a和b到ab的最近公共祖先这条路径上的桥全部无效。这样,每次只需在树上操作sum--。这样复杂度就降下来了。

#include <iostream>
const int MAX = 100002;
int n,m;
int result;
struct Edge
{
	int to;
	int next;
}e[MAX*10],tree[MAX*10];
int index[MAX],index2[MAX],edgeNum,edgeT;
int seq;

int low[MAX],dfn[MAX];
int p[MAX],res[MAX];
int level[MAX],pre[MAX];
bool vis[MAX],bridge[MAX];

int min(int x, int y)
{
	return x < y ? x : y;
}

void addEdge(int from, int to)
{
	e[edgeNum].to = to;
	e[edgeNum].next = index[from];
	index[from] = edgeNum++;
}

void addTree(int from, int to)
{
	tree[edgeT].to = to;
	tree[edgeT].next = index2[from];
	index2[from] = edgeT++;
}

void makeSet()
{
	for(int i = 1; i <= n; i++)
		p[i] = i;
}

int findSet(int x)
{
	if(x != p[x])
		p[x] = findSet(p[x]);
	return p[x];
}

void Union(int x, int y)
{
	x = findSet(x);
	y = findSet(y);
	if(x == y)
		return;
	p[x] = y;
}

void bridge_dfs(int u, int v)
{
	int repeat = 0;
	low[u] = dfn[u] = seq++;
	for(int i = index[u]; i != -1; i = e[i].next)
	{
		int w = e[i].to;
		if(v == w)
			repeat++;
		if(dfn[w] < 0)
		{
			bridge_dfs(w,u);
			low[u] = min(low[u],low[w]);
			if(low[w] > dfn[u])
			{
				result++;
				res[result] = i;
				//bridge[w] = 1;
			}
			else
				Union(w,u);
		}
		else if(v != w || repeat != 1)
			low[u] = min(low[u],dfn[w]);
	}
}

void lca_dfs(int u, int deep)
{
	for(int i = index2[u]; i != -1; i = tree[i].next)
	{
		int v = tree[i].to;
		if(!vis[v])
		{
			vis[v] = true;
			pre[v] = u;
			level[v] = deep+1;
			lca_dfs(v,deep+1);
		}
	}
}

void lca(int u, int v)
{
	while(level[u] > level[v])
	{
		if(bridge[u])
		{
			result--;
			bridge[u] = 0;
		}
		u = pre[u];
	}
	while(level[v] > level[u])
	{
		if(bridge[v])
		{
			result--;
			bridge[v] = 0;
		}
		v = pre[v];
	}
	while(u != v)
	{
		if(bridge[u])
		{
			bridge[u] = 0;
			result--;
		}
		if(bridge[v])
		{
			bridge[v] = 0;
			result--;
		}
		u = pre[u];
		v = pre[v];
	}
}

int main()
{
	int i,j;
	int a,b;
	int q;
	int cases = 1;
	while(true)
	{
		scanf("%d %d",&n,&m);
		if(n == 0 && m == 0)
			break;
		edgeNum = 0;
		edgeT = 0;
		result = 0;
		seq = 0;
		memset(index,-1,sizeof(index));
		memset(index2,-1,sizeof(index2));
		memset(dfn,-1,sizeof(dfn));
		memset(vis,0,sizeof(vis));
		memset(level,0,sizeof(level));
		memset(bridge,0,sizeof(bridge));
		makeSet();
		for(i = 0; i < m; i++)
		{
			scanf("%d %d",&a,&b);
			addEdge(a,b);
			addEdge(b,a);
		}
		scanf("%d",&q);
		printf("Case %d:\n",cases++);
		bridge_dfs(1,-1);		//找到边的双连通 缩点
		int x,y;
		for(i = 1; i <= n; i++)	//将缩点集合转化为一颗树
		{
			for(j = index[i]; j != -1; j = e[j].next)
			{
				x = findSet(i);
				y = findSet(e[j].to);
				if(x != y)
					addTree(x,y);
			}
		}
		//
		memset(vis,0,sizeof(vis));
		vis[p[1]] = true;
		level[p[1]] = 1;
		lca_dfs(p[1],1);
		for(i = 1; i <= result; i++)
			bridge[findSet(e[res[i]].to)] = 1;
		while(q--)
		{
			scanf("%d %d",&a,&b);
			a = findSet(a);
			b = findSet(b);
			if(a != b)
				lca(a,b);
			printf("%d\n",result);
		}
		printf("\n");
	}
	return 0;
}

 

 

 

你可能感兴趣的:(J#)