魔表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),最终抵达了它的数学核心。
- 魔表01------建立模 12 运算基础,发现操作可交换、效果可线性叠加
- 魔表02 ------将全部 64 种操作用线性方程组建模,得到 \(18 \times 64\) 矩阵
- 魔表03------利用旋转 R、补 C、对角镜像 D 三种对称性,将 64 种操作归约为 6 个原型
- 魔表04------用代码从 6 个原型生成完整的 64 列矩阵 A
- 魔表05 ------解剖矩阵:发现 P/Q 联动、不变量、空间分解,将模型简化为 \(14 \times 30\)
- 魔表06 ------在 \(\mathbb{Z}_{12}\) 下直接消元,得到预变换法和包含 16 个自由变量的通解公式
- 本文------将 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.
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 问题,百度百科.