☰
QUBO建模集合划分:从罚系数到量子退火求解实践
2026/11/2 9:54:44 网站建设 项目流程

量子计算这几个字眼放在一起的时候,大多数人脑子里浮现的是实验室里的稀释制冷机、超导环路和一堆看不懂的微波线路。但我接触到的第一个真正跑通的量子计算工程问题,是一个特别朴素的问题:把一堆东西分成几组,怎么分最省。这个问题学名叫集合划分问题,而把它送进量子优化求解流程里的那套建模语言,叫QUBO。我第一次看到有人拿量子退火设备去解一个十几个元素的划分问题时,反应是"这不是大炮打蚊子吗",但把模型拆开看一遍之后,我理解了这套方法的真正价值:它提供了一种把带约束的组合优化问题统一翻译成"无约束二值二次最小化"的标准管道,一旦接上这条管道,后面换什么求解器、什么硬件、什么算法,输入格式都是同一张矩阵。

这篇东西适合谁?如果你写过背包、排班、装箱这类模型,想搞清楚怎么把它们搬到量子优化框架上去验证一下;或者你是搞算法的,听说 QUBO 但一直没弄明白那个惩罚项系数到底怎么定;又或者你只是好奇,一个"分堆"的问题为什么能被写成矩阵乘法——都能从下面的推导和代码里找到可以直接照抄的部分。我会把从问题定义到矩阵生成、从罚系数推导到结果校验的完整链路走一遍,中间那些文档里不会写的坑,也一并交代。

1. 集合划分这道题,难在哪儿

先把问题本身钉死,后面所有推导都建立在精确定义上。给定一个有限元素集合,以及若干候选子集(每个候选子集覆盖若干元素、并带一个代价),目标是挑出一批候选子集,使得每个元素被恰好覆盖一次,同时总代价最小。这个"恰好一次"是整句话的重量所在——它既不允许漏,也不允许重。漏了叫不可行,重了也叫不可行,而这两类不可行在数值求解过程里都会以"能量偏高但很接近最优"的形式出现,非常容易骗过校验环节。

它的计算难度属于典型的 NP-hard 家族。直观感受一下规模:n 个元素的候选子集理论上最多有 2^n - 1 个,8 个元素就是 255 个候选、255 个二值变量,10 个元素就直接破千。而可行解的数量对应贝尔数,10 个元素的情形下约十一万多个,20 个元素就是天文数字。这意味着穷举在中等规模就彻底失效,必须依靠启发式或者专用硬件,这正是量子优化能插上话的位置。

不过我得先泼一盆冷水:量子优化解决的从来不是"规模",而是"结构"。它并不能让一个百万变量的排班问题突然变得可解,它的价值在于提供一类物理过程(退火、绝热演化)去搜索能量景观,对某些特定稀疏结构的问题可能更快找到低能态。搞清这一点,才不会对后面跑出来的结果有不切实际的期待。

1.1 三种长得像、模型却完全不同的"划分"

这是我带的实习生最常混淆的地方。集合论里有三个兄弟,约束只差一个符号,但落到 QUBO 上,惩罚项的写法天差地别:

问题类型约束条件数学形式惩罚项构造难度
集合划分 Set Partitioning每个元素恰好覆盖一次Σ a_ij·x_j = 1低,平方即可
集合覆盖 Set Covering每个元素至少覆盖一次Σ a_ij·x_j ≥ 1中,需处理不等式方向
集合打包 Set Packing每个元素至多覆盖一次Σ a_ij·x_j ≤ 1中高,常需松弛变量

划分问题之所以最"友好",是因为等式约束的两侧都是常数,平方展开后没有额外的辅助变量,阶数天然不超过 2。覆盖问题的不等式在转成平方惩罚时,要判断违反量是正还是负,直接用 (Σ - 1)^2 会把"覆盖两次"这种在覆盖问题里合法的解也惩罚掉,导致模型错误地收紧。打包问题更麻烦,因为要表达"只允许负向违反",通常只能引入松弛变量把不等式变成等式,变量数直接膨胀。

