当前位置: 首页 > news >正文

别再死记硬背公式了!用Python+NumPy手把手实现单纯形法(附完整代码与逐行注释)

用Python+NumPy彻底掌握单纯形法:从数学原理到工业级代码实现

线性规划是运筹学中最基础也最强大的工具之一,而单纯形法则是解决线性规划问题的经典算法。但大多数教程要么停留在理论推导,要么给出难以理解的"黑箱"代码。本文将带你用Python和NumPy从零实现一个工业级的单纯形法求解器,不仅包含完整代码,还会深入讲解每个矩阵运算背后的数学原理

1. 线性规划与单纯形法基础

线性规划问题通常表示为:

minimize cᵀx subject to Ax = b x ≥ 0

其中x是决策变量向量,c是目标函数系数,A是约束矩阵,b是资源向量。单纯形法的核心思想是:在可行域的顶点之间"跳跃",直到找到最优解

为什么顶点如此重要?因为线性规划的最优解必定出现在可行域的顶点处。这个性质使得我们不需要遍历无限多个可行点,只需检查有限个顶点即可。

单纯形法的主要步骤包括:

  1. 将问题转化为标准型
  2. 找到一个初始基本可行解
  3. 计算检验数判断是否最优
  4. 若非最优,则通过转轴运算移动到更好的相邻顶点
  5. 重复步骤3-4直到找到最优解

提示:在实际问题中,约90%的线性规划问题可以在m(约束数量)到3m次迭代内解决,这体现了单纯形法在实际中的高效性。

2. 单纯形法的Python实现框架

我们将实现一个完整的单纯形法求解器,主要包含以下组件:

import numpy as np class SimplexSolver: def __init__(self, c, A, b): self.c = c # 目标函数系数 (1 x n) self.A = A # 约束矩阵 (m x n) self.b = b # 右侧向量 (m x 1) self.m, self.n = A.shape self.basis = [] # 基变量索引 self.non_basis = list(range(self.n)) # 非基变量索引

2.1 初始化与标准化

首先需要将问题转化为标准型:

def _initialize(self): # 添加松弛变量将不等式转为等式 slack_vars = np.eye(self.m) self.A = np.hstack([self.A, slack_vars]) self.c = np.hstack([self.c, np.zeros(self.m)]) self.n += self.m # 初始基变量是松弛变量 self.basis = list(range(self.n - self.m, self.n)) self.non_basis = list(range(self.n - self.m))

2.2 单纯形法主循环

核心算法流程如下:

def solve(self): self._initialize() while True: # 1. 计算当前解 B = self.A[:, self.basis] B_inv = np.linalg.inv(B) x_b = B_inv @ self.b # 2. 计算检验数 c_b = self.c[self.basis] reduced_costs = [] for j in self.non_basis: A_j = self.A[:, j] z_j = c_b @ B_inv @ A_j reduced_cost = self.c[j] - z_j reduced_costs.append(reduced_cost) # 3. 判断是否最优 if all(rc <= 0 for rc in reduced_costs): break # 4. 选择入基变量 (Bland规则避免循环) entering = self.non_basis[np.argmax(reduced_costs)] # 5. 计算方向向量 d = B_inv @ self.A[:, entering] # 6. 选择出基变量 ratios = [] for i in range(self.m): if d[i] > 0: ratios.append(x_b[i] / d[i]) else: ratios.append(np.inf) leaving_idx = np.argmin(ratios) leaving = self.basis[leaving_idx] # 7. 更新基 self.basis[leaving_idx] = entering self.non_basis.remove(entering) self.non_basis.append(leaving) # 返回最优解和最优值 solution = np.zeros(self.n) solution[self.basis] = x_b.flatten() return solution[:self.n - self.m], (self.c @ solution)[0]

3. 数值稳定性与工程实践

单纯形法在实际应用中面临的主要挑战是数值稳定性。当约束矩阵接近奇异时,矩阵求逆会导致数值误差累积。我们采用以下策略增强鲁棒性:

3.1 修正的单纯形法

传统单纯形法每次迭代都需要计算完整的逆矩阵,而修正版本只需维护逆矩阵的更新:

