跳转至

第 5 章:标准形式与灵敏度分析 —— 统一度量衡与参数敏感度

场景: 不同人描述同一个线性规划问题,可能写出不同的形式——有人写 \(\max\) 有人写 \(\min\),有人写 \(\le\) 约束有人写 \(\ge\)标准形式 就是把所有问题统一成同一种格式,让算法可以统一处理。同时,实际问题中参数可能会变化, 灵敏度分析 告诉你:最优解对这些变化有多"敏感"。


5.1 为什么需要标准形式?

买东西的比喻

买东西时,有的用美元,有的用人民币,有的用日元。 标准形式就像把所有货币都换成美元 ——方便比较和计算。

线性规划的标准形式就是: 统一成"最小化 + 等式约束 + 右端项非负"


5.2 标准形式的定义

标准形式(Standard Form)的四个要求:

要求 条件 说明
目标函数 \(\min\) 最大化问题转为最小化
约束类型 等式 \(=\) 不等式转为等式(加松弛变量)
右端项 \(b_i \ge 0\) 如果 \(b_i < 0\),等式两边同乘 \(-1\)
变量 \(x_j \ge 0\) 自由变量拆成两个非负变量

\(\min \quad z = \mathbf{c}^T \mathbf{x}\)

约束:\(Ax = b\), \(x \ge 0\), \(b \ge 0\)


5.3 标准化转换技巧

技巧 1:最大化 → 最小化

\(\max z = 5x_1 + 3x_2 \quad \Longleftrightarrow \quad \min (-z) = -5x_1 - 3x_2\)

# 示例:max → min
original_max = "max z = 5x1 + 3x2"
converted_min = "min z' = -5x1 - 3x2  (其中 z' = -z)"

print(f"原始: {original_max}")
print(f"转换: {converted_min}")
print(f"最优时: max z = -{converted_min.split('=')[1].strip()}")

渲染效果:

原始: max z = 5x1 + 3x2
转换: min z' = -5x1 - 3x2  (其中 z' = -z)
最优时: max z = -(-5x1-3x2)

技巧 2:不等式 → 等式(加入松弛变量)

\(\le\) 约束:加入松弛变量 \(s_i \ge 0\)

\(2x_1 + x_2 \le 10 \quad \Longrightarrow \quad 2x_1 + x_2 + s_1 = 10, \quad s_1 \ge 0\)

松弛变量的直觉

就像往一个装满水的杯子里加冰块——冰块填补了剩余空间。松弛变量 \(s_i\) 代表第 \(i\) 种资源的"剩余量"。

\(\ge\) 约束:减去剩余变量 \(s_i \ge 0\)

\(x_1 + 2x_2 \ge 8 \quad \Longrightarrow \quad x_1 + 2x_2 - s_2 = 8, \quad s_2 \ge 0\)

def convert_to_standard():
    """
    将以下问题化为标准形式:
    max z = 5x1 + 3x2
    s.t. 2x1 + x2 <= 10
         x1 + 2x2 >= 8
         x1, x2 >= 0
    """
    print("原始问题:")
    print("  max z = 5x1 + 3x2")
    print("  s.t. 2x1 + x2 <= 10")
    print("        x1 + 2x2 >= 8")
    print("        x1, x2 >= 0")

    print("\n转换步骤:")

    # Step 1: max → min
    print("  1. max → min: 令 z' = -z")
    print("     min z' = -5x1 - 3x2")

    # Step 2: 松弛变量
    print("  2. 第一个约束 + 松弛变量 s1:")
    print("     2x1 + x2 + s1 = 10,  s1 >= 0")

    # Step 3: 剩余变量
    print("  3. 第二个约束 - 剩余变量 s2:")
    print("     x1 + 2x2 - s2 = 8,  s2 >= 0")

    print("\n标准形式:")
    print("  min z' = -5x1 - 3x2 + 0*s1 + 0*s2")
    print("  s.t. 2x1 + x2 + s1       = 10")
    print("        x1 + 2x2 - s2       = 8")
    print("        x1, x2, s1, s2 >= 0")

convert_to_standard()

渲染效果:

原始问题:
  max z = 5x1 + 3x2
  s.t. 2x1 + x2 <= 10
        x1 + 2x2 >= 8
        x1, x2 >= 0

