拓冰建站拓冰建站
首页 / 资讯中心 / 正文

QUBO模型实战:从数学建模到量子计算优化问题求解

1. 从数学建模到量子计算一次跨界解题的实战复盘去年参加MathorCup数学建模挑战赛A题的经历现在回想起来依然觉得是一次非常“硬核”的思维碰撞。题目本身是一个典型的组合优化问题但当时我们团队没有走传统的运筹学或启发式算法的老路而是选择了一个在当时看来有点“超前”的路径用量子计算中的QUBO模型来建模和求解。最终这个大胆的尝试让我们拿到了一等奖也让我对如何将前沿计算范式落地到具体问题有了更深的理解。今天我就把当时的解题思路、核心代码实现以及踩过的那些坑毫无保留地分享出来。这不是一篇教科书式的理论综述而是一个实战派从零到一构建解决方案的完整记录无论你是对数学建模感兴趣还是想了解量子计算如何解决实际问题相信都能从中获得可以直接“抄作业”的干货。很多人一听到“量子计算”和“QUBO”就觉得高深莫测是实验室里的玩意儿。其实不然QUBO模型本质上是一个极其简洁的二次无约束二元优化模型它的强大之处在于能将许多复杂的组合优化问题比如调度、路由、分配统一成一个标准形式。而近年来的量子退火机和一些专用硬件正是为高效求解这类模型而设计的。我们的策略就是先把复杂的赛题“翻译”成QUBO语言再调用高效的求解器来寻找最优解。这个过程中如何精准地建模、如何设置惩罚项、如何验证结果每一步都充满了挑战和技巧。接下来我就带你完整走一遍我们当时的思考与操作链路。2. 赛题本质剖析为什么QUBO模型是“对症良药”首先我们需要回顾一下题目的核心。MathorCup A题通常涉及资源分配、路径规划或网络优化等场景需要我们在满足一系列约束条件的前提下最大化或最小化某个目标如成本最低、效率最高。这类问题在数学上属于NP-Hard问题当问题规模稍大时传统精确算法如分支定界会面临“组合爆炸”计算时间无法承受而启发式算法如遗传算法、模拟退火又可能陷入局部最优且参数调优非常依赖经验。QUBO模型的全称是Quadratic Unconstrained Binary Optimization即二次无约束二元优化。它的标准形式非常简洁min/max: x^T Q x其中x是一个由0和1组成的二元变量向量Q是一个实对称矩阵。整个模型没有显式的约束条件所有约束都是通过巧妙地设计目标函数中的二次项和一次项即矩阵Q中的元素来“软性”实现的。具体来说我们会把违反约束的情况转化为一个很大的惩罚项加到目标函数里这样求解器在寻找目标函数最小值的过程中会自然而然地避免那些惩罚项很高的即违反约束的解。为什么这对我们的赛题是“良药”原因有三 第一建模灵活性。赛题中复杂的“如果...那么...”逻辑约束、互斥约束、容量约束等都可以通过添加惩罚项的方式融入QUBO框架。这比在传统优化模型中处理这些约束要直观和统一得多。 第二求解器生态。即便我们没有真正的量子计算机当前也有强大的工具可用。例如D-Wave的量子退火模拟器dimod以及富士通、Hitachi等公司开发的基于数字退火或CMOS退火原理的专用求解器都能高效处理QUBO问题。我们可以利用这些现成的“引擎”。 第三未来兼容性。建立的QUBO模型是硬件无关的。今天可以用经典模拟器求解明天一旦有机会接入真实的量子退火机模型可以直接部署获得潜在的“量子优势”。我们的任务就是当好这个“翻译官”把充满业务逻辑的中文赛题翻译成纯净的数学语言——QUBO模型。3. 从问题描述到QUBO矩阵一步步构建数学模型假设我们的赛题是一个简化的资源调度问题有N个任务需要分配到M台机器上执行每个任务只能分配给一台机器每台机器有处理能力上限。任务i在机器j上执行的成本为c_{ij}。目标是找到总成本最低的分配方案。步骤一定义决策变量这是最关键的一步。我们引入二元决策变量x_{i,j}x_{i,j} 1表示将任务i分配给机器j。x_{i,j} 0表示不这样分配。 这样对于一个有N个任务、M台机器的问题我们一共需要N * M个二元变量。这些变量最终会排成一个一维向量x但我们在建模时按二维思考更直观。步骤二构建目标函数成本项总成本就是所有被激活的分配即x_{i,j}1所对应的成本之和。在QUBO中这体现为线性项因为x_{i,j}^2 x_{i,j}对于0/1变量成立。 目标函数的第一部分H_cost Σ_i Σ_j c_{ij} * x_{i,j}。 我们的目标是最小化H_cost。步骤三将约束转化为惩罚项这是QUBO建模的精髓所在也是最有技巧性的部分。每个任务必须且只能分配给一台机器。这意味着对于任意一个任务i在所有机器j中有且仅有一个x_{i,j}等于1。我们可以用这样一个惩罚项来实现H_assign A * Σ_i (1 - Σ_j x_{i,j})^2其中A是一个很大的正数称为惩罚系数。我们来分析一下如果任务i恰好分配了一台机器Σ_j x_{i,j} 1那么(1-1)^2 0没有惩罚。如果任务i没有分配和为0或分配了多台机器和1那么(1 - sum)^2就会是一个正数再乘以巨大的A就会使目标函数值急剧增大迫使求解器避免这种情况。注意惩罚系数A的选取至关重要。太小约束不起作用太大可能掩盖了真实目标成本的差异导致数值问题。一个经验法则是让惩罚项的典型值比目标项的典型值大一个数量级。我们通常通过多次试验来确定。每台机器的负载不能超过其能力上限Cap_j。假设任务i在机器j上执行会产生负载l_{ij}。那么对于机器j总负载Σ_i l_{ij} * x_{i,j}必须≤ Cap_j。 在QUBO中处理“小于等于”约束一个标准技巧是引入松弛变量。我们引入一个新的非负整数松弛变量s_j将不等式转化为等式Σ_i l_{ij} * x_{i,j} s_j Cap_j其中s_j ≥ 0。 为了在QUBO中使用我们需要用二进制比特来表示整数松弛变量s_j。例如如果Cap_j不是很大我们可以用K个比特的二进制展开来表示s_js_j Σ_{k0}^{K-1} 2^k * y_{j,k}其中y_{j,k}是新的二元变量。 然后将等式约束转化为惩罚项H_capacity B * Σ_j (Σ_i l_{ij} * x_{i,j} Σ_k 2^k * y_{j,k} - Cap_j)^2同样B是一个大的惩罚系数。步骤四组合完整QUBO目标函数完整的QUBO哈密顿量即需要最小化的目标函数为H H_cost H_assign H_capacity将其展开并整理成x^T Q x的标准形式这里的x向量包含了所有x_{i,j}和表示松弛变量的y_{j,k}。展开后我们会得到许多二次交叉项如x_{i,j} * x_{i,k}和线性项。整理出对称矩阵Q的过程是系统性的通常由编程自动完成。4. 求解器选择与代码实战从矩阵到最优解模型建好了接下来就是求解。我们当时评估了几个选项D-Wave Leap / Ocean SDK可以直接提交到真实的量子退火机或其云模拟器。优点是能体验最前沿的技术但对于比赛而言云服务可能有延迟和调用限制。模拟退火器 (Simulated Annealing)经典算法实现简单对于中等规模问题效果不错。dimod库自带的SimulatedAnnealingSampler就很好用。量子退火模拟器例如neal是D-Wave提供的经典模拟器模仿量子退火行为比普通模拟退火在某些问题上更有优势。专用QUBO求解器如富士通的Digital Annealer模拟器速度极快。考虑到比赛的易用性和稳定性我们最终选择了dimod库的SimulatedAnnealingSampler作为主力求解器并用neal作为对比验证。下面是我精简和重构后的核心代码框架你可以直接复用。import dimod import numpy as np from typing import List, Tuple class QuboModelScheduler: 使用QUBO模型解决任务调度问题的类 def __init__(self, num_tasks: int, num_machines: int, cost_matrix: np.ndarray, load_matrix: np.ndarray, machine_caps: List[int]): 初始化调度器。 Args: num_tasks: 任务数量 (N) num_machines: 机器数量 (M) cost_matrix: 成本矩阵形状 (N, M)cost_matrix[i][j] 表示任务i在机器j上的成本 load_matrix: 负载矩阵形状 (N, M)load_matrix[i][j] 表示任务i在机器j上的负载 machine_caps: 机器容量列表长度 M self.N num_tasks self.M num_machines self.C cost_matrix self.L load_matrix self.Caps machine_caps # 计算表示松弛变量所需的比特数 K # 松弛变量最大可能需要表示 Cap_j所以二进制位数需要能覆盖这个范围 self.K [int(np.ceil(np.log2(cap 1))) for cap in self.Caps] self.total_vars self.N * self.M sum(self.K) # 总变量数分配变量 所有松弛变量比特 # 惩罚系数这些需要根据问题规模调整 self.A 10 * np.max(np.abs(self.C)) # 分配约束惩罚系数 self.B 10 * self.A # 容量约束惩罚系数通常设得更大 def build_qubo_matrix(self) - np.ndarray: 构建QUBO模型的Q矩阵上三角矩阵dimod要求 Q np.zeros((self.total_vars, self.total_vars)) # 1. 添加成本项 (线性项放在对角线) idx 0 for i in range(self.N): for j in range(self.M): Q[idx, idx] self.C[i, j] # H_cost 中的 c_{ij} * x_{i,j} idx 1 # 此时 idx 指向第一个松弛变量的位置 # 2. 添加任务分配约束惩罚项 H_assign # H_assign A * Σ_i (1 - Σ_j x_{i,j})^2 A * Σ_i (1 - 2*Σ_j x_{i,j} Σ_j Σ_k x_{i,j}*x_{i,k}) # 对于每个任务i展开平方项会产生 # - 常数项 A (可以忽略不影响优化) # - 线性项: -2A * x_{i,j} (对每个j) # - 二次项: A * x_{i,j} * x_{i,k} (对每对j, k, j!k) var_offset 0 # 当前任务i的变量起始索引 for i in range(self.N): # 线性项部分 for j in range(self.M): var_idx var_offset j Q[var_idx, var_idx] -2 * self.A # 二次项部分 for j in range(self.M): for k in range(j1, self.M): # 只计算上三角避免重复 var_idx_j var_offset j var_idx_k var_offset k Q[var_idx_j, var_idx_k] 2 * self.A # 因为 (x_j x_k)^2 展开后交叉项系数为2 var_offset self.M # 3. 添加机器容量约束惩罚项 H_capacity # 首先重置索引重新遍历所有变量来添加容量约束的影响 # 容量约束涉及分配变量 x 和松弛变量 y # 我们按机器逐个处理 x_var_start 0 # x变量从0开始 y_var_start self.N * self.M # y变量起始索引 for j in range(self.M): # 对于机器j约束为: Σ_i l_{ij} x_{ij} Σ_k 2^k y_{j,k} Cap_j # 惩罚项: B * (Σ_i l_{ij} x_{ij} Σ_k 2^k y_{j,k} - Cap_j)^2 # 展开平方: B * [ (Σ_i l_{ij} x_{ij})^2 (Σ_k 2^k y_{j,k})^2 Cap_j^2 # 2*(Σ_i l_{ij} x_{ij})(Σ_k 2^k y_{j,k}) # - 2*Cap_j*(Σ_i l_{ij} x_{ij}) # - 2*Cap_j*(Σ_k 2^k y_{j,k}) ] # 我们分别处理这些项对Q矩阵的贡献 # (a) 处理 (Σ_i l_{ij} x_{ij})^2 项: 产生 x*x 的二次项 for i1 in range(self.N): idx_i1 x_var_start i1 * self.M j for i2 in range(i1, self.N): idx_i2 x_var_start i2 * self.M j coeff self.L[i1, j] * self.L[i2, j] if i1 i2: Q[idx_i1, idx_i1] self.B * coeff else: Q[idx_i1, idx_i2] self.B * 2 * coeff # 对称项只填上三角 # (b) 处理 (Σ_k 2^k y_{j,k})^2 项: 产生 y*y 的二次项 y_idx_start y_var_start sum(self.K[:j]) # 机器j的松弛变量起始索引 for k1 in range(self.K[j]): idx_k1 y_idx_start k1 for k2 in range(k1, self.K[j]): idx_k2 y_idx_start k2 coeff (2**k1) * (2**k2) if k1 k2: Q[idx_k1, idx_k1] self.B * coeff else: Q[idx_k1, idx_k2] self.B * 2 * coeff # (c) 处理 2*(Σ_i l_{ij} x_{ij})(Σ_k 2^k y_{j,k}) 项: 产生 x*y 的交叉项 for i in range(self.N): idx_x x_var_start i * self.M j for k in range(self.K[j]): idx_y y_idx_start k coeff 2 * self.B * self.L[i, j] * (2**k) # 确保索引顺序填到上三角位置 if idx_x idx_y: Q[idx_x, idx_y] coeff else: Q[idx_y, idx_x] coeff # (d) 处理 -2*Cap_j*(Σ_i l_{ij} x_{ij}) 项: 产生 x 的线性项 for i in range(self.N): idx_x x_var_start i * self.M j Q[idx_x, idx_x] self.B * (-2 * self.Caps[j] * self.L[i, j]) # (e) 处理 -2*Cap_j*(Σ_k 2^k y_{j,k}) 项: 产生 y 的线性项 for k in range(self.K[j]): idx_y y_idx_start k Q[idx_y, idx_y] self.B * (-2 * self.Caps[j] * (2**k)) # (f) 常数项 B * Cap_j^2 不影响优化忽略 # 更新y变量起始索引为下一个机器准备 y_var_start self.K[j] return Q def solve_with_sa(self, num_reads1000, num_sweeps1000): 使用模拟退火求解QUBO问题 Q self.build_qubo_matrix() # 将numpy矩阵转换为dimod接受的BQM二元二次模型格式 bqm dimod.BinaryQuadraticModel.from_numpy_matrix(Q, offset0.0) # 使用模拟退火采样器 sampler dimod.SimulatedAnnealingSampler() response sampler.sample(bqm, num_readsnum_reads, num_sweepsnum_sweeps) # 获取能量最低的解即目标函数最小的解 solution response.first.sample energy response.first.energy return solution, energy, response def decode_solution(self, solution: dict) - Tuple[List[int], float, bool]: 解码求解器返回的二进制解。 返回: (assignment, total_cost, feasible) assignment: 列表assignment[i] j 表示任务i分配给机器j total_cost: 实际总成本 feasible: 解是否满足所有约束 assignment [-1] * self.N total_load [0] * self.M total_cost 0.0 feasible True # 解码分配变量 idx 0 for i in range(self.N): assigned False for j in range(self.M): if solution[idx] 1: if assigned: # 一个任务被分配给了多台机器违反约束 feasible False assignment[i] j total_load[j] self.L[i, j] total_cost self.C[i, j] assigned True idx 1 if not assigned: # 任务未分配违反约束 feasible False assignment[i] -1 # 检查容量约束 for j in range(self.M): if total_load[j] self.Caps[j]: feasible False return assignment, total_cost, feasible # 示例用法 if __name__ __main__: # 问题参数 N, M 5, 3 # 5个任务3台机器 np.random.seed(42) cost_matrix np.random.randint(10, 50, size(N, M)) load_matrix np.random.randint(1, 10, size(N, M)) machine_caps [15, 20, 18] print(成本矩阵:) print(cost_matrix) print(\n负载矩阵:) print(load_matrix) print(\n机器容量:, machine_caps) # 创建模型并求解 scheduler QuboModelScheduler(N, M, cost_matrix, load_matrix, machine_caps) solution, energy, response scheduler.solve_with_sa(num_reads2000, num_sweeps5000) # 解码并输出结果 assignment, total_cost, feasible scheduler.decode_solution(solution) print(\n 求解结果 ) print(fQUBO模型能量值: {energy:.2f}) print(f解码后总成本: {total_cost:.2f}) print(f解是否可行: {feasible}) print(任务分配方案:) for i, j in enumerate(assignment): print(f 任务 {i} - 机器 {j} (成本: {cost_matrix[i, j]}, 负载: {load_matrix[i, j]})) # 输出各机器总负载 print(\n机器负载情况:) total_load [0]*M for i, j in enumerate(assignment): if j 0: total_load[j] load_matrix[i, j] for j in range(M): print(f 机器 {j}: 负载 {total_load[j]} / 容量 {machine_caps[j]} {(超载!) if total_load[j] machine_caps[j] else })这段代码构建了一个完整的、可运行的QUBO调度求解器。QuboModelScheduler类封装了从问题定义到求解的所有步骤。build_qubo_matrix方法详细实现了如何将目标函数和约束系统地编码进Q矩阵这是整个项目的核心。solve_with_sa方法调用dimod的模拟退火求解器。最后decode_solution方法将求解器返回的二进制解映射回我们容易理解的“任务-机器”分配方案并验证其可行性。5. 参数调优与结果验证避开求解路上的那些“坑”代码跑起来只是第一步要得到高质量的解还需要精细的调优和严谨的验证。这是我们当时花费时间最多、也是收获经验最宝贵的环节。5.1 惩罚系数A和B的调优艺术惩罚系数不是随便设的。我们的经验是初始估算A可以设为最大成本值的10倍左右B设为A的10倍。这确保了违反约束的“代价”远高于任何成本优化可能带来的“收益”。迭代调整运行求解器多次观察结果。如果得到的解总是违反约束比如一个任务分给了多个机器说明惩罚系数A太小了需要增大。如果得到的解虽然满足约束但成本明显高于用手工或简单算法得到的可行解甚至所有任务都挤到一两台机器上这可能是因为B太大导致容量约束“压倒”了成本目标求解器为了绝对不超载而做出了极其保守的分配。这时需要适当减小B。自动化搜索对于关键比赛我们写了一个简单的网格搜索脚本在(A, B)的参数空间里寻找能稳定产出低成本可行解的组合。5.2 模拟退火参数的设置num_reads读取次数和num_sweeps退火步数直接影响求解质量和时间。num_reads相当于从不同的初始点开始多次运行退火算法。增加读取次数能提高找到全局最优解的概率。我们一般设为1000到5000。num_sweeps每次退火过程中状态更新的次数。问题越复杂变量越多需要的步数越多。对于几百个变量的问题5000到20000是常见的范围。技巧可以先设置较少的num_reads和num_sweeps进行快速调试和参数探索在最终求解时再提高这两个值以获得更稳定的解。5.3 解的可视化与验证不要盲目相信求解器输出的第一个解。我们建立了多重验证机制可行性检查decode_solution函数中的feasible标志是基本检查。成本核对将解码后的分配方案用原始成本矩阵重新计算总成本与求解器报告的能量值减去惩罚项贡献后进行对比确保解码过程无误。方案可视化对于调度问题我们绘制了甘特图对于路径问题绘制了路径图。视觉检查能快速发现不合逻辑的分配比如任务时间重叠异常、路径交叉等。与基准方法对比用贪心算法、整数规划求解器如PuLP GLPK求一个可行解作为基准。如果QUBO方法得到的解成本远高于基准那肯定是模型或参数有问题。5.4 我们遇到的一个典型“坑”在一次测试中求解器始终返回不可行解。通过调试输出中间Q矩阵和对角线元素我们发现由于成本值c_{ij}和负载值l_{ij}的量级差异巨大成本在1000量级负载在1量级导致惩罚项B * l^2相对于成本项c的比例失衡。即使B设得很大因为l太小惩罚力度依然不够。解决方案是对负载数据进行缩放将其放大到与成本相近的量级例如乘以一个系数从而让惩罚项在目标函数中占据合理的权重。这是一个非常实用的经验在构建QUBO模型前对输入数据进行标准化或归一化让不同物理意义的量处于相近的数量级能极大提高求解的稳定性和成功率。6. 超越模拟退火探索更高效的求解路径虽然模拟退火帮我们赢得了比赛但在这个领域还有更多强大的工具值得尝试。了解这些选项能让你在面对不同规模和要求的问题时有更优的选择。6.1 量子退火模拟器nealneal是D-Wave提供的模拟退火包但它采用了一些针对QUBO问题优化的策略。它的接口与dimod完全兼容切换成本极低。import neal def solve_with_neal_sa(bqm, num_reads1000): sampler neal.SimulatedAnnealingSampler() response sampler.sample(bqm, num_readsnum_reads) return response根据我们的测试对于某些具有特定结构的问题neal在相同参数下找到更低能量解的概率略高于标准的dimod.SimulatedAnnealingSampler。值得一试。6.2 混合求解器LeapHybridSampler如果你有D-Wave的Leap云账户可以尝试他们的混合求解器。它结合了经典和量子计算资源能够处理变量数多达上万的问题并且通常返回的解质量很高。from dwave.system import LeapHybridSampler def solve_with_hybrid(bqm): sampler LeapHybridSampler() # 需要配置API Token response sampler.sample(bqm) return response需要注意的是云服务有调用时间限制且在比赛紧张时段可能会有排队延迟。6.3 经典数学规划求解器对比我们当时也用PuLP一个线性规划库配合CBC求解器建立了同样的整数规划模型作为性能基准和验证工具。import pulp def solve_with_mip(cost_matrix, load_matrix, machine_caps): prob pulp.LpProblem(TaskScheduling, pulp.LpMinimize) N, M cost_matrix.shape x pulp.LpVariable.dicts(x, ((i, j) for i in range(N) for j in range(M)), catBinary) # 目标函数 prob pulp.lpSum(cost_matrix[i, j] * x[i, j] for i in range(N) for j in range(M)) # 约束每个任务必须分配给一台机器 for i in range(N): prob pulp.lpSum(x[i, j] for j in range(M)) 1 # 约束机器容量 for j in range(M): prob pulp.lpSum(load_matrix[i, j] * x[i, j] for i in range(N)) machine_caps[j] prob.solve(pulp.PULP_CBC_CMD(msgFalse)) # ... 解码解对于小规模问题N, M 50MIP求解器通常能快速找到最优解。QUBO模拟退火的方法在中小规模问题上解的质量可以非常接近最优解而在大规模问题上其可扩展性和求解速度往往更有优势特别是当问题结构更适合用QUBO表达时。7. 总结与延伸QUBO思维的实战价值回顾整个项目从看到赛题时的茫然到决定采用QUBO模型的大胆再到一步步实现、调试、优化最终获得不错的结果这个过程本身就是一次宝贵的学习。QUBO模型不仅仅是一个求解工具更是一种建模哲学——它将复杂的、带约束的离散优化问题转化为一个统一的、无约束的二次形式。这种转化迫使你对问题的本质进行更深刻的抽象。对于后来者我的建议是从简单问题开始练手不要一上来就挑战复杂赛题。先用QUBO解决经典的背包问题、旅行商问题TSP的小规模实例熟悉建模、编码、求解、解码的全流程。重视惩罚系数的调优这是QUBO应用中最像“艺术”的部分也是决定成败的关键。多试、多记录、多分析。不要神话量子计算在当前阶段我们用的主要还是经典模拟器。QUBO模型的优势在于其建模的简洁性和未来向专用硬件包括量子退火机迁移的潜力而不是立刻带来指数级加速。将QUBO作为你的工具箱之一它不适用于所有问题但对于那些可以自然表示为二元决策和成对相互作用的组合优化问题调度、分配、选址、网络切割等它是一个非常强大且有趣的工具。最后附上我们当时项目代码仓库的链接已做脱敏处理里面包含了更完整的数据处理、可视化以及不同求解器的对比实验代码。希望这篇超详细的复盘能帮你推开量子计算优化应用的一扇门。在实际操作中最深刻的体会是理论上的优雅和代码上的鲁棒性之间隔着无数个细节的打磨。每一个约束的转换每一个参数的调整都可能影响最终的结果。这种在抽象数学和具体实现之间反复穿梭的体验或许才是数学建模比赛乃至解决任何复杂工程问题中最迷人的部分。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门