注意:在动手写矩阵之前,务必确认你手上的业务是"恰好一次"还是"至少一次"。我见过一个排班模型,业务上其实允许一个人在某个班次被安排两次(分别是不同角色),结果模型按划分问题写死了,跑出来的解永远缺人,排查了半天才发现是问题定义错了,不是求解器的问题。

这类问题的实际落点比想象中多:航班机组排班是典型的划分模型(每个航段恰好一个机组),装箱问题的某些变体(每件货物恰好进一个箱子),特征选择里"每个特征组恰好选一个代表",甚至某些资源分配场景(每台设备的每个时段恰好分配一个任务)。只要业务规则里出现"恰好一次"这个词,基本就可以往这个框架里套。

1.2 为什么偏偏选 QUBO 这条路线

有几种建模路线可选:整数规划(ILP)、约束规划(CP)、SAT 编码、以及 QUBO/Ising。选 QUBO 的核心理由只有一个——它是量子退火硬件原生能吃的格式。

Ising 模型描述的是自旋系统的能量,形式是 Σ J_ij·s_i·s_j + Σ h_i·s_i,其中 s_i 取 ±1。量子退火设备在物理层面做的就是让一个自旋系统从初始哈密顿量演化到目标哈密顿量,最终停留在能量较低的自旋配置上。把 s_i = 2x_i - 1 代进去,Ising 就变成了 QUBO:变量从 ±1 换成 0/1,能量函数从"自旋相互作用"变成"二值二次型"。这两者是严格等价的,只是坐标系不同。

换句话说,QUBO 不是数学上的最优选择,在经典求解器上它甚至比直接写 ILP 更弱——约束被软化成了惩罚,可行性没有保证,还得额外校验。但它是唯一能直接映射到量子退火耦合矩阵的格式,这就是它的全部价值,也是我们忍受它所有缺点的理由。如果你只是想在经典机器上求精确解,老老实实上 ILP 求解器,别绕这个弯。

2. QUBO 的骨架:一条能量函数撑起全部

把问题翻译成 QUBO,本质上是设计一个函数 E(x),使得"E(x) 最小"这件事恰好对应"业务目标最优且所有约束满足"。这个函数的形式被死死限定为:

E(x) = Σ_i Q_ii·x_i + Σ_{i<j} Q_ij·x_i·x_j,其中 x_i ∈ {0,1}

写成矩阵就是 E(x) = x^T Q x。注意这里有个非常容易出错的约定问题:x^T Q x 展开后,x_i·x_j 这一项的系数是 Q_ij + Q_ji。如果 Q 是对称矩阵(Q_ij = Q_ji),那这项系数是 2·Q_ij;如果 Q 只填上三角,那系数就是 Q_ij。两种写法都合法,但混着用就会把二次项系数搞错一倍,而错一倍的后果是惩罚项与目标项的相对力度失衡,模型会开始输出各种奇奇怪怪的不可行解。我习惯统一用对称矩阵存,计算能量时直接做 x @ Q @ x,代码里不出现"乘以 2"这种手工修正,减少出错面。

2.1 标准形式与三个组成部分

一个完整的 QUBO 模型由三块拼起来,缺一不可:

第一块是目标项,编码业务上想最小化的东西。集合划分里通常是所选子集的代价之和,线性项,直接加到对角线上。

第二块是约束惩罚项,编码"必须满足"的规则。这一块是 QUBO 建模的技术核心,决定了模型的正确性。

第三块是常数偏移,严格来说是 2.2 节那个平方展开后的尾巴。常数不影响最优解的位置,但影响能量值的读数,做结果比对和罚系数调优的时候必须带着它算,不然会算出负能量、能量为负的比较结果,让人怀疑人生。

