You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

8.0 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

SPOJ 2878 KNIGHTS - Knights of the Round Table

:本题大多数网上题解是写的洛谷的链接,但洛谷现在无法做判题操作,提示Unkwon Error,只好找到了原题,尝试修改代码并不断完善。 洛谷链接

一、前言

n 个人,其中 k 个人能进行一次会议当且仅当:k是奇数,这k个人坐一圈能满足任意相邻两人不相互憎恶。问有多少个人不能参加任何一场会议。

二、思路

首先它是圆桌骑士开会,所以他们开会的样子是一个环,并且避免平票需要只有奇数个人(不能一人开会)。将仇恨关系建图不好做,于是 建立补图,将可以坐在一起的骑士连边,这样形成了一个可能的座位形状,然后去里面找 尽可能多的奇环,输出不被任何一个奇环包括的骑士。

思考过程

  • ① 在一张无向图中尝试找出奇数个节点的环,简称奇数环
  • ② 学习过Tarjan算法,算法在执行完后,就生成了多个点双,同时,缩点后的图,就不再有环。
  • ③ 原来有环,后来没环,环去了哪里?是去了每个点双中,也就是说,如果我们在缩点过程中,就找出了一堆点双,点双里面是可以有环的,但可能:
    • 全是奇数个节点环
    • 全是偶数个节点环
    • 有奇数个节点环,有偶数个节点环【这个也算是奇数环,下面有证明
  • ④ 在一个图中,找奇环可以用黑白染色法,如果发现矛盾,就是发现奇数环,否则就是没有奇数环
  • ⑤ 对于在奇环中的点进行标识,最后从来没有被标识过的点,就是被放弃的人员。

三、相关性质及证明

对于一个点双连通分量,里面若是有一个奇环,那么该点双连通分量里面所有的点都可以被包括在奇环之中

显然,奇环内不光点数是奇数,边数也是奇数。假如一个点双里面存在一个奇环:

那么和这个奇环的连接方式无非两种(显然所有连接方式都可以拆解成这最简单的两种):

  • 有奇数个点的链与它连接

这里用 1 个点作例子:

我们发现这个点显然与左边的几个点组成了一个大小为 5 的奇环。

  • 有偶数个点的链与它连接

这里用 2 个点做例子:

显然,这两个红色点和右边的三个黑色点组成了一个大小为 5 的奇环。

那么各位应该能找到一个规律:对于一条长度为 x 的链,它与奇环中的 a , b 两点相连,而在奇环上 a,b 之间恰好存在一条长度为奇数的路径和一条长度为偶数的路径,假如 x 是偶数,那么和长度为奇数的路径组合即可得到奇环,否则与长度为偶数的组合。

证毕。

Code

#include <bits/stdc++.h>
using namespace std;
const int N = 1010, M = 1e6 + 10;

// 链式前向星
int e[M], h[N], idx, w[M], ne[M];
void add(int a, int b, int c = 0) {
    e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}
int g[N][N];  // 记录任意两个节点之间的憎恨关系
int n, m;     // n个节点m个关系,本题不是m条边因为建的是补图
int color[N]; // 黑白染色法标识的颜色
int st[N];    // 是不是点双中的节点
int res[N];   // 标识数组出现在奇数环的点双中就是1没有出现过就是0

// 二分染色法
int dfs(int u, int fa) {
    for (int i = h[u]; ~i; i = ne[i]) {
        int v = e[i];
        if (v == fa || !st[v]) continue;       // 必须是点双里的点,才能搜索,要不你搜索的就不对啦看明白这个st数组的意义的吧~
        if (color[v] == -1) {                  // 如果v点未染色
            color[v] = color[u] ^ 1;           // 反向黑白染色
            if (dfs(v, u)) return 1;           // 继续探索v节点如果v节点完成后发现了冲突则u也汇报冲突
        } else if (color[v] != (color[u] ^ 1)) // 如果已染色,并且,存在染色冲突
            return 1;                          // 报告冲突
    }
    return 0;
}

// Tarjan算法求点双
int dfn[N], low[N], stk[N], top, ts;
vector<int> bcc[N];
int bcnt;
void tarjan(int u, int fa) {
    dfn[u] = low[u] = ++ts;
    stk[++top] = u;

    for (int i = h[u]; ~i; i = ne[i]) {
        int v = e[i];
        if (!dfn[v]) {
            tarjan(v, u);
            low[u] = min(low[u], low[v]);

            if (low[v] >= dfn[u]) { // 出现点双
                // 业务需要,初始化临时变量数组
                memset(st, 0, sizeof st);        // 一个点双一清空,防止误判
                memset(color, -1, sizeof color); // 黑白染色的标识数组也清空
                
                // 利用栈内元素弹出,可以获取此环中有哪些节点,对节点标识已访问,并且计数
                int x;
                bcnt++;
                do {
                    x = stk[top--];
                    bcc[bcnt].push_back(x);
                    st[x] = 1;
                } while (x != v);       // 将子树出栈
                bcc[bcnt].push_back(u); // 把割点/树根也丢到点双里

                st[u] = 1;                       // 将当前点双之中的全部打上标记
                color[u] = 0;                    // u标识为黑色01
                // 二分图染色
                // ① 环中节点数量必须大于等于3
                // ② 二分图染色存在冲突=>是奇数环
                // ③ 无向图进行黑白染色必须设置fa参数防止走回头路
                // ④ 枚举每个点双中的节点,标识已节点出现在奇数环中
                if (bcc[bcnt].size() >= 3 && dfs(u, -1))
                    for (int i : bcc[bcnt]) res[i] = 1;
            }
        } else
            low[u] = min(low[u], dfn[v]);
    }
}

int main() {
#ifndef ONLINE_JUDGE
    freopen("SP2878.in", "r", stdin);
#endif
    while (scanf("%d %d", &n, &m) && m + n) {
        memset(g, 0, sizeof g);  // 多组测试数据,每次清空憎恨数组
        memset(h, -1, sizeof h); // 初始化链式前向星
        idx = 0;

        memset(dfn, 0, sizeof dfn); // Tarjan算法要测试多轮清空数组【时间戳数组】
        memset(low, 0, sizeof low); // Tarjan算法要测试多轮清空数组【回溯祖先数组】
        memset(res, 0, sizeof res); // 清空是否在奇数环中的标识数组

        // 读入m条憎恨关系注意不要直接建图因为最终的摆放方式不是憎恨的相邻摆放而是不憎恨的相邻摆放
        while (m--) {
            int a, b;
            scanf("%d %d", &a, &b);
            g[a][b] = g[b][a] = 1;
        }
        // 建立补图
        for (int i = 1; i <= n; i++)
            for (int j = i + 1; j <= n; j++)        // i<j这样循环次数少一次干两事~
                if (!g[i][j]) add(i, j), add(j, i); // 无向图

        // 跑Tarjan算法缩点每个点都是一个点双连通分量
        for (int i = 1; i <= n; i++)
            if (!dfn[i]) tarjan(i, -1); // 可能会不连通,找点双

        // 计算未被加入到任何一个奇数环中的人员有多少个
        int ans = 0;
        for (int i = 1; i <= n; i++) ans += (res[i] == 0); // 加上不被点双标记的
        printf("%d\n", ans);
    }
    return 0;
}