
简介这份资源面向运筹学学习者与Java开发者提供线性规划算法的完整可运行实现帮助理解从数学模型到代码落地的全过程。包内共11个文件以Java源码与编译后的class文件为核心辅以IntelliJ工程配置的xml、iml及gitignore等压缩包约13KB体量轻便导入IDE即可阅读调试。代码围绕LP类展开涵盖模型标准化、人工变量引入、两阶段法求解与主求解入口Main类通过控制台接收目标函数系数、约束系数与右端常数并输出最优解数据结构上使用数组与列表存储系数和解信息。已有3479人学习下载适合课程实验、算法作业与自学参考读者可据此掌握标准型转换、松弛变量与人工变量的处理思路并在此基础上扩展测试用例、优化求解性能积累运筹学算法的实际编程经验。1. 线性规划算法实现一个 Java 版单纯形法求解器能跑多快第一次在项目里遇到排产优化我下意识想调 Python 的求解库结果部署环境只认 JVM连个原生依赖都不让加。最后翻出这份 Java 版线性规划算法实现把单纯形法从教科书公式落成能直接new出来调用的类才算把活干完。它干的事很明确给定一组决策变量、一个目标函数和若干约束条件求出最优解与目标值覆盖标准型最大化问题、等式与不等式约束、松弛变量引入、单纯形表迭代、无界与无可行解判定这些运筹学核心环节。适合两类人一是要在 Java 后端里嵌优化能力的开发者二是想拿一份能调试的代码把单纯形法真正看懂的学生或转行者。下面按「能干什么、怎么跑、怎么改」拆开讲。2. 项目结构与技术选型为什么是纯 Java 手写单纯形法2.1 目录骨架与模块职责这份资源是典型的轻量工程结构没有 Spring、没有 Maven 多模块核心就是一组普通 Java 类加一个入口。按常见组织方式目录大致长这样linear-programming-java/ ├── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── lp/ │ │ │ ├── LinearProgram.java // 问题建模目标函数、约束、变量 │ │ │ ├── SimplexSolver.java // 单纯形法主循环 │ │ │ ├── SimplexTableau.java // 单纯形表矩阵、基变量、检验数 │ │ │ ├── Constraint.java // 单条约束系数、关系符、右端项 │ │ │ ├── Solution.java // 求解结果状态、最优解、目标值 │ │ │ └── SolverStatus.java // 枚举OPTIMAL/UNBOUNDED/INFEASIBLE │ │ └── resources/ │ └── test/ │ └── java/lp/SimplexSolverTest.java // 若干标准算例断言 ├── pom.xml // 或 build.gradle └── README.mdLinearProgram负责收集用户输入SimplexTableau负责把问题转成表格形态并做行变换SimplexSolver是调度者Solution把结果封装成对象返回。这种拆分的好处是想换求解算法比如对偶单纯形、内点法只要新增一个 Solver 实现建模层不用动。我一般会把Constraint设计成不可变对象避免迭代过程中被意外改写这是血泪经验——早期版本里约束系数被行变换污染排查了半天才发现是引用共享。2.2 技术选型零第三方依赖的取舍整个项目只依赖 JDK 标准库矩阵运算用double[][]手写没有引入 Apache Commons Math 或 ojAlgo。这个选择在工程上有明确理由一是部署环境受限时少一个 jar 就少一份冲突风险二是单纯形法的矩阵规模通常不大几十到几百个变量手写双重循环的性能完全够用没必要为通用矩阵库付出学习成本。代价是数值稳定性要自己兜底浮点比较必须带容差这点后面避坑章节会细说。构建工具用 Maven 或 Gradle 都行pom.xml里只需要 JUnit 做测试。JDK 版本建议 8 及以上代码里如果用了var或List.of就要求 11拿到源码先看pom.xml的maven.compiler.source确认。运行入口一般是一个带main方法的 Demo 类构造问题、调用求解、打印结果三步走。提示如果源码里没有pom.xml说明是纯 IDE 工程用javac直接编译src/main/java下所有文件即可注意包名与目录层级对应。2.3 从建模到求解的调用链理解调用链比背公式有用。用户先new LinearProgram()逐个addConstraint()并setObjective()然后交给SimplexSolver.solve(program)。Solver 内部第一步做标准化把约束补松弛变量、约束补剩余变量并引入人工变量、自由变量拆成两个非负变量。第二步构建初始单纯形表把目标函数系数取负放进检验数行。第三步进入主循环选入基变量检验数为负且最小、选离基变量最小比值检验、做枢轴变换直到所有检验数非负或判定无界。最后从基变量里还原出原始变量的取值组装Solution返回。这条链路每一步都能打断点比看公式推导直观得多。3. 核心代码拆解单纯形表构建与枢轴迭代3.1 问题建模类的字段设计先看建模层。LinearProgram要存目标函数系数、约束列表、变量个数和优化方向。常见写法public class LinearProgram { private final int variableCount; // 决策变量个数 private final double[] objective; // 目标函数系数长度 variableCount private final ListConstraint constraints new ArrayList(); private boolean maximize true; // 默认最大化 public LinearProgram(int variableCount) { this.variableCount variableCount; this.objective new double[variableCount]; } // 设置第 i 个变量的目标系数 public LinearProgram setObjectiveCoefficient(int i, double c) { objective[i] c; return this; } public LinearProgram addConstraint(Constraint c) { constraints.add(c); return this; } }字段全部final或私有通过链式方法修改保证建模阶段状态可控。variableCount单独存而不是从objective.length推是为了在用户还没设满系数时也能校验约束长度。maximize标志很关键最小化问题在标准化时要把目标系数整体取反求解完再把目标值取反回来这个转换漏掉就会得到符号相反的结果属于新手高频翻车点。3.2 标准化松弛变量与人工变量的引入单纯形法要求所有约束是等式、所有变量非负。标准化这一步决定了后续表格的列数。核心逻辑// 将原始问题转为标准型返回扩展后的系数矩阵与基变量下标 private StandardForm standardize(LinearProgram lp) { int m lp.getConstraints().size(); int n lp.getVariableCount(); int slackCount 0, artificialCount 0; for (Constraint c : lp.getConstraints()) { if (c.getType() ConstraintType.LE || c.getType() ConstraintType.GE) slackCount; if (c.getType() ConstraintType.GE || c.getType() ConstraintType.EQ) artificialCount; } int totalCols n slackCount artificialCount; double[][] a new double[m][totalCols]; double[] b new double[m]; int[] basis new int[m]; int slackIdx n, artIdx n slackCount; for (int i 0; i m; i) { Constraint c lp.getConstraints().get(i); System.arraycopy(c.getCoefficients(), 0, a[i], 0, n); b[i] c.getRightHandSide(); if (b[i] 0) { // 右端项为负时整行取反关系符翻转 for (int j 0; j totalCols; j) a[i][j] -a[i][j]; b[i] -b[i]; } switch (c.getType()) { case LE: a[i][slackIdx] 1.0; basis[i] slackIdx; break; case GE: a[i][slackIdx] -1.0; slackIdx; a[i][artIdx] 1.0; basis[i] artIdx; break; case EQ: a[i][artIdx] 1.0; basis[i] artIdx; break; } } return new StandardForm(a, b, basis, n, totalCols); }这段代码有三个参数要点。slackCount统计和约束数量因为两者各需要一个松弛或剩余变量artificialCount统计和约束因为这两类需要人工变量构造初始可行基。右端项为负时整行取反是必须的否则初始基解会出现负值违反非负约束。basis[i]记录第 i 行当前哪个变量在基里后续枢轴变换要同步更新它。人工变量引入后目标函数里要给它一个大 M 惩罚大 M 法或走两阶段法源码里通常选大 M 法实现简单但 M 取值要足够大太小会压不住人工变量。3.3 枢轴变换入基、离基与行消元主循环是性能与正确性的核心。每次迭代做三件事找入基列、找离基行、做高斯消元。代码骨架while (true) { int enter -1; double minReduced -EPS; for (int j 0; j cols; j) { // 选检验数最小且为负的列入基 if (reduced[j] minReduced) { minReduced reduced[j]; enter j; } } if (enter -1) return SolverStatus.OPTIMAL; // 所有检验数非负最优 int leave -1; double minRatio Double.MAX_VALUE; for (int i 0; i rows; i) { // 最小比值检验选离基行 if (a[i][enter] EPS) { double ratio b[i] / a[i][enter]; if (ratio minRatio) { minRatio ratio; leave i; } } } if (leave -1) return SolverStatus.UNBOUNDED; // 入基列全非正无界 pivot(a, b, reduced, leave, enter); // 枢轴变换 basis[leave] enter; }EPS是浮点容差通常取1e-9到1e-6。入基规则用「最负检验数」是 Dantzig 规则收敛快但可能循环如果遇到退化问题反复迭代可以换成 Bland 规则选下标最小的负检验数列保证有限步终止。最小比值检验里只考虑a[i][enter] EPS的行等于零或负系数的行不参与否则会算出负的右端项。pivot方法先归一化主元行再对其他行做消元同时更新检验数行这一步的循环顺序写错会导致检验数没同步表现为迭代不收敛。3.4 结果还原与状态判定求解结束后原始变量的值要从最终表里按基变量下标取。Solution封装public class Solution { private final SolverStatus status; private final double[] variables; // 原始决策变量取值 private final double objectiveValue; public static Solution optimal(double[] vars, double obj) { return new Solution(SolverStatus.OPTIMAL, vars, obj); } // 无界、无可行解用对应工厂方法variables 为 null }还原时遍历basis若basis[i] originalVarCount则variables[basis[i]] b[i]其余变量为 0。目标值用原始目标系数点乘变量得到不要直接读检验数行里的常数项——那是取反后的值容易符号搞错。状态判定除了OPTIMAL和UNBOUNDED还要处理大 M 法下人工变量仍留在基里且值大于零的情况此时判INFEASIBLE。这三个状态覆盖了运筹学里线性规划的全部结局调用方按状态分支处理即可。4. 跑起来编译、算例与结果验证4.1 环境准备与编译命令拿到源码先确认 JDK 版本命令行java -version看一眼。有 Maven 的话直接cd linear-programming-java mvn -q clean compile mvn -q test # 跑标准算例断言 mvn -q exec:java -Dexec.mainClasslp.Demo没有 Maven 就手动编译注意包名lp对应目录lp/javac -d out $(find src/main/java -name *.java) java -cp out lp.Demo-d out指定输出目录find把所有源文件喂给编译器。如果报「找不到符号」多半是某个类没被find匹配到检查文件扩展名和目录层级。跑测试时如果断言失败先看失败算例的期望值和实际值差多少差一个符号通常是最大化/最小化转换没做差很大则可能是枢轴选行选错。4.2 一个可对照的标准算例用经典的生产计划问题验证两种产品利润分别 3 和 5资源约束为x1 4、2x2 12、3x1 2x2 18求最大利润。建模代码LinearProgram lp new LinearProgram(2) .setObjectiveCoefficient(0, 3) .setObjectiveCoefficient(1, 5) .addConstraint(new Constraint(new double[]{1, 0}, ConstraintType.LE, 4)) .addConstraint(new Constraint(new double[]{0, 2}, ConstraintType.LE, 12)) .addConstraint(new Constraint(new double[]{3, 2}, ConstraintType.LE, 18)); Solution s new SimplexSolver().solve(lp); System.out.println(s.getStatus()); // OPTIMAL System.out.println(Arrays.toString(s.getVariables())); // [2.0, 6.0] System.out.println(s.getObjectiveValue()); // 36.0手算验证交点(2,6)处目标值3*25*636与程序输出一致。这个算例的好处是约束都是只涉及松弛变量适合第一次跑通时排除人工变量的干扰。跑通后再换成含和的算例观察大 M 法是否正确把人工变量赶出基。我一般会准备三组算例唯一最优、多重最优某个检验数为 0、无界分别验证状态判定分支。4.3 结果怎么读变量、目标值与状态Solution.getVariables()返回的是原始决策变量长度等于建模时的variableCount松弛变量和人工变量不会出现在里面。getObjectiveValue()已经处理过最大化/最小化的符号转换直接就是业务语义上的目标值。状态为UNBOUNDED时变量数组为null调用方必须先判状态再取值否则空指针。状态为INFEASIBLE说明约束互相矛盾这时候该回头检查业务约束而不是怀疑求解器。把这三个字段打印出来对照手算是验证移植是否正确的最快方式。5. 避坑与排查单纯形法落地时的五个高频翻车点5.1 浮点比较不带容差导致死循环现象某些算例迭代几百次不退出检验数在-1e-16附近反复横跳。原因直接用reduced[j] 0判断入基浮点误差让本该为 0 的检验数被判成负数选了个无效列。解决所有比较统一用EPS容差入基条件写成reduced[j] -EPS比值检验写成a[i][enter] EPS最优判定写成所有检验数 -EPS。容差取1e-9对大多数业务规模够用变量上千时可放宽到1e-7。5.2 右端项为负没做行翻转现象初始基解出现负值后续比值检验算出负数结果完全错乱。原因标准化时只补了松弛变量没检查b[i] 0。解决对b[i] 0的约束整行乘以 -1同时关系符翻转变翻转后重新判断该补松弛还是人工变量。这一步顺序不能反先翻转再补变量否则补出来的系数符号是错的。5.3 大 M 取值不当压不住人工变量现象明明有可行解却返回INFEASIBLE或者人工变量残留在基里。原因M 取1e6但目标系数本身量级到1e8惩罚相对太小。解决M 取目标系数最大绝对值的一百倍以上或者干脆改用两阶段法第一阶段单独最小化人工变量之和数值上更稳。业务数据量级差异大时两阶段法是更保险的选择。5.4 最小化问题忘了目标系数取反现象求最小值的问题程序返回的目标值符号相反变量取值也不对。原因标准化时只处理了约束没把maximizefalse的目标系数取反。解决进入求解前若为最小化把目标系数整体乘 -1求解完把目标值再乘 -1 还原。这个转换要放在标准化之前且只做一次重复取反会绕回原符号排查时容易看花眼。5.5 退化问题引发循环迭代现象目标值不再改进但迭代次数持续增长基变量组合周期性重复。原因退化基可行解下最小比值出现并列Dantzig 规则可能循环。解决换 Bland 规则入基选下标最小的负检验数列离基在并列时选下标最小的行能保证有限步终止。性能会略降但换来确定性生产环境里确定性比那点速度重要。6. 二次开发把求解器接进业务系统的改造思路拿到这份代码多数人不是拿来跑算例而是要嵌进排产、配载、预算分配这类业务里。改造的第一步是加一层建模适配业务对象订单、车辆、班次到LinearProgram的映射单独写一个 Converter别让业务代码直接碰系数数组。这样约束规则变了只改 Converter求解器保持稳定。第二步是数值校验业务数据常有空值、负库存、超大系数进求解器前统一做量纲归一化把系数缩放到同一数量级能显著降低浮点误差引发的误判。第三步是结果解释。Solution只给变量值和目标值业务方要的是「哪个订单分到哪条线」所以在 Converter 旁边再写一个反解器把变量下标映射回业务实体。第四步是性能兜底变量规模上到几千时手写双重循环的枢轴变换会成为瓶颈常见做法是把系数矩阵换成稀疏存储或者对入基列选择加一个候选集限制。验证方法很直接拿历史人工排产结果做对照目标值不应劣于人工方案差异超过 5% 就逐约束排查建模是否漏项。改造点改动位置验证方式业务建模适配新增 Converter 类单元测试覆盖典型订单组合量纲归一化标准化前预处理对比归一化前后目标值偏差结果反解新增 ResultMapper抽样人工核对分配结果稀疏化提速SimplexTableau 内部大算例压测迭代耗时我踩过最深的一个坑是直接拿业务原始系数进求解器量级从 0.001 到 100000 混在一起容差怎么调都不对最后加了归一化才稳定。从那以后我每次接新的优化场景都强制先跑一遍量纲检查和退化算例确认数值行为正常再谈业务逻辑。希望这份拆解帮到你把单纯形法从公式变成手里能改能调的工具。本文还有配套的精品资源点击获取