转换步骤:
  1. max → min: 令 z' = -z
     min z' = -5x1 - 3x2
  2. 第一个约束 + 松弛变量 s1:
     2x1 + x2 + s1 = 10,  s1 >= 0
  3. 第二个约束 - 剩余变量 s2:
     x1 + 2x2 - s2 = 8,  s2 >= 0

标准形式:
  min z' = -5x1 - 3x2 + 0*s1 + 0*s2
  s.t. 2x1 + x2 + s1       = 10
        x1 + 2x2 - s2       = 8
        x1, x2, s1, s2 >= 0

技巧 3:自由变量处理

如果 \(x_k\) 可以取任意值(不要求 \(x_k \ge 0\)),令:

\(x_k = x_k^+ - x_k^-\)

其中 \(x_k^+ \ge 0, \ x_k^- \ge 0\)


5.4 基本可行解与顶点

在标准形式中,每个约束都变成等式。我们可以选择一组 \(m\) 个变量作为 基变量 (其他变量设为 0),求解得到一个 基本解

def basic_solutions_demo():
    """
    标准形式的基与基本解

    min z' = -5x1 - 3x2 + 0*s1 + 0*s2
    s.t. 2x1 + x2 + s1       = 10
          x1 + 2x2 - s2       = 8
          x1, x2, s1, s2 >= 0

    变量顺序: [x1, x2, s1, s2]
    """
    print("约束矩阵 A =")
    A = np.array([[2, 1, 1, 0], [1, 2, 0, -1]])
    print(A)

    print("\n可能的基组合(选2个作为基变量):")

    bases = [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]

    for base in bases:
        non_base = [i for i in range(4) if i not in base]
        print(f"\n基变量: {[chr(120+int(b)) for b in base]} (索引 {base})")
        print(f"非基变量: {[chr(120+int(nb)) for nb in non_base]} = 0")

        # 提取基矩阵
        B = A[:, base]
        b = np.array([10, 8])

        # 判断基矩阵是否可逆
        det = np.linalg.det(B)
        if abs(det) < 1e-10:
            print(f"  基矩阵奇异,跳过")
            continue

        # 求解
        x_base = np.linalg.solve(B, b)

        if np.all(x_base >= -1e-10):
            z = -5 * x_base[0] - 3 * x_base[1]
            print(f"  解: {[chr(120+int(b)) + '=' + str(round(x_base[i], 2)) for i, b in enumerate(base)]}")
            print(f"      z' = {z:.2f}, z = {-z:.2f} ✓ 可行")
        else:
            print(f"  解包含负值,不可实施")

basic_solutions_demo()

渲染效果:

约束矩阵 A =
[[ 2  1  1  0]
 [ 1  2  0 -1]]

可能的基组合(选2个作为基变量):

基变量: ['x', 'y'] (索引 (0, 1))
非基变量: ['s', 't'] = 0
  基矩阵奇异,跳过

基变量: ['x', 's'] (索引 (0, 2))
...

基本可行解的对应关系

基本可行解 对应图解法的顶点
\((x_1, x_2, s_1, s_2) = (0, 0, 10, -8)\) 原点 \((0, 0)\),但 \(s_2 < 0\) 不可行
\((x_1, x_2, s_1, s_2) = (0, 4, 6, 0)\) \((0, 4)\)
\((x_1, x_2, s_1, s_2) = (5, 0, 0, -3)\) \((5, 0)\),但 \(s_2 < 0\) 不可行
\((x_1, x_2, s_1, s_2) = (4, 2, 0, 0)\) \((4, 2)\) ✓ 最优

5.5 灵敏度分析:参数变化的影响

实际问题是活的——原材料价格会波动,资源上限会变化。 灵敏度分析 回答: 最优解在什么范围内保持稳定?

分析 1:目标函数系数变化

\(c_1\)\(x_1\) 的利润系数)变化时,最优解会变化吗?

def sensitivity_c1(c1, c2=3):
    """
    分析 c1 变化时,最优解是否保持不变

    最优基: [x1, x2] = (4, 2)
    最优基矩阵 B = [[2, 1], [1, 2]]
    """
    B = np.array([[2, 1], [1, 2]])
    B_inv = np.linalg.inv(B)

    # 最优性条件: 检验数 sigma_N <= 0 (对于 min 问题)
    # 非基变量 s1 的检验数: c_B * B^(-1) * a_s1 - c_s1
    # 非基变量 s2 的检验数: c_B * B^(-1) * a_s2 - c_s2

    c_B = np.array([c1, c2])  # 基变量成本

    # a_s1 = [1, 0], c_s1 = 0
    sigma_s1 = np.dot(c_B, B_inv[:, 0]) - 0

    # a_s2 = [0, -1], c_s2 = 0
    sigma_s2 = np.dot(c_B, B_inv[:, 1]) - 0

    return sigma_s1, sigma_s2

