无需求解线性方程,一个矩阵算出魔表通解!
魔表06:虚惊一场——在模 12 下直接求解
回顾
在上一篇文章中,我们对 \(18 \times 64\) 的矩阵 \(\mathbf{A}\) 做了系统分析,得到了三条核心结论:
- 64 种操作只有 30 种互不相同(P/Q 联动:组内 F 轮等价、B 轮等价)
- 18 个指针只有 14 个是独立的(4 个背面角被不变量锁死)
- 矩阵的秩 = 14,对应 14 个可控自由度
基于这些结论,我们把矩阵简化成了下面的 \(14 \times 30\) 形式。下图在第五篇中首次展示。

上一篇结尾写道:“下一篇,我们将讨论具体的求解算法——如何从给定的打乱状态出发,计算出一组可操作的基本操作序列。”
本文就来兑现这个承诺。
一、问题重述
1.1 我们要解的方程
简化后的 \(14×30\) 矩阵记为 \(\mathbf{M}\),\(14\) 个独立指针的状态记为 \(\mathbf{x}_{14}\),\(30\) 种操作各自的转动格数记为 \(\mathbf{k} = (k_1, k_2, \dots, k_{30})^T\)。
令 \(\mathbf{b} = -\mathbf{x}_{14} \bmod 12\),方程简写为:
\(14\) 个方程,\(30\) 个未知数。矩阵的秩 \(= 14\),解空间维数 \(= 30 - 14 = 16\)。每个自由变量可取 \(0 \sim 11\),共 \(12^{16}\) 组解。本文的目标是求出通解(包含全部 \(12^{16}\) 组解的表达式)。
1.2 贯穿全文的例子
为了避免只讲理论,我们继续用第二篇文章中的打乱状态来贯穿全文。读者应该已经见过它了。
全状态(18 维):
去掉背面四个角 \(B_1, B_3, B_7, B_9\)(不变量 \(B_1 = -F_3\) 等),得到 14 维简化状态:
取负(模 12),得到方程右端项:
下面的所有计算,都围绕 \(\mathbf{M}\mathbf{k} \equiv \mathbf{b} \pmod{12}\) 展开。
二、担忧:\(\mathbb{Z}_{12}\) 中的零因子
2.1 高斯消元法的核心操作
在先前的博客中,我写过一篇高斯消元法求解多元线性方程组,详细回顾了实数域上的高斯消元法的完整过程。消元法中最重要的步骤是主元归一——把主元所在行整体除以主元的值,使主元变成 \(1\),然后用它消去其他行中对应列的非零元。
在模 12 下,“除以 \(a\)”等价于“乘以 \(a\) 的乘法逆元”。而一个数在 \(\mathbb{Z}_{12}\) 中有逆元的充要条件是它与模数 12 互质:
我在另一篇博客扩展欧几里得算法中详细介绍了如何用扩展欧几里得算法求模意义下的乘法逆元,可供参考学习。
2.2 谁有逆元,谁没有
\(\mathbb{Z}_{12}\) 中的 12 个元素可以分为两类:
| 类别 | 元素 | 性质 |
|---|---|---|
| 单位(有逆元) | \(1,\;5,\;7,\;11\) | 可以做除法 |
| 零因子 | \(2,\;3,\;4,\;6,\;8,\;9,\;10\) | 无逆元,不能做除法 |
| 零 | \(0\) | 跳过 |
2.3 小矩阵逐步计算
为方便后续理解,在这里我们选取一个小矩阵,一步步手把手带大家计算一遍,如何使用高斯消元法求解模 12 下的线性方程组。非常熟悉这个过程的读者可以自行跳过这一部分。
题目:求解线性方程组 \(Mx\equiv b \pmod{12}\),其中:
点击展开——逐步计算过程
0. 准备工作
列出模 12 下的所有逆元:
写出增广矩阵 \([M \mid b]\):
1. 以 \(M_{11}=1\) 为主元,消去第一列
第 2 行第 1 列已经是 0,无需操作。
第 3 行: \(R_3 \leftarrow R_3 - 11 \cdot R_1\)
增广矩阵变为:
2. 将 \(M_{22}\) 主元化 1,消去第二列
第 2 行: \(R_2 \leftarrow 11 \cdot R_2\)
增广矩阵变为:
第 3 行: \(R_3 \leftarrow R_3 - 2 \cdot R_1\)
增广矩阵变为:
3. 回代消元,化为简化行阶梯型
第 1 行: \(R_1 \leftarrow R_1 - 1 \cdot R_2\)
最终增广矩阵:
4.写出通解
对应方程组为:
\(x_4\) 是自由变量,令 \(x_4 = t\)(\(t \in \{0,1,2,\dots,11\}\))。
注意 \(-11 \equiv 1 \pmod{12}\),所以:
注意到只有一个自由变量 \(t\),且 \(t\) 的取值有 \(12\) 种,所以此题共有 \(12\) 种解。
2.4 担忧的来源
我们观察到,虽然矩阵 \(\mathbf{M}\) 的原始元素只有 \(0\)、\(1\)、\(11\),也就是初始状态不会碰到零因子主元,但是消元过程中,行加减会产生新的值。例如:
一旦某步消元后,主元位置上出现了 \(2\) 或 \(10\)(或其他零因子),我们就无法做“除以主元”了——这个数没有逆元,主元归一这步直接卡死。从上面那个手算的小矩阵的例子中,我们也可以看到,在计算的过程中,是有可能会出现无逆元的数字 \(2\) 的。假如说,我们在上个例子中,需要拿第三行 \((0,\;2,\;1,\;11,\;6)\) 来主元归一,就会发现由于 \(2\) 没有逆元,而无法实现主元归一。
按常理,这时候应该求助于中国剩余定理(CRT):把模 12 的问题拆成模 3 和模 4 两个小问题,各自消元后再拼回来,绕过零因子的麻烦。
但是——真的需要吗? 我们连试都没试过。万一就像我们举的例子一样,矩阵 \(\mathbf{M}\) 的结构碰巧让所有主元都落在可逆的位置上呢?
三、直接消元:先试再说
3.1 算法概述
构造增广矩阵 \([\mathbf{M} \mid \mathbf{b}]\)(\(14 \times 31\)),在 \(\mathbb{Z}_{12}\) 下做标准的高斯消元。大致步骤和实数域上的一样,回顾高斯消元法求解多元线性方程组可帮助唤醒记忆:
- 从左到右扫描列
- 在当前列、当前行以下的区域中找非零元素
- 找到后做行交换(也可能需要列交换来选更好的主元)
- 用扩展欧几里得算法求主元的逆元,将主元归一
- 消去该列其他行中的非零元
- 重复直到所有行处理完毕或无可选主元
唯一与实数域不同的是第 4 步——除法变成了模逆。我们只需在每一步验证:主元是否属于 \(\{1,5,7,11\}\)(可逆)。
由于在此题中,模 12 下只有 \(\{1,5,7,11\}\) 可逆,并且逆元正是他们本身。为简化算法,我们可以不用每次都适用扩展欧几里得算法求解一遍他们的逆元,直接使用他们本身即可。
3.2 计算机编程实现
以下代码完整实现了上述算法。点击标签切换语言查看。
// ============================================================
// 魔表 14×30 矩阵 — Z_12 高斯消元求解 (C++)
// 参考:高斯消元法求解多元线性方程组 https://www.cnblogs.com/h-elden/p/18821309
//
// 注意:模 12 下,单位 {1, 5, 7, 11} 的逆元就是它们本身
// 因此无需扩展欧几里得算法,直接乘自身即可归一化。
// ============================================================
#include <iostream>
#include <cstring>
using namespace std;
const int N = 14; // 行数 (独立指针数)
const int M = 31; // 列数 (30 种操作 + 1 列增广)
const int MOD = 12;
int a[N][M]; // 增广矩阵 [M | b]
int col_perm[30]; // 列置换: col_perm[new_pos] = orig_idx
int pivot_col[14]; // 主元列 (置换后的位置)
int sol[30]; // 解向量 k (30 维)
int rank_g; // 秩
// 14×30 矩阵 M 的数据 (由 6 原型 + 对称变换生成)
int M_data[14][30] = {
{1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0},
{1, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0},
{1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1},
{1, 0, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0},
{1, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0},
{1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 1, 0},
{1, 1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1},
{1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0},
{1, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0},
{0, 11, 11, 0, 0, 11, 0, 11, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 11, 0, 0, 0, 11, 0, 0, 11, 11, 0, 0, 11},
{0, 11, 0, 0, 0, 11, 0, 11, 11, 0, 0, 0, 11, 0, 0, 11, 11, 0, 11, 0, 0, 11, 0, 0, 0, 11, 11, 0, 0, 11},
{0, 11, 11, 0, 0, 11, 0, 11, 11, 0, 0, 11, 11, 0, 0, 11, 11, 0, 11, 0, 0, 11, 11, 0, 0, 11, 11, 0, 0, 11},
{0, 11, 11, 0, 0, 11, 0, 0, 11, 0, 0, 11, 11, 0, 0, 0, 11, 0, 11, 0, 0, 11, 11, 0, 0, 0, 11, 0, 0, 11},
{0, 11, 0, 0, 0, 11, 0, 0, 11, 0, 0, 11, 11, 0, 0, 11, 11, 0, 0, 0, 0, 11, 11, 0, 0, 11, 11, 0, 0, 11},
};
// ---- Z_12 高斯消元 (14×30 欠定系统) ----
int gauss_z12() {
int r, c;
// 初始化列置换为恒等映射
for (c = 0; c < 30; c++) col_perm[c] = c;
for (r = 0, c = 0; c < 30 && r < N; c++) {
// 1. 在当前列剩余行中找第一个非零元素
int t = -1;
for (int i = r; i < N; i++) {
if (a[i][c] != 0) { t = i; break; }
}
// 2. 若当前列全零,向右换列
if (t == -1) {
bool found = false;
for (int c2 = c + 1; c2 < 30; c2++) {
for (int i = r; i < N; i++) {
if (a[i][c2] != 0) {
t = i;
// 交换列 c 与列 c2
for (int i2 = 0; i2 < N; i2++)
swap(a[i2][c], a[i2][c2]);
swap(col_perm[c], col_perm[c2]);
found = true;
break;
}
}
if (found) break;
}
if (!found) continue; // 该列及右边全零,跳过
}
// 3. 行交换到主元行
if (t != r)
for (int j = c; j < M; j++)
swap(a[r][j], a[t][j]);
int pivot = a[r][c];
// 4. 检查主元是否可逆 ({1,5,7,11} 为自逆元)
if (pivot != 1 && pivot != 5 && pivot != 7 && pivot != 11) {
return -1;
}
// 5. 主元归一 (乘以自身,因为 1,5,7,11 自逆)
for (int j = c; j < M; j++)
a[r][j] = (a[r][j] * pivot) % MOD;
// 6. 消去其他所有行的该列 (RREF)
for (int i = 0; i < N; i++) {
if (i == r || a[i][c] == 0) continue;
int factor = a[i][c];
for (int j = c; j < M; j++)
a[i][j] = (a[i][j] - factor * a[r][j] % MOD + MOD) % MOD;
}
pivot_col[r] = c;
r++;
}
rank_g = r;
// 7. 检查一致性
for (int i = r; i < N; i++) {
bool all_zero = true;
for (int j = 0; j < 30; j++)
if (a[i][j] != 0) { all_zero = false; break; }
if (all_zero && a[i][30] != 0) return -1; // 矛盾,无解
}
return 0; // 成功
}
// ---- 从 RREF 提取解 (自由变量置 0) ----
void extract_solution() {
memset(sol, 0, sizeof(sol));
for (int i = 0; i < rank_g; i++) {
int orig_col = col_perm[pivot_col[i]];
sol[orig_col] = a[i][30]; // 增广列即为解
}
}
// ---- 主程序 ----
int main() {
// ===== 步骤 1: 构造增广矩阵 =====
// b = -x mod 12 (本文例子的右端项)
int b[14] = {4, 10, 3, 8, 11, 0, 10, 8, 2, 3, 9, 1, 6, 9};
// 构造增广矩阵 a[0..13][0..30]
for (int i = 0; i < N; i++) {
for (int j = 0; j < 30; j++)
a[i][j] = M_data[i][j] % MOD;
a[i][30] = b[i] % MOD;
}
// ===== 步骤 2: 高斯消元 =====
int ret = gauss_z12();
if (ret == -1) {
cout << "无解" << endl;
return 0;
}
cout << "秩 = " << rank_g << " / " << N << endl;
cout << "主元列 (原始列号): ";
for (int i = 0; i < rank_g; i++)
cout << col_perm[pivot_col[i]] << " ";
cout << endl;
// ===== 步骤 3: 提取解 =====
extract_solution();
cout << "\nk (mod 12, 非零分量):" << endl;
int nz = 0;
for (int i = 0; i < 30; i++) {
if (sol[i] != 0) {
nz++;
cout << " k[" << i << "] = " << sol[i] << "\n";
}
}
cout << "非零操作数: " << nz << " / 30" << endl;
return 0;
}
注:完整的 14×30 矩阵数据(
M_data)前文已经给出,此处直接硬编码进程序中。
# ============================================================
# 魔表 14x30 矩阵 — Z_12 高斯消元求解 (Python)
# ============================================================
import numpy as np
# ---- 构建魔表 14x30 矩阵 (与前面文章相同) ----
def to_matrices(v):
return v[:9].reshape(3,3), v[9:].reshape(3,3)
def to_vector(F, B):
return np.concatenate([F.flatten(), B.flatten()])
def R_op(v, k=1):
F, B = to_matrices(v)
return to_vector(np.rot90(F, k=-int(k)), np.rot90(B, k=int(k)))
def C_op(v):
F, B = to_matrices(v)
return to_vector(np.fliplr(-B), np.fliplr(-F))
def D_op(v):
F, B = to_matrices(v)
return to_vector(F.T, np.rot90(B, k=2).T)
v0 = np.array([1,1,1,1,1,1,1,1,1, 11,0,11,0,0,0,11,0,11], dtype=int)
v1 = np.array([1,0,0,0,0,0,0,0,0, 0,11,11,0,11,11,0,0,0], dtype=int)
v2 = np.array([1,1,0,1,1,1,1,1,1, 0,0,11,0,0,0,11,0,11], dtype=int)
v3 = np.array([1,1,1,1,1,1,1,1,0, 11,0,11,0,0,0,0,0,11], dtype=int)
v4 = np.array([1,0,1,0,0,0,0,0,0, 11,11,11,11,11,11,0,0,0], dtype=int)
v5 = np.array([1,0,0,0,0,0,0,0,1, 0,11,11,11,11,11,11,11,0], dtype=int)
ul_vecs, ul_lbls = [], []
def add(l, v):
ul_vecs.append(v % 12)
ul_lbls.append(l)
add("empty", v0)
add("{UL,UR,DL,DR}", C_op(v0))
add("{UL}", v1)
add("{UR,DL,DR}", C_op(v1))
add("{UR}", v2)
add("{UL,DL,DR}", C_op(v2))
add("{DL}", D_op(v2))
add("{UL,UR,DR}", C_op(D_op(v2)))
add("{DR}", v3)
add("{UL,UR,DL}", C_op(v3))
add("{UL,UR}", v4)
add("{DL,DR}", C_op(v4))
add("{UL,DL}", D_op(v4))
add("{UR,DR}", C_op(D_op(v4)))
add("{UL,DR}", v5)
add("{UR,DL}", C_op(v5))
def bits(n):
m = {"UL":0,"UR":1,"DR":2,"DL":3}
b = 0
for k,v in m.items():
if k in n: b |= (1 << v)
return b
def rotr(b, k):
k %= 4
return ((b << k) | (b >> (4 - k))) & 0xF
lookup = {bits(l): v for l, v in zip(ul_lbls, ul_vecs)}
A18 = np.zeros((18, 64), dtype=int)
col = 0
for lbl, vec_ul in zip(ul_lbls, ul_vecs):
S = bits(lbl)
for k in range(4):
S0 = rotr(S, -k)
A18[:, col] = R_op(lookup[S0], k) % 12
col += 1
wheels = ["UL", "UR", "DR", "DL"]
unique_info = []
seen = set()
for col_idx in range(64):
vec = tuple(A18[:, col_idx])
if vec not in seen:
seen.add(vec)
lbl_idx = col_idx // 4
k = col_idx % 4
unique_info.append((vec, ul_lbls[lbl_idx], wheels[k]))
back_corners = {9, 11, 15, 17}
row_idx = [i for i in range(18) if i not in back_corners]
M = np.zeros((14, 30), dtype=int)
for c, (vec, _, _) in enumerate(unique_info):
for r, ri in enumerate(row_idx):
M[r, c] = int(vec[ri])
col_labels = [f"{lbl}, {w}" for _, lbl, w in unique_info]
# ---- Z_12 高斯消元 ----
def solve_z12(M, b):
"""
在 Z_12 下解 M·k = b (mod 12)
返回 (success, k, pivot_cols, bad_pivots)
"""
m, n = M.shape
aug = np.hstack([M.astype(int), b.reshape(-1, 1).astype(int)]) % 12
col_idx = list(range(n))
pivot_cols = []
bad_pivots = []
row = 0
for col in range(n):
if row >= m: break
# 在剩余行、剩余列中找非零元
found_r, found_c = None, None
for c2 in range(col, n):
for r in range(row, m):
if aug[r, c2] != 0:
found_r, found_c = r, c2
break
if found_r is not None: break
if found_r is None: continue
# 列交换
if found_c != col:
aug[:, [col, found_c]] = aug[:, [found_c, col]]
col_idx[col], col_idx[found_c] = col_idx[found_c], col_idx[col]
# 行交换
if found_r != row:
aug[[row, found_r]] = aug[[found_r, row]]
pivot = aug[row, col]
unit_set = {1, 5, 7, 11}
if pivot not in unit_set:
bad_pivots.append((row, col, col_idx[col], int(pivot)))
try:
inv = pow(int(pivot), -1, 12)
except ValueError:
return False, None, pivot_cols, bad_pivots
else:
inv = pow(int(pivot), -1, 12)
# 主元归一
aug[row] = (aug[row] * inv) % 12
# 消去其他行
for r in range(m):
if r != row and aug[r, col] != 0:
factor = aug[r, col]
aug[r] = (aug[r] - factor * aug[row]) % 12
pivot_cols.append((col, col_idx[col], int(pivot)))
row += 1
# 构造解
k = np.zeros(n, dtype=int)
for i, (pc_pos, pc_orig, pc_val) in enumerate(pivot_cols):
k[pc_orig] = int(aug[i, -1])
result = (M @ k) % 12
match = np.all(result == b)
return match, k, pivot_cols, bad_pivots
# ---- 在主程序中运行 ----
b = np.array([4, 10, 3, 8, 11, 0, 10, 8, 2, 3, 9, 1, 6, 9], dtype=int)
ok, k, pivot_cols, bad = solve_z12(M, b)
print(f"求解: {'成功' if ok else '失败'}")
print(f"秩 = {len(pivot_cols)}")
print(f"不良主元 = {len(bad)}")
print(f"\n主元一览:")
for pos, orig, val in pivot_cols:
print(f" 置换位置 {pos:2d}, 原始列 {orig:2d}, 主元值 {val:2d} "
f"({col_labels[orig]})")
print(f"\n解 k (非零分量):")
for i in range(30):
if k[i] != 0:
amt = k[i]
d = "顺时针" if 1 <= amt <= 6 else "逆时针"
g = amt if 1 <= amt <= 6 else 12 - amt
print(f" k[{i:2d}] = {k[i]:2d} {col_labels[i]:35s} → {d} {g} 格")
print(f"\n非零操作数: {np.sum(k != 0)}")
3.3 运行结果
运行上面的 Python 程序,部分输出如下:
秩 = 14
不良主元 = 0
主元一览:
置换位置 0, 原始列 0, 主元值 1 (∅, UL)
置换位置 1, 原始列 1, 主元值 11 ({UL,UR,DL,DR}, UL)
置换位置 2, 原始列 2, 主元值 11 ({UL}, UL)
置换位置 3, 原始列 3, 主元值 11 ({UL}, UR)
置换位置 4, 原始列 4, 主元值 11 ({UR,DL,DR}, UL)
置换位置 5, 原始列 6, 主元值 1 ({UR}, UL)
置换位置 6, 原始列 7, 主元值 11 ({UR}, UR)
置换位置 7, 原始列 9, 主元值 11 ({UL,DL,DR}, UR)
置换位置 8, 原始列 10, 主元值 1 ({DL}, UL)
置换位置 9, 原始列 11, 主元值 11 ({DL}, DL)
置换位置 10, 原始列 13, 主元值 1 ({UL,UR,DR}, DL)
置换位置 11, 原始列 14, 主元值 11 ({DR}, UL)
置换位置 12, 原始列 18, 主元值 1 ({UL,UR}, UL)
置换位置 13, 原始列 19, 主元值 1 ({UL,UR}, DR)
14 个主元,全部是 1 或 11。零个零因子。
14 个被选为主元的原始列号是:
其余 16 列(\(5, 8, 12, 15, 16, 17, 20\dots29\))是自由列——没有被选为主元,因为它们与前面的列线性相关。
3.4 得到的解
令 16 个自由变量 \(= 0\),回代得到 \(\mathbf{k}\)。有 \(13\) 个非零分量:
| 分量 \(i\) | 按钮集 | 拨轮 | \(k_i\) | 转动方式 |
|---|---|---|---|---|
| 0 | \(\varnothing\) | UL | 5 | 顺时针 5 格 |
| 1 | \(\{\mathrm{UL},\mathrm{UR},\mathrm{DL},\mathrm{DR}\}\) | UL | 1 | 顺时针 1 格 |
| 2 | \(\{\mathrm{UL}\}\) | UL | 6 | 顺时针 6 格 |
| 3 | \(\{\mathrm{UL}\}\) | UR | 7 | 逆时针 5 格 |
| 6 | \(\{\mathrm{UR}\}\) | UL | 10 | 逆时针 2 格 |
| 7 | \(\{\mathrm{UR}\}\) | UR | 5 | 顺时针 5 格 |
| 9 | \(\{\mathrm{UL},\mathrm{DL},\mathrm{DR}\}\) | UR | 3 | 顺时针 3 格 |
| 10 | \(\{\mathrm{DL}\}\) | UL | 1 | 顺时针 1 格 |
| 11 | \(\{\mathrm{DL}\}\) | DL | 2 | 顺时针 2 格 |
| 13 | \(\{\mathrm{UL},\mathrm{UR},\mathrm{DR}\}\) | DL | 11 | 逆时针 1 格 |
| 14 | \(\{\mathrm{DR}\}\) | UL | 8 | 逆时针 4 格 |
| 18 | \(\{\mathrm{UL},\mathrm{UR}\}\) | UL | 9 | 逆时针 3 格 |
| 19 | \(\{\mathrm{UL},\mathrm{UR}\}\) | DR | 2 | 顺时针 2 格 |
代入验证:\(\mathbf{M}\mathbf{k} \bmod 12 = \mathbf{b}\) ✓。13 步可解此状态。
四、一次成功,处处成功
上面我们只跑了一个具体的 \(\mathbf{b}\)。直观上自然会问:
换一个打乱状态,会不会某一步的主元突然变成 \(2\) 或 \(10\)?
答案很明确:完全不会。 理由非常简单,也极其关键。
4.1 消元的过程由谁决定
消元中的每一个决策,全部来自矩阵 \(\mathbf{M}\) 本身:
- 在哪一列找主元?看该列在剩余行中有无非零元。这由 \(\mathbf{M}\) 的列空间结构决定。
- 行交换:交换哪两行?哪行有非零元就换哪行。由 \(\mathbf{M}\) 决定。
- 列交换:换到哪一列?由 \(\mathbf{M}\) 决定。
- 主元归一:乘以几?主元的值是多少?由 \(\mathbf{M}\) 决定。
- 行加减:从其他行减多少倍的主元行?其他行在该列的值是多少?由 \(\mathbf{M}\) 决定。
增广列 \(\mathbf{b}\) 完全被动。 它只是增广矩阵的第 31 列,跟着主矩阵一起被行变换,从来不参与决策。
4.2 逻辑结论
因此:
主元的位置和值,对所有 \(\mathbf{b}\) 完全相同。
一次消元、观测到零个不可逆主元 等价于 证明了全体 \(12^{14}\) 种状态(约 1.28 千万亿种)全部可直接用高斯消元法求解。
这是因为影响主元的变量只有一个,就是 \(\mathbf{M}\),而 \(\mathbf{M}\) 是永恒不变的。
本文第二节铺垫了那么久零因子的担忧,仿佛必须引入中国剩余定理才能绕过它。结果证明——对魔表这个具体的 \(\mathbf{M}\) 矩阵,零因子压根没出现在主元位置上。 CRT 拆分完全是杞人忧天:我们的求解代码不需要拆分模数、不需要分别消元再合成。直接在 \(\mathbb{Z}_{12}\) 上做一次高斯消元就够了。
五、预变换法:一劳永逸
既然消元过程对所有 \(\mathbf{b}\) 都相同,那就更进一步——把消元过程"封装"成一个预计算好的变换。以后对任意新状态,不需要再做消元。
5.1 数学抽象
高斯消元的全过程,可以抽象为两个矩阵的乘积作用在 \(\mathbf{M}\) 上:
其中:
| 符号 | 维度 | 含义 |
|---|---|---|
| \(\mathbf{E}\) | \(14 \times 14\) | 行变换矩阵。累积了所有的行交换、行缩放(乘逆元)、行加减。\(\mathbb{Z}_{12}\) 上可逆。 |
| \(\mathbf{P}\) | \(30 \times 30\) | 列置换矩阵。记录了消元过程中所有的列交换。\(0\) 或 \(1\),每行每列恰好一个 \(1\)。 |
| \(\mathbf{R}\) | \(14 \times 30\) | 简化行阶梯形(RREF)。前 14 列为 \(\mathbf{I}_{14}\)(单位阵),后 16 列为 \(\mathbf{F}\)(自由列系数)。 |
| \(\mathbf{F}\) | \(14 \times 16\) | 自由列系数矩阵。\(\mathbf{R}\) 中第 15~30 列的部分。 |
5.2 通解公式
将方程 \(\mathbf{M}\mathbf{k} \equiv \mathbf{b}\) 两端左乘 \(\mathbf{E}\):
其中 \(\mathbf{k}' = \mathbf{P}^{-1}\mathbf{k}\) 是重排后的操作向量(前 14 个分量对应主元列,后 16 个对应自由列),\(\mathbf{b}' = \mathbf{E}\mathbf{b}\) 是变换后的右端项。
由于 \(\mathbf{R} = [\mathbf{I}_{14} \mid \mathbf{F}]\),方程展开为:
移项得:
其中 \(\mathbf{k}'_{\text{free}} \in \mathbb{Z}_{12}^{16}\) 是自由变量,可取任意值(共 \(12^{16}\) 种选择)。令 \(\mathbf{k}'_{\text{free}} = \mathbf{0}\) 即得特解。
将 \(\mathbf{k}'\) 用列置换 \(\mathbf{P}\) 还原为原始列顺序,得到最终通解。设 \(\mathbf{t} = \mathbf{k}'_{\text{free}}\) 为任意 \(16\) 维自由向量,完整表达式为:
合并为一步:
特别地,当 \(\mathbf{t} = \mathbf{0}\) 时,退化为此前的特解(\(\mathbf{k}'_{\text{free}} = \mathbf{0}\),即自由变量全置零)。
\(\mathbf{t} \neq \mathbf{0}\) 时,\(\mathbf{F}\mathbf{t}\) 用来调整 \(14\) 个主元变量的值,即用自由列的操作来替换部分主元列的操作。总效果不变,但 \(\mathbf{k}\) 中非零分量的分布可能改变了。这就是下一篇做稀疏优化的手柄。
5.3 实际操作流程
预处理(只做一次):
- 对 \(\mathbf{M}\)(不需要 \(\mathbf{b}\))做高斯消元
- 同时追踪:每做一次行操作,就同步更新 \(\mathbf{E}\)(初始为 \(\mathbf{I}_{14}\));每做一次列交换,就同步更新 \(\mathbf{P}\)(初始为 \(\mathbf{I}_{30}\))
- 消元结束后,输出三个东西:\(\mathbf{E}\)、\(\mathbf{P}\)、主元列/自由列的划分
对任意新状态 \(\mathbf{x}\)(每次使用):
- \(\mathbf{b} = -\mathbf{x} \bmod 12\)
- \(\mathbf{b}' = \mathbf{E} \cdot \mathbf{b} \bmod 12\)(一次 \(14 \times 14\) 矩阵乘 \(14\) 维向量)
- 选 \(\mathbf{k}'_{\text{free}}\)(例如全零),算 \(\mathbf{k}'_{\text{pivot}} = \mathbf{b}' - \mathbf{F}\mathbf{k}'_{\text{free}}\)
- \(\mathbf{k} = \mathbf{P} \cdot \mathbf{k}'\)
完全没有消元步骤。 求解变成了两次小矩阵乘法。
5.4 计算机编程实现
以下代码实现了预处理和通解函数的完整流程。
// ============================================================
// 魔表 14x30 矩阵 — 预变换法 (C++)
// 一次性记录 E 和 P,此后对任意 b 直接求解
// 模 12 下 {1,5,7,11} 自逆,无需扩展欧几里得算法
// ============================================================
#include <iostream>
#include <cstring>
using namespace std;
const int N = 14; // 行数
const int C = 30; // 列数
const int MOD = 12;
int a[N][C]; // 工作矩阵 (M 的副本, 消元中被修改)
int E[N][N]; // 行变换矩阵 (初始 = I)
int col_perm[C]; // 列置换
int pivot_orig[14]; // 主元列 (原始索引)
int free_orig[16]; // 自由列 (原始索引)
int F[N][16]; // 自由列系数矩阵
int rank_g; // 秩
int nf; // 自由列个数
// 14×30 矩阵 M_data (略, 与第一节相同)
int M_data[14][30] = { /* ... */ };
// ---- 构建 Solver (预处理, 只做一次) ----
void build_solver(int M_data[N][C]) {
// 初始化工作矩阵 a = M_data
for (int i = 0; i < N; i++)
for (int j = 0; j < C; j++)
a[i][j] = (M_data[i][j] % MOD + MOD) % MOD;
// E = I_14
memset(E, 0, sizeof(E));
for (int i = 0; i < N; i++) E[i][i] = 1;
// col_perm = identity
for (int j = 0; j < C; j++) col_perm[j] = j;
int row = 0;
for (int col = 0; col < C && row < N; col++) {
// 在剩余行、剩余列中找非零元
int t = -1, found_c = -1;
for (int c2 = col; c2 < C && t < 0; c2++)
for (int i = row; i < N; i++)
if (a[i][c2] != 0) {
t = i;
found_c = c2;
break;
}
if (t < 0) continue;
// 列交换
if (found_c != col) {
for (int i = 0; i < N; i++)
swap(a[i][col], a[i][found_c]);
swap(col_perm[col], col_perm[found_c]);
}
// 行交换
if (t != row) {
for (int j = 0; j < C; j++)
swap(a[row][j], a[t][j]);
for (int j = 0; j < N; j++)
swap(E[row][j], E[t][j]);
}
int pivot = a[row][col];
// pivot 必为自逆元 {1,5,7,11}
int inv = pivot;
// 主元归一: 作用于 a 和 E
for (int j = 0; j < C; j++)
a[row][j] = (a[row][j] * inv) % MOD;
for (int j = 0; j < N; j++)
E[row][j] = (E[row][j] * inv) % MOD;
// 消去其他行
for (int i = 0; i < N; i++) {
if (i == row || a[i][col] == 0) continue;
int factor = a[i][col];
for (int j = 0; j < C; j++)
a[i][j] = (a[i][j] - factor * a[row][j] % MOD + MOD) % MOD;
for (int j = 0; j < N; j++)
E[i][j] = (E[i][j] - factor * E[row][j] % MOD + MOD) % MOD;
}
pivot_orig[row] = col_perm[col];
row++;
}
rank_g = row;
// 识别自由列
bool is_pivot[30] = {};
for (int i = 0; i < rank_g; i++) is_pivot[pivot_orig[i]] = true;
nf = 0;
for (int j = 0; j < C; j++)
if (!is_pivot[j])
free_orig[nf++] = j;
// 提取 F 矩阵
for (int i = 0; i < N; i++)
for (int j = 0; j < nf; j++)
F[i][j] = a[i][free_orig[j]];
}
// ---- 使用预计算的结果求解任意 b ----
// k_free 为自由变量数组 (长度 = nf = 16), 若为 NULL 则全置 0
void solve(int b[], int k[], int k_free[]) {
// b' = E @ b (mod 12)
int bp[N] = {};
for (int i = 0; i < N; i++)
for (int j = 0; j < N; j++)
bp[i] = (bp[i] + E[i][j] * b[j]) % MOD;
// k'_pivot = b' - F @ k_free
int kp[N];
for (int i = 0; i < N; i++) {
kp[i] = bp[i];
for (int j = 0; j < nf; j++) {
int kf = k_free ? k_free[j] : 0;
kp[i] = (kp[i] - F[i][j] * kf % MOD + MOD) % MOD;
}
}
// 构造 k' (置换后顺序)
// 前 rank_g 个是主元变量, 后 nf 个是自由变量
int k_perm[30] = {};
for (int i = 0; i < rank_g; i++) k_perm[i] = kp[i];
for (int j = 0; j < nf; j++)
k_perm[rank_g + j] = k_free ? k_free[j] : 0;
// 逆置换恢复原始列顺序
int inv_perm[30];
for (int j = 0; j < C; j++)
inv_perm[col_perm[j]] = j;
memset(k, 0, sizeof(int) * C);
for (int j = 0; j < C; j++)
k[j] = k_perm[inv_perm[j]];
}
// ---- 使用示例 ----
int main() {
// 构建 Solver (一次性)
build_solver(M_data);
cout << "预处理完毕。秩 = " << rank_g << endl;
cout << "主元列: ";
for (int i = 0; i < rank_g; i++) cout << pivot_orig[i] << " ";
cout << endl;
// 求解: 任意状态, 直接秒解
int b[14] = {4, 10, 3, 8, 11, 0, 10, 8, 2, 3, 9, 1, 6, 9};
int k[30];
solve(b, k, NULL); // k_free = NULL 即全置零
int nz = 0;
for (int i = 0; i < 30; i++) {
if (k[i] != 0) {
nz++;
cout << " k[" << i << "]\t=\t" << k[i] << endl;
}
}
cout << "\n非零操作数: " << nz << " / 30" << endl;
return 0;
}
# ============================================================
# 魔表 14x30 矩阵 — 预变换法 (Python)
# 构建 E 和 P, 之后每次求解只需矩阵乘法
# 前面的 M 和 col_labels 沿用之前构建好的
# ============================================================
import numpy as np
def build_solver(M):
"""
预处理: 对 M 做高斯消元, 同时记录 E 和 P。
返回 (E, P, pivot_cols, free_cols, F, col_perm)
"""
m, n = M.shape
E = np.eye(m, dtype=int) # 行变换矩阵
A = M.copy() % 12 # 工作矩阵
col_perm = list(range(n)) # 列置换追踪
pivot_cols = [] # 主元列 (原始索引)
col_positions = [] # 主元列在置换后的位置
row = 0
for col in range(n):
if row >= m: break
# 在剩余区域找非零元
found_r, found_c = None, None
for c2 in range(col, n):
for r in range(row, m):
if A[r, c2] != 0:
found_r, found_c = r, c2
break
if found_r is not None: break
if found_r is None: continue
# 列交换
if found_c != col:
A[:, [col, found_c]] = A[:, [found_c, col]]
col_perm[col], col_perm[found_c] = col_perm[found_c], col_perm[col]
# 行交换
if found_r != row:
A[[row, found_r]] = A[[found_r, row]]
E[[row, found_r]] = E[[found_r, row]]
pivot = A[row, col]
inv = pow(int(pivot), -1, 12)
# 主元归一
A[row] = (A[row] * inv) % 12
E[row] = (E[row] * inv) % 12
# 消去其他行
for r in range(m):
if r != row and A[r, col] != 0:
factor = A[r, col]
A[r] = (A[r] - factor * A[row]) % 12
E[r] = (E[r] - factor * E[row]) % 12
pivot_cols.append(col_perm[col])
col_positions.append(col)
row += 1
rank = row
free_cols = [j for j in range(n) if j not in pivot_cols]
# 构建列置换矩阵 P: P[orig, new_pos] = 1
P = np.zeros((n, n), dtype=int)
for new_pos, orig in enumerate(col_perm):
P[orig, new_pos] = 1
# 构建逆置换 (用于从置换后坐标恢复原始坐标)
inv_perm = {new: orig for new, orig in enumerate(col_perm)}
# 提取自由列系数 F: 从 A 的前 rank 行、后 (n-rank) 列
F = A[:rank, rank:] % 12 if n - rank > 0 else np.zeros((rank, 0), dtype=int)
return E, P, pivot_cols, free_cols, F, col_perm, inv_perm
def solve_precomputed(E, P, F, pivot_cols, free_cols, inv_perm, b, k_free=None):
"""
使用预计算的结果求解 M·k = b (mod 12)。
参数:
E, P, F, pivot_cols, free_cols, inv_perm: build_solver 的返回值
b: 右端向量 (mod 12)
k_free: 自由变量值, 默认全零
返回: k (解向量)
"""
m, n = len(b), P.shape[0]
if k_free is None:
k_free = np.zeros(len(free_cols), dtype=int)
k_free = np.array(k_free, dtype=int) % 12
# b' = E @ b
bp = (E @ b) % 12
# k'_pivot = b' - F @ k_free
kp = (bp - F @ k_free) % 12
# 构造 k' (置换后顺序)
k_perm = np.zeros(n, dtype=int)
k_perm[:len(kp)] = kp
k_perm[len(kp):len(kp) + len(k_free)] = k_free
# 逆置换: k[orig] = k_perm[new_pos]
k = np.zeros(n, dtype=int)
for new_pos in range(n):
orig = inv_perm[new_pos]
k[orig] = k_perm[new_pos]
return k
# ---- 在主程序中使用 ----
# 构建 Solver (一次性)
E, P, pivot_cols, free_cols, F, col_perm, inv_perm = build_solver(M)
print("预处理完毕。")
print(f" 主元列: {pivot_cols}")
print(f" 自由列: {free_cols}")
print(f" E 矩阵: {E.shape}")
print(f" F 矩阵: {F.shape}")
# 求解本文的例子
b = np.array([4, 10, 3, 8, 11, 0, 10, 8, 2, 3, 9, 1, 6, 9], dtype=int)
k = solve_precomputed(E, P, F, pivot_cols, free_cols, inv_perm, b)
result = (M @ k) % 12
print(f"\n验证: {'通过' if np.all(result == b) else '失败'}")
print(f"非零操作数: {np.sum(k != 0)}")
print("解 k (非零分量):")
for i in range(30):
if k[i] != 0:
d = "CW" if 1 <= k[i] <= 6 else "CCW"
g = k[i] if 1 <= k[i] <= 6 else 12 - k[i]
print(f" k[{i:2d}] = {k[i]:2d} ({col_labels[i]}) {d} {g}")
# 对新状态, 秒解
b_new = np.array([2, 7, 5, 1, 0, 3, 8, 2, 4, 6, 5, 9, 3, 1], dtype=int)
k_new = solve_precomputed(E, P, F, pivot_cols, free_cols, inv_perm, b_new)
result_new = (M @ k_new) % 12
print(f"\n新状态验证: {'通过' if np.all(result_new == b_new) else '失败'}")
print(f"非零操作数: {np.sum(k_new != 0)}")
5.5 计算结果
\(\mathbf{E}\) 矩阵(\(14 \times 14\))是:
点击展开——行变换矩阵 E(14×14)
b0 b1 b2 b3 b4 b5 b6 b7 b8 b9 b10 b11 b12 b13
R0 1 11 1 11 1 11 1 11 1 2 1 10 1 2
R1 0 0 0 0 0 0 0 0 0 11 0 1 0 11
R2 0 0 0 0 0 0 0 0 0 11 1 0 0 0
R3 11 1 0 1 11 0 0 0 0 11 0 1 11 0
R4 0 0 0 1 0 0 0 11 0 0 0 0 0 0
R5 0 1 11 11 0 0 0 1 0 0 11 0 0 0
R6 0 0 0 0 0 0 0 0 0 0 0 11 1 0
R7 0 0 0 11 1 0 0 0 0 0 0 0 0 0
R8 0 0 0 0 0 0 11 1 0 0 0 0 0 11
R9 0 0 0 0 0 0 0 0 0 1 0 11 0 0
R10 0 0 0 11 1 11 0 1 0 0 0 0 0 0
R11 0 0 0 1 11 1 0 0 11 11 0 1 0 11
R12 0 0 0 0 0 0 0 0 0 1 11 0 11 1
R13 0 11 0 1 0 1 0 11 0 0 0 0 0 0
\(\mathbf{P}\) 是 \(30 \times 30\) 置换矩阵。更直观的理解是它的列置换映射:消元过程将 14 个主元列换到了前 14 位,16 个自由列换到了后 16 位。具体对应关系如下表,记作 \(\text{col\_perm}[\text{new\_pos}] = \text{orig\_col}\):
| 置换后位置 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 原始列号 | 0 | 1 | 2 | 3 | 4 | 6 | 7 | 9 | 10 | 11 | 13 | 14 | 18 | 19 |
| 置换后位置 | 14 | 15 | 16 | 17 | 18 | 19 | 20-29 |
|---|---|---|---|---|---|---|---|
| 原始列号 | 5 | 15 | 16 | 17 | 12 | 8 | 20,21,…,29 |
点击展开——P 矩阵文本 (30×30, 1 = 该列在该置换位置)
置换后位置: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
列 0 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
列 1 . 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . .
列 2 . . 1 . . . . . . . . . . . . . . . . . . . . . . . . . . .
列 3 . . . 1 . . . . . . . . . . . . . . . . . . . . . . . . . .
列 4 . . . . 1 . . . . . . . . . . . . . . . . . . . . . . . . .
列 5 . . . . . . . . . . . . . . 1 . . . . . . . . . . . . . . .
列 6 . . . . . 1 . . . . . . . . . . . . . . . . . . . . . . . .
列 7 . . . . . . 1 . . . . . . . . . . . . . . . . . . . . . . .
列 8 . . . . . . . . . . . . . . . . . . . 1 . . . . . . . . . .
列 9 . . . . . . . 1 . . . . . . . . . . . . . . . . . . . . . .
列10 . . . . . . . . 1 . . . . . . . . . . . . . . . . . . . . .
列11 . . . . . . . . . 1 . . . . . . . . . . . . . . . . . . . .
列12 . . . . . . . . . . . . . . . . . . 1 . . . . . . . . . . .
列13 . . . . . . . . . . 1 . . . . . . . . . . . . . . . . . . .
列14 . . . . . . . . . . . 1 . . . . . . . . . . . . . . . . . .
列15 . . . . . . . . . . . . . . . 1 . . . . . . . . . . . . . .
列16 . . . . . . . . . . . . . . . . 1 . . . . . . . . . . . . .
列17 . . . . . . . . . . . . . . . . . 1 . . . . . . . . . . . .
列18 . . . . . . . . . . . . 1 . . . . . . . . . . . . . . . . .
列19 . . . . . . . . . . . . . 1 . . . . . . . . . . . . . . . .
列20 . . . . . . . . . . . . . . . . . . . . 1 . . . . . . . . .
列21 . . . . . . . . . . . . . . . . . . . . . 1 . . . . . . . .
列22 . . . . . . . . . . . . . . . . . . . . . . 1 . . . . . . .
列23 . . . . . . . . . . . . . . . . . . . . . . . 1 . . . . . .
列24 . . . . . . . . . . . . . . . . . . . . . . . . 1 . . . . .
列25 . . . . . . . . . . . . . . . . . . . . . . . . . 1 . . . .
列26 . . . . . . . . . . . . . . . . . . . . . . . . . . 1 . . .
列27 . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 . .
列28 . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 .
列29 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
\(\mathbf{F}\) 矩阵(\(14 \times 16\))记录了 16 个自由列与 14 个主元变量之间的系数关系。它的 16 列依次对应自由列 5, 8, 12, 15, 16, 17, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29。
点击展开——F 矩阵文本 (14×16)
c5 c8 c12 c15 c16 c17 c20 c21 c22 c23 c24 c25 c26 c27 c28 c29
R0 11 0 11 0 11 11 0 0 11 0 0 11 10 11 11 10
R1 1 0 1 0 1 1 0 0 1 0 0 1 1 0 0 1
R2 0 11 0 0 0 0 0 11 1 0 0 0 0 0 0 0
R3 1 11 0 11 0 0 0 0 0 0 0 0 0 1 0 1
R4 0 0 0 11 0 0 1 0 0 11 0 0 0 0 0 0
R5 0 1 0 1 0 1 11 1 0 1 1 0 1 0 1 0
R6 0 1 0 0 0 0 0 0 0 0 0 1 0 0 0 0
R7 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 0
R8 0 1 0 1 1 0 0 0 0 1 0 1 1 0 1 0
R9 0 1 0 0 0 0 0 1 0 0 0 0 0 0 0 0
R10 0 0 0 1 0 0 11 0 0 1 1 0 0 0 0 0
R11 0 11 1 11 0 0 1 11 1 11 0 0 0 1 0 1
R12 0 0 0 0 0 0 0 1 11 0 0 11 0 0 0 0
R13 0 0 0 0 0 0 1 0 0 11 11 0 0 0 0 0
六、在例子上完整走一遍
回到本文的例子,用预变换法解一遍。
6.1 计算 \(\mathbf{b}' = \mathbf{E}\mathbf{b}\)
以 \(b'_0\) 为例,计算过程:
全部 \(14\) 个分量计算完成:
6.2 得到解
令 \(\mathbf{k}'_{\text{free}} = \mathbf{0}\),则 \(\mathbf{k}'_{\text{pivot}} = \mathbf{b}'\)。用列置换还原后,非零分量即第三节表格中的 13 个操作,与前文 3.4 节的解完全一致。
6.3 翻译为复原指令
| 步骤 | 按钮状态 | 拨轮 | 转动方向 | 转动量 |
|---|---|---|---|---|
| 1 | 全部弹起(\(\varnothing\)) | UL | 顺时针 | 5 格 |
| 2 | 全部按下 | UL | 顺时针 | 1 格 |
| 3 | 只按 UL | UL | 顺时针 | 6 格 |
| 4 | 只按 UL | UR | 逆时针 | 5 格 |
| 5 | 只按 UR | UL | 逆时针 | 2 格 |
| 6 | 只按 UR | UR | 顺时针 | 5 格 |
| 7 | 按 UL,DL,DR(不按 UR) | UR | 顺时针 | 3 格 |
| 8 | 只按 DL | UL | 顺时针 | 1 格 |
| 9 | 只按 DL | DL | 顺时针 | 2 格 |
| 10 | 按 UL,UR,DR(不按 DL) | DL | 逆时针 | 1 格 |
| 11 | 只按 DR | UL | 逆时针 | 4 格 |
| 12 | 按 UL,UR | UL | 逆时针 | 3 格 |
| 13 | 按 UL,UR | DR | 顺时针 | 2 格 |
由于第一篇的性质 3(操作可交换),这 13 种操作可以按任意顺序执行,效果相同。总计 13 步。
七、解的初步分析:离最优还有多远
7.1 ≤14 步 vs 上帝数 12
魔表的状态空间有 \(12^{14}\) 个状态,我们通过给出通解的公式以及求解的思路,理论上,针对 \(12^{14}\) 个状态中的任何一种情况,我们都能够求出所有 \(12^{16}\) 种解。
特别地,如果我们将自由变量置为0,就一定能找到一个小于等于 14 步骤的解。
正如本文中给出的例子,我们通过将自由变量置为 0,获得了一个 13 步骤的解。
这个数字 \(14\) 当然不是最优解。已知的结论是:上帝数为 12——任何可解状态都能在 \(12\) 步以内还原,且确实存在不少需要整整 \(12\) 步才能还原的"最远状态"。
7.2 零空间就是优化空间
通解公式中,\(\mathbf{k}'_{\text{free}} \in \mathbb{Z}_{12}^{16}\) 取了全零。但零空间有 16 维——这意味着有 16 个独立的自由度可以用来"调整"解的结构。
调整的原理是:利用 \(\mathbf{F}\) 矩阵(\(14 \times 16\)),把某些主元列的操作用自由列来替换。本质上是在做”操作组合的等价替换“——这一替换不会改变最终效果(因为自由列与主元列线性相关,替换是在零空间内进行的),但它可以改变 \(\mathbf{k}\) 中非零分量的个数。
一个直觉类比:假设 A 操作的列 = B 操作的列 + C 操作的列(在模 12 下),那么通解中可以把 \(k_A\) 改成 0,同时把 \(k_B\) 和 \(k_C\) 各加 1——效果不变,但非零分量数从 1 变成了 2(变差了),也可能反过来从 2 变成 1(变好了)。
目标:在 \(12^{16}\) 种 \(\mathbf{k}'_{\text{free}}\) 中,找到使 \(\|\mathbf{k}\|_0\) 最小的那个。
这就是我们下一篇要解决的问题——\(\mathbb{Z}_{12}\) 上的稀疏优化。
结语
本文做了一件在动笔前自己也没想到的事:从头到尾担忧着 \(\mathbb{Z}_{12}\) 的零因子,结果发现完全是杞人忧天。
回顾全过程:
- 设问:\(\mathbb{Z}_{12}\) 有 6 个零因子,它们会阻碍高斯消元吗?
- 试探:不管三七二十一,直接跑消元。14 个主元全是 1 或 11。
- 论证:主元值由 \(\mathbf{M}\) 决定、与 \(\mathbf{b}\) 无关。一次成功 = 处处成功。
- 升华:将消元过程封装为 \(\mathbf{E} \cdot \mathbf{M} \cdot \mathbf{P} = \mathbf{R}\),预计算一次,此后任意新状态秒出解。
- 收获:不只是一个特解,而是带 16 个自由变量的完整通解。自由变量对应的零空间,正是优化步数的杠杆。
从”能不能解“到”怎么解最快“,中间的桥就是本文的通解公式。下一篇,我们将用这道公式深入零空间,追寻步数的最小值——也就是魔表的最优解。
参考资料
Tom Rokicki,Rubik's Clock God's Number is 12,cube20.org.

本文基于第五篇建立的 14×30 简化矩阵,直接在 Z₁₂ 上完成高斯消元求解。14 个主元全是自逆元,无需中国剩余定理。将消元过程封装为预变换 E·M·P = R,得到含 16 个自由变量的通解公式,用该公式计算出的特解最多 14 步即可复原魔表。
浙公网安备 33010602011771号