qinghuan
← 回到知识探索

计算机趣闻 / 可下载小项目

相互冲突的人能分进两组吗?先找出那条绕不回来的圈

三个人两两不能同组时,换谁先分组都无效。用两种颜色安排成员,并让失败结果带上一条可核对的奇数环。

展开本文目录
  1. 下载完整练习包
  2. 从三个人的最小矛盾开始
  3. 沿队列传播要求,别漏掉孤立的小圈
  4. 为何冲突边能拼成一条奇数环?
  5. 运行后检查颜色或反例的每一条边
  6. 这个结论没有承诺两组同样大
  7. 记住这三点
  8. 别把例子用过头
  9. 自己试一小步
  10. 查原始资料
开始前,会这些就够了
  • 会读列表、循环、队列
  • 先看 图与 BFS 中顶点、边和邻接表
遇到不熟悉的词,先看这 2 个解释
二分图
能将所有顶点分成两组,使每条边都跨组。
奇数环
沿边绕回起点,经过边数为奇数的圈。
打开完整术语小抄 ↗

有些分组要求是“两个人不能在同一组”。把人画成点,把禁同组关系画成连线,问题就变成:能否只用红、蓝两种颜色,让每条线两端颜色不同?人数多少本身并不决定答案。

我们不猜一个失败名单,而是让程序给出两种可核对的结果:要么完整分组,要么一条边数为奇数的闭合路线。这里的冲突只表示题目约束,不涉及真实人员数据。

把例子带到自己的电脑上

下载完整练习包

需要 Python 3.10 或更新版本,无第三方依赖;制作时实测 Windows / Python 3.14.0。使用标准库,不需要安装第三方包。下载 ZIP 后选择“全部提取”,进入能看到 README.md 的文件夹。

下载 ZIP · 5 个文件
包里有什么
  • bipartite.py:完整可运行示例
  • README.md:运行说明与独立练习
  • observed.stdout.txt:本次实际输出
  • verification.json:源码和产物验证记录
  • SHA256SUMS.txt:内容摘要

在这个文件夹打开 PowerShell,逐行运行

python --version
python -B bipartite.py

解压到独立练习文件夹,在含 bipartite.py 的目录打开终端运行。无向图二染色与奇数环证据;不求组大小均衡、最大匹配或删边最优;主函数至多1000顶点,独立穷举至多6顶点。

找不到命令或文件时,先按 README 检查环境和所在目录。文中标出的阅读时间只估算文章篇幅,安装环境和动手练习可按自己的节奏进行。

从三个人的最小矛盾开始

A 不能和 B 同组,B 不能和 C 同组。若 A 红、B 蓝,C 就必须红。目前没有矛盾;再加上 C 不能与 A 同组,就要求两个红色不同色。把初始红蓝全部交换也一样,问题来自这三条关系。

沿着一条边就必须换一次颜色。绕偶数条边回到起点时换色次数为偶数,可以与原色一致;绕奇数条边回来就要求起点变成另一色。因此任何奇数环都会阻止两组安排。四个人首尾相连的方圈却可以红蓝交替。

沿队列传播要求,别漏掉孤立的小圈

程序先选一个未分组的人涂成 0,把他加入队列;每次取出一个人,把尚未分色的邻居涂成 1-color。已经分色的邻居若与当前同色,这条边就是冲突现场。0 和 1 只代表两组,没有优劣之分。

一个大图可能分成几块互不相连的关系,所以外层循环必须继续从所有未访问点启动。只从 0 出发,可能漏掉另一块中的三角形。孤立点没有约束,可以放任一组;本例统一放 0,不声称分组唯一或人数平衡。

为何冲突边能拼成一条奇数环?

每个首次发现的点都保存“是从谁走来的”。遇到同色端点 u、v 时,分别沿父节点向根回退,找到第一处共同祖先。u 到祖先、祖先到 v,再接上冲突边 v→u,就得到一条闭合路线。