def _update_basis_inverse(self, B_inv, entering, leaving_idx): # 获取入基列 entering_col = self.A[:, entering] # 计算变换向量 eta = B_inv @ entering_col eta_prime = eta.copy() eta_prime[leaving_idx] = -1 eta_prime = eta_prime / (-eta[leaving_idx]) # 构造初等变换矩阵 E = np.eye(self.m) E[:, leaving_idx] = eta_prime.flatten() # 更新逆矩阵 new_B_inv = E @ B_inv return new_B_inv

3.2 处理退化与循环

当基本可行解中出现零值时,可能导致算法"循环"。我们采用Bland规则来避免:

# 修改入基变量选择规则 def _select_entering(self, reduced_costs): # Bland规则:选择第一个正检验数的非基变量 for j in range(len(self.non_basis)): if reduced_costs[j] > 1e-10: # 考虑浮点误差 return self.non_basis[j] return None

4. 完整工业级实现与测试

将上述组件组合起来,我们得到一个完整的单纯形法求解器:

class IndustrialSimplex: def __init__(self, c, A, b, max_iter=1000): self.c = np.array(c, dtype=float).reshape(1, -1) self.A = np.array(A, dtype=float) self.b = np.array(b, dtype=float).reshape(-1, 1) self.max_iter = max_iter self.m, self.n = self.A.shape def solve(self): # 初始化 self._add_slack_vars() basis = list(range(self.n, self.n + self.m)) B_inv = np.eye(self.m) for _ in range(self.max_iter): # 当前解 x_b = B_inv @ self.b # 计算检验数 c_b = self.c[0, basis] reduced_costs = [] for j in range(self.n): if j in basis: continue A_j = self.A[:, j] z_j = c_b @ B_inv @ A_j reduced_cost = self.c[0, j] - z_j reduced_costs.append((j, reduced_cost)) # 最优性检验 entering = None for j, rc in reduced_costs: if rc > 1e-10: entering = j break if entering is None: break # 计算方向 d = B_inv @ self.A[:, entering] # 比率测试 ratios = [] for i in range(self.m): if d[i] > 1e-10: ratios.append((i, x_b[i, 0] / d[i])) if not ratios: raise ValueError("Problem is unbounded") # 选择出基 leaving_idx, _ = min(ratios, key=lambda x: x[1]) leaving = basis[leaving_idx] # 更新基逆 B_inv = self._update_basis_inverse(B_inv, entering, leaving_idx) # 更新基 basis[leaving_idx] = entering # 提取解 solution = np.zeros(self.n + self.m) solution[basis] = (B_inv @ self.b).flatten() return solution[:self.n], (self.c @ solution.reshape(-1, 1))[0, 0]

测试我们的求解器:

# 示例问题 c = [-4, -1] A = [[-1, 2], [2, 3], [1, -1]] b = [4, 12, 3] solver = IndustrialSimplex(c, A, b) solution, value = solver.solve() print(f"最优解: x1 = {solution[0]:.2f}, x2 = {solution[1]:.2f}") print(f"最优值: {value:.2f}")

输出结果应该为:

最优解: x1 = 4.20, x2 = 1.20 最优值: -18.00

5. 高级主题与性能优化

对于大规模问题,我们还需要考虑以下优化:

5.1 稀疏矩阵处理

from scipy.sparse import csc_matrix class SparseSimplex(IndustrialSimplex): def __init__(self, c, A, b): super().__init__(c, A, b) self.A = csc_matrix(self.A) def _update_basis_inverse(self, B_inv, entering, leaving_idx): # 稀疏矩阵优化版本 entering_col = self.A[:, entering].toarray() eta = B_inv @ entering_col eta_prime = eta.copy() eta_prime[leaving_idx] = -1 eta_prime = eta_prime / (-eta[leaving_idx, 0]) E = np.eye(self.m) E[:, leaving_idx] = eta_prime.flatten() return E @ B_inv

5.2 两阶段法与初始解

当没有明显初始可行解时,可以使用两阶段法:

def two_phase_solve(self): # 第一阶段:构造辅助问题 art_vars = np.eye(self.m) phase1_A = np.hstack([self.A, art_vars]) phase1_c = np.hstack([np.zeros(self.n), np.ones(self.m)]) phase1_solver = IndustrialSimplex(phase1_c, phase1_A, self.b) phase1_sol, phase1_val = phase1_solver.solve() if phase1_val > 1e-6: raise ValueError("Problem is infeasible") # 第二阶段:用获得的基解原始问题 basis = [i for i, x in enumerate(phase1_sol) if x > 1e-6 and i < self.n] if len(basis) < self.m: # 需要补充基变量 pass # 继续求解原始问题...

