10000个状态,全部找到最优解——我的魔表求解器和上帝数对上了

魔表07:最少步数------把最优解交给约束求解器

回顾

上一篇文章中,我们将魔表 \(14 \times 30\) 矩阵 \(\mathbf{M}\) 在 \(\mathbb{Z}_{12}\) 下做了高斯消元,得到了预变换法的核心公式:

\\\boxed{\\; \\mathbf{k} = \\mathbf{P} \\cdot \\begin{pmatrix} \\mathbf{E}\\mathbf{b} - \\mathbf{F}\\mathbf{t} \\\\\[2pt \mathbf{t} \end{pmatrix} \qquad \mathbf{t} \in \mathbb{Z}_{12}^{16} \;} \]

其中 \(\mathbf{E}\)(\(14 \times 14\))是行变换矩阵,\(\mathbf{P}\)(\(30 \times 30\))是列置换矩阵,\(\mathbf{F}\)(\(14 \times 16\))是自由列系数矩阵。\(\mathbf{t}\) 是 \(16\) 维自由变量,可取任意 \(0 \sim 11\) 的值。

如上一篇结尾所说,本篇的目标是:

在 \(12^{16}\) 种 \(\mathbf{t}\) 中,找到使 \(\|\mathbf{k}\|_0\) 最小的那个------也就是魔表的最优解


一、问题重述

1.1 我们要最小化什么

令 \(\mathbf{k}{\text{pivot}} = \mathbf{E}\mathbf{b} - \mathbf{F}\mathbf{t}\)(14 维),\(\mathbf{k}{\text{free}} = \mathbf{t}\)(16 维)。经过列置换 \(\mathbf{P}\) 还原后,最终操作向量 \(\mathbf{k}\) 的非零分量数等于两者非零分量数之和。

记 \(\|\cdot\|_0\) 为 \(\ell_0\)范数(向量中非零分量的个数),我们的目标是:

\\\boxed{\\; \\min_{\\mathbf{t} \\in \\mathbb{Z}_{12}\^{16}} \\; \\bigl\\\|\\mathbf{E}\\mathbf{b} - \\mathbf{F}\\mathbf{t}\\bigr\\\|_0 \\;+\\; \\bigl\\\|\\mathbf{t}\\bigr\\\|_0 \\;} \\

换言之:在 \(12^{16}\) 种 \(\mathbf{t}\)(约 \(1.8 \times 10^{17}\) 种)中,找到让总操作数最少的那一个。

1.2 博客例子

继续用第二篇文章中的打乱状态。14 维简化状态为:

\\\mathbf{x}_{14} = (8, 2, 9,\\; 4, 1, 0,\\; 2, 4, 10,\\; 9, 3, 11, 6, 3)\^T \\

