单纯形法 前置知识:线性规划基础 
引入 算法竞赛中,经常使用单纯形法解决线性规划问题。但是,由于算法竞赛中遇到的线性规划问题大多有着更特殊的结构,常常可以转化为网络流问题,因此,单纯形法并不常用,效率也不如专门为网络流问题设计的算法。
基本概念 假设要求解如下一个有 𝑛 n 𝑚   + 𝑛 m + n 标准形式  线性规划问题:
m i n 𝑥 𝑧 = 𝑐 𝑇 𝑥 s u b j e c t   t o   𝐴 𝑥 = 𝑏 , 𝑥 ≥ 0 . min x z = c T x subject to  A x = b , x ≥ 0. 不妨假设这 𝑚 m 𝐴 A r a n k  𝐴   = 𝑚   ≤ 𝑛 rank  A = m ≤ n 
一个例子 在严格叙述单纯形法的步骤之前,本节首先考察一个具体的例子,以方便理解。
例子  考虑线性规划问题
 m a x 1 0 𝑥 1 + 1 2 𝑥 2 + 1 2 𝑥 3 s u b j e c t   t o   𝑥 1 + 2 𝑥 2 + 2 𝑥 3 ≤ 2 0 , 2 𝑥 1 + 𝑥 2 + 2 𝑥 3 ≤ 2 0 , 2 𝑥 1 + 2 𝑥 2 + 𝑥 3 ≤ 2 0 , 𝑥 1 , 𝑥 2 , 𝑥 3 ≥ 0 . max 10 x 1 + 12 x 2 + 12 x 3 subject to  x 1 + 2 x 2 + 2 x 3 ≤ 20 , 2 x 1 + x 2 + 2 x 3 ≤ 20 , 2 x 1 + 2 x 2 + x 3 ≤ 20 , x 1 , x 2 , x 3 ≥ 0. 通过添加松弛变量,就得到它的标准形式:
 m i n − 1 0 𝑥 1 − 1 2 𝑥 2 − 1 2 𝑥 3 s u b j e c t   t o   𝑥 1 + 2 𝑥 2 + 2 𝑥 3 + 𝑥 4 = 2 0 , 2 𝑥 1 + 𝑥 2 + 2 𝑥 3 + 𝑥 5 = 2 0 , 2 𝑥 1 + 2 𝑥 2 + 𝑥 3 + 𝑥 6 = 2 0 , 𝑥 1 , 𝑥 2 , 𝑥 3 , 𝑥 4 , 𝑥 5 , 𝑥 6 ≥ 0 . min − 10 x 1 − 12 x 2 − 12 x 3 subject to  x 1 + 2 x 2 + 2 x 3 + x 4 = 20 , 2 x 1 + x 2 + 2 x 3 + x 5 = 20 , 2 x 1 + 2 x 2 + x 3 + x 6 = 20 , x 1 , x 2 , x 3 , x 4 , x 5 , x 6 ≥ 0. 观察该问题的等式约束,它们其实相当于将变量 𝑥 4 , 𝑥 5 , 𝑥 6 x 4 , x 5 , x 6 𝑥 1 , 𝑥 2 , 𝑥 3 x 1 , x 2 , x 3 
 m i n 𝑥 𝑖 ≥ 0 𝑧 = 0 − 1 0 𝑥 1 − 1 2 𝑥 2 − 1 2 𝑥 3 s u b j e c t   t o 𝑥 4 = 2 0 − 𝑥 1 − 2 𝑥 2 − 2 𝑥 3 , 𝑥 5 = 2 0 − 2 𝑥 1 − 𝑥 2 − 2 𝑥 3 , 𝑥 6 = 2 0 − 2 𝑥 1 − 2 𝑥 2 − 𝑥 3 . min x i ≥ 0 z = 0 − 10 x 1 − 12 x 2 − 12 x 3 subject to x 4 = 20 − x 1 − 2 x 2 − 2 x 3 , x 5 = 20 − 2 x 1 − x 2 − 2 x 3 , x 6 = 20 − 2 x 1 − 2 x 2 − x 3 . 从这个形式中,可以清楚地看到,如果令 𝑥 1   = 𝑥 2   = 𝑥 3   = 0 x 1 = x 2 = x 3 = 0 
 𝑥 = ( 0 , 0 , 0 , 2 0 , 2 0 , 2 0 ) 𝑇 . x = ( 0 , 0 , 0 , 20 , 20 , 20 ) T . 且它对应的价值为 𝑧   = 0 z = 0 𝑥 1 , 𝑥 2 , 𝑥 3 x 1 , x 2 , x 3 𝑥 4 , 𝑥 5 , 𝑥 6 x 4 , x 5 , x 6 
 这组可行解显然不是最优解。只要适当地增加 𝑥 1 , 𝑥 2 , 𝑥 3 x 1 , x 2 , x 3 𝑥 4 , 𝑥 5 , 𝑥 6 x 4 , x 5 , x 6 𝑥 1 , 𝑥 2 , 𝑥 3 x 1 , x 2 , x 3 𝑥 1 x 1 𝑥 1 x 1 𝑥 4 , 𝑥 5 , 𝑥 6   ≥ 0 x 4 , x 5 , x 6 ≥ 0 𝑥 1 x 1 
 m i n { 2 0 1 , 2 0 2 , 2 0 2 } = 1 0 . min { 20 1 , 20 2 , 20 2 } = 10. 此时,可行解变为
 𝑥 = ( 1 0 , 0 , 0 , 1 0 , 0 , 0 ) 𝑇 . x = ( 10 , 0 , 0 , 10 , 0 , 0 ) T . 因为 𝑥 1 x 1 𝑥 5 , 𝑥 6 x 5 , x 6 𝑥 5 x 5 
 𝑥 1 = 1 0 − 0 . 5 𝑥 5 − 0 . 5 𝑥 2 − 𝑥 3 x 1 = 10 − 0.5 x 5 − 0.5 x 2 − x 3 代入到原来的问题中,就可以将原问题改写作
 m i n 𝑥 𝑖 ≥ 0 𝑧 = − 1 0 0 + 5 𝑥 5 − 7 𝑥 2 − 2 𝑥 3 s u b j e c t   t o 𝑥 4 = 1 0 + 0 . 5 𝑥 5 − 1 . 5 𝑥 2 − 𝑥 3 , 𝑥 1 = 1 0 − 0 . 5 𝑥 5 − 0 . 5 𝑥 2 − 𝑥 3 , 𝑥 6 = 0 + 𝑥 5 − 𝑥 2 + 𝑥 3 . min x i ≥ 0 z = − 100 + 5 x 5 − 7 x 2 − 2 x 3 subject to x 4 = 10 + 0.5 x 5 − 1.5 x 2 − x 3 , x 1 = 10 − 0.5 x 5 − 0.5 x 2 − x 3 , x 6 = 0 + x 5 − x 2 + x 3 . 这就回到了初始的情形。
 继续观察当前的目标函数。非基变量 𝑥 3 x 3 𝑥 3 x 3 𝑥 4 , 𝑥 1 , 𝑥 6   ≥ 0 x 4 , x 1 , x 6 ≥ 0 𝑥 3 x 3 
 m i n { 1 0 1 , 1 0 1 } = 1 0 . min { 10 1 , 10 1 } = 10. 注意到,因为 𝑥 6 x 6 𝑥 3 x 3 𝑥 3 x 3 𝑥 6 x 6 𝑥 3 x 3 1 0 10 𝑥 1 , 𝑥 4 x 1 , x 4 𝑥 4 x 4 
 𝑥 3 = 1 0 + 0 . 5 𝑥 5 − 1 . 5 𝑥 2 − 𝑥 4 x 3 = 10 + 0.5 x 5 − 1.5 x 2 − x 4 代入上述问题中,问题变形为
 m i n 𝑥 𝑖 ≥ 0 𝑧 = − 1 2 0 + 4 𝑥 5 − 4 𝑥 2 + 2 𝑥 4 s u b j e c t   t o 𝑥 3 = 1 0 + 0 . 5 𝑥 5 − 1 . 5 𝑥 2 − 𝑥 4 , 𝑥 1 = 0 − 𝑥 5 + 𝑥 2 + 𝑥 4 , 𝑥 6 = 1 0 + 1 . 5 𝑥 5 − 2 . 5 𝑥 2 − 𝑥 4 . min x i ≥ 0 z = − 120 + 4 x 5 − 4 x 2 + 2 x 4 subject to x 3 = 10 + 0.5 x 5 − 1.5 x 2 − x 4 , x 1 = 0 − x 5 + x 2 + x 4 , x 6 = 10 + 1.5 x 5 − 2.5 x 2 − x 4 . 只需要代入 𝑥 5   = 𝑥 2   = 𝑥 4   = 0 x 5 = x 2 = x 4 = 0 
 𝑥 = ( 0 , 0 , 1 0 , 0 , 0 , 1 0 ) 𝑇 , x = ( 0 , 0 , 10 , 0 , 0 , 10 ) T , 以及它对应的价值为 𝑧   =   − 1 2 0 z = − 120 
 重复之前的操作。因为 𝑥 2 x 2 𝑥 3 , 𝑥 6 x 3 , x 6 
 m i n { 1 0 1 . 5 , 1 0 2 . 5 } = 4 . min { 10 1.5 , 10 2.5 } = 4. 因为大括号中的最小值出现在变量 𝑥 6 x 6 𝑥 2   = 4 x 2 = 4 
 𝑥 2 = 4 + 0 . 6 𝑥 5 − 0 . 4 𝑥 6 − 0 . 4 𝑥 4 x 2 = 4 + 0.6 x 5 − 0.4 x 6 − 0.4 x 4 代入上述问题,就可以将原问题改写为
 m i n 𝑥 𝑖 ≥ 0 𝑧 = − 1 3 6 + 1 . 6 𝑥 5 + 1 . 6 𝑥 6 + 3 . 6 𝑥 4 s u b j e c t   t o 𝑥 3 = 4 − 0 . 4 𝑥 5 + 0 . 6 𝑥 6 − 0 . 4 𝑥 4 , 𝑥 1 = 4 − 0 . 4 𝑥 5 − 0 . 4 𝑥 6 + 0 . 6 𝑥 4 , 𝑥 2 = 4 + 1 . 5 𝑥 5 − 2 . 5 𝑥 6 − 𝑥 4 . min x i ≥ 0 z = − 136 + 1.6 x 5 + 1.6 x 6 + 3.6 x 4 subject to x 3 = 4 − 0.4 x 5 + 0.6 x 6 − 0.4 x 4 , x 1 = 4 − 0.4 x 5 − 0.4 x 6 + 0.6 x 4 , x 2 = 4 + 1.5 x 5 − 2.5 x 6 − x 4 . 仍然令非基变量 𝑥 5 , 𝑥 6 , 𝑥 4 x 5 , x 6 , x 4 
 𝑥 = ( 4 , 4 , 4 , 0 , 0 , 0 ) 𝑇 . x = ( 4 , 4 , 4 , 0 , 0 , 0 ) T . 对应的价值为 𝑧   =   − 1 3 6 z = − 136 
 因为目标函数中所有非基变量的系数都是正数,无法继续前文所述过程以改进目标函数,所以,当前的可行解就是最优解。算法终止。