很多人写完模型发现能量值是负的,第一反应是代码 bug,其实十有八九是常数项没加回来。这件事在调罚系数的时候尤其致命,因为你要比较"可行解能量"和"不可行解能量"的大小关系,漏掉常数会让这个比较彻底失真。

2.2 约束怎么"折"进目标函数

集合划分的约束是 Σ_j a_ij·x_j = 1(对每个元素 i)。等式约束转惩罚项的经典做法是罚函数法:构造 (Σ_j a_ij·x_j - 1)^2,约束满足时这一项恰好为 0,违反时为正。再乘上一个权重 λ,把整个惩罚项加到目标函数上去:

E(x) = Σ_j c_j·x_j + λ · Σ_i (Σ_j a_ij·x_j - 1)^2

为什么用平方而不是绝对值?这是 QUBO 建模里最经典的一个"形式决定选择"的例子。绝对值会引入分段线性,展开后不能写成二值变量的二次型,硬件不接受。而平方展开后最高次项是 x_j·x_k,正好是二次,完美落在 QUBO 的允许范围内。不是平方在数学上更优,而是平方刚好是 QUBO 能容纳的最高阶。理解这一点,你就明白为什么所有约束都得想方设法"压到二次以内"。

展开过程需要一点耐心,我把每一步写清楚。设第 i 个元素的覆盖求和记为 S_i = Σ_j a_ij·x_j,则:

(S_i - 1)^2 = S_i^2 - 2·S_i + 1 S_i^2 = Σ_j a_ij^2·x_j^2 + 2·Σ_{j<k} a_ij·a_ik·x_j·x_k

关键的一步来了:因为 x_j ∈ {0,1},所以 x_j^2 = x_j。这条恒等式把所有二次的对角项"降"回了一次,于是:

S_i^2 = Σ_j a_ij^2·x_j + 2·Σ_{j<k} a_ij·a_ik·x_j·x_k

合并后得到每个约束对应的贡献:

  • 对角项(线性):λ · (a_ij^2 - 2·a_ij)
  • 非对角项(耦合):λ · 2·a_ij·a_ik,填在 (j,k) 和 (k,j) 各一半
  • 常数:λ · 1

如果覆盖矩阵是 0/1 矩阵(绝大多数集合划分问题都是),a_ij^2 = a_ij,对角项简化为 -λ·a_ij。这个负号很重要——它意味着惩罚项会把变量往"选上"的方向推,而目标项 c_j 通常是正的、往"不选"推,两者形成拉锯,λ 的大小决定了谁赢。

2.3 二次项的边界:为什么不能随便加阶

有些业务约束天然是三阶的,比如"这三个选项里最多只能选一个"。朴素写法是惩罚 x_1·x_2·x_3,但这是三次项,QUBO 矩阵装不下。标准解法是引入辅助变量 y = x_1·x_2,然后用两个新的二次约束去锁定 y 的取值:y ≤ x_1、y ≤ x_2、y ≥ x_1 + x_2 - 1。每引入一个辅助变量,变量数加一,耦合边数大幅增加。

代价有多真实?举个例子,一个 100 变量的模型里如果有 20 个三阶约束,处理完就是 120 个变量、几百条额外的耦合边。而量子退火硬件上每个物理量子比特只能连接有限个邻居,多出来的边要靠"链"(把多个物理比特串起来表示一个逻辑变量)来实现。链一长,链的断裂概率就上升,最终读出来的比特串里同一个逻辑变量可能对应着互相矛盾的物理比特值,必须靠多数投票修复,修复本身又可能改变解的质量。所以变量数每涨一点,工程的复杂度是超线性上涨的。

我的实践体会是:能通过问题重构避免的辅助变量,一定避免。比如"三个选项最多选一个"如果可以改写成"这三个选项属于同一组,组内互斥",那就可以直接用一个组级别的划分约束来表达,完全不需要三次项。建模阶段的每一次偷懒,最后都会在硬件映射阶段加倍还回来。