取负得 \(\mathbf{b} = -\mathbf{x}_{14} \bmod 12\)。通过预变换得 \(\mathbf{b}' = \mathbf{E}\mathbf{b}\):

\\\mathbf{b}' = (5,\\; 1,\\; 6,\\; 7,\\; 0,\\; 10,\\; 5,\\; 3,\\; 1,\\; 2,\\; 11,\\; 8,\\; 9,\\; 2)\^T \\

令 \(\mathbf{t} = \mathbf{0}\) 时得到特解:13 步 (详见博客 06 第六节)。但这不是最优解------13 步中很可能有些操作是冗余的,可以通过选一个合适的 \(\mathbf{t} \neq \mathbf{0}\) 来消除。


二、F 矩阵的一个细节

在进入优化之前,有一个关于 \(\mathbf{F}\) 矩阵的性质值得注意。

2.1 实数域 vs 模 12

\(\mathbf{F}\) 是 \(14 \times 16\) 矩阵。在实数域 \(\mathbb{R}\) 上,\(\mathbf{F}\) 的秩是 \(14\)(满行秩)。但在 \(\mathbb{Z}_{12}\) 下,秩只有 \(13\)------比满行秩少 \(1\)。

这意味着 \(\mathbf{F}\) 在 \(\mathbb{Z}_{12}\) 下有一个非零左零向量:

\\\mathbf{w} = (0,\\;0,\\;11,\\;0,\\;0,\\;0,\\;11,\\;0,\\;0,\\;0,\\;0,\\;0,\\;11,\\;0)\^T \\

满足 \(\mathbf{w}^T\mathbf{F} \equiv \mathbf{0} \pmod{12}\)(对所有 16 列)。

这也就是说,\(\mathbf{F}\) 矩阵的行2, 行6, 行12 线性相关

\\\mathbf{F}_{2} + \\mathbf{F}_{6} + \\mathbf{F}_{12} \\equiv 0 \\

2.2 这意味着什么

对任意 \(\mathbf{t}\),左乘 \(\mathbf{w}^T\):

\\\begin{aligned} \\mathbf{w}\^T(\\mathbf{E}\\mathbf{b} - \\mathbf{F}\\mathbf{t}) \&\\equiv \\mathbf{w}\^T\\mathbf{E}\\mathbf{b} - \\mathbf{w}\^T\\mathbf{F}\\mathbf{t} \\\\ \&\\equiv \\mathbf{w}\^T\\mathbf{E}\\mathbf{b} \\pmod{12} \\end{aligned} \\

\(\mathbf{w}^T\mathbf{E}\mathbf{b}\) 的值不随 \(\mathbf{t}\) 改变。对于博客例子:

\\\mathbf{w}\^T\\mathbf{E}\\mathbf{b} \\equiv 11 \\cdot b'_2 + 11 \\cdot b'_6 + 11 \\cdot b'_{12} \\equiv 11(6 + 5 + 9) \\equiv 11 \\times 20 \\equiv 4 \\pmod{12} \\

由于结果 \(\mathbf{w}^T\mathbf{E}\mathbf{b} \neq 0\),所以本例对于任意的 \(\mathbf{t}\) 来说,\(\mathbf{E}\mathbf{b} - \mathbf{F}\mathbf{t}\) 中位置 \(\{2, 6, 12\}\) 至少有一个不能变为 \(0\)------也就是说至少 \(1\) 个 pivot 操作是强制的

需要说明的是,这个约束在第三节的 CP-SAT 建模中不需要显式添加 ,此条件被隐含满足。换句话说,\(\mathbf{w}\) 提供的是关于解空间结构的理论洞察,但在代码层面,它并不对应任何显式的约束。

这个信息对优化有方向性帮助,但本质上,我们还是需要系统地搜索最优 \(\mathbf{t}\)。


三、用 CP-SAT 建模

3.1 为什么是约束求解器

\(\ell_0\) 最小化在整数域上是 NP 难问题。直接枚举 \(12^{16}\) 种 \(\mathbf{t}\) 不可能。但现代**约束规划(Constraint Programming, CP)**求解器专门处理这类组合优化问题。

我们选择 Google 的 OR-Tools CP-SAT,一个成熟的有限域整数优化求解器。它内部实现了分支定界、冲突分析、域传播等算法,我们只需要把问题用数学约束表达出来,剩下的交给它。

3.2 变量与约束

引入三类变量:

变量 类型 含义
\(x_0,\dots,x_{13}\) 整数,\(0\sim11\) \(\mathbf{k}_{\text{pivot}}\) 的 14 个分量
\(t_0,\dots,t_{15}\) 整数,\(0\sim11\) \(\mathbf{t}\) 的 16 个分量
\(p_0,\dots,p_{13},\;q_0,\dots,q_{15}\) 布尔,\((0/1)\) 指示对应的 \(x\) 或 \(t\) 是否为非零

约束 1:模等式的显式形式。 对于每行 \(i\),引入辅助整数变量 \(y_i\)(代表"借 12"的次数),将模等式写为普通线性等式:

\x_i + \\sum_{j=0}\^{15} F_{ij} \\cdot t_j = b'_i + 12 \\cdot y_i \\

\(y_i\) 的值域并非任意。从 \(\sum_j F_{ij} \cdot t_j\) 的最大值可以推算 \(y_i\) 的精确上界(约 \(0 \sim 100\) 之间,取决于 \(F\) 各行的稀疏度)。等式两边都非负,避免了模运算引入的符号复杂性。

为什么不用 AddModuloEquality OR-Tools 提供了内置的模等式约束。但在实际测试中,我们发现它对大表达式(涉及 16 个 \(t\) 变量)的内部分解存在域传播问题------会将某些数学上正确的解错误地判定为不可行。显式等式 x + Ft = Eb + 12y 绕开了这个问题,并且数学上完全等价。

约束 2:指示变量的含义。 用半具体化约束(OnlyEnforceIf)建立 \(p_i \leftrightarrow (x_i \neq 0)\) 和 \(q_j \leftrightarrow (t_j \neq 0)\) 的对应关系:

  • \(p_i = 0 \;\Longrightarrow\; x_i = 0\)
  • \(p_i = 1 \;\Longrightarrow\; x_i \neq 0\)

对 \(q_j\) 和 \(t_j\) 同理。

目标函数:

\\\min \\; \\sum_{i=0}\^{13} p_i \\;+\\; \\sum_{j=0}\^{15} q_j \\

整个模型 74 个变量、74 条约束 (直接读取 CP-SAT 内部 proto 的统计值,含 14 个整数 \(x\)、16 个整数 \(t\)、14 个整数 \(y\)、14 个布尔 \(p\)、16 个布尔 \(q\) 以及等价的线性等式、指示约束和目标函数)。CP-SAT 可以高效处理。

3.3 关键代码

python 复制代码
def solve_optimal(b_prime, F):
    """返回最小步数和对应的 x, t"""
    model = cp_model.CpModel()

    # 变量
    t = [model.NewIntVar(0, 11, f't{j}') for j in range(16)]
    x = [model.NewIntVar(0, 11, f'x{i}') for i in range(14)]
    y = [model.NewIntVar(0, y_max[i], f'y{i}') for i in range(14)]
    p = [model.NewBoolVar(f'p{i}') for i in range(14)]
    q = [model.NewBoolVar(f'q{j}') for j in range(16)]

    # 约束 1:x + Ft = b' + 12y
    for i in range(14):
        ft = sum(F[i][j] * t[j] for j in range(16))
        model.Add(x[i] + ft == b_prime[i] + 12 * y[i])

    # 约束 2:p ↔ x≠0, q ↔ t≠0
    for i in range(14):
        model.Add(x[i] == 0).OnlyEnforceIf(p[i].Not())
        model.Add(x[i] != 0).OnlyEnforceIf(p[i])
    for j in range(16):
        model.Add(t[j] == 0).OnlyEnforceIf(q[j].Not())
        model.Add(t[j] != 0).OnlyEnforceIf(q[j])

    # 目标
    model.Minimize(sum(p) + sum(q))

    # 求解
    solver = cp_model.CpSolver()
    status = solver.Solve(model)

    x_opt = [solver.Value(x[i]) for i in range(14)]
    t_opt = [solver.Value(t[j]) for j in range(16)]
    steps = int(solver.ObjectiveValue())
    return steps, x_opt, t_opt

四、博客例子:从 13 步到 9 步

将博客例子的 \(\mathbf{b}'\) 输入 CP-SAT 模型:

  • t = 0 特解:13 步
  • CP-SAT 最优解9 步,耗时约 0.14 秒,状态 OPTIMAL(已证明全局最优)

4.1 解的构成

CP-SAT 找到的 \(\mathbf{x}\) 和 \(\mathbf{t}\) 分别为:

\\\begin{aligned} \\mathbf{x} \&= (0, 0, 8, 9, 0, 0, 0, 3, 0, 0, 11, 0, 0, 2)\^T \\\\ \\mathbf{t} \&= (0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 5, 8, 10, 0, 0)\^T \\end{aligned} \\

\(\mathbf{x}\) 中有 \(5\) 个非零元,\(\mathbf{t}\) 中有 \(4\) 个非零元,合计 9 步 。比 \(\mathbf{t} = \mathbf{0}\) 时的 13 步减少了 \(4\) 步。

将 \(\mathbf{x}, \mathbf{t}\) 按置换 \(\mathbf{P}\) 还原为 30 维的 \(\mathbf{k}\),然后从 30 种操作中提取非零分量:

步骤 按钮状态 拨轮 \(k_i\) 动作
1 只按 UL UL 8 逆时针 4 格
2 只按 UL UR 9 逆时针 3 格
3 按 UL, DL, DR(不按 UR) UR 3 顺时针 3 格
4 按 UL, UR, DR(不按 DL) DL 11 逆时针 1 格
5 按 UL, UR DR 2 顺时针 2 格
6 按 DL, DR DR 2 顺时针 2 格
7 按 UR, DR UR 5 顺时针 5 格
8 按 UL, DR UL 8 逆时针 4 格
9 按 UL, DR UR 10 逆时针 2 格

代入验证:\(\mathbf{M}\mathbf{k} \equiv \mathbf{b} \pmod{12}\) ✓。9 步可解此状态。

4.2 交叉验证

为确认这个 9 步解确实是全局最优,我们用一位独立开发者公开发布的最优求解器 OptClock 对同一状态进行求解。该求解器也返回了 9 步,转换为我们的 30 操作格式后,非零分量完全一致。其输出内容如下:

复制代码
9 moves is optimal:
 DUDD u3 DDUD u' DUUU d4' DDUU u2 UUDD d2 UDUD d5 DUUU u3' DUUD u2' DUUD d4'
Optimal solution found in 0.46 seconds.

两个完全独立的实现得到了相同的 9 步解,互相印证。


五、大规模验证:与 cube20.org 对比

5.1 为什么要做大规模验证

我们 30 种操作模型得到的"最优步数"和 cube20.org 基于 15 个群生成元得到的"上帝数",是否指向同一个分布?

这是检验我们模型正确性的终极测试。如果是,那么两种不同的操作模型就是等价的群表示。

5.2 实验设计

  • 随机生成 \(10{,}000\) 个合法状态(14 个独立坐标,每个 \(0 \sim 11\) 均匀随机)
  • 固定随机种子 np.random.seed(42) 以便复现
  • 每题限时 \(30\) 秒,用 CP-SAT 求全局最优
  • 需要 OPTIMAL 状态(已证明最优),不接受 FEASIBLE(仅找到解但未证明最优)

5.3 结果

在 i9-12900H 平台上,10,000 题总计耗时约 \(31\) 分钟:

复制代码
总样本数:       10000
已证明最优:     10000 (100.0%)
超时/未确定:    0 (0.0%)
平均耗时:       185ms/题
最慢耗时:       1705ms

无一超时。全部 10,000 个状态都证得了全局最优解。

5.4 步数分布

步数 样本数 样本占比 cube20 总体占比 偏差
6 5 0.05% 0.03% +0.02%
7 57 0.57% 0.67% −0.10%
8 795 7.95% 7.91% +0.04%
9 4146 41.46% 41.13% +0.33%
10 4759 47.59% 47.76% −0.17%
11 238 2.38% 2.48% −0.10%

(距离 0~5 和 12 在总体中占比极低(合计 \(< 0.001\%\)),在 \(10{,}000\) 样本中不出现在预期之内。)

  • 样本加权平均:9.43
  • 总体加权平均(cube20):9.43

由下图对比饼状图也可以直观看出,我们抽取 10000 个样本测试的结果,和总体分布数据偏差极小。

5.5 偏差有多小?

\(10{,}000\) 个随机样本中,步数 \(d\) 的样本比例 \(\hat{p}\) 近似服从正态分布,标准误:

\SE = \\sqrt{\\frac{\\hat{p}(1-\\hat{p})}{n}} \\

以步数 9 为例(\(\hat{p} \approx 41.46\%\),\(n = 10{,}000\)):

\SE = \\sqrt{\\frac{0.4146 \\times 0.59}{10000}} \\approx 0.0049 = 0.49\\% \\

\(95\%\) 置信区间为 \(\hat{p} \pm 2 \times SE \approx \pm 1.0\%\)。

上表中全部偏差都在 \(\pm 0.33\%\) 以内,远小于置信区间宽度。换言之:

在 \(0.5\%\) 的统计精度下,两种操作模型的最优步数分布完全一致。

这个结果还有另一层含义。cube20 的分布是用 BFS 穷举全部 \(12^{14}\) 个状态得到的"真实值"。我们的样本占比与真实分布高度吻合,说明我们 CP-SAT 求出的每一个最优步数都经得起统计检验。如果我们的算法存在系统性错误(比如经常返回次优解),分布就会整体偏大,根本不可能在 \(0.5\%\) 精度上对上。换言之,独立于任何求解器之外、通过统计一致性获得的证据,本身就验证了我们算法的正确性。

5.6 cube20 距离12 状态验证

除了随机状态,我们还从 cube20.org 公开发布的 dist12.txt(包含全部 \(39{,}248\) 个距离为 \(12\) 的"最远状态")中提取了前 \(100\) 个,用我们的模型逐一求解。

距离 12 的"最远状态"比随机状态难得多------证明"不存在 11 步解"需要更深的搜索树。这 100 个状态平均每题 7.6 秒 ,全部 \(100\) 个总计 \(762\) 秒(约 \(13\) 分钟)。

全部 \(100\) 个状态都得到了 \(12\) 步解,异常数为 \(0\)。与 cube20 的上帝数结论完全吻合。

测试环境:Intel Core i9-12900H,16 GB RAM,Windows 11,Python 3.11.4,OR-Tools CP-SAT 9.15。CP-SAT 默认以单线程搜索为主,同一题目在不同 CPU 上的耗时主要取决于单核性能。


六、讨论:两种模型,一个分布

6.1 我们做了什么

用 \(30\) 种(button_set, wheel)操作构建 \(14 \times 30\) 矩阵 \(\mathbf{M}\),在 \(\mathbb{Z}_{12}\) 下消元得到 \(\mathbf{E}, \mathbf{P}, \mathbf{F}\) 三个预计算矩阵,将最少步数求解转化为一个约束优化问题,交给 CP-SAT 求解器进行计算。

6.2 cube20 做了什么

用 \(15\) 种群生成元(touch pattern)在 \(9\) 维 coset 坐标上做双向 BFS 搜索,穷举全部 \(12^{14}\) 个状态,确定了上帝数 \(= 12\) 和完整的距离分布。

6.3 为什么分布一致

操作集不一样(\(30\) vs \(15\)),坐标表示不一样(\(14\) 维 vs \(9\) 维 coset),求解方法不一样(CP-SAT vs BFS),但最优步数分布完美吻合。

原因在于:尽管操作的表象不同,它们生成的群是相同的 ------\(\mathbb{Z}_{12}^{14}\)。两组操作互为等价生成元。上帝数是群的性质,不随生成元的选取而改变。


七、系列终章:回顾与总结

七篇博客,从零开始探索一个看似简单的小玩具(魔表 / Rubik's Clock),最终抵达了它的数学核心。

  1. 魔表01------建立模 12 运算基础,发现操作可交换、效果可线性叠加
  2. 魔表02 ------将全部 64 种操作用线性方程组建模,得到 \(18 \times 64\) 矩阵
  3. 魔表03------利用旋转 R、补 C、对角镜像 D 三种对称性,将 64 种操作归约为 6 个原型
  4. 魔表04------用代码从 6 个原型生成完整的 64 列矩阵 A
  5. 魔表05 ------解剖矩阵:发现 P/Q 联动、不变量、空间分解,将模型简化为 \(14 \times 30\)
  6. 魔表06 ------在 \(\mathbb{Z}_{12}\) 下直接消元,得到预变换法和包含 16 个自由变量的通解公式
  7. 本文------将 L0 稀疏优化建模为约束规划问题,用 CP-SAT 求全局最优,大规模验证与 cube20 结论一致

核心脉络:

\\\text{机械结构} \\;\\longrightarrow\\; \\text{对称性} \\;\\longrightarrow\\; \\text{线性代数} \\;\\longrightarrow\\; \\text{最优解} \\

数学建模是核心------矩阵 \(\mathbf{M}\)、行变换 \(\mathbf{E}\)、列置换 \(\mathbf{P}\)、通解公式,这些都来自对魔表结构的理解。CP-SAT 只是最后一公里的工具------它帮我们找到了最优解,但真正让这一切成为可能的是前面六篇博文打下的数学基础。


参考资料

Tom Rokicki,Rubik's Clock God's Number is 12,cube20.org.

链接:https://cube20.org/clock/

OR-Tools CP-SAT Solver,Google.

链接:https://developers.google.com/optimization/cp/cp_solver

OptClock: an optimal Rubik's Clock solver,SpeedSolving 论坛.

链接:https://www.speedsolving.com/threads/optclock-optimal-rubiks-clock-solver.47747/

NP-hard 问题,百度百科.

链接:https://baike.baidu.com/item/NP-hard/10680083