Files
python/TangDou/AcWing/DCC/SP2878.md
HuangHai 1f397eca87 'commit'
2025-08-30 18:35:01 +08:00

167 lines
8.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

## [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$算法,算法在执行完后,就生成了多个点双,同时,缩点后的图,就不再有环。
- ③ 原来有环,后来没环,环去了哪里?是去了每个点双中,也就是说,如果我们在缩点过程中,就找出了一堆点双,点双里面是可以有环的,但可能:
- 全是奇数个节点环
- 全是偶数个节点环
- 有奇数个节点环,有偶数个节点环【**这个也算是奇数环,下面有证明**】
- ④ 在一个图中,找奇环可以用黑白染色法,如果发现矛盾,就是发现奇数环,否则就是没有奇数环
- ⑤ 对于在奇环中的点进行标识,最后从来没有被标识过的点,就是被放弃的人员。
### 三、相关性质及证明
> **对于一个点双连通分量,里面若是有一个奇环,那么该点双连通分量里面所有的点都可以被包括在奇环之中**
显然,奇环内不光点数是奇数,边数也是奇数。假如一个点双里面存在一个奇环:
![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/20230724084122.png)
那么和这个奇环的连接方式无非两种(显然所有连接方式都可以拆解成这最简单的两种):
- **有奇数个点的链与它连接**
这里用 $1$ 个点作例子:
![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/20230724084204.png)
我们发现这个点显然与左边的几个点组成了一个大小为 $5$ 的奇环。
- **有偶数个点的链与它连接**
这里用 $2$ 个点做例子:
![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/20230724084235.png)
显然,这两个红色点和右边的三个黑色点组成了一个大小为 $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;
}
```