3. 把集合划分翻译成 QUBO 矩阵

理论推导完了,接下来是真正动手的部分。我按实际做项目的顺序来:先定变量、再估规模、然后逐项填矩阵、最后验证。

3.1 决策变量与规模估算

集合划分的决策变量是"是否选择某个候选子集",每个候选子集一个二值变量。变量数 = 候选子集数,约束数 = 元素数。看似简单,但规模估算最容易被忽略的一点是:候选子集的数量不是元素数,而是元素数的指数级。

举个实际例子。假设要给 12 个任务分配执行小组,每个任务可以选择任意一个非空的任务组合来执行(组合内任务同时执行,代价是组内最大耗时)。那么候选组合最多 2^12 - 1 = 4095 个,也就是 4095 个二值变量。QUBO 矩阵是 4095 × 4095,非零耦合最多约 840 万条。这个规模对经典启发式还好,对量子硬件来说是完全不可行的——当前主流量子退火设备的物理比特数量级在几千,还要扣掉链占用的冗余,逻辑变量能承载几百个就算不错了。

所以真正的工程做法是列池化:不枚举全部候选子集,而是先用启发式(贪心、局部搜索、列生成)生成一批高质量候选列,通常控制在元素数的 3 到 10 倍。12 个任务,生成 60 到 120 个候选列,变量数就降到了硬件可以承受的范围。代价是可能漏掉最优解,但换来了可求解性。这个取舍必须提前和业务方说清楚,否则跑出来的"最优解"其实只是"候选池内的最优解",被人拿去和全局最优一比就尴尬了。

3.2 覆盖约束的展开推导

我用一个最小算例把推导走完,这样每一步都能对着数验证。设有 3 个元素 {1, 2, 3},候选子集为:

列编号覆盖元素代价 c_j
0{1, 2}4
1{3}2
2{1}3
3{2, 3}5

覆盖矩阵 A(行是元素,列是候选):

列0 列1 列2 列3 元素1 1 0 1 0 元素2 1 0 0 1 元素3 0 1 0 1

目标函数项:Q_00 += 4, Q_11 += 2, Q_22 += 3, Q_33 += 5。

惩罚项按上一节的公式逐元素累加。列 0 覆盖了元素 1 和元素 2,所以它在两行里都有 a=1,对角贡献是 -λ 两次,即 Q_00 += -2λ。列 1 只覆盖元素 3,Q_11 += -λ。列 2 只覆盖元素 1,Q_22 += -λ。列 3 覆盖元素 2 和 3,Q_33 += -2λ。

耦合项看每一行内部的两两组合。元素 1 这一行,列 0 和列 2 都是 1,所以产生耦合 Q_02 += λ(对称填法里上下各 λ/2,用 x^TQx 展开后正好是 2·(λ/2) = λ)。元素 2 这一行,列 0 和列 3 都是 1,Q_03 += λ。元素 3 这一行,列 1 和列 3 都是 1,Q_13 += λ。其余位置为 0。

常数项是 3λ(三个元素各贡献一个 1)。

校验一下:选列 0 和列 1,总代价 6,覆盖 {1,2} ∪ {3} = 全部且不重,是可行解。代入能量:目标 6,惩罚 0,常数 3λ,总能量 6 + 3λ。选列 2 和列 3,代价 3 + 5 = 8,覆盖 {1} ∪ {2,3},也可行,能量 8 + 3λ。选列 1 和列 2,代价 5,覆盖 {1,3},元素 2 缺失,惩罚项里元素 2 那一行 (0 - 1)^2 = 1,总惩罚 1·λ,能量 5 + 3λ + λ = 5 + 4λ。只要 λ > 3,这个不可行解的能量就高于第一个可行解(6 + 3λ),模型就能正确区分。这里已经能看出 λ 的下界跟代价的差距有关,第 4 节会专门讲这个。

3.3 一份可以直接跑的 Python 实现