这个例子中,算法从一组可行解出发,不断地改进目标函数,直到无法继续改进。这就是单纯形法的基本思想。
基本可行解 由于 𝐴 A 𝑚 m 𝐵   ⊆ { 1 , 2 , ⋯ , 𝑛 } B ⊆ { 1 , 2 , ⋯ , n } 𝐴 𝐵 A B 𝑥 𝐵 x B 𝑥 𝑁 x N 
𝑥 𝐵 = 𝐴 − 1 𝐵 𝑏 − 𝐴 − 1 𝐵 𝐴 𝑁 𝑥 𝑁 . x B = A B − 1 b − A B − 1 A N x N . 其中,𝑁   = { 1 , 2 , ⋯ , 𝑛 }   ∖ 𝐵 N = { 1 , 2 , ⋯ , n } ∖ B 𝐴 𝐵 , 𝐴 𝑁 A B , A N 𝐴 A 𝑖   ∈ 𝐵 i ∈ B 𝑖   ∈ 𝑁 i ∈ N 𝑥 𝐵 , 𝑥 𝑁 x B , x N 𝑥 x 𝑖   ∈ 𝐵 i ∈ B 𝑖   ∈ 𝑁 i ∈ N 𝑖   ∈ 𝐵 i ∈ B 𝑥 𝑖 x i 基变量 (basic variable);否则,称 𝑥 𝑖 x i 非基变量 (non-basic variable)。基变量的全体称为一组 基 (basis),本文用对应的标号集合 𝐵 B 
「基」  「基」这个名称,可以从线性代数的角度理解。设 𝐴 A 𝑉 V 𝐵 B 𝑉 V 
在基变量 𝑥 𝐵 x B 𝑥 𝑁   = 0 x N = 0 
𝑥 = ( 𝑥 𝐵 , 𝑥 𝑁 ) = ( 𝐴 − 1 𝐵 𝑏 , 0 ) . x = ( x B , x N ) = ( A B − 1 b , 0 ) . 这样得到的解称为线性规划问题的一个 基本解 (basic solution)。如果它还满足所有非负约束,即 𝑥   ≥ 0 x ≥ 0 基本可行解 (basic feasible solution, BFS)。在单纯形法的迭代过程中,需要始终保持当前的解为一组基本可行解。
转轴 单纯形法的每次迭代就称为一次 转轴 (pivoting)。从结果上看,每次转轴总是移除一个旧的基变量,再添加一个新的基变量,进而改进目标函数的值。
「转轴」  「转轴」这个名称,同样可以从线性代数的角度理解。如上文所述,基 𝐵 B 𝑉 V 𝑉 V 
为了确定需要添加的基变量,可以将目标函数利用非基变量表示为
𝑐 𝑇 𝑥 = 𝑐 𝑇 𝐵 𝑥 𝐵 + 𝑐 𝑇 𝑁 𝑥 𝑁 = 𝑐 𝑇 𝐵 𝐴 − 1 𝐵 𝑏 + ( 𝑐 𝑇 𝑁 − 𝑐 𝑇 𝐵 𝐴 − 1 𝐵 𝐴 𝑁 ) 𝑥 𝑁 . c T x = c B T x B + c N T x N = c B T A B − 1 b + ( c N T − c B T A B − 1 A N ) x N . 令 𝑥 𝑁   = 0 x N = 0 𝑧   = 𝑐 𝑇 𝐵 𝐴 − 1 𝐵 𝑏 z = c B T A B − 1 b 𝑥 𝑁 x N 
˜ 𝑐 𝑁 = 𝜕 𝑧 𝜕 𝑥 𝑁 = 𝑐 𝑁 − 𝐴 𝑇 𝑁 ( 𝐴 − 1 𝐵 ) 𝑇 𝑐 𝐵 . c ~ N = ∂ z ∂ x N = c N − A N T ( A B − 1 ) T c B . 注意到 𝑐 𝐵   − 𝐴 𝑇 𝐵 ( 𝐴 − 1 𝐵 ) 𝑇 𝑐 𝐵   = 0 c B − A B T ( A B − 1 ) T c B = 0 
˜ 𝑐 = ( ˜ 𝑐 𝑇 𝐵 , ˜ 𝑐 𝑇 𝑁 ) 𝑇 = 𝑐 − 𝐴 𝑇 ( 𝐴 − 1 𝐵 ) 𝑇 𝑐 𝐵 c ~ = ( c ~ B T , c ~ N T ) T = c − A T ( A B − 1 ) T c B 为线性规划问题在可行基本解 𝑥 x 约化成本 (reduced cost)。分量 ˜ 𝑐 𝑖   < 0 c ~ i < 0 𝑥 𝑖 x i 入基变量 (entering variable)。因为在转轴后,𝑥 𝑖 x i 
选择完入基变量后,还需要选择需要移除的旧的基变量。为此,只需要确定,在增加 𝑥 𝑖 x i 𝑥 𝑁   = ( 𝑥 𝑖 , 𝑥 𝑁 ∖ { 𝑖 } )   = ( 𝑥 𝑖 , 0 ) x N = ( x i , x N ∖ { i } ) = ( x i , 0 ) 𝑥 𝐵 x B 
𝑥 𝐵 = 𝐴 − 1 𝐵 𝑏 − 𝐴 − 1 𝐵 𝐴 𝑖 𝑥 𝑖 . x B = A B − 1 b − A B − 1 A i x i . 因此,𝑥 𝑖 x i 
𝜃 = m i n { ( 𝐴 − 1 𝐵 𝑏 ) 𝑗 ( 𝐴 − 1 𝐵 𝐴 𝑖 ) 𝑗 : ( 𝐴 − 1 𝐵 𝐴 𝑖 ) 𝑗 > 0 } . θ = min { ( A B − 1 b ) j ( A B − 1 A i ) j : ( A B − 1 A i ) j > 0 } . 最先变为零的变量就是使得该表达式取得最小值的下标 𝑗 j 𝑥 𝐵 𝑗 x B j 𝑥 𝑖 x i 𝑥 𝑖 x i 𝑥 𝐵 𝑗 x B j 出基变量 (leaving variable)。确定出基变量的方法称为 最小比值检验 (minimum ratio test)。
设入基变量为 𝑥 𝑖 x i 𝑥 𝑖 ′ x i ′ 𝑥 𝐵 ∖ { 𝑖 } ∪ { 𝑖 ′ } x B ∖ { i } ∪ { i ′ } 𝑥 𝑁 ∖ { 𝑖 ′ } ∪ { 𝑖 } x N ∖ { i ′ } ∪ { i } 
终止条件 单纯形法,就是从一组基本可行解出发,不断进行转轴的过程。上一节对转轴的讨论并不是完整的,它忽略了一些特殊的情形。有些特殊情形对应着算法的终止,有些则需要额外的处理。
首先,入基变量未必存在,即 ˜ 𝑐   ≥ 0 c ~ ≥ 0 互补松弛条件 。令 𝑦   = ( 𝐴 − 1 𝐵 ) 𝑇 𝑐 𝐵 y = ( A B − 1 ) T c B 𝑥 x 
𝑥 𝑇 ( 𝑐 − 𝐴 𝑇 𝑦 ) = ˜ 𝑐 𝑇 𝑥 = ˜ 𝑐 𝑇 𝐵 𝑥 𝐵 + ˜ 𝑐 𝑇 𝑁 𝑥 𝑁 = 0 . x T ( c − A T y ) = c ~ T x = c ~ B T x B + c ~ N T x N = 0. 因此,只需要 𝑦 y 𝐴 𝑇 𝑦   ≤ 𝑐 A T y ≤ c 𝑥 x 𝑦 y ˜ 𝑐   ≥ 0 c ~ ≥ 0 
「影子价格」  向量 𝑦   = ( 𝐴 − 1 𝐵 ) 𝑇 𝑐 𝐵 y = ( A B − 1 ) T c B 对偶向量 (dual vector)。当 𝐵 B 𝑦 y 𝑦 y 
 𝜕 ( 𝑐 𝑇 𝑥 ) 𝜕 𝑏 = ( 𝐴 − 1 𝐵 ) 𝑇 𝑐 𝐵 = 𝑦 , ∂ ( c T x ) ∂ b = ( A B − 1 ) T c B = y , 它也称为 影子价格 (shadow price)。
