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