把上面的推导写成通用代码。这段代码的作用是:输入代价向量和覆盖矩阵,输出 QUBO 矩阵和常数项。我用对称填法,能量计算直接 x@Q@x,不做任何手工修正。

import numpy as np import itertools def build_partition_qubo(cost, A, lam): """ cost: 长度 n 的候选列代价向量 A: m x n 的 0/1 覆盖矩阵(行=元素,列=候选子集) lam: 惩罚系数 返回: Q (n x n 对称矩阵), offset (常数项) """ m, n = A.shape Q = np.zeros((n, n), dtype=float) # 1. 目标项:直接加在对角线上 for j in range(n): Q[j, j] += cost[j] # 2. 惩罚项:每个元素一行,展开 (S_i - 1)^2 for i in range(m): row = A[i] # 第 i 个元素的覆盖情况 idx = np.nonzero(row)[0] # 覆盖该元素的所有候选列 # 对角项:a^2 - 2a = a - 2a = -a (对 0/1 矩阵) for j in idx: Q[j, j] += lam * (row[j] ** 2 - 2 * row[j]) # 非对角项:2 * a_ij * a_ik,对称填,各一半 for j, k in itertools.combinations(idx, 2): coupling = lam * 2 * row[j] * row[k] Q[j, k] += coupling / 2.0 Q[k, j] += coupling / 2.0 offset = lam * m # 每个约束贡献一个常数 1 return Q, offset def energy(x, Q, offset): x = np.asarray(x, dtype=float) return float(x @ Q @ x + offset) def brute_force(Q, offset): """小规模暴力枚举,用于验证模型正确性""" n = Q.shape[0] best, best_x = None, None for bits in itertools.product([0, 1], repeat=n): e = energy(bits, Q, offset) if best is None or e < best: best, best_x = e, bits return best, best_x if __name__ == "__main__": cost = np.array([4, 2, 3, 5]) A = np.array([ [1, 0, 1, 0], [1, 0, 0, 1], [0, 1, 0, 1], ]) Q, off = build_partition_qubo(cost, A, lam=20.0) best_e, best_x = brute_force(Q, off) chosen = [j for j, b in enumerate(best_x) if b] print("最优变量组合:", chosen) print("总代价:", cost[chosen].sum(), " 能量:", best_e) # 校验:所选列的覆盖次数必须全部为 1 cover = A[:, chosen].sum(axis=1) print("各元素覆盖次数:", cover, " 是否可行:", bool((cover == 1).all()))

跑出来的结果应该是选择列 0 和列 1,代价 6,能量 6 + 60 = 66。这段代码的价值不在于求解,而在于给你一个真值参考。后面不管你用量子退火、QAOA 还是模拟退火,只要变量不超过 20 个,都可以拿这个小脚本的暴力结果去对答案。模型对不对,先在小算例上确认清楚,再上大规模,这是我吃了亏之后养成的习惯。

3.4 另一条常见支线:两路最小差

集合划分还有一个特别常见的特例,就是把一堆数分成两堆,让两堆的和尽量接近。这个问题的 QUBO 写法很漂亮,值得单独看一眼,因为它是少见的"完全不需要惩罚项"的集合划分变体。

设数字为 a_1 到 a_n,x_i = 1 表示放进 A 堆,0 表示放进 B 堆。令 s_i = 2x_i - 1 ∈ {-1, +1},则两堆之差为 Σ a_i·s_i,目标是让这个差的平方最小(平方是为了消掉符号,等价于让差接近零):

E = (Σ_i a_i·s_i)^2 = Σ_i a_i^2 + 2·Σ_{i<j} a_i·a_j·s_i·s_j

代入 s_i·s_j = (2x_i - 1)(2x_j - 1) = 4x_i·x_j - 2x_i - 2x_j + 1,展开整理后:

  • 对角项:Q_ii = -4·a_i·(S - a_i),其中 S 是所有数之和
  • 非对角项:Q_ij = 4·a_i·a_j(i < j,对称填一半)
  • 常数:S^2