树中颜色等于从根走过边数的奇偶性。同色端点到共同祖先的两段路合起来有偶数条边,再添一条冲突边便是奇数。程序去掉共用尾巴,所以除首尾重复外不重复顶点。自环是长度 1 的直接反例;重复边不改变能否分组。

运行后检查颜色或反例的每一条边

保存 bipartite.py 后运行 python -B bipartite.py。triangle 返回 bipartite=false 和 odd_cycle;square_plus_isolated 返回五个颜色,最后一位对应没有连线的孤立点。不要把具体红蓝排列当作唯一标准。

独立核对不调用另一份 BFS:它把每个小图的全部 2^V 种红蓝分配直接列出,检查是否存在一种满足所有边。程序枚举 0 到 5 个顶点的全部无向简单图,另测自环、重复边、断开的三角形和非法输入;反例检查器逐边确认圈确实存在、边数奇数、没有中途重复顶点。

展开收起代码 · 小实验python · 100 行

长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。

小实验 · python
"""Two teams or an odd cycle; standard-library teaching program."""
from collections import deque
from itertools import combinations, product
import json


def partition(n, edges):
    edges = tuple(edges)
    if type(n) is not int or not 0 <= n <= 1000:
        raise ValueError("n must be an integer in 0..1000")
    adjacency = [[] for _ in range(n)]
    for u, v in edges:
        if any(type(x) is not int or not 0 <= x < n for x in (u, v)):
            raise ValueError("unknown vertex")
        adjacency[u].append(v)
        adjacency[v].append(u)
    color, parent = [None] * n, [-1] * n
    for root in range(n):
        if color[root] is not None:
            continue
        color[root] = 0
        queue = deque([root])
        while queue:
            u = queue.popleft()
            for v in adjacency[u]:
                if color[v] is None:
                    color[v], parent[v] = 1 - color[u], u
                    queue.append(v)
                elif color[v] == color[u]:
                    a, b = [u], [v]
                    while parent[a[-1]] != -1:
                        a.append(parent[a[-1]])
                    while parent[b[-1]] != -1:
                        b.append(parent[b[-1]])
                    ancestors = set(a)
                    j = next(i for i, x in enumerate(b) if x in ancestors)
                    i = a.index(b[j])
                    cycle = a[:i + 1] + list(reversed(b[:j])) + [u]
                    return {"bipartite": False, "colors": None, "odd_cycle": cycle}
    return {"bipartite": True, "colors": color, "odd_cycle": None}


def exhaustive_coloring(n, edges):
    # Independent oracle: enumerate every assignment, no traversal or parent tree.
    if n > 6:
        raise ValueError("oracle limited to six vertices")
    return any(all(bits[u] != bits[v] for u, v in edges)
               for bits in product((0, 1), repeat=n))


def verify_witness(n, edges, result):
    if result["bipartite"]:
        colors = result["colors"]
        return len(colors) == n and all(colors[u] != colors[v] for u, v in edges)
    cycle = result["odd_cycle"]
    edge_set = {frozenset((u, v)) for u, v in edges}
    return (cycle[0] == cycle[-1] and (len(cycle) - 1) % 2 == 1
            and len(set(cycle[:-1])) == len(cycle) - 1
            and all(frozenset((u, v)) in edge_set for u, v in zip(cycle, cycle[1:])))


def main():
    checks = 0
    def check(ok):
        nonlocal checks
        if not ok:
            raise AssertionError("bipartite check failed")
        checks += 1

    graph_count = 0
    for n in range(6):
        possible = list(combinations(range(n), 2))
        for flags in product((0, 1), repeat=len(possible)):
            edges = [e for e, include in zip(possible, flags) if include]
            result = partition(n, edges)
            check(result["bipartite"] == exhaustive_coloring(n, edges))
            check(verify_witness(n, edges, result))
            graph_count += 1
    for n, edges in [(1, [(0, 0)]), (2, [(0, 1), (0, 1)]),
                     (4, [(0, 1), (1, 2), (2, 0)]), (4, [])]:
        result = partition(n, edges)
        check(result["bipartite"] == exhaustive_coloring(n, edges))
        check(verify_witness(n, edges, result))
    check(partition(3, iter([(0, 1), (1, 2)]))["bipartite"])
    for args in [(True, []), (-1, []), (2, [(0, 2)]), (2, [(False, 1)])]:
        try:
            partition(*args)
        except ValueError:
            check(True)
        else:
            raise AssertionError("invalid graph accepted")
    triangle = partition(3, [(0, 1), (1, 2), (2, 0)])
    square = partition(5, [(0, 1), (1, 2), (2, 3), (3, 0)])
    print(json.dumps({"checks_passed": checks, "exhaustive_graphs": graph_count,
                      "triangle": triangle, "square_plus_isolated": square,
                      "scope": "undirected graphs; two-color feasibility, not balanced teams or matching"}, indent=2))


