每日算法 — 使用java实现单纯形法:线性规划最优解搜索与对偶理论

引言

线性规划(Linear Programming, LP)是运筹学中最基础也是应用最广泛的优化工具,从生产计划到投资组合优化,从物流配送到机器学习中的支持向量机,都能看到它的身影。单纯形法(Simplex Method)由George Dantzig于1947年提出,是求解线性规划问题的经典算法。尽管存在多项式时间的内点法,单纯形法在实际工程中仍然因其高效性和直观性而被广泛使用。本文将用Java完整实现单纯形表算法,并讲解对偶理论的工程意义。

问题建模

标准型线性规划问题定义为:

最大化目标函数:c^T x

约束条件:Ax <= b, x >= 0

其中A是m x n的约束矩阵,b是m维资源向量,c是n维价值向量。

松弛变量引入

为了使用单纯形法,我们需要将不等式约束转化为等式约束。引入松弛变量s >= 0:

Ax + s = b

这样,初始基变量就可以选择所有松弛变量,构成单位矩阵作为初始可行基。

算法原理

单纯形表结构

单纯形表是一个(m+1) x (n+m+1)的矩阵,包含:

  • 第0行:目标函数系数(检验数)
  • 第1~m行:约束等式系数
  • 最后一列:右端项b的值

核心步骤

  1. 初始基可行解:选择松弛变量作为初始基变量
  2. 最优性检验:若所有检验数(reduced cost)非正,则当前解最优
  3. 入基变量选择:选择检验数最大的非基变量入基(保证目标函数严格增加)
  4. 出基变量选择:使用最小比值法则(Minimum Ratio Test)确定出基变量
  5. 转轴运算:高斯消元更新单纯形表
  6. 重复步骤2-5直至最优

对偶理论

每个线性规划问题(原问题)都有一个对应的对偶问题。对偶定理告诉我们:原问题的最优值等于对偶问题的最优值。这一性质在经济学中被称为”影子价格”,在机器学习中对应着SVM的KKT条件。

Java完整实现

import java.util.*;

/**
 * 单纯形法求解标准型线性规划问题
 * 标准型: max c^T * x, 约束 Ax <= b, x >= 0
 */
public class SimplexMethod {

    private static final double EPS = 1e-9;
    private static final double INF = 1e18;

    private final int m;           // 约束数量(行数)
    private final int n;           // 原始变量数量
    private final double[][] a;    // 单纯形表: (m+1) x (n+m+1)
    private final int[] basis;     // 基变量索引,basis[i]表示第i行对应的基变量
    private final int[] nonBasis;  // 非基变量索引