因为"每个数必须分到某一堆"这件事在变量定义里已经天然满足了(x_i 非 0 即 1),所以完全没有约束需要惩罚,整个模型就是一个纯目标函数的 QUBO。这类"变量定义自带可行性"的问题是最舒服的,罚系数的调参地狱完全绕过去了。

不过它有个隐藏的坑:这个模型的最优解天然存在对称性,A/B 两堆互换是等价解,能量曲面上一模一样。跑出来在两个对称解之间跳来跳去,是正常现象,不是求解器不稳定。如果下游流程需要唯一解,加一条小的打破对称的线性项就够了,比如给最大的那个数强制 x_1 = 1 对应的偏置。

4. 罚系数:整个模型里最容易翻车的地方

我见过的 QUBO 建模失败案例里,八成以上不是推导错了,是 λ 定错了。这个参数的重要性被严重低估,大多数教程只说一句"取一个足够大的值",然后就没有然后了。什么叫足够大?大到什么程度会出问题?这两个问题都得答。

4.1 下界从哪来

λ 的作用是确保"最优不可行解的能量"严格大于"最优可行解的能量"。记最优可行解的能量为 E_feasible,任何一个违反约束的解相对它都多出一个惩罚增量。要使模型正确,对任意不可行解 x,必须满足:

E(x) = E_obj(x) + λ·V(x) > E_feasible

其中 V(x) 是约束违反量(平方和)。最保守的估计方法是:假设目标函数可以被改善到理论下界(比如全部选最便宜的列,代价为 c_min),那么目标项的最大可能改善量 Δ = E_obj(可行最优) - c_min。只要 λ > Δ,任何不可行解都不可能"靠目标项省钱"来抵消惩罚。

实用做法是取 λ = α·max_j |c_j| 或者 λ = α·Σ_j |c_j|,α 通常取 1 到 3。第二种更保守,对集合划分这种代价全为正的问题很安全。上面那个小算例里,Σ|c_j| = 14,我取 λ = 20,超出了 14,实测可行。

一个必须记住的坑:λ 的下界不是固定值,它依赖目标函数的数值范围。如果你的代价数据做过归一化或者量纲变化,λ 必须重新算。我见过有人把单位从"分钟"改成"小时",数据整体除以 60,结果 λ 还是老值,模型立刻开始输出大量不可行解——因为惩罚力度相对目标项被放大了 60 倍,导致模型过度保守,宁可违反约束也不肯选合适的列……等等,这个方向反了,放大惩罚会更容易满足约束,实际情况是 λ 相对变小导致约束失守。所以方向要判断清楚:数据缩小而 λ 不变,等于相对惩罚力度变大,约束会更难被违反,但目标项的区分度被压扁了,模型会把所有可行解看成几乎等价的,输出一堆低质量但可行的解。这两种失效模式的表现完全不同,排查时先看输出解是"不可行"还是"可行但很差"。

4.2 系数尺度失衡与精度陷阱

λ 不能无限大,原因在于硬件和数值精度。QUBO 矩阵的系数在真实硬件上会被量化到有限位宽的整数,主流设备的耦合系数和偏置都有固定的取值范围(比如偏置在 -4 到 4 之间,耦合在 -1 到 1 之间,具体视设备而定)。如果你的矩阵里目标项系数是 1,惩罚项系数是 300,直接提交会因为超范围被自动缩放,缩放后目标项的系数被压成 0.003,有效精度损失殆尽,本来有区别的解在量化后变成一模一样,搜索结果就退化成随机撒点。

我在一个 80 变量的划分问题里实测过这个现象:λ 取 1000 的时候,可行解比例是 100%,但解的质量比随机贪心还差;λ 降到 50,可行解比例 95%,但其中有 5% 的不可行解就是最优能量解,需要后处理修复;λ 取 150 是甜点区,可行率 100% 且质量接近经典启发式。这个甜点区只能靠实验扫,没有公式能直接给出来。

