Knights of the Round Table

这题还是比较难了,我刷了两天,终于弄懂什么意思了,杯具!
点双连通,指的是,去掉任何一个点以及这个点的临边,都不影响整个图的连通性。可以这样认为,任意两点都有两条完全不同的路可达,也就构成了圈。

题目大意:国王身边有n个骑士,每次开会需要奇数个骑士来开会,会议桌是个圆桌,给出m个点对<A, B>,表示骑士A和B不能坐在一起,问:有多少个骑士,无论如何,不可能坐到会议桌旁,则国王就会开除他们。

分析:想一想,如果骑士A可以做到圆桌旁,得满足两个条件:1. 圆桌上的有奇数个人。2. 圆桌每两个相邻的骑士不hate对方。就可以建一个补图,顶点集合为所有的骑士,如果任意一对骑士可以坐在一起,则可以在图上加上一条边。则如果能找到一个奇圈,则这个圈里的所有人都符合条件,都可以留下。如果某一个骑士,不在任何一个奇圈内,则会被国王开除。

既然是圈,就表示这个圈所表示的子图是个双连通的,而又有一个定理是这样描述的,对于一个(点)双连通分量,如果找到一个奇圈,则这个分量的其他点也必然在某一个奇圈内。这样就好做了,只要找到图的割点,找到所有的双连通分支,判断一下分支中是否存在奇圈,如果存在,则整个分支的点都可以标记为留下,最后那总的点个数,减去可以留下的,剩下的就是要被国王开除的。图的割点可能存在于两个连通分支分支中,但缩点后割点和分支不会再构成双连通分量,也就不会出现分支跨度大于1的奇圈。

上面那个定理只适用于点双连通,不用于边双连通。看图
1           4

| \       /   \ 

|   \   /       \

|     2          5

|   /   \       /

| /       \   /

3          6

点2是割点,就会形成两个双连通分支(1, 2, 3), (2, 4, 5 , 6),然后用定理可以判断出分支1有奇圈,分支2没有,ans = 6 - 3 = 3。图中没有割边,整个图就一个连通分支,如果用定理就是任意一个点都在某奇圈内,显然4, 5, 6 不可能。所以这个定理用于点双连通。既然如此,这题就用求割点的方法就可以过了。

一个顶点u是割点,当且仅当满足(1)或(2)
(1) u为树根,且u有多于一个子树。
(2) u不为树根,且满足存在(u,v)为树枝边(或称父子边,即u为v在搜索树中的父亲),使得DFN(u)<=Low(v)。

这里如果是第一种情况,显然去掉树根,就会形成两棵树,也就是说连通分支增加了,u当然就是割点了。

第二种情况,<u, v>为树枝边,如果dfn(u) <= low[v],说明v以及v的子树最多会有反向边连在u上,绝对不会连通u以前的结点,所以,去掉u后,连通分支增加了,所以u就是割点。

这道题求得是连通分支中的奇圈,第一种情况可以忽略,可以不用考虑,如果第一个点只有一个树枝边,也就是说,第一个点的度为1,那么他就不是割点,但对于这道题没有多大影响,这道题直接用dfs是由于u作为根节点,DFN(u)肯定小于LOW(v),当误判为割点的时候,也不会找到奇圈,最后还是会被去掉,一个结点不算奇圈的。

在判奇圈的时候有这样一个定理:一个图是二部图当且仅当它不包含奇数环。这样我们就可以先假设是二分图,用交叉染色的方法,初始化一对相邻点异色,依边染色,如果有一点未被染色,相邻点染成异色,如果遇到相邻点都已染色,且为同色,则证明有奇圈,如果,染完整个连通分支都没有遇到相邻点异色的话,就证明没有奇圈。

在处理连通分支的时候,可以在dfs的时候就用栈来保存边,类似Tarjan的方法,如果遇到DFN(U) <= LOW(V),则u为割点,边出栈即可。