其次,出基变量未必存在,即 𝐴 − 1 𝐵 𝐴 𝑖   ≤ 0 A B − 1 A i ≤ 0 𝑥 𝑖 x i − ∞ − ∞ 
最后,入基变量和出基变量的选择可能并非是唯一的。不恰当的选择方式可能会导致过多地转轴,甚至使得算法陷入循环,无法正常终止。这类情形的处理稍微复杂,需要应用一些 转轴规则  以防止陷入循环并减少转轴次数。
单纯形表 具体实现转轴过程时,只需要维护每次转轴之后,线性规划问题的系数矩阵:
˜ 𝑇 𝐵 = ( − 𝑧 𝐵 ˜ 𝑐 𝑇 𝑁 𝑥 𝐴 − 1 𝐵 𝐴 𝑁 ) = ( − 𝑐 𝑇 𝐵 𝐴 − 1 𝐵 𝑏 𝑐 𝑇 − 𝑐 𝑇 𝐵 𝐴 − 1 𝐵 𝐴 𝑁 𝐴 − 1 𝐵 𝑏 𝐴 − 1 𝐵 𝐴 𝑁 ) . T ~ B = ( − z B c ~ N T x A B − 1 A N ) = ( − c B T A B − 1 b c T − c B T A B − 1 A N A B − 1 b A B − 1 A N ) . 它对应着线性规划问题:
m i n 𝑥 ≥ 0 𝑐 𝑇 𝐵 𝐴 − 1 𝐵 𝑏 + ˜ 𝑐 𝑇 𝑁 𝑥 𝑁 s u b j e c t   t o 𝑥 𝐵 = 𝐴 − 1 𝐵 𝑏 − 𝐴 − 1 𝐵 𝐴 𝑁 𝑥 𝑁 . min x ≥ 0 c B T A B − 1 b + c ~ N T x N subject to x B = A B − 1 b − A B − 1 A N x N . 矩阵 ˜ 𝑇 𝐵 T ~ B 𝐵 B 压缩单纯形表 (condensed simplex tableau)。表的左上角 ( ˜ 𝑇 𝐵 ) 0 0 ( T ~ B ) 00 0 0 𝑖 i ( ˜ 𝑇 𝐵 ) 0 𝑖 ( T ~ B ) 0 i 𝑖 i 𝑥 𝑁 𝑖 x N i 𝑗 j 𝑖 i ( ˜ 𝑇 𝐵 ) 𝑗 0 ( T ~ B ) j 0 𝑗 j 𝑥 𝐵 𝑗 x B j 𝐴 − 1 𝐵 𝐴 𝑁 A B − 1 A N 𝑥 𝑁 x N 𝑥 𝐵 x B 
容易看出,转轴需要的所有信息都可以从压缩单纯形表中直接获得。具体地,利用压缩单纯形表,单次转轴包括如下操作:
选取列 𝑖   = 1 , ⋯ , 𝑛   − 𝑚 i = 1 , ⋯ , n − m ( ˜ 𝑇 𝐵 ) 0 𝑖   < 0 ( T ~ B ) 0 i < 0 𝑖 i − ( ˜ 𝑇 𝐵 ) 0 0 − ( T ~ B ) 00  选取行 𝑗   = 1 , ⋯ , 𝑚 j = 1 , ⋯ , m ( ˜ 𝑇 𝐵 ) 𝑗 𝑖   > 0 ( T ~ B ) j i > 0 ( ˜ 𝑇 𝐵 ) 𝑗 0 / ( ˜ 𝑇 𝐵 ) 𝑗 𝑖 ( T ~ B ) j 0 / ( T ~ B ) j i 𝑗 j  令变量 𝑥 𝑁 𝑖 x N i 𝑥 𝐵 𝑗 x B j  现在,具体地讨论一下如何更新单纯形表。在更新单纯形表之前,第 𝑗 j 
𝑥 𝐵 𝑗 = ( ˜ 𝑇 𝐵 ) 𝑗 0 − 𝑛 − 𝑚 ∑ 𝑖 = 1 ( ˜ 𝑇 𝐵 ) 𝑗 𝑖 𝑥 𝑁 𝑖 . x B j = ( T ~ B ) j 0 − ∑ i = 1 n − m ( T ~ B ) j i x N i . 为了更新单纯形表,需要用 𝑥 𝑁 ∖ { 𝑁 𝑖 } ∪ { 𝐵 𝑗 } x N ∖ { N i } ∪ { B j } 𝑥 𝑁 𝑖 x N i 
𝑥 𝑁 𝑖 = ( ˜ 𝑇 𝐵 ) 𝑗 0 ( ˜ 𝑇 𝐵 ) 𝑗 𝑖 − 1 ( ˜ 𝑇 𝐵 ) 𝑗 𝑖 𝑥 𝐵 𝑗 − ∑ 𝑖 ′ ≠ 𝑖 ( ˜ 𝑇 𝐵 ) 𝑗 𝑖 ′ ( ˜ 𝑇 𝐵 ) 𝑗 𝑖 𝑥 𝑁 𝑖 ′ . x N i = ( T ~ B ) j 0 ( T ~ B ) j i − 1 ( T ~ B ) j i x B j − ∑ i ′ ≠ i ( T ~ B ) j i ′ ( T ~ B ) j i x N i ′ . 将它代入其余的式子中,就得到
𝑥 𝐵 𝑗 ′ = ( ( ˜ 𝑇 𝐵 ) 𝑗 ′ 0 − ( ˜ 𝑇 𝐵 ) 𝑗 ′ 𝑖 ( ˜ 𝑇 𝐵 ) 𝑗 0 ( ˜ 𝑇 𝐵 ) 𝑗 𝑖 ) + ( ˜ 𝑇 𝐵 ) 𝑗 ′ 𝑖 ( ˜ 𝑇 𝐵 ) 𝑗 𝑖 𝑥 𝐵 𝑗 − ∑ 𝑖 ′ ≠ 𝑖 ( ( ˜ 𝑇 𝐵 ) 𝑗 ′ 𝑖 ′ − ( ˜ 𝑇 𝐵 ) 𝑗 ′ 𝑖 ( ˜ 𝑇 𝐵 ) 𝑗 𝑖 ′ ( ˜ 𝑇 𝐵 ) 𝑗 𝑖 ) 𝑥 𝑁 𝑖 . x B j ′ = ( ( T ~ B ) j ′ 0 − ( T ~ B ) j ′ i ( T ~ B ) j 0 ( T ~ B ) j i ) + ( T ~ B ) j ′ i ( T ~ B ) j i x B j − ∑ i ′ ≠ i ( ( T ~ B ) j ′ i ′ − ( T ~ B ) j ′ i ( T ~ B ) j i ′ ( T ~ B ) j i ) x N i . 第 0 0 − 𝑧 − z 
更新第 𝑗 j 𝛼   = ( ˜ 𝑇 𝐵 ) 𝑗 𝑖 α = ( T ~ B ) j i 𝑖 i 1 1 𝛼 α  更新第 𝑗 ′   ≠ 𝑗 j ′ ≠ j 𝛽   = ( ˜ 𝑇 𝐵 ) 𝑗 ′ 𝑖 β = ( T ~ B ) j ′ i 𝑗 j 0 0 𝛽 β 𝑗 j  「单纯形表」  单纯形表 (simplex tableau)指矩阵
 𝑇 𝐵 = ( − 𝑧 ˜ 𝑐 𝑇 𝑥 𝐴 − 1 𝐵 𝐴 ) = ( − 𝑐 𝑇 𝐵 𝐴 − 1 𝐵 𝑏 𝑐 𝑇 − 𝑐 𝑇 𝐵 𝐴 − 1 𝐵 𝐴 𝐴 − 1 𝐵 𝑏 𝐴 − 1 𝐵 𝐴 ) . T B = ( − z c ~ T x A B − 1 A ) = ( − c B T A B − 1 b c T − c B T A B − 1 A A B − 1 b A B − 1 A ) . 相较于压缩单纯形表,它多了 𝑚 m 𝑚 m 𝑗 j 𝑒 𝑗 e j 𝑗 j 1 1 0 0 
 利用单纯形表可以更方便地理解更新单纯形表的步骤。因为所有的单纯形表 𝑇 𝐵 T B 𝑇 0 T 0 𝐿 𝐵 L B 
 𝑇 𝐵 = ( − 𝑐 𝑇 𝐵 𝐴 − 1 𝐵 𝑏 𝑐 𝑇 − 𝑐 𝑇 𝐵 𝐴 − 1 𝐵 𝐴 𝐴 − 1 𝐵 𝑏 𝐴 − 1 𝐵 𝐴 ) = ( 1 − 𝑐 𝑇 𝐵 𝐴 − 1 𝐵 𝑂 𝐴 − 1 𝐵 ) ( 0 𝑐 𝑇 𝑏 𝐴 ) = 𝐿 𝐵 𝑇 0 , T B = ( − c B T A B − 1 b c T − c B T A B − 1 A A B − 1 b A B − 1 A ) = ( 1 − c B T A B − 1 O A B − 1 ) ( 0 c T b A ) = L B T 0 , 所以,这些单纯形表和 𝑇 0 T 0 初等行变换  相互转化。因此,在更新单纯形表时,只需要施行初等行变换,使得入基变量对应列变为 𝑒 𝑗 e j 