另一个隐形杀手是系数间的动态范围。集合划分里如果某些候选列的代价差异极大(比如从 0.1 到 10000),矩阵元素跨越五个数量级,无论怎么缩放都会丢精度。解决思路是预处理:把代价做线性映射压缩到合理区间,或者牺牲一点最优性,把极端列直接剪掉。做工程的人得接受这一点——数值稳定性永远比理论最优性重要。

4.3 我常用的三步定参流程

第一步,算理论下界。用 Σ_j |c_j| 作为保守估计,得到 λ_lower。这一步只是给个搜索区间的起点,不追求精确。

第二步,在小规模算例上扫描。取规模 15 到 20 的算例,λ 从 λ_lower 的 0.5 倍扫到 5 倍,每个值跑 50 次(因为退火是概率性的,单次结果不可靠),统计三项指标:可行解比例、最优解命中率、平均能量。画出曲线,找可行率 100% 且平均能量最低的那个区间。这一步大概花半小时,能省掉后面几天的排查时间。

第三步,规模放大后重扫。这一点很多人会漏——λ 的甜点区会随着变量数变化,因为约束数量变了,累积惩罚的量级也变了。20 变量上定好的 λ 拿到 200 变量上大概率要重新调。经验规律是变量数翻十倍,λ 大约需要上浮 1.2 到 1.5 倍,但具体还是要实测。

5. 求解与结果校验

模型建好只是前半程,跑通和跑对是两件事。

5.1 先做经典基线,别跳过

这一步我强烈建议不要省略。在碰任何量子求解器之前,先用模拟退火或者禁忌搜索在同一个 QUBO 矩阵上跑一遍,得到一个基线解。原因有三:第一,验证矩阵本身是对的——如果连经典启发式都找不到明显的可行解,那问题出在建模不在硬件;第二,给量子结果一个参照系,量子解好于基线才是真的提升,否则只是噪声;第三,基线解可以用来校准罚系数,因为你知道一个靠谱的可行解长什么样。

我的典型配置是用模拟退火跑 200 个随机初始点、每点 10 万次迭代,对这个规模的 QUBO 基本能摸到接近最优。代码上可以用现成的 anneal 库,也可以自己写一个二十行的 Metropolis 循环,后者更可控,便于记录能量轨迹。

5.2 求解器选型与参数设置

可选的落点有几类:量子退火设备、门电路上的 QAOA 变体、以及各种经典启发式。我的建议是分阶段走。先用经典模拟退火把模型和罚系数调好;再上 QAOA 做小规模验证(变量数控制在 10 到 20 以内,受限于线路深度和噪声);最后如果确实有硬件资源,再考虑量子退火设备。

用量子退火设备时最需要关注的参数是链强度和退火时间。链强度决定了多个物理比特绑成逻辑变量时的粘合力度,设小了链会断,读出的比特串自相矛盾,需要链修复;设大了会压过问题本身耦合的作用,等于把模型扭曲了。经验做法是先扫描链强度从 1.0 到 2.0 倍的最大耦合系数,选断链率最低又不明显损害解质量的值。退火时间则建议从默认值出发做延长测试,很多时候把时间拉长三到五倍,可行解比例会有明显改善。

5.3 让结果可信的检查清单

每次跑完,我会固定做下面几项检查,顺序不能变:

  1. 解码:把比特串还原成业务意义上的选择方案,而不是停留在 0/1 序列上。
  2. 可行性校验:逐条检查元素覆盖次数是否等于 1。这一步用业务数据算,不用矩阵算,避免因为矩阵拼错而"自己验证自己"。
  3. 能量复核:用独立实现的能量计算函数重新算一遍,和求解器报告的能量对比。差距大说明常数项或系数约定有出入。
  4. 多样本统计:单次结果不看,至少 20 次,看可行解比例和解质量分布。
  5. 与基线差值的显著性:如果量子解只比经典解好 1%,在样本量只有几十的情况下基本就是噪声,别急着下结论。