if __name__ == "__main__":
    main()

怎样运行:在独立练习文件夹中执行 python -B bipartite.py。2026-10-18 已在 Windows、Python 3.14.0 运行此份源码并通过自带检查。无向图二染色与奇数环证据;不求组大小均衡、最大匹配或删边最优;主函数至多1000顶点,独立穷举至多6顶点。

运行后对照结果
{
  "checks_passed": 2213,
  "exhaustive_graphs": 1100,
  "triangle": {
    "bipartite": false,
    "colors": null,
    "odd_cycle": [
      1,
      0,
      2,
      1
    ]
  },
  "square_plus_isolated": {
    "bipartite": true,
    "colors": [
      0,
      1,
      0,
      1,
      0
    ],
    "odd_cycle": null
  },
  "scope": "undirected graphs; two-color feasibility, not balanced teams or matching"
}

这个结论没有承诺两组同样大

按邻接表扫描时,分色与一次证据回溯用 O(V+E) 时间,保存图需 O(V+E),额外队列、颜色和父节点需 O(V)。穷举核对要尝试指数数量的颜色分配,只限小图找错,不是正式算法的成本。

二分图回答能否满足所有异组关系。它没有寻找最多配对、没有按人数平衡、没有解决三组或更多组、也没有自动找出“删除最少哪些冲突”。这些都是额外目标。程序上限 1000 个顶点;没有网络或多线程运行。

记住这三点

  • 奇数环是不能二染色的具体证据。
  • 必须遍历全部连通部分;颜色编号与组大小都不是最优性结论。
  • 成功检查所有边,失败检查整条闭合路线。

别把例子用过头

  • 把任意环都当作失败,忽略偶数环可以交替分组。
  • 只从一个顶点出发,漏掉别的连通部分。
  • 把分组可行误读成分组均衡或最大匹配。

自己试一小步

六个人的分组结果可以给别人独立核验吗?

  1. 自行画六个顶点,先包含一个四边形和两个孤立点。
  2. 加入两条自选关系,并在运行前预测结果。
  3. 若不可行,列出一条奇数环;若可行,列出两组并逐边检查。

做到这些,就算完成

  • 不要把答案写死为程序的某个颜色顺序。
  • 解释新增边是否改变连通部分,以及是否形成奇数环。
  • 成功时逐边检查颜色不同;失败时验证环闭合且边数为奇数。

查原始资料

想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。

  • Algorithms 作者:BipartiteX.java (新标签页)↗Robert Sedgewick / Kevin Wayne · 英文

    支持二分图、奇数环、分量遍历与 BFS 染色。

    2026-10-18 已实际打开核对:类契约及构造器按全部未访问顶点启动 BFS;本篇 Python 为原创实现。

  • NetworkX:二分图 color 接口 (新标签页)↗NetworkX 开发者 · 英文

    支持两种颜色分配及不可二染色时拒绝结果的契约。

    2026-10-18 已实际打开核对:color 的返回字典、异常和路径图示例;未安装或运行 NetworkX。

  • Python 3.14:deque (新标签页)↗Python Software Foundation · 英文

    支持用双端队列保存待访问顶点。

    2026-10-18 已实际打开核对:deque 的两端追加和移除说明;只使用标准库。