#include<stdio.h>
#include<string.h>
#define NN 1004
#define MM 1000004
int map[NN][NN];
int dfn[NN]; // 深搜标号 
int low[NN]; // 能搜到的最小标号 
int col[NN];
int odd[NN]; // 保存任意在奇圈的点 
int mark[NN];
int idx, n, time, top;

typedef struct node{
	int v, vis;
	struct node *nxt, *op;// op表示反向边 
}NODE;

NODE edg[MM];
NODE *link[NN];
NODE *stack[MM];
int Min(int a, int b){
    return a < b ? a : b;    
}

void Add(int u, int v){// 加边 
	edg[idx].v = v;
	edg[idx].vis = 0;
	edg[idx].nxt = link[u];
	edg[idx].op = edg + idx + 1;
	link[u] = edg + idx++;
	edg[idx].v = u;
	edg[idx].vis = 0;
	edg[idx].nxt = link[v];
	edg[idx].op = edg + idx - 1;
	link[v] = edg + idx++;
}

int find(int u){//用交叉染色的方法查找奇圈 
	int v;
	for (NODE *p = link[u]; p; p = p->nxt){
		v = p->v;
		if (mark[v]){
			if (col[v] == -1){
				col[v] = !col[u];
				return find(v);
			}else if (col[v] == col[u]){// 如果相邻两点同色,说明有奇圈 
				return 1;
			}
		}
	}
	return 0;
}

void Col(int u){// 取出分支的点,并着色 
	int i;
	NODE *p;
	memset(mark, 0, sizeof(mark));
	do{// 先将分支中的点用mark[]标记一下 
		 p = stack[top];
		 mark[p->v] = 1;
		 mark[p->op->v] = 1;
		 top--;
	}while(p->op->v != u);
	memset(col, -1, sizeof(col));
	col[u] = 0;
	if (find(u)){// 如果有奇圈 
		for (i = 1; i <= n; i++){
			if (mark[i]) odd[i] |= 1; // 如果点i在任意奇圈内,都可以用odd[]标记 
		}
	}
}

void dfs(int u){
	int v;
	dfn[u] = low[u] = ++time;
	for (NODE *p = link[u]; p; p = p->nxt){
		v = p->v;
		if (p->vis) continue;
		p->vis = p->op->vis = 1; // 将反向边也置1,无向边只走一次 
		stack[++top] = p; 
		if (!dfn[v]){
            dfs(v);
            low[u] = Min(low[u], low[v]);
            if (dfn[u] <= low[v]){ // u为割点的条件 
				Col(u);
			}
		}else{
            low[u] = Min(low[u], dfn[v]);
		}
	}
}

void Solve(){
	int cnt, i;
	time = 0;
	top = 0;
	memset(odd, 0, sizeof(odd));
	memset(dfn, 0, sizeof(dfn));
	memset(low, 0, sizeof(low));
	for (i = 1; i <= n; i++)
		if(!dfn[i]) // 图有可能不连通 
			dfs(i);
		cnt = 0;
		for (i = 1; i <= n; i++){
			if (odd[i]) cnt++;
		}
		printf("%d\n", n - cnt);
}

int main()
{
    int m, i, j, u, v;
    while(scanf("%d%d", &n, &m) != EOF){
		if (n == 0 && m == 0) break;
		for(i = 1; i <= n; i++){
			for(j = 1; j <= n; j++){
				map[i][j] = 1;
			}
		}
		
		while(m--){
			scanf("%d%d", &u, &v);
			map[u][v] = map[v][u] = 0;
		}
		
		memset(link, 0, sizeof(link));
		for(i = 1; i <= n; i++){// 建补图 
			for(j = 1; j < i; j++){
				if (map[i][j] == 1) Add(i, j);
			}
		}
		Solve();
    }
    return 0;
}
posted on 2010-08-12 09:47  ylfdrib  阅读(1065)  评论(1编辑  收藏  举报