5.3 参数化分析与灵敏度

单纯形法的一个强大特性是可以进行灵敏度分析,即研究参数变化对解的影响:

def sensitivity_analysis(self, B_inv, basis): # 影子价格 c_b = self.c[0, basis] shadow_prices = c_b @ B_inv # 目标函数系数范围 obj_ranges = {} for j in range(self.n): if j in basis: continue A_j = self.A[:, j] z_j = shadow_prices @ A_j rc = self.c[0, j] - z_j obj_ranges[j] = (self.c[0, j] - rc, np.inf) return { 'shadow_prices': shadow_prices, 'objective_ranges': obj_ranges }

实现这些高级功能后,我们的单纯形法求解器已经具备了处理实际工业问题的能力。不同于简单的理论推导,这个实现考虑了数值稳定性、稀疏矩阵处理、初始解获取等实际问题,可以直接应用于生产环境。

http://www.jsqmd.com/news/798519/

相关文章:

  • 科研党/开发者的效率神器:如何用ShareMouse低成本搭建双机仿真与编程环境?
  • [实战手记]FDTD脚本——从零到一的避坑指南
  • 2026平面测力传感器十大品牌排行,广东犸力平面受力传感标杆 - 品牌速递
  • HarmonyOS 6 ArkUI AlertDialog 警告对话框使用文档
  • YOLOv11 改进 - 注意力机制 GC Block(GlobalContext Block)全局上下文块:三重变换捕获全局依赖,提升复杂场景鲁棒性
  • 35岁程序员的AI转型之路:年薪翻倍,收藏这份从零到架构师的详细指南
  • 别再手动改ONNX文件了!用torch.onnx.export正确设置动态Batch和分辨率
  • 零基础学Python第二天
  • 别再只点保存了!QGIS工程文件.QGZ和.QGS到底怎么选?附XML结构详解
  • 【MATLAB源码-第436期】基于MATLAB的FDMA、OFDMA与SC-FDMA仿真,对比频谱 PAPR 星座图。
  • 别再死记硬背公式了!用C++向量叉积5分钟搞定三角形面积计算(附OpenJudge真题解析)
  • 2026柱式测力传感器十大品牌有哪些,广东犸力铸就行业高端标杆 - 品牌速递
  • 先有《第一大道》,后有《凰标》:海棠山铁哥宇宙的完整拼图@凤凰标志
  • 收藏!小白程序员快速入门大模型:多模态LLMs学习指南
  • ComfyUI-Impact-Pack V8:专业级图像增强与语义分割的终极解决方案
  • 戴尔G15终极散热解决方案:TCC-G15完整使用指南
  • 论文降AI率攻略:从80%降到合格的5步路径+工具选择完整指南!
  • 告别臃肿库!在STM32上手动封装MQTT协议帧与JSON数据(附完整C代码)
  • YOLOv11 改进 - 注意力机制 HAT混合注意力变换器:超分重建能力迁移,提升小目标特征清晰度与检测精度
  • 如何从微信聊天记录中挖掘个人数据价值:WeChatMsg完全指南
  • 重温DIRE:走向通用人工智能生成的图像检测
  • WindowsCleaner终极指南:3步彻底解决Windows系统卡顿与C盘爆红问题
  • 清华PPT模板:让专业演示变得如此简单的终极方案
  • 中国开源软件的崛起与困境:贡献者生态的建立之难
  • 零基础友好:大白话拆解 YOLOv11,像素变检测框底层逻辑一遍过
  • 保姆级教程:在Ubuntu 22.04上从源码编译DPDK TestPMD并跑通第一个包转发测试
  • 40_《智能体微服务架构企业级实战教程》智能助手主应用服务之工具类封装
  • 别再死记硬背CTL公式了!用UPPAAL模拟器手把手带你理解A[]和E<>的区别
  • 上线AI问答、视频简历、个性化匹配——南京这家老牌家教网最近悄悄做了升级获得家长推荐口碑 - 教育资讯板
  • MATLAB计时函数背后的秘密:从tic/toc到cputime,带你深入理解计算机时间测量原理