更新压缩单纯形表的参考实现如下:
参考实现   1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
10 
11 
12 
13 
14 
15 
16 // Pivot on (N[x], B[y]). 
void   pivot ( int   x ,   int   y )   { 
   std :: swap ( N [ x ],   B [ y ]); 
   long   double   v   =   -1   /   tab [ x ][ y ]; 
   for   ( int   j   =   0 ;   j   <=   m   +   1 ;   ++ j )   { 
     tab [ x ][ j ]   =   j   ==   y   ?   - v   :   v   *   tab [ x ][ j ]; 
   } 
   for   ( int   i   =   0 ;   i   <=   n ;   ++ i )   { 
     if   ( i   ==   x )   continue ; 
     v   =   tab [ i ][ y ]; 
     tab [ i ][ y ]   =   0 ; 
     for   ( int   j   =   0 ;   j   <=   m   +   1 ;   ++ j )   { 
       tab [ i ][ j ]   +=   v   *   tab [ x ][ j ]; 
     } 
   } 
} 
由该实现可知,单次更新单纯形表的时间复杂度为 𝑂 ( 𝑚 𝑛 ) O ( m n ) 转轴规则  时会说明,确定出基变量和入基变量的复杂度同样不会超过 𝑂 ( 𝑚 𝑛 ) O ( m n ) 𝑂 ( 𝑚 𝑛 ) O ( m n ) 
为方便理解,此处列出前文所示例子中,利用压缩单纯形表计算的详细步骤。
例子(续)  初始时,压缩单纯形表如下所示:
 𝑥 1 𝑥 2 𝑥 3 0 − 1 0 − 1 2 − 1 2 𝑥 4 = 2 0 1 2 2 𝑥 5 = 2 0 2 1 2 𝑥 6 = 2 0 2 2 1 x 1 x 2 x 3 0 − 10 − 12 − 12 x 4 = 20 1 2 2 x 5 = 20 2 1 2 x 6 = 20 2 2 1 根据第 0 0 𝑥 1 , 𝑥 2 , 𝑥 3 x 1 , x 2 , x 3 𝑥 1 x 1 𝑥 5 , 𝑥 6 x 5 , x 6 𝑥 5 x 5 
 𝑥 5 𝑥 2 𝑥 3 1 0 0 5 − 7 − 2 𝑥 4 = 1 0 − 0 . 5 1 . 5 1 𝑥 1 = 1 0 0 . 5 0 . 5 1 𝑥 6 = 0 − 1 1 − 1 x 5 x 2 x 3 100 5 − 7 − 2 x 4 = 10 − 0.5 1.5 1 x 1 = 10 0.5 0.5 1 x 6 = 0 − 1 1 − 1 根据第 0 0 𝑥 2 , 𝑥 3 x 2 , x 3 𝑥 3 x 3 𝑥 4 , 𝑥 1 x 4 , x 1 𝑥 4 x 4 
 𝑥 5 𝑥 2 𝑥 4 1 2 0 4 − 4 2 𝑥 3 = 1 0 − 0 . 5 1 . 5 1 𝑥 1 = 0 1 − 1 − 1 𝑥 6 = 1 0 − 1 . 5 2 . 5 1 x 5 x 2 x 4 120 4 − 4 2 x 3 = 10 − 0.5 1.5 1 x 1 = 0 1 − 1 − 1 x 6 = 10 − 1.5 2.5 1 根据第 0 0 𝑥 2 x 2 𝑥 2 x 2 𝑥 6 x 6 𝑥 6 x 6 
 𝑥 5 𝑥 4 𝑥 6 1 3 6 1 . 6 3 . 6 1 . 6 𝑥 3 = 4 0 . 4 0 . 4 − 0 . 6 𝑥 1 = 4 0 . 4 − 0 . 6 0 . 4 𝑥 2 = 4 − 0 . 6 0 . 4 0 . 4 x 5 x 4 x 6 136 1.6 3.6 1.6 x 3 = 4 0.4 0.4 − 0.6 x 1 = 4 0.4 − 0.6 0.4 x 2 = 4 − 0.6 0.4 0.4 根据第 0 0 
 𝑥 = ( 4 , 4 , 4 , 0 , 0 , 0 ) 𝑇 x = ( 4 , 4 , 4 , 0 , 0 , 0 ) T 就是最优解,(最小化问题的)最优价值为 − 1 3 6 − 136 
