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