单纯形算法从一个基本可行解出发,朝着目标函数值下降的方向迭代,直到最优。从对偶的角度来看,原问题目标函数下降的方向,就是对偶问题的对偶解可行的方向,当对偶解可行时,目标函数达到最优。
本文介绍对偶单纯形法。它的思路是从对偶可行解出发,朝着原问题可行的方向迭代,直到原问题可行,于是得到最优解。
考虑线性规划问题:
min c T x s.t. A x = b x ≥ 0 \begin{aligned} \min~ & c^T x \\ \text{s.t.}~ & Ax=b\\ & x\geq 0 \end{aligned} min s.t. cTxAx=bx≥0
和它的对偶问题:
max b T y s.t. A T y ≤ c \begin{aligned} \max~ & b^T y \\ \text{s.t.}~ & A^T y \leq c \end{aligned} max s.t. bTyATy≤c
其中 c , x ∈ R n c, x \in \mathbb{R}^n c,x∈Rn, y ∈ R m y\in\mathbb{R}^m y∈Rm, A ∈ R m × n A\in\mathbb{R}^{m\times n} A∈Rm×n, b ∈ R m ≥ 0 b\in \mathbb{R}^m \geq \mathbf{0} b∈Rm≥0。
令 B , N B, N B,N 分别代表基矩阵和非基矩阵。对偶问题可以写成如下形式:
max b T y s.t. B T y ≤ c B N T y ≤ c N \begin{aligned} \max~ & b^T y \\ \text{s.t.}~ & B^T y \leq c_B\\ & N^T y \leq c_N \end{aligned} max s.t. bTyBTy≤cBNTy≤cN
回顾原问题的基本解 x x x,称为 原始解,是这样的形式:
x = [ x B x N ] = [ B − 1 b 0 ] . x = \begin{bmatrix} x_B\\ x_N \end{bmatrix} = \begin{bmatrix} B^{-1}b\\ \mathbf{0} \end{bmatrix}. x=[xBxN]=[B−1b0].
当 B − 1 b ≥ 0 B^{-1}b \geq 0 B−1b≥0 时, x ≥ 0 x \geq 0 x≥0,它就是基本可行解。
再看对偶问题,它的基本解 y y y,称为 对偶解,是这样的形式:
y = ( B T ) − 1 c B . y = (B^T)^{-1}c_B. y=(BT)−1cB.
检查对偶问题的两个约束:第一个 B T y 0 ≤ c B B^Ty_0\leq c_B BTy0≤cB 自然成立;第二个约束 c N − N T y ≥ 0 c_N - N^T y \geq 0 cN−NTy≥0,即
c N − N T y = c N − N T ( B T ) − 1 c B = c N − N T ( B − 1 ) T c B = c N − ( B − 1 N ) T c B ≥ 0. \begin{aligned} c_N - N^T y &= c_N - N^T (B^T)^{-1} c_B\\ & = c_N - N^T (B^{-1})^T c_B\\ &= c_N - (B^{-1}N)^T c_B \\ & \geq 0. \end{aligned} cN−NTy=cN−NT(BT)−1cB=cN−NT(B−1)TcB=cN−(B−1N)TcB≥0.
令 μ : = c N − ( B − 1 N ) T c B \mu := c_N - (B^{-1}N)^T c_B μ:=cN−(B−1N)TcB,如果 μ ≥ 0 \mu \geq 0 μ≥0,那么 y y y 就是对偶问题的基本可行解。
回顾单纯形算法, μ \mu μ 实际上是原问题的 Reduced Cost,即原问题目标函数关于 x N x_N xN 的导数。因此,对偶可行意味着原始“最优”,即原问题的目标函数值关于 x N x_N xN 无法降低。如果原始解 x x x 也是可行的,即 B − 1 b ≥ 0 B^{-1}b \geq 0 B−1b≥0,那么 x x x 和 y y y 分别为原问题和对偶问题的最优解。
如何找到初始的对偶可行解?本文不做介绍,详情见文末的参考文献[1]。
对偶可行保证原始解的最优性,但可能损失可行性。迭代的思路就让原始解满足最优性的同时,也变得可行。
已知基矩阵 B B B,我们介绍出入基的规则。
已知对偶可行解 y = ( B T ) − 1 c B y = (B^T)^{-1}c_B y=(BT)−1cB。令 b ~ : = B − 1 b \tilde{b}:= B^{-1}b b~:=B−1b,如果 b ~ ≥ 0 \tilde{b}\geq 0 b~≥0,那么原始解可行, x x x 和 y y y 即为原问题和对偶问题的最优解。
否则存在分量 b ~ i < 0 \tilde{b}_i < 0 b~i<0 。为了让 x x x 变得可行,自然的想法是让 B B B 的第 i i i 行对应的变量 x B i x_{B_i} xBi 出基,所以 x B i x_{B_i} xBi 是出基变量。
那么入基变量如何计算?
先把对偶问题换一种写法。令 y ~ = B T y \tilde{y} = B^T y y~=BTy,于是 y = ( B T ) − 1 y ~ y=(B^T)^{-1}\tilde{y} y=(BT)−1y~。代入对偶问题,得到下面的等价形式:
max b ~ T y ~ s.t. y ~ ≤ c B ( B − 1 N ) T y ~ ≤ c N \begin{aligned} \max~ & \tilde{b}^T \tilde{y} \\ \text{s.t.}~ & \tilde{y} \leq c_B\\ & (B^{-1}N)^T \tilde{y} \leq c_N \end{aligned} max s.t. b~Ty~y~≤cB(B−1N)Ty~≤cN
前面假设 b ~ i < 0 \tilde{b}_i < 0 b~i<0,那么减少 y ~ i \tilde{y}_i y~i 可以增加目标函数值,同时需要保证
( B − 1 N ) T y ~ ≤ c N . (B^{-1}N)^T \tilde{y} \leq c_N. (B−1N)Ty~≤cN.
令 J J J 代表非基变量的下标。 ∀ j ∈ J \forall j\in J ∀j∈J,令 a ~ j = B − 1 a j \tilde{a}_j = B^{-1}a_j a~j=B−1aj,其中 a j a_j aj 代表 A A A 的第 j j j 列。上面的约束条件可以写成:
a ~ j T y ~ ≤ c j , ∀ j ∈ J . \tilde{a}_j^T \tilde{y} \leq c_j,\quad \forall j\in J. a~jTy~≤cj,∀j∈J.
令 y ~ i \tilde{y}_i y~i 减小 δ \delta δ,其中 δ > 0 \delta > 0 δ>0,我们有
a ~ j T y ~ − a ~ i j δ ≤ c j , ∀ j ∈ J . \tilde{a}_j^T \tilde{y} - \tilde{a}_{ij}\delta \leq c_j,\quad \forall j\in J. a~jTy~−a~ijδ≤cj,∀j∈J.
如果 a ~ i j > 0 \tilde{a}_{ij} > 0 a~ij>0, ∀ j ∈ J \forall j\in J ∀j∈J,那么 δ \delta δ 可以为无穷大,对偶问题的最优目标函数值为 + ∞ +\infty +∞,那么原问题无可行解。
假设存在 j ∈ J j\in J j∈J,使得 a ~ i j < 0 \tilde{a}_{ij} < 0 a~ij<0。为了满足约束条件,我们有
δ : = min { c j − a ~ j T y ~ − a ~ i j and a ~ i j < 0 , ∀ j ∈ J } . \delta := \min \left\{\frac{c_j - \tilde{a}^T_j \tilde{y}}{-\tilde{a}_{ij}} \text{ and } \tilde{a}_{ij} < 0, ~\forall j\in J\right\}. δ:=min{−a~ijcj−a~jTy~ and a~ij<0, ∀j∈J}.
注意到
a ~ j T y ~ = ( B − 1 a j ) T B T y = a j T ( B − 1 ) T B T y = a j T y , \begin{aligned} \tilde{a}^T_j \tilde{y} & = (B^{-1}a_j)^T B^Ty \\ & = a_j^T (B^{-1})^TB^T y\\ & = a^T_j y, \end{aligned} a~jTy~=(B−1aj)TBTy=ajT(B−1)TBTy=ajTy,
我们有
δ = min { c j − a j T y − a ~ i j and a ~ i j < 0 , ∀ j ∈ J } . \delta = \min \left\{\frac{c_j - a^T_j y}{-\tilde{a}_{ij}} \text{ and } \tilde{a}_{ij} < 0, ~\forall j\in J\right\}. δ=min{−a~ijcj−ajTy and a~ij<0, ∀j∈J}.
上式称为 Minimum Ratio Test,取得最小值对应的下标 j j j 即为入基变量 x j x_j xj 的下标。
第0步:输入对偶可行的基矩阵 B B B。
第1步:判断原始解是否可行。如果 b ~ ≥ 0 \tilde{b} \geq 0 b~≥0,当前是最优解,算法停止。
第2步:计算入基变量和出基变量。如果存在 b ~ i < 0 \tilde{b}_i < 0 b~i<0,那么 x i x_i xi 是出基变量。如果存在 j ∈ J j \in J j∈J 使得 a ~ i j < 0 \tilde{a}_{ij} < 0 a~ij<0,根据 Minimum Ratio Test, 找到入基变量 x j x_j xj。
第3步:判断问题是否无界。如果 a ~ i j ≥ 0 \tilde{a}_{ij} \geq 0 a~ij≥0, ∀ i \forall i ∀i,则对偶问题无界,原问题无可行解,算法停止。
第4步:执行出入基操作,更新基矩阵 B B B,然后执行第1步。
下面我们用Python来实现对偶单纯形算法。
先定义算法的输入和输出。
class DualSimplex(object):
"""
对偶单纯形算法(基本版)。
Note:
1、系数矩阵满秩。
2、未处理退化情形。
3、输入对偶可行解(对应的列)。
"""
def __init__(self, c, A, b, v1):
"""
:param c: n * 1 vector
:param A: m * n matrix
:param b: m * 1 vector
:param v1: dual feasible solution, list of column indices
"""
# 输入
self._c = np.array(c)
self._A = np.array(A)
self._b = np.array(b)
self._basic_vars = v0
self._m = len(A)
self._n = len(c)
self._non_basic_vars = self._init_non_basic_vars()
# 辅助变量
self._iter_num = 0
self._B_inv = None
self._N_bar = None # N_bar = B^{-1}N
self._y = None # dual solution (i.e., shadow price)
# 输出
self._sol = None # primal solution
self._obj_dual = None # dual objective
self._status = None
接下来要实现对偶单纯形算法 DualSimplex.solve()
,思路如下。
class DualSimplex(object):
# ...
# 其它函数省略……
def solve(self):
self._iter_num = 0 # 记录迭代次数
self._check_init_solution() # 检查初始的对偶解是否可行
self._update_solutions()
self._update_obj()
self._print_info()
while not self._is_optimal(): # 判断是否最优或者不可行
if self._status == "INFEASIBLE":
break
self._pivot() # 迭代(选主元入基,执行Minimum Ratio Test,然后出基)
self._update_solutions()
self._update_obj()
self._iter_num += 1
if self._status != 'INFEASIBLE':
self._status = 'OPTIMAL'
上面的关键步骤是实现每次迭代的出入基操作,即 SimplexA._pivot()
。
class DualSimplex(object):
# ...
# 其它函数省略……
def _pivot(self):
# 出基变量 x_i
i = int(np.argmin(self._sol))
# 出基变量 x_i 在 self._basic_vars 中的index
i_ind = None
for k in range(self._m):
if self._basic_vars[k] == i:
i_ind = k
break
# 判断原问题是否可行
# 对偶问题无界,则原问题不可行
if not self._check_feasibility(i_ind):
self._status = 'INFEASIBLE'
return
# 入基变量 x_j 在 self._basic_vars 中的index
j_ind = self._minimum_ratio_test(i_ind)
# 入基变量 x_j
j = self._non_basic_vars[j_ind]
# update basic vars
for k in range(self._m):
if self._basic_vars[k] == i:
self._basic_vars[k] = j
break
# update non basic vars
self._non_basic_vars[j_ind] = i
def _check_feasibility(self, row_ind):
N = np.array([self._A[:, j] for j in self._non_basic_vars]).transpose()
self._N_bar = self._B_inv @ N
for x in self._N_bar[row_ind]:
if x < 0:
return True
return False
def _minimum_ratio_test(self, ind_out):
N = np.array([self._A[:, j] for j in self._non_basic_vars]).transpose()
c_N = np.array([self._c[j] for j in self._non_basic_vars])
c_bar = c_N - self._y @ N
self._N_bar = self._B_inv @ N
a_bar = self._N_bar[ind_out] * -1
ratios = list(map(lambda c, a: c/a if a > 0 else np.infty, c_bar, a_bar))
return int(np.argmin(ratios))
完整代码
本文介绍了对偶单纯形法,但是还有两个问题没有解决:一个是对偶可行解的初始化;另一个处理退化情形。解决思路与单纯形法的处理思路类似,本文不做介绍,详情可以看下面的参考文献[1]。
我们已经知道了单纯形算法,为什么还需要对偶单纯形算法?它有什么样的应用场景?
具体来说,有如下两点:
1、对偶单纯形法在实际中表现不错。不仅如此,原问题如果退化,其对偶问题可能是正常的。
2、对偶单纯形法可应用于整数规划的求解。分支定界是求解整数规划问题的经典方法,每个分支需要求解一个线性规划子问题。子问题相比原来问题,增加了新的约束,一般导致原始解不可行,却不会违背对偶可行。于是可以用之前的对偶解作为子问题的初始可行解。
[1] Mihai Banciu. Dual Simplex (lecture notes). Bucknell University, 2010.
[2] David P. Williamson. ORIE 6300 Mathematical Programming I (lecture notes). 2014.