    /**
     * 构造单纯形法求解器
     * @param A 约束矩阵 m x n
     * @param b 右端项向量 m
     * @param c 目标函数系数 n
     */
    public SimplexMethod(double[][] A, double[] b, double[] c) {
        this.m = A.length;
        this.n = A[0].length;
        // 单纯形表: m个约束 + 1个目标函数行
        // 列: n个原始变量 + m个松弛变量 + 1个右端项
        this.a = new double[m + 1][n + m + 1];
        this.basis = new int[m];
        this.nonBasis = new int[n];

        // 初始化非基变量(原始变量)
        for (int j = 0; j < n; j++) {
            nonBasis[j] = j;
        }

        // 初始化基变量(松弛变量)
        for (int i = 0; i < m; i++) {
            basis[i] = n + i;
        }

        // 填充约束行
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                a[i][j] = A[i][j];
            }
            // 松弛变量构成单位矩阵
            a[i][n + i] = 1.0;
            a[i][n + m] = b[i];
        }

        // 填充目标函数行(注意:单纯形表中存的是负的检验数)
        for (int j = 0; j < n; j++) {
            a[m][j] = -c[j];
        }
        a[m][n + m] = 0.0; // 初始目标函数值
    }

    /**
     * 执行单纯形法迭代求解
     * @return 最优目标函数值,若问题无界返回INF
     */
    public double solve() {
        while (true) {
            // 步骤1: 寻找入基变量(检验数最大的非基变量)
            int enter = -1;
            double maxReducedCost = -EPS;
            for (int j = 0; j < n + m; j++) {
                // 只考虑非基变量
                if (!isBasis(j) && a[m][j] > maxReducedCost) {
                    maxReducedCost = a[m][j];
                    enter = j;
                }
            }

            // 所有检验数 <= 0,当前解最优
            if (enter == -1) {
                break;
            }

            // 步骤2: 寻找出基变量(最小比值法则)
            int leave = -1;
            double minRatio = INF;
            for (int i = 0; i < m; i++) {
                if (a[i][enter] > EPS) {
                    double ratio = a[i][n + m] / a[i][enter];
                    if (ratio < minRatio - EPS ||
                        (Math.abs(ratio - minRatio) < EPS && basis[i] < basis[leave])) {
                        minRatio = ratio;
                        leave = i;
                    }
                }
            }

            // 若没有任何约束限制入基变量,则问题无界
            if (leave == -1) {
                return INF;
            }

            // 步骤3: 转轴运算(高斯消元)
            pivot(leave, enter);
        }

        return a[m][n + m];
    }

    /**
     * 转轴运算:以a[leave][enter]为主元进行高斯消元
     */
    private void pivot(int leave, int enter) {
        double pivotVal = a[leave][enter];

        // 主元行归一化
        for (int j = 0; j <= n + m; j++) {
            a[leave][j] /= pivotVal;
        }

        // 消去其他行的enter列
        for (int i = 0; i <= m; i++) {
            if (i != leave && Math.abs(a[i][enter]) > EPS) {
                double factor = a[i][enter];
                for (int j = 0; j <= n + m; j++) {
                    a[i][j] -= factor * a[leave][j];
                }
            }
        }

        // 更新基变量和非基变量集合
        int oldBasis = basis[leave];
        basis[leave] = enter;
        // 找到enter原来在非基中的位置,替换为oldBasis
        for (int j = 0; j < n; j++) {
            if (nonBasis[j] == enter) {
                nonBasis[j] = oldBasis;
                break;
            }
        }
    }

    /**
     * 判断变量是否在基中
     */
    private boolean isBasis(int var) {
        for (int b : basis) {
            if (b == var) return true;
        }
        return false;
    }

    /**
     * 获取最优解中各变量的取值
     * @return 所有变量(包括松弛变量)的取值数组
     */
    public double[] getSolution() {
        double[] x = new double[n + m];
        Arrays.fill(x, 0.0);
        for (int i = 0; i < m; i++) {
            x[basis[i]] = a[i][n + m];
        }
        return x;
    }

    /**
     * 获取原始变量的最优解
     * @return 长度为n的解向量
     */
    public double[] getOriginalSolution() {
        double[] sol = getSolution();
        double[] result = new double[n];
        System.arraycopy(sol, 0, result, 0, n);
        return result;
    }

    /**
     * 打印单纯形表(调试用)
     */
    public void printTableau() {
        System.out.println("单纯形表:");
        for (int i = 0; i <= m; i++) {
            System.out.printf("Row %d: ", i);
            for (int j = 0; j <= n + m; j++) {
                System.out.printf("%8.3f ", a[i][j]);
            }
            System.out.println();
        }
        System.out.println("基变量: " + Arrays.toString(basis));
    }

    public static void main(String[] args) {
        // 测试案例: max z = 3x1 + x2 + 2x3
        // 约束:
        //   x1 + x2 + 3x3 <= 30
        //   2x1 + 2x2 + 5x3 <= 24
        //   4x1 + x2 + 2x3 <= 36
        //   x1, x2, x3 >= 0
        double[][] A = {
            {1, 1, 3},
            {2, 2, 5},
            {4, 1, 2}
        };
        double[] b = {30, 24, 36};
        double[] c = {3, 1, 2};

        SimplexMethod simplex = new SimplexMethod(A, b, c);
        double optimal = simplex.solve();

        System.out.println("最优目标函数值: " + optimal);
        double[] solution = simplex.getOriginalSolution();
        System.out.println("最优解: x1=" + solution[0] +
                           ", x2=" + solution[1] +
                           ", x3=" + solution[2]);
    }
}

算法扩展:两阶段法

上述实现假设原点(所有决策变量为0)是可行解。对于包含”>=”或”=”约束的问题,需要使用两阶段单纯形法:

  1. 第一阶段:引入人工变量,构造辅助问题最小化人工变量之和,寻找初始可行基
  2. 第二阶段:以第一阶段得到的可行基为起点,求解原问题

对偶理论的工程意义

原问题 对偶问题 经济学解释
max c^T x min b^T y 利润最大化 ↔ 成本最小化
约束Ax <= b 变量y >= 0 资源限制 ↔ 影子价格
变量x >= 0 约束A^T y >= c 产品产量 ↔ 机会成本

在资源分配问题中,对偶变量y_i表示第i种资源的影子价格——即该资源增加一个单位时目标函数的改进量。

复杂度与局限性

特性 说明
最坏时间复杂度 指数级(Klee-Minty构造型例)
平均时间复杂度 多项式级,实际表现极优
空间复杂度 O(mn)
数值稳定性 需处理浮点精度问题(本实现使用EPS)

总结

单纯形法不仅是一个算法,更是一种思维方式——通过在可行域的顶点之间移动来寻找最优解。理解单纯形法后,读者可以进一步学习:

  • 对偶单纯形法:从对偶可行但原问题不可行的解出发
  • 内点法:Karmarkar算法,具有多项式时间复杂度
  • 整数线性规划:分支定界法与割平面法
  • 网络单纯形法:针对网络流问题的特殊优化

单纯形法的核心思想——基变换与转轴运算——也是许多现代优化算法的基础组件。