|
|
|
|
## [SPOJ 2878 KNIGHTS - Knights of the Round Table](https://www.spoj.com/problems/KNIGHTS/)
|
|
|
|
|
|
|
|
|
|
> **注**:本题大多数网上题解是写的洛谷的链接,但洛谷现在无法做判题操作,提示`Unkwon Error`,只好找到了原题,尝试修改代码并不断完善。
|
|
|
|
|
> **[洛谷链接](https://www.luogu.org/problem/SP2878)**
|
|
|
|
|
|
|
|
|
|
### 一、前言
|
|
|
|
|
有 $n$ 个人,其中 $k$ 个人能进行一次会议当且仅当:$k$是奇数,这$k$个人坐一圈能满足任意相邻两人不相互憎恶。问有多少个人不能参加任何一场会议。
|
|
|
|
|
|
|
|
|
|
### 二、思路
|
|
|
|
|
首先它是圆桌骑士开会,所以他们开会的样子是一个环,并且避免平票需要只有奇数个人(不能一人开会)。将仇恨关系建图不好做,于是 **建立补图,将可以坐在一起的骑士连边**,这样形成了一个可能的座位形状,然后去里面找 **尽可能多的奇环,输出不被任何一个奇环包括的骑士。**
|
|
|
|
|
|
|
|
|
|
**思考过程**
|
|
|
|
|
|
|
|
|
|
- ① 在一张无向图中尝试找出奇数个节点的环,简称奇数环
|
|
|
|
|
- ② 学习过$Tarjan$算法,算法在执行完后,就生成了多个点双,同时,缩点后的图,就不再有环。
|
|
|
|
|
- ③ 原来有环,后来没环,环去了哪里?是去了每个点双中,也就是说,如果我们在缩点过程中,就找出了一堆点双,点双里面是可以有环的,但可能:
|
|
|
|
|
- 全是奇数个节点环
|
|
|
|
|
- 全是偶数个节点环
|
|
|
|
|
- 有奇数个节点环,有偶数个节点环【**这个也算是奇数环,下面有证明**】
|
|
|
|
|
- ④ 在一个图中,找奇环可以用黑白染色法,如果发现矛盾,就是发现奇数环,否则就是没有奇数环
|
|
|
|
|
- ⑤ 对于在奇环中的点进行标识,最后从来没有被标识过的点,就是被放弃的人员。
|
|
|
|
|
|
|
|
|
|
### 三、相关性质及证明
|
|
|
|
|
|
|
|
|
|
> **对于一个点双连通分量,里面若是有一个奇环,那么该点双连通分量里面所有的点都可以被包括在奇环之中**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
显然,奇环内不光点数是奇数,边数也是奇数。假如一个点双里面存在一个奇环:
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
那么和这个奇环的连接方式无非两种(显然所有连接方式都可以拆解成这最简单的两种):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
- **有奇数个点的链与它连接**
|
|
|
|
|
|
|
|
|
|
这里用 $1$ 个点作例子:
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
我们发现这个点显然与左边的几个点组成了一个大小为 $5$ 的奇环。
|
|
|
|
|
|
|
|
|
|
- **有偶数个点的链与它连接**
|
|
|
|
|
|
|
|
|
|
这里用 $2$ 个点做例子:
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
显然,这两个红色点和右边的三个黑色点组成了一个大小为 $5$ 的奇环。
|
|
|
|
|
|
|
|
|
|
那么各位应该能找到一个规律:对于一条长度为 $x$ 的链,它与奇环中的 $a , b$ 两点相连,而在奇环上 $a,b$ 之间恰好存在一条长度为奇数的路径和一条长度为偶数的路径,假如 $x$ 是偶数,那么和长度为奇数的路径组合即可得到奇环,否则与长度为偶数的组合。
|
|
|
|
|
|
|
|
|
|
**证毕。**
|
|
|
|
|
|
|
|
|
|
### $Code$
|
|
|
|
|
|
|
|
|
|
```cpp {.line-numbers}
|
|
|
|
|
#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标识为黑色,0:黑,1:白
|
|
|
|
|
|
|
|
|
|
// 二分图染色
|
|
|
|
|
// ① 环中节点数量必须大于等于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;
|
|
|
|
|
}
|
|
|
|
|
```
|