除了利用单纯形表实现单纯形法之外,还可以使用修正单纯形法(revised simplex method),它进一步改进了算法的时空复杂度,将单次更新的复杂度进一步降低到了 𝑂 ( 𝑚 2 ) O ( m 2 ) 𝑚   ≪ 𝑛 m ≪ n 𝐴 A 
几何背景 本节介绍单纯形法的几何背景。
对线性规划问题的可行域
D = { 𝑥 ∈ 𝐑 𝑛 : 𝐴 𝑥 = 𝑏 ,   𝑥 ≥ 0 } D = { x ∈ R n : A x = b ,   x ≥ 0 } 的 分析  指出:
线性规划问题的最优解(如果存在)必然可以选取为可行域 D D  每一个顶点的坐标,都可以通过 𝑛 n 𝑚 m 𝑛   − 𝑚 n − m 𝑥 𝑁 x N 0 0 𝐴 𝑥   = 𝑏 A x = b 𝑚 m 𝑥 𝐵 x B 𝐴 𝐵 𝑥 𝐵   = 𝑏 A B x B = b 𝐴 𝐵 A B 𝑥 𝐵   = 𝐴 − 1 𝐵 𝑏 x B = A B − 1 b ( 𝑥 𝐵 , 𝑥 𝑁 )   = ( 𝐴 − 1 𝐵 𝑏 , 0 ) ( x B , x N ) = ( A B − 1 b , 0 ) 𝑥 𝐵   ≥ 0 x B ≥ 0 D D  容易看出,顶点解的概念,和前文定义的基本可行解是一致的。因此,只要在所有基本可行解内找到最优的那个,就能获得原问题的最优解。虽然这大幅简化了问题,但是,可行域的顶点的个数是指数级的,穷举并不现实。
为了解决这一困难,可以考虑沿着可行域的 边  移动,从一个顶点移动到与之相邻的顶点。因为相邻的顶点必定位于同一条边上,所以,它们至少满足 𝑛   − 1 n − 1 𝑥 x 相邻的 (adjacent)的基本可行解 𝑥 ′ x ′ 
因此,单纯形法从一个基本可行解出发,不断进行转轴,改进目标函数的过程,其实就是在相应的可行域上,从一个顶点出发,不断向相邻顶点移动,进而改进目标函数的过程。
例子(续)  本文讨论的例子中,可行域是一个有五个顶点的三维多面体,如下图所示:
 
 前述求解过程,从几何直观上看,就对应着多面体的顶点间的如下路径:
 ( 0 , 0 , 0 ) → ( 0 , 0 , 1 0 ) → ( 1 0 , 0 , 0 ) → ( 4 , 4 , 4 ) . ( 0 , 0 , 0 ) → ( 0 , 0 , 10 ) → ( 10 , 0 , 0 ) → ( 4 , 4 , 4 ) . 实现细节 利用单纯形表,已经能够求解许多线性规划问题。然而,对于最一般的情形,单纯形法中仍有许多细节值得深入探讨。