特别提醒:不要用"解的能量更低"作为唯一正确性判据。我踩过一次坑,模型里罚系数设得偏小,优化器很聪明地找到了一个能量极低但严重违反约束的解,能量值比任何可行解都漂亮。如果当时只看能量读数,那个模型会被判定为"性能优秀"。从那以后,我的报告模板里第一列永远是"可行解比例",第二列才是"平均能量"。

6. 常见问题与排查实录

6.1 问题速查表

现象最可能的原因排查手段处理方式
输出解全部不可行罚系数过小打印违反量分布提高 λ 至 Σ|c_j| 的 1.5 倍后重试
可行但质量远差于基线罚系数过大,目标项被压扁检查矩阵系数最大最小值之比降 λ,或先归一化代价数据
能量值与手算不符常数项缺失或二次项约定混用用一个小算例手工展开比对统一用对称矩阵 + x^TQx
硬件读数里同一变量时 0 时 1链断裂统计链断裂率提高链强度,或减少变量数
每次跑差异极大退火时间过短或初态随机性大增加重复次数看分布延长退火时间,增加读次数
最优解始终找不到候选池里根本没有该解用经典精确解对照候选池重新做列池化,扩大候选
大规模上完全失效变量数超出逻辑比特承载查 embedding 成功率缩小问题或改用区域分解

6.2 几条不太会写在文档里的经验

第一条,永远先做可行性,再做最优性。集合划分这类问题的不可行解在能量上往往只比可行解差一点点,优化过程会在边界上反复横跳。我在实践中会给模型加一个"预筛选"阶段:先用一个宽松的罚系数跑一遍,统计哪些约束最容易被违反,那些约束对应的惩罚权重单独上调。这个做法比全局调 λ 更精细,尤其是当各元素的候选列数量差异很大时(有的元素有 50 个候选列,有的只有 2 个),全局统一 λ 会让候选多的元素惩罚力度相对不足。

第二条,善用问题的分解结构。集合划分的覆盖矩阵经常是稀疏的、分块的——某些候选列只涉及一组元素,另一组元素跟它们完全无关。这时候可以在 QUBO 之前先做连通分量分解,把大问题拆成几个互不相关的小问题分别求解。这个预处理在硬件资源紧张的时候价值巨大,我在一个 300 变量的案例里靠分解把最大的连通块压到了 60 变量,直接从不 hardware 可解变成可解。

第三条,不要迷信一次退火的结果分布。量子退火的输出是一个采样分布,不是确定解。正确的心态是把它当成一个"带有物理先验的采样器",你要从几十上百次采样里挑最好的,而不是期待它一次给出答案。相应地,评估指标也要从"找到最优解的概率"而不是"单次是否最优"来看。我见过团队因为第一次跑出来结果很差就否定了整条路线,其实只是采样次数不够。

第四条,把模型和求解器彻底解耦。建模代码只负责输出 QUBO 矩阵和常数项,求解部分是一个可替换的插件。这样你可以今天用模拟退火、明天换 QAOA、后天接硬件,同一个矩阵一路跑到底,结果才可比。我早期的代码把建模和求解糊在一起,换一次后端就要改一堆地方,后来重构花了整整两天,纯属给自己找事。

最后分享一个小技巧:在写正式代码之前,先拿 5 到 8 个变量的小算例手算一遍能量,把每个非零矩阵元素的值、每一项的来源都写在纸上。这个动作看起来笨,但它能把你对 QUBO 的一切模糊理解逼到明面上——常数项到底放在哪、对称矩阵要不要除以二、0/1 矩阵的对角简化能不能用,全部会在手算中暴露。我到现在带新人还是让他们先手算一遍,比看十篇教程管用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询