線性規劃-單純形算法詳解

附轉載鏈接:https://www.hrwhisper.me/introduction-to-simplex-algorithm/

 

本文將詳細的介紹單純形算法,包括但不限於

  • LP問題
  • 單純形算法原理
  • 無界、無解、循環等情況
  • python代碼實現

 

線性規劃問題

首先引入如下的問題:

假設食物的各種營養成分、價格如下表:

Food Energy(能量) Protein(蛋白質) Calcium(鈣) Price
Oatmeal(燕麥) 110 4 2 3
Whole milk(全奶) 160 8 285 9
Cherry pie(草莓派) 420 4 22 20
Pork with beans(豬肉) 260 14 80 19


要求我們買的食物中,至少要有2000的能量,55的蛋白質,800的鈣,怎樣買最省錢?

設買燕麥、全奶、草莓派、豬肉爲x1,x2,x3,x4

於是我們可以寫出如下的不等式組

example_for_introduction_to_linear_programming_formulation

其實這些不等式組就是線性規劃方程(Linear programming formulation):

簡單的說,線性規劃就是在給定限制的情況下,求解目標。

可行域

來看一個算法導論中的例子,考慮如下的線性規劃:

 

maxs.t.x1+x24x1–x22x1+x25x1–2x2x1,x2≤≤≥≥810−20

我們可以畫出下面的圖:

example_for_feasible_region

看圖a,灰色的區域就是這幾個約束條件要求x1,x2所在的區域,而我們最後的解x1,x2也要在這裏面。我們把這個區域稱爲可行域(feasible region)

圖b可以直觀的看出,最優解爲8, 而 x1= 2 , x2=6

 

線性規劃標準形式

線性規劃的標準形式如下:

 

mins.t.AxxcTx≤b≥0

 

就是

  • 求的是min(算法導論的是max,本文爲min)
  • 所有的約束爲<=的形式
  • 所有的變量均 >=0

如何變爲標準形式?

  • 原來是max, 直接*-1求min
  • 若原來約束爲=,轉爲 >= 和<=
  • 約束原來爲 >= 同樣的*-1,就改變了<=
  • 若有變量 xi < 0 ,那麼用 x‘ – x”來替代,其中 x’>=0 x”>=0

 

線性規劃鬆弛形式

鬆弛形式爲:

 

mins.t.AxxcTx=b≥0

就是通過引入變量把原來的 <= ,變爲=的鬆弛形式.

如:

 

x1+x2x1+x2x1,x2≤2≥1≥0

寫爲鬆弛形式就是

 

x1+x2+x3x1+x2+x4x1,x2,x3,x4=2=1≥0

<= vs <

有砸場子的同學會問(╯‵□′)╯︵┻━┻,爲什麼我們的線性規劃的形式都是可以 <= 或者 >=的形式的?把等號去掉可以麼?