松弛形式 将一般形式的线性规划问题转化为标准形式的 方法  已经讨论过了。但是,为了方便利用单纯形法求解,还需要保证系数矩阵 𝐴 A 
将线性规划问题转化为 不等式形式 (inequality form),即 m i n { 𝑐 𝑇 𝑥   : 𝐴 𝑥   ≤ 𝑏 ,   𝑥   ≥ 0 } min { c T x : A x ≤ b ,   x ≥ 0 }  通过添加松弛变量 𝑠 s m i n { 𝑐 𝑇 𝑥   : 𝐴 𝑥   + 𝑠   = 𝑏 ,   𝑥   ≥ 0 ,   𝑠   ≥ 0 } min { c T x : A x + s = b ,   x ≥ 0 ,   s ≥ 0 }  这样做的好处是,最终得到的标准形式的系数矩阵 ( 𝐴 , 𝐼 ) ( A , I ) ( 𝑥 , 𝑠 )   = ( 0 , 𝑏 ) ( x , s ) = ( 0 , b ) 松弛形式 (slack form)。
初始基本可行解 前文描述的单纯形法总是假定已知一组基本可行解。有些时候,很容易找到一组基本可行解。例如,如果上述松弛形式中 𝑏   ≥ 0 b ≥ 0 ( 𝑥 , 𝑠 )   = ( 0 , 𝑏 ) ( x , s ) = ( 0 , b ) 
对于一般的情形,可以采用 两阶段法 (two-phase method)。两阶段法中,需要做两次单纯形法。第一阶段求解一个可行性线性规划问题,获得原问题的一个基本可行解。第二阶段从这个基本可行解出发,应用单纯形法求解原问题。
假设有标准形式的问题 m i n { 𝑐 𝑇 𝑥   : 𝐴 𝑥   = 𝑏   ≥ 0 ,   𝑥   ≥ 0 } min { c T x : A x = b ≥ 0 ,   x ≥ 0 } 
m i n { 1 𝑇 𝑥 𝑎 : 𝐴 𝑥 + 𝑥 𝑎 = 𝑏 ,   𝑥 ≥ 0 ,   𝑠 ≥ 0 } . min { 1 T x a : A x + x a = b ,   x ≥ 0 ,   s ≥ 0 } . 这本质是可行性线性规划问题,其中,新添加的变量 𝑥 𝑎 x a 人工变量 (artificial variable)。它一定有基本可行解 ( 𝑥 , 𝑥 𝑎 )   = ( 0 , 𝑏 ) ( x , x a ) = ( 0 , b ) 0 0 𝑥   ≥ 0 x ≥ 0 𝐴 𝑥   = 𝑏 A x = b 0 0 
不显式引入人工变量的第一阶段实现  实现第一阶段时,没有必要显式地引入人工变量。对于任意选取的初始基 𝐵 B 
 𝑥 𝐵 + 𝐴 − 1 𝐵 𝐴 𝑁 𝑥 𝑁 = 𝐴 − 1 𝐵 𝑏 . x B + A B − 1 A N x N = A B − 1 b . 如果 ( 𝐴 − 1 𝐵 𝑏 ) 𝑗   ≥ 0 ( A B − 1 b ) j ≥ 0 𝑥 − 𝐵 𝑗 x B j − 
 𝑥 𝐵 𝑗 − 𝑥 − 𝐵 𝑗 + ( 𝐴 − 1 𝐵 𝐴 𝑁 ) ( 𝑗 ) 𝑥 𝑁 = ( 𝐴 − 1 𝐵 𝑏 ) 𝑗 . x B j − x B j − + ( A B − 1 A N ) ( j ) x N = ( A B − 1 b ) j . 记下标集合 𝐿   : = { 𝑗   : ( 𝐴 − 1 𝐵 𝑏 ) 𝑗   < 0 } L := { j : ( A B − 1 b ) j < 0 } 
 𝑥 𝑁 𝑥 𝐵 ∼ 𝐿 𝑥 𝐵 𝐿 𝑥 − 𝐵 𝐿 0 0 𝑇 0 𝑇 0 𝑇 1 𝑇 𝑥 𝐵 ∼ 𝐿 = ( 𝐴 − 1 𝐵 𝑏 ) ∼ 𝐿 ( 𝐴 − 1 𝐵 𝐴 𝑁 ) ( ∼ 𝐿 ) 𝐼 𝑂 𝑂 𝑥 − 𝐵 𝐿 = ( 𝐴 − 1 𝐵 𝑏 ) 𝐿 ( 𝐴 − 1 𝐵 𝐴 𝑁 ) ( 𝐿 ) 𝑂 𝐼 − 𝐼 x N x B ∼ L x B L x B L − 0 0 T 0 T 0 T 1 T x B ∼ L = ( A B − 1 b ) ∼ L ( A B − 1 A N ) ( ∼ L ) I O O x B L − = ( A B − 1 b ) L ( A B − 1 A N ) ( L ) O I − I 类似于将单纯形表简化为压缩单纯形表,可以将其适当地简化:(将 𝑥 − 𝐵 𝐿 x B L − 1 𝑇 1 T 0 0 
 𝑥 𝑁 1 𝑇 𝑏 𝐿 1 𝑇 ( 𝐴 − 1 𝐵 𝐴 𝑁 ) 𝐿 𝑥 𝐵 ∼ 𝐿 = ( 𝐴 − 1 𝐵 𝑏 ) ∼ 𝐿 ( 𝐴 − 1 𝐵 𝐴 𝑁 ) ( ∼ 𝐿 ) − 𝑥 𝐵 𝐿 = ( 𝐴 − 1 𝐵 𝑏 ) 𝐿 ( 𝐴 − 1 𝐵 𝐴 𝑁 ) 𝐿 x N 1 T b L 1 T ( A B − 1 A N ) L x B ∼ L = ( A B − 1 b ) ∼ L ( A B − 1 A N ) ( ∼ L ) − x B L = ( A B − 1 b ) L ( A B − 1 A N ) L 这与正常的压缩单纯形表大体一致,只是最后一行的变量上标记了负号,表示该行仍然含有人工变量,即原来的松弛变量仍然不可行。利用该表,转轴过程如下:
 如果 𝐿   = ∅ L = ∅  否则,根据第 0 0 𝑥 𝑁 𝑖 x N i  再根据第 𝑖 i 𝑥 𝐵 𝑗 x B j 
 a r g  m i n 𝑗 { ( ˜ 𝑇 𝐵 ) 𝑗 0 ( ˜ 𝑇 𝐵 ) 𝑗 𝑖 : ( 𝑗 ∉ 𝐿 ∧ ( ˜ 𝑇 𝐵 ) 𝑗 𝑖 > 0 ) ∨ ( 𝑗 ∈ 𝐿 ∧ ( ˜ 𝑇 𝐵 ) 𝑗 𝑖 < 0 ) } arg  min j { ( T ~ B ) j 0 ( T ~ B ) j i : ( j ∉ L ∧ ( T ~ B ) j i > 0 ) ∨ ( j ∈ L ∧ ( T ~ B ) j i < 0 ) } 作为出基变量所在行。如果存在多个这样的出基变量,优先选取不可行的出基变量。
令 𝑥 𝑁 𝑖 x N i 𝑥 𝐵 𝑗 x B j 
如果 𝑗   ∈ 𝐿 j ∈ L 𝑗 j 𝐿 L ( ˜ 𝑇 𝐵 ) 0 𝑖 ( T ~ B ) 0 i  之所以可以略去人工变量所在列,是因为如果它们仍然是基变量,那么它们对应的列就是 𝑒 𝑗 e j 
 参考实现如下:
 参考实现   1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
24 
25 
26 
27 
28 
29 
30 
31 
32 
33 
34 
35 
36 
37 
38 
39 
40 
41 
42 
43 
44 
45 // First phase: find an initial BFS. 
// Return false if no feasible solution. 
bool   initialize ()   { 
   int   neg_count   =   0 ; 
   for   ( int   j   =   0 ;   j   <   m ;   ++ j )   { 
     if   ( tab [ n ][ j ]   <   - eps )   { 
       for   ( int   i   =   0 ;   i   <=   n ;   ++ i )   { 
         tab [ i ][ m   +   1 ]   +=   tab [ i ][ j ]; 
       } 
       B [ j ]   =   ~ B [ j ]; 
       ++ neg_count ; 
     } 
   } 
   while   ( neg_count )   { 
     int   x   =   -1 ; 
     long   double   mi   =   - eps ; 
     for   ( int   i   =   0 ;   i   <   n ;   ++ i )   { 
       if   ( tab [ i ][ m   +   1 ]   <   mi )   { 
         x   =   i ; 
         mi   =   tab [ i ][ m   +   1 ]; 
       } 
     } 
     if   ( x   ==   -1 )   return   false ; 
     int   y   =   -1 ; 
     mi   =   INFINITY ; 
     for   ( int   j   =   0 ;   j   <   m ;   ++ j )   { 
       if   (( B [ j ]   <   0   &&   tab [ x ][ j ]   <   - eps )   ||   ( B [ j ]   >=   0   &&   tab [ x ][ j ]   >   eps ))   { 
         auto   tmp   =   tab [ n ][ j ]   /   tab [ x ][ j ]   +   ( B [ j ]   <   0   ?   - eps   :   eps ); 
         if   ( tmp   <   mi )   { 
           y   =   j ; 
           mi   =   tmp ; 
         } 
       } 
     } 
     if   ( B [ y ]   <   0 )   { 
       -- neg_count ; 
       B [ y ]   =   ~ B [ y ]; 
       pivot ( x ,   y ); 
       tab [ x ][ m   +   1 ]   +=   1 ; 
     }   else   { 
       pivot ( x ,   y ); 
     } 
   } 
   return   true ; 
} 
在一阶段开始前,额外添加一行用于记录一阶段的目标函数。转轴时,对整个表进行转轴,包括第二阶段的目标函数。这样,在第一阶段完成时,第二阶段的目标函数也一并相应地更新,可以直接开始二阶段的单纯形法。
两阶段法也可以通过一次单纯形法实现。只需要取充分大的正数 𝑀 M 
m i n { 𝑐 𝑇 𝑥 + 𝑀 1 𝑇 𝑥 𝑎 : 𝐴 𝑥 + 𝑥 𝑎 = 𝑏 ,   𝑥 ≥ 0 ,   𝑠 ≥ 0 } min { c T x + M 1 T x a : A x + x a = b ,   x ≥ 0 ,   s ≥ 0 } 得到原问题的最优解。实现时,并不会赋予 𝑀 M 大 𝑀 M  (big 𝑀 M 
朴素算法的实际效率是指数级的  因为松弛形式总是存在初始基本解,只是未必是可行的,所以,一种简单的寻找初始基本可行解的想法是,从一个不可行的基本解出发,反复利用转轴操作,将不可行的基变量出基,并选择对应行中同样为负的数字对应列的非基变量入基,直到所有基变量都是非负数为止。参考实现如下:
 参考实现   1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
24 
25 
26 // First phase: find an initial BFS. 
// Return false if no feasible solution. 
bool   initialize ()   { 
   while   ( true )   { 
     int   y   =   -1 ; 
     long   double   mi   =   - eps ; 
     for   ( int   j   =   0 ;   j   <   m ;   ++ j )   { 
       if   ( tab [ n ][ j ]   <=   mi )   { 
         y   =   j ; 
         mi   =   tab [ n ][ j ]; 
       } 
     } 
     // No row with a negative basic variable => Feasible. 
     if   ( y   ==   -1 )   break ; 
     int   x   =   -1 ; 
     for   ( int   i   =   0 ;   i   <   n ;   ++ i )   { 
       if   ( tab [ i ][ y ]   <   - eps   &&   ( x   ==   -1   ||   N [ i ]   >   N [ x ]))   { 
         x   =   i ; 
       } 
     } 
     // No column with a negative entry => Infeasible. 
     if   ( x   ==   -1 )   return   false ; 
     pivot ( x ,   y ); 
   } 
   return   true ; 
} 
这样做虽然简单,但是相较于二阶段法,它没有一个描述当前基的不可行程度的目标函数,所以缺乏明确的改进方向。实际测试可以发现,相较于通常只需要 𝑂 ( 𝑚 ) O ( m ) 𝑀 M 𝑂 ( 2 𝑚 ) O ( 2 m ) 𝑛 , 𝑚 n , m 𝑛 , 𝑚   < 5 0 n , m < 50 
转轴规则 转轴时,如果出现多个可选的入基变量或出基变量,就需要用到 转轴规则 (pivot rule)来决定选择哪个变量入基或出基。利用单纯形表,本节讨论的所有规则都能够在 𝑂 ( 𝑚 𝑛 ) O ( m n ) 𝑂 ( 𝑚 𝑛 ) O ( m n ) 
入基变量的选择往往决定了算法终止前转轴的次数。常见的规则如下:
选择最先找到的入基变量; 选择标号最小的入基变量;(Bland 规则的一部分) 选择约化成本绝对值(即 | 𝑐 𝑖 | | c i |  选择单次转轴价值函数改进(即 | 𝑐 𝑖 | 𝜃 𝑖 | c i | θ i  选择对应着最陡的边的入基变量,即沿着边移动单位长度引起的价值函数改进(即 | 𝑐 𝑖 | / ‖ 𝐴 − 1 𝐵 𝐴 𝑖 ‖ | c i | / ‖ A B − 1 A i ‖  随机选择一个入基变量。 实践中,最陡边规则的效率最高2 𝑚 2 m 
出基变量的选择往往决定了算法是否会陷入循环。如果存在多个最优价值相同的基本可行解,那么,算法就有可能一直在这些基本可行解之间循环。这些情形并不常见,因此很多单纯形法的实现并不会指定出基变量的选择规则。常见的避免循环的规则有两种:
Bland 规则效率很低,因为规则本身对入基和出基变量的选择方法是一样的,这很容易造成同一个变量反复入基再出基。相对来说,字典序规则更为实用。字典序规则相当于对线性规划问题中的参数进行微扰
参考实现 本节提供一个基于压缩单纯形表的二阶段单纯形法的参考实现。
Luogu P13337【模板】线性规划   1 
  2 
  3 
  4 
  5 
  6 
  7 
  8 
  9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
 26 
 27 
 28 
 29 
 30 
 31 
 32 
 33 
 34 
 35 
 36 
 37 
 38 
 39 
 40 
 41 
 42 
 43 
 44 
 45 
 46 
 47 
 48 
 49 
 50 
 51 
 52 
 53 
 54 
 55 
 56 
 57 
 58 
 59 
 60 
 61 
 62 
 63 
 64 
 65 
 66 
 67 
 68 
 69 
 70 
 71 
 72 
 73 
 74 
 75 
 76 
 77 
 78 
 79 
 80 
 81 
 82 
 83 
 84 
 85 
 86 
 87 
 88 
 89 
 90 
 91 
 92 
 93 
 94 
 95 
 96 
 97 
 98 
 99 
100 
101 
102 
103 
104 
105 
106 
107 
108 
109 
110 
111 
112 
113 
114 
115 
116 
117 
118 
119 
120 
121 
122 
123 
124 
125 
126 
127 
128 
129 
130 
131 
132 
133 
134 
135 
136 
137 
138 
139 
140 
141 
142 
143 
144 
145 
146 
147 
148 
149 
150 
151 
152 #include   <algorithm> 
#include   <climits> 
#include   <cmath> 
#include   <iomanip> 
#include   <iostream> 
#include   <numeric> 
#include   <vector> 
int   m ,   n ;    // Number of constraints and variables. 
std :: vector < std :: vector < long   double >> 
     tab ;                               // Compressed tableau (transposed) with 
                                      // first-phase objective attached. 
std :: vector < int >   B ,   N ;                 // Basic and nonbasic variables. 
constexpr   long   double   eps   =   1e-12l ;    // Precision. 
// Pivot on (N[x], B[y]). 
void   pivot ( int   x ,   int   y )   { 
   std :: swap ( N [ x ],   B [ y ]); 
   long   double   v   =   -1   /   tab [ x ][ y ]; 
   for   ( int   j   =   0 ;   j   <=   m   +   1 ;   ++ j )   { 
     tab [ x ][ j ]   =   j   ==   y   ?   - v   :   v   *   tab [ x ][ j ]; 
   } 
   for   ( int   i   =   0 ;   i   <=   n ;   ++ i )   { 
     if   ( i   ==   x )   continue ; 
     v   =   tab [ i ][ y ]; 
     tab [ i ][ y ]   =   0 ; 
     for   ( int   j   =   0 ;   j   <=   m   +   1 ;   ++ j )   { 
       tab [ i ][ j ]   +=   v   *   tab [ x ][ j ]; 
     } 
   } 
} 
// First phase: find an initial BFS. 
// Return false if no feasible solution. 
bool   initialize ()   { 
   int   neg_count   =   0 ; 
   for   ( int   j   =   0 ;   j   <   m ;   ++ j )   { 
     if   ( tab [ n ][ j ]   <   - eps )   { 
       for   ( int   i   =   0 ;   i   <=   n ;   ++ i )   { 
         tab [ i ][ m   +   1 ]   +=   tab [ i ][ j ]; 
       } 
       B [ j ]   =   ~ B [ j ]; 
       ++ neg_count ; 
     } 
   } 
   while   ( neg_count )   { 
     int   x   =   -1 ; 
     long   double   mi   =   - eps ; 
     for   ( int   i   =   0 ;   i   <   n ;   ++ i )   { 
       if   ( tab [ i ][ m   +   1 ]   <   mi )   { 
         x   =   i ; 
         mi   =   tab [ i ][ m   +   1 ]; 
       } 
     } 
     if   ( x   ==   -1 )   return   false ; 
     int   y   =   -1 ; 
     mi   =   INFINITY ; 
     for   ( int   j   =   0 ;   j   <   m ;   ++ j )   { 
       if   (( B [ j ]   <   0   &&   tab [ x ][ j ]   <   - eps )   ||   ( B [ j ]   >=   0   &&   tab [ x ][ j ]   >   eps ))   { 
         auto   tmp   =   tab [ n ][ j ]   /   tab [ x ][ j ]   +   ( B [ j ]   <   0   ?   - eps   :   eps ); 
         if   ( tmp   <   mi )   { 
           y   =   j ; 
           mi   =   tmp ; 
         } 
       } 
     } 
     if   ( B [ y ]   <   0 )   { 
       -- neg_count ; 
       B [ y ]   =   ~ B [ y ]; 
       pivot ( x ,   y ); 
       tab [ x ][ m   +   1 ]   +=   1 ; 
     }   else   { 
       pivot ( x ,   y ); 
     } 
   } 
   return   true ; 
} 
// Second phase: find an optimal BFS. 
// Return false if the problem is unbounded. 
bool   simplex ()   { 
   while   ( true )   { 
     int   x   =   -1 ; 
     long   double   mi   =   - eps ; 
     for   ( int   i   =   0 ;   i   <   n ;   ++ i )   { 
       if   ( tab [ i ][ m ]   <   mi )   { 
         x   =   i ; 
         mi   =   tab [ i ][ m ]; 
       } 
     } 
     // No column with a negative reduced cost => Optimal. 
     if   ( x   ==   -1 )   break ; 
     int   y   =   -1 ; 
     mi   =   INFINITY ; 
     for   ( int   j   =   0 ;   j   <   m ;   ++ j )   { 
       if   ( tab [ x ][ j ]   <=   eps )   continue ; 
       if   ( tab [ n ][ j ]   /   tab [ x ][ j ]   <   mi )   { 
         y   =   j ; 
         mi   =   tab [ n ][ j ]   /   tab [ x ][ j ]; 
       } 
     } 
     // No row with a positive ratio => Unbounded. 
     if   ( y   ==   -1 )   return   false ; 
     pivot ( x ,   y ); 
   } 
   return   true ; 
} 
int   solve ()   { 
   B . resize ( m ); 
   N . resize ( n ); 
   std :: iota ( B . begin (),   B . end (),   n ); 
   std :: iota ( N . begin (),   N . end (),   0 ); 
   return   initialize ()   ?   ( simplex ()   ?   0   :   1 )   :   -1 ; 
} 
int   main ()   { 
   std :: ios :: sync_with_stdio ( false ),   std :: cin . tie ( nullptr ); 
   std :: cout   <<   std :: fixed   <<   std :: setprecision ( 8 ); 
   std :: cin   >>   n   >>   m ; 
   tab . assign ( n   +   1 ,   std :: vector < long   double > ( m   +   2 )); 
   for   ( int   i   =   0 ;   i   <   n ;   ++ i )   { 
     std :: cin   >>   tab [ i ][ m ]; 
     tab [ i ][ m ]   =   - tab [ i ][ m ]; 
   } 
   for   ( int   j   =   0 ;   j   <   m ;   ++ j )   { 
     for   ( int   i   =   0 ;   i   <=   n ;   ++ i )   { 
       std :: cin   >>   tab [ i ][ j ]; 
     } 
   } 
   switch   ( solve ())   { 
     case   -1 : 
       std :: cout   <<   "Infeasible"   <<   std :: endl ; 
       break ; 
     case   1 : 
       std :: cout   <<   "Unbounded"   <<   std :: endl ; 
       break ; 
     case   0 :   { 
       std :: cout   <<   tab [ n ][ m ]   <<   std :: endl ; 
       std :: vector < long   double >   x ( n ); 
       for   ( int   j   =   0 ;   j   <   m ;   ++ j )   { 
         if   ( B [ j ]   <   n )   x [ B [ j ]]   =   tab [ n ][ j ]; 
       } 
       for   ( int   i   =   0 ;   i   <   n ;   ++ i )   { 
         std :: cout   <<   x [ i ]   <<   ' ' ; 
       } 
       std :: cout   <<   std :: endl ; 
       break ; 
     } 
   } 
   return   0 ; 
} 
例题 「NOI2008」志愿者招募 总共 𝑛 n 𝑖 i 𝑏 𝑖 b i 𝑚 m 𝑗 j [ 𝑙 𝑗 , 𝑟 𝑗 ] [ l j , r j ] 𝑐 𝑖 c i 
解答  设第 𝑗 j 𝑥 𝑗 x j 
 m a x 𝑥 𝑚 ∑ 𝑗 = 1 𝑐 𝑗 𝑥 𝑗 s u b j e c t   t o   𝑛 ∑ 𝑖 = 1 𝑎 𝑖 𝑗 𝑥 𝑗 ≥ 𝑏 𝑖 ,   𝑖 = 1 , ⋯ , 𝑛 , 𝑥 𝑗 ≥ 0 ,   𝑗 = 1 , ⋯ , 𝑚 . max x ∑ j = 1 m c j x j subject to  ∑ i = 1 n a i j x j ≥ b i ,   i = 1 , ⋯ , n , x j ≥ 0 ,   j = 1 , ⋯ , m . 其中,系数
 𝑎 𝑖 𝑗 = { 1 , 𝑙 𝑗 ≤ 𝑖 ≤ 𝑟 𝑗 , 0 , o t h e r w i s e . a i j = { 1 , l j ≤ i ≤ r j , 0 , otherwise. 原问题没有显然的初始可行解。因此,不妨考虑其 对偶问题 :
 m i n 𝑦 𝑛 ∑ 𝑖 = 1 𝑏 𝑖 𝑦 𝑖 s u b j e c t   t o   𝑛 ∑ 𝑗 = 1 𝑎 𝑖 𝑗 𝑦 𝑖 ≤ 𝑐 𝑗 ,   𝑗 = 1 , ⋯ , 𝑚 , 𝑦 𝑖 ≥ 0 ,   𝑖 = 1 , ⋯ , 𝑛 . min y ∑ i = 1 n b i y i subject to  ∑ j = 1 n a i j y i ≤ c j ,   j = 1 , ⋯ , m , y i ≥ 0 ,   i = 1 , ⋯ , n . 通过添加松弛变量,容易得到一组初始可行解,可以直接略过一阶段,通过单纯形法求解。根据对偶原理,得到的解就是原问题的解。
  1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
24 
25 
26 
27 
28 
29 
30 
31 
32 
33 
34 
35 
36 
37 
38 
39 
40 
41 
42 
43 
44 
45 
46 
47 
48 
49 
50 
51 
52 
53 
54 
55 
56 
57 
58 
59 
60 
61 #include   <cmath> 
#include   <cstring> 
#include   <iostream> 
using   namespace   std ; 
constexpr   int   M   =   10005 ,   N   =   1005 ,   INF   =   1e9 ; 
int   n ,   m ; 
double   a [ M ][ N ],   b [ M ],   c [ N ],   v ; 
void   pivot ( int   l ,   int   e )   {    // 转轴操作函数 
   b [ l ]   /=   a [ l ][ e ]; 
   for   ( int   j   =   1 ;   j   <=   n ;   j ++ ) 
     if   ( j   !=   e )   a [ l ][ j ]   /=   a [ l ][ e ]; 
   a [ l ][ e ]   =   1   /   a [ l ][ e ]; 
   for   ( int   i   =   1 ;   i   <=   m ;   i ++ ) 
     if   ( i   !=   l   &&   fabs ( a [ i ][ e ])   >   0 )   { 
       b [ i ]   -=   a [ i ][ e ]   *   b [ l ]; 
       for   ( int   j   =   1 ;   j   <=   n ;   j ++ ) 
         if   ( j   !=   e )   a [ i ][ j ]   -=   a [ i ][ e ]   *   a [ l ][ j ]; 
       a [ i ][ e ]   =   - a [ i ][ e ]   *   a [ l ][ e ]; 
     } 
   v   +=   c [ e ]   *   b [ l ]; 
   for   ( int   j   =   1 ;   j   <=   n ;   j ++ ) 
     if   ( j   !=   e )   c [ j ]   -=   c [ e ]   *   a [ l ][ j ]; 
   c [ e ]   =   - c [ e ]   *   a [ l ][ e ]; 
   // swap(B[l],N[e]) 
} 
double   simplex ()   { 
   while   ( true )   { 
     int   e   =   0 ,   l   =   0 ; 
     for   ( e   =   1 ;   e   <=   n ;   e ++ ) 
       if   ( c [ e ]   >   ( double ) 0 )   break ; 
     if   ( e   ==   n   +   1 )   return   v ;    // 此时v即为最优解 
     double   mn   =   INF ; 
     for   ( int   i   =   1 ;   i   <=   m ;   i ++ )   { 
       if   ( a [ i ][ e ]   >   ( double ) 0   &&   mn   >   b [ i ]   /   a [ i ][ e ])   { 
         mn   =   b [ i ]   /   a [ i ][ e ];    // 找对这个e限制最紧的l 
         l   =   i ; 
       } 
     } 
     if   ( mn   ==   INF )   return   INF ;    // unbounded 
     pivot ( l ,   e );                  // 转动l,e 
   } 
} 
int   main ()   { 
   cin . tie ( nullptr ) -> sync_with_stdio ( false ); 
   cin   >>   n   >>   m ; 
   for   ( int   i   =   1 ;   i   <=   n ;   i ++ )   cin   >>   c [ i ]; 
   for   ( int   i   =   1 ;   i   <=   m ;   i ++ )   { 
     int   s ,   t ; 
     cin   >>   s   >>   t ; 
     for   ( int   j   =   s ;   j   <=   t ;   j ++ )   a [ i ][ j ]   =   1 ;    // 表示第i种志愿者在j时间可以服务 
     cin   >>   b [ i ]; 
   } 
   cout   <<   ( int )( simplex ()   +   0.5 ); 
} 
习题 参考资料 线性规划之单纯形法【超详解 + 图解】 2016 国家集训队论文 算法导论 Matoušek, Jiří, and Bernd Gärtner. Understanding and using linear programming. Vol. 1. Berlin: Springer, 2007. Inayatullah, Syed, Nasir Touheed, and Muhammad Imtiaz. "A streamlined artificial variable free version of simplex method." PloS one 10, no. 3 (2015): e0116156. Floudas, Christodoulos A., and Panos M. Pardalos, eds. Encyclopedia of optimization. Springer Science & Business Media, 2008. 2025/9/7 21:50:39 ,更新历史 在 GitHub 上编辑此页! StudyingFather , H-J-Granger , Ir1d , countercurrent-time , Enter-tainer , NachtgeistW , Tiphereth-A , AngelKitty , CCXXXI , Early0v0 , MegaOwIer , sshwy , Xeonacid , ZnPdCo , c-forrest , cjsoft , diauweb , ezoixx130 , GekkaSaori , Gesrua , Henry-ZHR , Konano , ksyx , LovelyBuggies , lychees , Makkiy , mgt , minghu6 , orzzzjq , P-Y-Y , PotassiumWings , SamZhangQingChuan , Suyun514 , weiyong1024 , Backl1ght , baker221 , Chrogeek , FFjet , GaisaiYuno , GavinZhengOI , HeRaNO , HomuraCat , iamtwz , ImpleLee , kenlig , kxccc , Marcythm , Molmin , opsiff , ouuan , Peanut-Tang , Raisetsu41 , SukkaW , yusancky CC BY-SA 4.0  和 SATA