天天看點

對偶單純形算法

單純形算法從一個基本可行解出發,朝着目标函數值下降的方向疊代,直到最優。從對偶的角度來看,原問題目标函數下降的方向,就是對偶問題的對偶解可行的方向,當對偶解可行時,目标函數達到最優。

本文介紹對偶單純形法。它的思路是從對偶可行解出發,朝着原問題可行的方向疊代,直到原問題可行,于是得到最優解。

對偶可行解

考慮線性規劃問題:

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≤cB​NTy≤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=[xB​xN​​]=[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~jT​y~​≤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~jT​y~​−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~ij​cj​−a~jT​y~​​ 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~jT​y~​​=(B−1aj​)TBTy=ajT​(B−1)TBTy=ajT​y,​

我們有

δ = 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~ij​cj​−ajT​y​ 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.

繼續閱讀