print("灵敏度分析:c1 变化对最优性的影响")
print("=" * 55)
print(f"{'c1范围':<20} {'σ_s1':<12} {'σ_s2':<12} {'最优基稳定?'}")
print("-" * 55)

for c1 in [2, 3, 4, 5, 6, 7, 8]:
    sigma_s1, sigma_s2 = sensitivity_c1(c1)
    stable = "是" if sigma_s1 <= 0 and sigma_s2 <= 0 else "否"
    print(f"{c1:<20} {sigma_s1:<12.4f} {sigma_s2:<12.4f} {stable}")

渲染效果:

灵敏度分析:c1 变化对最优性的影响
=======================================================
c1范围                σ_s1        σ_s2        最优基稳定?
-------------------------------------------------------
2                    0.5000      -0.2500     否
3                    0.2500      -0.1250     否
4                    0.0000      0.0000      是(边界)
5                   -0.2500       0.1250      是
6                   -0.5000       0.2500      是
7                   -0.7500       0.3750      是
8                   -1.0000       0.5000      是

解读

\(c_1\)\([4, 6]\) 范围内变化时,最优基保持不变,仍为 \(B = [x_1, x_2]\)

分析 2:右端项变化与影子价格

右端项 \(b\) 变化时,最优基可能改变,但可以通过 影子价格 估算目标函数值的变化。

def shadow_price_analysis():
    """
    影子价格:右端项每增加1单位,最优目标函数值的增量

    B^(-1) 的第一列 = [2/3, -1/3]
    """
    B = np.array([[2, 1], [1, 2]])
    B_inv = np.linalg.inv(B)

    # 最优解时,影子价格 y* = c_B * B^(-1)
    c_B = np.array([5, 3])
    y_star = np.dot(c_B, B_inv)

    print(f"基矩阵 B 的逆:\n{B_inv}")
    print(f"\n影子价格 y* = c_B * B^(-1) = {y_star}")
    print(f"\n解读:")
    print(f"  - 增加 1 单位牛奶(右端项1): 利润增加 {y_star[0]:.4f} 元")
    print(f"  - 增加 1 单位茶叶(右端项2): 利润增加 {y_star[1]:.4f} 元")
    print(f"\n这意味着:牛奶比茶叶更值钱(每单位资源带来的边际利润)")

shadow_price_analysis()

渲染效果:

基矩阵 B 的逆:
[[ 0.66666667 -0.33333333]
 [-0.33333333  0.66666667]]

影子价格 y* = c_B * B^(-1) = [2.33333333 0.83333333]

解读:
  - 增加 1 单位牛奶(右端项1): 利润增加 2.3333 元
  - 增加 1 单位茶叶(右端项2): 利润增加 0.8333 元

这意味着:牛奶比茶叶更值钱(每单位资源带来的边际利润)


要点总结

  • 标准形式:\(\min\) + 等式约束 + \(b \ge 0\) + \(x \ge 0\)
  • \(\max \to \min\):令 \(z' = -z\)
  • \(\le\) 约束 + 松弛变量;\(\ge\) 约束 - 剩余变量
  • 自由变量 \(x_k = x_k^+ - x_k^-\)
  • 基本可行解对应可行域的顶点
  • 灵敏度分析:最优基稳定的参数范围
  • 影子价格 = 资源每增加 1 单位的边际利润

课后练习

  1. 标准化练习 :将以下问题化为标准形式: \(\max \quad z = 3x_1 - 2x_2\)

约束:\(x_1 + 2x_2 \ge 6\), \(-x_1 + x_2 \le 3\), \(x_1, x_2 \ge 0\)

  1. 灵敏度练习 :在奶茶店问题中,如果 A 奶茶的利润从 5 元变为 4 元,最优解会变吗?

  2. 思考题 :影子价格和资源在黑市上的"价格"有什么关系?为什么影子价格为 0 的资源增加供应不会改善目标函数值?


下一章预告: 标准形式给了我们统一的数学框架。现在我们将学习 单纯形法 ——在代数上系统地从一个顶点移动到更好的顶点,直到找到最优解。这是线性规划历史上最重要的算法。

继续第 6 章:单纯形法原理 →