就是不可以( ̄ε(# ̄)

舉個例子

 

maxs.t.xx≤1

 

maxs.t.xx<1

顯然第二個是無解的。

 

單純形算法的思想與例子

如何求解線性規劃問題呢?

有一些工具如GLPK,Gurobi 等,不在本文的介紹範圍內。

本文要介紹的是單純形算法,它是求解線性規劃的經典方法,雖然它的執行時間在最壞的情況下是非多項式的(指數時間複雜度),但是,在絕大部分情況下或者說實際運行過程中卻是多項式時間。

它主要就三個步驟

  1. 找到一個初始的基本可行解
  2. 不斷的進行旋轉(pivot)操作
  3. 重複2直到結果不能改進爲止

以下面的線性規劃爲例:

 

mins.t.−x1x1x1x1−+,14x2x23x2x2−++,6x3x3x3x3x3≤≤≤≤≥42360

將其寫爲鬆弛的形式:

 

mins.t.−x1−14x2–6x3x1+x2+x3+x4x1+x5x3++x63x2+x3+x7x1,x2,x3,x4,x5,x6,x7====≥04236

其實,就是等價於(仍然要求 x1,x2,x3,x4,x5,x6,x7 >=0):

 

z=−x1−14x2–6x3x4=4−x1–x2−x3x5=2–x1x6=3–x3x7=6–3x2–x3

在上述的等式的左邊稱爲基本變量,而右邊稱爲非基本變量

現在來考慮基本解就是把等式右邊的所有非基本變量設爲0,然後計算左邊基本變量的值。

這裏,容易得到基本解爲:(x1,x2….x7) = (0,0,0,4,2,3,6),而目標值z = 0,其實就是把基本變量xi設置爲bi。

一般而言,基本解是可行的,我們稱其爲基本可行解。初始的基本解不可行的情況見後面的討論,這裏假設初始的基本解就是基本可行解,因此三個步驟中第一步完成了。

現在開始,來討論上面的第二個步驟,就是旋轉的操作。

我們每次選擇一個在目標函數中的係數爲負的非基本變量xe,然後儘可能的增加xe而不違反約束,並將xe用基本變量xl表示, 然後把xe變爲基本變量,xl變爲非基本變量。

這裏,假設我們選擇增加x1,那麼在上述的等式(不包括目標函數z那行)中,第1個等式限制了x1 <=4(因爲x4>=0),第2個等式有最嚴格的限制,它限制了x1 <=2,因此我們最多隻能將x1增加到2,根據上面的第二個等式,我們有: x1 = 2 – x5,帶入上面的等式就實現了xe和xl的替換:

 

z=−2−14x2–6x3+x5x4=2–x2−x3+x5x1=2–x5x6=3–x3x7=6–3x2–x3

這樣其實就是一個轉動(pivot)的過程,一次轉動選取一個非基本變量(也叫替入變量)xe 和一個基本變量(也叫替出變量) xl ,然後替換二者的角色。執行一次轉動的過程與之前所描述的線性規劃是等價的。

同樣的,將非基本變量設爲0,於是得到:(x1,x2….x7) = (2,0,0,2,0,3,6), Z = -2,說明我們的目標減少到了-2

接下來是單純形算法的第三步,就是不斷的進行轉動,直到無法進行改進爲止,繼續看看剛纔的例子:

我們接着再執行一次轉動,這次我們可以選擇增大x2或者x3,而不能選擇x5,因爲增大x5之後,z也增大,而我們要求的是最小化z。假設選擇了x2,那麼第1個等式限制了x2 <=2 , 第4個等式限制了x2 <= 2,假設我們選擇x4爲替出變量,於是有: x2 = 2 – x3 – x4 + x5 ,帶入得:

 

z=−30+8x3+14x4−13x5x2=2−x3−x4+x5x1=2–x5x6=3–x3x7=2x3+3x4–3x5

此時,我們的基本解變爲(x1,x2….x7) = (2,2,0,0,0,3,0), Z = -30

我們可以繼續的選擇增大x5,第4個等式具有最嚴格的限制(0 – 3x5 >=0),我們有x5 = 2/3 x3 + x4 – 1/3 x7

帶入得

 

z=−30–23x3+x4+133x7x2=2–13x3−13x7x1=2–23x3–x4+13x7x6=3–x3x5=23x3+x4–13x7

此時,我們的基本解變爲(x1,x2….x7) = (2,2,0,0,0,3,0), Z = -30,這時候並沒有增加,但是下一步,我們可以選擇增加 x3。第2個和第3個有最嚴格的限制,我們選第2個的話,得:x3 = 3 – 3/2 x1 – 3/2 x4 + 1/2 x7,然後老樣子,繼續帶入:

 

z=−32+x1+2x4+4x7x2=1+12x1+12x4–12x7x3=3–32x1–32x4+12x7x6=32x1+32x4–12x7x5=2–x1

現在,已經沒有可以繼續增大的值了,停止轉動,z=-32就是我們的解,而此時,基本解爲:(x1,x2….x7) = (0,1,3,0,2,0,0),看看最開始的目標函數:z = -x1 -14x2 – 6x3 ,我們將x2=1,x3=3帶入得,z=-32,說明我們經過一系列的旋轉,最後得到了目標值。

 

退化(Degeneracy)

在旋轉的過程中,可能會存在保持目標值不變的情況,這種現象稱爲退化。比如上面的例子中,兩次等於-30.

可以說退化可能會導致循環(cycling)的情況,這是使得單純形算法不會終止的唯一原因。還好上面的例子中,我們沒有產生循環的情況,再次旋轉,目標值繼續降低。

《算法導論》是這樣介紹退化產生循環的:

Degeneracy can prevent the simplex algorithm from terminating, because it can lead to a phenomenon known as cycling: the slack forms at two different iterations of SIMPLEX are identical. Because of degeneracy, SIMPLEX could choose a sequence of pivot operations that leave the objective value unchanged but repeat a slack form within the sequence. Since SIMPLEX is a deterministic algorithm, if it cycles, then it will cycle through the same series of slack forms forever, never terminating.

如何避免退化?一個方法就是使用Bland規則

在選擇替入變量和替出變量的時候,我們總是選擇滿足條件的下標最小值

  • 替入變量xe:目標條件中,係數爲負數的第一個作爲替入變量
  • 替出變量xl:對所有的約束條件中,選擇對xe約束最緊的第一個

在上面的例子中,我也是這麼做的。^ ^

另一個方法是加入隨機擾動。

 

無界(unbounded)的情況

有的線性規劃問題是無界的,舉個栗子

for example

對於下面的線性規劃

 

mins.t.−−x1–x2x1–x2x1+x2x1,x2≤1≤1≥0

畫出區域爲:

example_for_unbounded_case

顯然可以不斷的增大。讓我們來看看單純形算法是如何應對的:

上述的寫成鬆弛形式爲:

 

mins.t.−−x1–x2x1–x2+x1+x2x1,x2,x3,x4≥0x3+x4=1=1

也就是,

 

zx3x4===−x1–x21–x1+x21+x1–x2

選擇x1 爲替入變量,x3爲替出變量,有:

 

zx1x4===−1–2x2+x31+x2–x32–x3

這時候我們只能選擇x2 爲替入變量,才能使得目標值變小,但是我們發現,對於x2沒有任何的約束,也就是說,x2可以無限大,所以這是沒有邊界的情況。

這個情況是我們有一個替入變量,但是找不到一個替出變量導致的,這時候就是無界的情況了,寫算法的時候注意判斷一下即可。

 

單純形算法的具體實現

說了那麼多,代碼怎麼寫呢?

看一下最開始的線性規劃的問題(已經是鬆弛形式):

 

mins.t.−x1−14x2–6x3x1+x2+x3+x4x1+x5x3++x63x2+x3+x7x1,x2,x3,x4,x5,x6,x7====≥04236

我們可以得到下面的矩陣:

 

C=(−1−14−60000)B=⎛⎝⎜⎜⎜⎜4236⎞⎠⎟⎟⎟⎟A=⎛⎝⎜⎜⎜⎜1100100310111000010000100001⎞⎠⎟⎟⎟⎟

  • 矩陣A:就是約束條件的係數(等號左邊的係數)
  • 矩陣B:就是約束條件的值(等號右邊)
  • 矩陣C:目標函數的係數值

我們將其拼接起來:

 

S1=⎛⎝⎜⎜⎜⎜⎜04236−11100−141003−6101101000001000001000001⎞⎠⎟⎟⎟⎟⎟

左下角爲B,右上角爲C,右下角爲A,那麼左上角呢?我們放的是-z,初始時-z = 0!

將上面那個矩陣和寫成 基本變量 = 非基本變量的形式對比:

 

z=−x1−14x2–6x3x4=4−x1–x2−x3x5=2–x1x6=3–x3x7=6–3x2–x3

我們發現,對於B、C就是一樣的,而A取決於基本變量和非基本變量,非基本變量符號相反,基本變量符號相同。

接着以最開始的線性規劃求解過程的第二步爲例,來看看我們的矩陣是如何進行運算的,第二步我們的結果如下(我們選擇了x1爲替入變量,x5爲替出變量):

 

z=−2−14x2–6x3+x5x4=2–x2−x3+x5x1=2–x5x6=3–x3x7=6–3x2–x3

首先看看約束條件的式子,x1 = 2 – x5 我們改寫成: 2 = x1 + x5 , 因此這行矩陣就是: (b,a1,a2…..a7) = (2,1,0,0,0,1,0,0),其它的類推,注意-z,因此我們的矩陣應該是如下形式的:

 

S2=⎛⎝⎜⎜⎜⎜⎜2223600100−141003−61011010001−11000001000001⎞⎠⎟⎟⎟⎟⎟

OK,那麼S1 如何變成S2的?

首先是第2行,我們是將 x1用x5表示(x1= x5 ),在等式的變換中,就是移項,然後每一個都除以x1的係數。其實用矩陣很簡單,這裏就是mat[2] /= mat[2][1] ,表示矩陣第二行都除以第二行第一個元素

其它行呢?只要有x1的,我們都用x1 = 2 – x5 來表示,就是其它行的x1的係數 * mat[2],然後相減,mat[i]= mat[i] – mat[2] * mat[i][1] ,這樣就實現了約束條件中替入和替出變量的替換!比如第一行,就是mat[1] = mat[1] – mat[2] * 1變成兩行直接相減

現在來看目標函數,對於目標函數,我們也是將x1用 2 – x5來表示,參照上面的思路,同樣的減法:mat[0] = mat[0] – mat[2] * -1 = mat[0] + mat[2] 。注意到我們的其實我們的z = -2,而左上角的爲 2,也就是-z,這就是我們爲啥說左上角是-z的原因。

用矩陣的形式來表示後,可以寫出simplex beta0.99代碼(去除版權信息、空行等,只需要21行!):

 

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

# -*- coding: utf-8 -*-

# @Date    : 2016/11/17

# @Author  : hrwhisper

 

import numpy as np

 

class Simplex(object):

    def __init__(self, obj, max_mode=False):

        self.max_mode = max_mode  # default is solve min LP, if want to solve max lp,should * -1

        self.mat = np.array([[0] + obj]) * (-1 if max_mode else 1)

 

    def add_constraint(self, a, b):

        self.mat = np.vstack([self.mat, [b] + a])

 

    def solve(self):

        m, n = self.mat.shape  # m - 1 is the number slack variables we should add

        temp, B = np.vstack([np.zeros((1, m - 1)), np.eye(m - 1)]), list(range(n - 1, n + m - 1))  # add diagonal array

        mat = self.mat = np.hstack([self.mat, temp])  # combine them!

        while mat[0, 1:].min() < 0:

            col = np.where(mat[0, 1:] < 0)[0][0] + 1  # use Bland's method to avoid degeneracy. use mat[0].argmin() ok?

            row = np.array([mat[i][0] / mat[i][col] if mat[i][col] > 0 else 0x7fffffff for i in

                            range(1, mat.shape[0])]).argmin() + 1  # find the theta index

            if mat[row][col] <= 0: return None  # the theta is ∞, the problem is unbounded

            mat[row] /= mat[row][col]

            ids = np.arange(mat.shape[0]) != row

            mat[ids] -= mat[row] * mat[ids, col:col + 1]  # for each i!= row do: mat[i]= mat[i] - mat[row] * mat[i][col]

            B[row] = col

        return mat[0][0] * (1 if self.max_mode else -1), {B[i]: mat[i, 0] for i in range(1, m) if B[i] < n}


一個調用的例子:

 

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

"""

       minimize -x1 - 14x2 - 6x3

       st

        x1 + x2 + x3 <=4

        x1 <= 2

        x3 <= 3

        3x2 + x3 <= 6

        x1 ,x2 ,x3 >= 0

       answer :-32

    """

t = Simplex([-1, -14, -6])

t.add_constraint([1, 1, 1], 4)

t.add_constraint([1, 0, 0], 2)

t.add_constraint([0, 0, 1], 3)

t.add_constraint([0, 3, 1], 6)

print(t.solve())

print(t.mat)


首先初始化目標函數,然後不斷的使用add_constraint添加約束條件。

 

注意在上面的Simplex類中,我們在初始化中加入了參數max_mode,處理最大值的情況。

然後在16~18行中,我們初始化了最開始的基本變量爲B, 需要鬆弛的變量有m-1個,合併(m-1) *( m-1)的一個對角陣和一行有m-1個0的數組(這是目標函數),然後將他們和原來的合併起來,這樣就構成了我們的S矩陣。

19行判斷是否還有元素可以繼續被增大(就是係數爲負)

20-22行選擇合適的替入和替出變量,若無替出變量,說明原問題無界,我們在23行處理了這種情況。

24~27就是旋轉的過程,進行矩陣的行變換。並用B數組記錄替入的替入變量。

28行我們返回目標值z,若爲最小值,則要*-1,最大值則不用(因爲一開始已經*-1了)。然後最後對應x的解就是基本變量爲對應的bi,非基本變量爲0,注意刪除我們鬆弛添加的變量(所以只要判斷下標是否 < n)

simplex 0.99 beta 就是這麼少的代碼這麼容易的就實現了!

來,跟我一起喊:python 大法好!

 

初始解 ≠ 基本可行解以及無解的情況

在你高呼python大法好的時候,!

但是我把它稱爲beta 0.99版本肯定是有原因的,絕大多數情況下,初始解就是基本可行解,但是也有例外啊!

而且還有無解的情況。(╯‵□′)╯︵┻━┻

栗子

栗子1

栗子1登場:

 

mins.t.x1+2x2x1+x2x1+x2x1,x2≤2≥1≥0

首先轉化爲標準形式(>= 改成 <=, *-1),然後再轉化爲鬆弛形式:

 

mins.t.x1+2x2x3=2–x1–x2x4=−1+x1+x2x1,x2,x3,x4≥0

而我們假設的非基本變量全爲0,於是有:(x1,x2,x3,x4) = (0,0,2,-1),但是x4 = -1是不滿足條件的。即初始解不是基本可行解。

example_for_feasible_solution

栗子2

再比如下面的例子(栗子2):

 

mins.t.x1+2x2x1+x2x1+x2x1,x2≥2≤1≥0

 

其實這個例子就是例子1改變了個符號而已,但是要>=2,然後又要<=1的情況,這個例子顯然是無解的。

example_for_no_feasible_solution

我們來看看初始解的情況,繼續轉化爲標準形式,然後再轉化爲鬆弛形式:

 

mins.t.x1+2x2x3=−2+x1+x2x4=1–x1–x2x1,x2,x3,x4≥0

同樣的,非基本變量全爲0,於是有:(x1,x2,x3,x4) = (0,0,-2,1),但是x3 = -2是不滿足條件的。即初始解不是基本可行解。

simplex beta0.99測試

在上面的兩個例子中,用我們的simplex beta0.99跑有啥結果呢?

第1個栗子,第一個矩陣爲初始的矩陣,接下來是結果和對應的x1,x2值,然後是最後的矩陣

[[ 0. 1. 2. 0. 0.]

[-1. -1. -1. 1. 0.]

[ 2. 1. 1. 0. 1.]]

(-0.0, {})

[[ 0. 1. 2. 0. 0.]

[-1. -1. -1. 1. 0.]

[ 2. 1. 1. 0. 1.]]

可以看到,由於c >=0,直接不迭代了,而這個問題用GLPK計算,正確的結果應該爲:z = 1, x1 = 1

第2個栗子:格式同上,結果如下

[[ 0. 1. 2. 0. 0.]

[-2. -1. -1. 1. 0.]

[ 1. 1. 1. 0. 1.]]

(-0.0, {})

[[ 0. 1. 2. 0. 0.]

[-2. -1. -1. 1. 0.]

[ 1. 1. 1. 0. 1.]]

這個應該是無解的。

初始化

從上面的例子中,simplex beta 0.99 可以說是錯誤的! simplex beta 0.99產生錯誤的原因就是總把初始解當作基本可行解!

拍拍,打臉( ̄ε(# ̄)

那麼如何做纔是正確的呢?

問題回到我們的單純形算法的第一步:找到一個初始的基本可行解。如何找?

我們首先思考上面的問題爲什麼會不可行。原因就是因爲有bi < 0!

因此,對於一個線性規劃問題,有如下的情況:

  • 若所有的bi >=0 ,說明初始的基本解就是基本可行解,在這種情況下,simplex beta 0.99是正確的。
  • 若有bi < 0, 我們需要進行初始化操作,判斷其是否有解(如栗子2),並返回一個基本可行解,然後運行simplex beta 0.99

第一種情況就是之前討論的,這裏討論第二種情況。

以第一個栗子爲例,構造輔助線性規劃(auxiliary linear program)如下:

 

mins.t.x0x1+x2–x0−x1–x2–x0x1,x2,x0≤≤≥02−1

然後求解這個輔助線性規劃Laux,如果Laux的最優解x0爲0的話,說明這個原線性方程組有解

下面是算法導論的證明,它證明的是最大化 x0 和我們最小化x0是一樣的。  

proof-for-auxiliary_lp

把Laux 寫成鬆弛形式:

 

z=x0x3=2–x1–x2+x0x4=−1+x1+x2+x0x1,x2,x3,x4,x0≥0

注意到這個初始解(x1,x2,x3,x4,x0) = (0,0,2,-1,0) 也不是基本可行解。現在馬上就可以看到引入x0的原因了,我們把x0做爲替入變量,選一個b最小的那一行的基本變量作爲替出變量(這裏是x4),進行一次旋轉操作,得:

 

z=1–x1–x2+x4x3=2–x1–x2+x0x0=1–x1–x2+x4

進行旋轉之後,初始解(x1,x2,x3,x4,x0) 變爲 (0,0,2,0,1),這就是因爲x0 的替入 ,使得所有的b >=0

有人可能會問,上面的例子中,只有一個負的,多個負的怎麼辦?還能保證麼?

答案是可以的,因爲我們選擇替出的是bi 爲負的最小的那一行的基本變量,而一開始,我們構建輔助函數時,x0的係數爲-1,因此,旋轉的時候,矩陣運算相當於其它每一行減去這一行,而b爲負,負負得正,必然最後所有的b都>=0。

現在,我們已經有一個基本可行解了,我們求解這個輔助線性規劃即可。

和上面的思想一樣,這裏要麼增大x1, 要麼增大x2,假設選擇x1,然後第二個等式有最嚴格的限制,選擇x0爲替出變量,得 x1 = 1 – x2 + x4 – x0

 

z=x0x3=1–x4+2x0x1=1–x2+x4–x0

此時,基本解爲:(x1,x2,x3,x4,x0)= (1,0,1,0,0), 此時z = x0 = 0,無法繼續增大某個變量使得z繼續減少,因此此時爲最優解,就是z =0,說明原問題有解。

接下來,我們要恢復原問題的目標函數,就是用現在的基本變量替代原目標函數中的基本變量(若x0是基本變量,那就要旋轉去掉它),此外由於x0 = 0,因此可以將其去掉:

 

z===x1+2x21+x2+x4–x01+x2+x4

其它的約束條件同理去掉x0可得:

 

z=1+x2+x4x3=1–x4x1=1–x2+x4

因此,現在,我們通過構造了一個輔助線性規劃Laux 將原來的問題轉化爲上面的線性規劃,並且它的初始解就是基本可行解:(x1,x2,x3,x4) = (1,0,1,0),然後求解這個新的線性規劃即可。

我們很幸運的發現(其實是博主偷懶舉了個簡單的例子(✿◡‿◡)),這裏無法通過增大任何的變量使得目標值變小,因此此時就是結果啦,而(x1,x2,x3,x4) = (1,0,1,0) 就是最後的解,z = 1。

下面總結一下上面的過程,

  1. 若bi都大於等於0 跳到9
  2. 引入x0,創建一個輔助線性規劃 Laux
  3. 將Laux寫成鬆弛形式
  4. 選擇bi最小的那一行的基本變量爲替出變量,x0爲替入變量,進行一次旋轉操作
  5. 求解Laux
  6. 若Laux的最優解爲0,那麼原問題有解,否則無解,return “no answer”
  7. 在有解的情況下,若x0爲基本解,那麼執行一次旋轉,把它變爲非基本變量
  8. 恢復原始的目標函數,但是將其基本變量替換掉
  9. 運行simplex beta 0.99 對新的線性規劃方程求解。

PS:有興趣的讀者可以計算一下例子2,會發現輔助函數的最優解不是0,而是0.5,說明無解

 

完整的單純形算法

結合simplex beta 0.99和初始化的過程,可以寫成如下的simplex 1.0代碼(去除版權信息,空行等,也只要40行左右,還是簡潔^ ^)

 

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

# -*- coding: utf-8 -*-

# @Date    : 2016/11/17

# @Author  : hrwhisper

 

import numpy as np

 

class Simplex(object):

    def __init__(self, obj, max_mode=False):  # default is solve min LP, if want to solve max lp,should * -1

        self.mat, self.max_mode = np.array([[0] + obj]) * (-1 if max_mode else 1), max_mode

 

    def add_constraint(self, a, b):

        self.mat = np.vstack([self.mat, [b] + a])

 

    def _simplex(self, mat, B, m, n):

        while mat[0, 1:].min() < 0:

            col = np.where(mat[0, 1:] < 0)[0][0] + 1  # use Bland's method to avoid degeneracy. use mat[0].argmin() ok?

            row = np.array([mat[i][0] / mat[i][col] if mat[i][col] > 0 else 0x7fffffff for i in

                            range(1, mat.shape[0])]).argmin() + 1  # find the theta index

            if mat[row][col] <= 0: return None  # the theta is ∞, the problem is unbounded

            self._pivot(mat, B, row, col)

        return mat[0][0] * (1 if self.max_mode else -1), {B[i]: mat[i, 0] for i in range(1, m) if B[i] < n}

 

    def _pivot(self, mat, B, row, col):

        mat[row] /= mat[row][col]

        ids = np.arange(mat.shape[0]) != row

        mat[ids] -= mat[row] * mat[ids, col:col + 1]  # for each i!= row do: mat[i]= mat[i] - mat[row] * mat[i][col]

        B[row] = col

 

    def solve(self):

        m, n = self.mat.shape  # m - 1 is the number slack variables we should add

        temp, B = np.vstack([np.zeros((1, m - 1)), np.eye(m - 1)]), list(range(n - 1, n + m - 1))  # add diagonal array

        mat = self.mat = np.hstack([self.mat, temp])  # combine them!

        if mat[1:, 0].min() < 0:  # is the initial basic solution feasible?

            row = mat[1:, 0].argmin() + 1  # find the index of min b

            temp, mat[0] = np.copy(mat[0]), 0  # set first row value to zero, and store the previous value

            mat = np.hstack([mat, np.array([1] + [-1] * (m - 1)).reshape((-1, 1))])

            self._pivot(mat, B, row, mat.shape[1] - 1)

            if self._simplex(mat, B, m, n)[0] != 0: return None  # the problem has no answer

 

            if mat.shape[1] - 1 in B:  # if the x0 in B, we should pivot it.

                self._pivot(mat, B, B.index(mat.shape[1] - 1), np.where(mat[0, 1:] != 0)[0][0] + 1)

            self.mat = np.vstack([temp, mat[1:, :-1]])  # recover the first line

            for i, x in enumerate(B[1:]):

                self.mat[0] -= self.mat[0, x] * self.mat[i + 1]

        return self._simplex(self.mat, B, m, n)

上面的代碼中,將旋轉操作獨立爲一個方法(23~27),將單純形算法的核心也獨立爲一個方法(14~21),這是考慮到要多次調用的原因,並且代碼之前的幾乎沒什麼變化,這裏不做過多的解釋。

主要變化在於solve方法,30~32和之前是一樣的,不解釋 ♪(^ ∇^*)

33行判斷是否有一個b < 0 ?如果有,說明初始解不可行。否則直接執行45行,調用單純形算法

34~44處理的是不可行的情況,

  • 34:首先找一個最小b的下標
  • 35和36作用在於保存原來的目標函數,並將第0行設爲0,然後添加x0 需要拼接矩陣,其實就是構造輔助線性規劃Laux
  • 37執行旋轉操作,使其初始解可行
  • 38行求解Laux 最優值是否爲0,是就是有解,否則無解
  • 40-41行若最後的x0是基本解,找一個第0行不是0的元素作爲替入變量,將x0替出
  • 42~44 恢復初始目標函數,刪除x0那一列,並且替換目標函數中的基本變量。

好了,代碼還是很短,其實能更短,但是會影響可讀性!

再來高呼: Python 大法好!

 

從幾何角度看單純形算法

上面我們介紹單純形算法的時候,是通過最直觀的等式變換(就是旋轉操作)介紹的。

我們知道,線性規劃就是在可行域圍成的多胞形中求解,現在從幾何的視圖來看看單純形算法。

 

只需考慮頂點

讓我再次召喚之前的圖:

example_for_feasible_region

直觀上看,最優解就在頂點上,不需要考慮內部點

一個引入的證明

only_consider_vertex_proof_1

我們假設x(0) 是最優解,連接x(1)和x(0) 與 x(2)和x(3)相交於點x’

我們可以把x(0) 分解,x(0) = λ1 x(1) + (1 – λ1)x’ 其中λ1 = p / (p + q)

同樣的把x‘ 分解,x’ = λ2 x(2) + (1 – λ2)x(3) 其中λ2 = r / (r + s)

因此有:x(0) = λ1 x(1) + (1 – λ1)λ2 x(2) + (1 – λ1) (1 – λ2)x(3),而λ1 + (1 – λ1)λ2 + (1 – λ1) (1 – λ2) = 1

設 cT x(1) 小於等於 cT x(2), cT x(3),因此有:

 

CTx(0)=λ1CTx(1)+(1–λ1)λ2CTx(2)+(1–λ1)(1–λ2)CTx(3)≥λ1CTx(1)+(1–λ1)λ2CTx(1)+(1–λ1)(1–λ2)CTx(1)=CTx(1)

因此,x(1) 並不比x(0) 差。

我們可以推廣到更多的情況。(見附件的68頁)

 

多邊形的頂點等價於矩陣的基

上面提到,最優解一定在頂點上,我們不需要考慮內部的點。

那麼,如何獲得頂點呢?

可以證明,頂點就是基,基就是頂點。(見附件的72-78頁)

我們只需要找到矩陣的基就好了。

 

頂點的遊走

我們知道,多邊形的頂點就是基,且最優解在頂點上,我們需要做的就是,按照一定的規則沿着邊遍歷頂點,直到不能更新了爲止。

如何從一個頂點到另一個頂點?更新到什麼時候爲止?

我們先討論第一個問題。

還是一開始介紹單純形算法的例子:

 

mins.t.−x1−14x2–6x3x1+x2+x3+x4x1+x5x3++x63x2+x3+x7x1,x2,x3,x4,x5,x6,x7====≥04236

其鬆弛條件係數A,目標函數係數C,用矩陣表示爲:

 

C=(−1−14−60000)B=⎛⎝⎜⎜⎜⎜4236⎞⎠⎟⎟⎟⎟A=⎛⎝⎜⎜⎜⎜1100100310111000010000100001⎞⎠⎟⎟⎟⎟

這裏假設我們初始的解X設爲(x1,x2,x3 ,x4 ,x6,x7) = (0,0,0,4,2,3,6).就是我們的初始在(0,0,0)的點上。

我們的初始點用紅點來表示,而綠色的線就是我們下一步走的邊,如下圖所示:
walk_though_vertex

要實現圖中綠色的邊,如何做呢?

其實就是之前旋轉的操作!想想我們之前的旋轉,我們要選擇x1爲替入變量,x5爲替出變量,然後執行旋轉。 (忘記的翻回去看,這裏不贅述)然後就得到新的基本解及其值爲:(x1,x2….x7) = (2,0,0,2,0,3,6)。注意,這時候我們已經到達新的點了!可以說就是沿着那條邊走的!

可以說,設邊的方向爲λ,我們沿着邊走的距離是θ,那麼,我們走的就是x ‘ = x – θλ。

那麼λ是什麼呢?其實就是選擇一個非基的列向量。

爲了說明,我們用a1 ~ a7 來表示矩陣A的對應的列,觀察一下上面的矩陣A,可以看到非基向量可以用基向量表示,比如這裏a1 = 1a4 + 1a5 + 0a6 + 0a7也就是 -a1+ 0a2 + 0a3 + 1a4 + 1a5 + 0a6 + 0a7 = 0

我們的λ 也就等於這個係數,就是λ = (-1,0,0,1,1,0,0)

那麼走多少呢?走過多會超出區域,過少會達不到頂點,答案就是2!想想我們之前選x5的原因:x5最大程度的限制了x1的值, x1 <= 2,於是我們定義θ就是限制最緊的值。換句話說,在S矩陣中,就是bi / x[i][1]最小的值(θ > 0)。

讓我們驗算一下:

(0,0,0,4,2,3,6) – 2 * (-1,0,0,1,1,0,0) = (2,0,0,2,0,3,6) ! (o゜▽゜)o☆[BINGO!] 和之前的完全一樣!

我們做一次旋轉的操作,其實就是一個頂點到另一個頂點的過程!很神奇吧!

仔細思考一下爲什麼之前的旋轉等價於這裏的非基向量表示的邊?

我們用原來的基向量(a4,a5,a6,a7) 來表示a1,其實可以換個角度想想之前的等式變換,我們在這裏表示a1 可以認爲是之前的將每行有x1帶入的過程。a1上爲1,說明這一行有x1,我們需要帶入。

 

停止條件

現在,我們已經在頂點上,然後沿着邊遊走了,那麼,我們遊走到什麼時候爲止呢?

注意,我們的目標是最小化目標函數,即求min CTx

假設我們從x沿着邊到達x’,我們有: x’ = x – θλ,我們的λ形式爲:[λ1,λ2,…..,-1,…..λm]那麼目標函數的值增加了: CTx’ – CTx = – θCTλ = -θ(-ce + Σλici ) = θ(ce – Σλici ) 其中,最後的求和符號是對e替入前的基向量求和的。由於θ >0 ,因此ce – Σλici >=0 說明我們可以停止了(x比x’不差)。我們把ce – Σλici 稱爲檢驗數(checking number)

我們把檢驗數寫成矩陣的形式就是:CT – CT_B B-1A (CT_B爲基向量的C的轉置)

下面證明對於頂點x,若檢驗數 CT – CT_B B-1A >=0 ,則x爲最優解。

設y 爲其它任意的可行解, 於是有Ay = b, y>=0

CT y>= CT_B B-1Ay = CT_B B-1b = CT_B XB = CTx.

就是說,其它的可行解y不比x好

注意我們旋轉的過程中,CT_B = 0,或者說 Σλici = 0 因此,若CN (非基的那些) 都 >=0 ,就可以停止了。這和之前的其實還是一樣的。

 

小結

用幾何的角度看待單純形算法,主要有幾點:

  1. 最優解可以在頂點上找到,不許考慮內部點
  2. 頂點 <=> 矩陣的基
  3. 一次旋轉就是一個頂點沿着一條邊λ走θ倍到另一個頂點的過程
  4. 當我們的檢驗數 >=0 停止迭代

當然也需要注意初始化單純形算法,比如之前的情況:

example_for_feasible_solution

我們的頂點要在可行域才行,而不要跑到(0,0)去了。初始方法和之前的一樣。

 

時間複雜度

現在,來討論一下單純形算法的時間複雜度吧。

在之前的算法中,我們每一次查找一個 負數cx需要O(N),(這裏用N爲了區分代碼中的N,這裏N = m + n)並且查找最小的θ所在的row需要O(m),然後執行旋轉,在旋轉中,我們對於i!=row的執行mat[i]= mat[i] – mat[row] * mat[i][col],需要O(m*N)

因此一次的複雜度爲O(m*N)

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

def _simplex(self, mat, B, m, n):

    while mat[0, 1:].min() < 0:

        col = np.where(mat[0, 1:] < 0)[0][0] + 1  # use Bland's method to avoid degeneracy. use mat[0].argmin() ok?

        row = np.array([mat[i][0] / mat[i][col] if mat[i][col] > 0 else 0x7fffffff for i in

                        range(1, mat.shape[0])]).argmin() + 1  # find the theta index

        if mat[row][col] <= 0: return None  # the theta is ∞, the problem is unbounded

        self._pivot(mat, B, row, col)

    return mat[0][0] * (1 if self.max_mode else -1), {B[i]: mat[i, 0] for i in range(1, m) if B[i] < n}

 

def _pivot(self, mat, B, row, col):

    mat[row] /= mat[row][col]

    ids = np.arange(mat.shape[0]) != row

    mat[ids] -= mat[row] * mat[ids, col:col + 1]  # for each i!= row do: mat[i]= mat[i] - mat[row] * mat[i][col]

    B[row] = col

那麼執行多少次呢?假設爲k次就是O(kmN)

在絕大多數的情況下,單純形算法也都是多項式時間的算法,自從1949年單純形算法提出後,人們也一度的以爲它就是多項式時間的,直到有人出來挑事情。。。。(╯‵□′)╯︵┻━┻

V. Klee and G. L. Minty[1972] 構造了一個例子:

 

maxs.t.xnδδxi−1≤≤xixixi≤≤≥11−δxi−10fori=1…nfori=2…nfori=1…n

在這個例子中,單純形算法將會遍歷2n個頂點。這個例子提出,說明單純形算法不是一個多項式時間複雜度的算法。但是爲什麼它實際運行時是多項式時間複雜度的?這個問題困擾了人們很久,直到2001年 Daniel A. Spielman 和 Shang-Hua Teng 提出了平滑型複雜度理論(smoothed complexity),完美的解決了這個問題。

  • Average-case analysis was first introduced to overcome the limitations of worst-case analysis, however the difficulty is saying what an average case is. The actual inputs and distribution of inputs may be different in practice from the assumptions made during the analysis.
  • Smoothed analysis is a hybrid of worst-case and average-case analyses that inherits advantages of both, by measuring the expected performance of algorithms under slight random perturbations of worst-case inputs.
  • The performance of an algorithm is measured in terms of both the input size, and the magnitude of the perturbations.
  • If the smoothed complexity of an algorithm is low, then it is unlikely that the algorithm will take long time to solve practical instances whose data are subject to slight noises and imprecisions.

可能原值會是非多項式時間的,但是在真實世界中,基本都是真實數據+噪聲的值,或者還要加上誤差,因此單純形算法“因禍得福”,一般爲多項式時間的。不同於大家做信號處理或者圖像處理時,將討厭的噪聲去掉,滕老師說:“噪聲是個好東西”。

 

小結

給定一個線性規劃L,就只有如下三種情形:

  1. 有一個有限目標值的最優解
  2. 不可行
  3. 無界

在本文中,我們對其三種情況都進行了討論,如果有啥疑問或錯誤歡迎提出。 ^ ^

單純形算法本身並不難,老師上課講的是幾何的角度,聽得我一愣一愣的,之後看算法導論(就是最開始的等式變換),通熟易懂,但矩陣還是跟着老師的思路寫的,然後對照兩者的思路發現略有不同,讓我糾結不已,覺得有必要整理一下~現在看來,其實這些方法都殊途同歸。

然後,寫這個blog累死了,用typora打latex,然後blog wordpress再轉成圖片(現在用mathjax直接支持latex公式拉),看了一下字數9000左右了 好累/(ㄒoㄒ)/~~

所以覺得好的話可以進行打賞 (逃

 

參考資料

  • 《算法導論》第三版 第29章 線性規劃
  • 國科大計算機算法分析與設計 – Dongbo Bu 
    • 線性規劃課件 下載地址:Lec8.pdf

     

 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章