线性规划之单纯形算法矩阵描述与python实现
声明
本文为本人原创,转载请注明出处。本文仅发表在博客园,作者LightningStar。
问题描述
所有的线性规划问题都可以归约到标准型的问题,规约过程比较简单且已经超出本文范围,不再描述,可以参考拓展阅读部分。下面直接给出线性规划标准型描述。
标准型描述
线性规划问题标准型的矩阵描述[1]:
目标:
约束:
我们的最终目标在约束条件下就是找到一个解\(\mathbf{x}\),使得\(z\)最大。注意我们的描述,我们的解是找到一组值\(\mathbf{x}\)使得\(z\)最大,这组值才是问题的一个解,\(z\)得最大值究竟是多少并不是问题的解。
在后文中,粗体小写字母一般表示向量,粗体大写字母一般表示矩阵,小写字母表示标量。
松弛型
在用单纯型法求解线性规划问题之前,必须先把线性规划问题转换成增广矩阵形式。增广矩阵形式引入非负松弛变量将不等式约束变成等式约束。
引入松弛变量\(\mathbf{\hat{x}} = \mathbf{b} - \mathbf{Ax}\),则有:
将上述公式表示为矩阵形式则为:
我们引入的松弛变量\(\mathbf{\hat{x}}\)又称基本变量,\(\mathbf{x}\)又称非基本变量。
单纯型算法
一个例子
为便于理解和描述,我们通过一个例子来讲解迭代过程:
划为矩阵表示为:
令$\mathbf{y}=[x_1, x_2, x_3, \hat{x_1}, \hat{x_2}, \hat{x_3}]^T, \mathbf{d}=[\mathbf{c}, 0]^T, z = \mathbf{d^Ty}, \mathbf{\hat{A}=[A, I]} $,则有:
为方便求解\(z\)的最大值,我们可以设计如下的增广矩阵[2],通过对增广矩阵的迭代计算可以得到\(z\)的最大值:迭代结束时增广矩阵右上角的值的相反数。
下面开始对增广矩阵进行迭代:
-
原线性规划问题的一个初始解是\(\mathbf{x=0}, \mathbf{\hat{x}=b}\),即\(\mathbf{y_0} = [0, 0, 0, 30, 24, 36]^T\),初始\(\mathbf{d_0}=[3, 1, 2, 0, 0, 0]^T\),\(z=\mathbf{d_0^T}\mathbf{y_0}=0\)
-
由\(\mathbf{d}\)可知,\(y_0\)的收益最大,因此选择增大\(y_0\)以获取更大收益。判断依据是\(max(\mathbf{d}) == d_0\),并且\(d_0 = 3 \gt 0\)
-
下面判断\(y_0\)最大可以是多少。取\(\mathbf{b} ./ \hat{A}[:,0] = [30, 12, 9]\)中的最小正整数,即\(y_0 = 9\)
-
依据高斯消元法[3],将增广矩阵第4行作为基础向量,将第4行作为基础向量的依据是\(\mathbf{b} ./ \hat{A}[:,0]\)的最小值就在增广矩阵的第4行。将增广矩阵中其他行的\(y_0\)的系数化为0,结果为
-
下面开始新一轮的迭代过程,\(max(\mathbf{d}) == d_2\),并且\(d_2 = 0.5 \gt 0\),因此选择增大\(y_2\)
-
取\(\mathbf{b} ./ \hat{A}[:,2] = [8.4, 1.5, 18]\)中的最小正整数,即\(y_2 = 1.5\)
-
取增广矩阵的第3行作为基本向量,对增广矩阵运用高斯消元法将\(y_2\)的其他行的系数划为0得
-
下面开始新一轮的迭代过程,\(max(\mathbf{d}) == d_1\),并且\(d_1 = 0.0625 \gt 0\),因此选择增大\(y_1\)
-
取\(\mathbf{b} ./ \hat{A}[:,1] = [-92, 4, 132]\)中的最小正整数,即\(y_1 = 4\)
-
取增广矩阵的第3行作为基本向量,对增广矩阵运用高斯消元法得
- \(max(\mathbf{d}) == d_1\),并且\(d_0 = 0\),因此迭代结束。最大值\(z=28\)(增广矩阵右上角的值的相反数)
到目前为止,我们已经求得了标准型问题中\(z\)的最大值,但是还没有给出一个解。我们仅仅知道如何求出\(z\)的最大值,但是什么样的\(\mathbf{x}\)会使得\(z\)取得最大值呢?这比知道\(z\)的最大值更重要。
现在观察一下我们已知的一些信息,已知\(z=28\),已知\(y_1 = 4\)。在迭代过程中我们似乎也求得了\(y_0 = 9\)和\(y_2=1.5\),但是实际上这是不对的。因为只有最后一次迭代的结果是准确的,而在迭代过程中得到的只是中间结果,因此我们只知道\(z=28, y_1 = 4\)。另外还有增广矩阵。在本文开头我们有公式:
现在我们将已知的值带入上述公式,得到:
通过解方程(本文不涉及如何解方程)可以得到一个可行解为:
又已知原始\(\mathbf{c} = [3, 1, 2]^T\),得\(z = \mathbf{c^T} \mathbf{x} = 28\)。
算法过程
问题描述
为了防止读者忘记我们要解决的问题,这里再啰嗦一下,我们要解决的是线性规划问题,并将所有的线性规划问题都归约到标准型上。因此最终问题变成了对标准型的求解。在上文中我们已经通过了一个例子来介绍如何单纯形算法的演算过程,并如何通过迭代的结果求得一个解。下面我们来将这个过程用算法的形式表示出来,但是这个算法仅包含迭代过程,至于如何通过迭代出来的结果求得解,则不是本文关心的内容。
算法的输入与输出
这里我们来搞清楚算法的输入和输出。我们在问题中已知的是\(\mathbf{c^T}\)和矩阵\(\mathbf{A}\),以及\(\mathbf{b}\)。因此这些已知值就是算法的输入。而算法的输出则是迭代的最后结果\(z\)的值和\((i, x_i)\)。\((i, x_i)\)是一个元组,其中\(i\)是\(\mathbf{x}\)中的第\(i\)个元素,而\(x_i\)是\(\mathbf{x}\)中的第\(i\)个元素的值(下标从0开始索引)。
算法python实现
python的代码可以当作伪码去阅读,这里直接给出python的实现过程。
def solve(c, A, b):
NUM_NON_BASIC_VARIABLES = c.shape[0]
NUM_BASIC_VARIABLES = b.shape[0]
# z = -d[-1]
d = np.hstack((c, np.zeros(NUM_BASIC_VARIABLES + 1)))
# 初始化增广矩阵
_A = np.hstack((A, np.identity(NUM_BASIC_VARIABLES)))
A_hat = np.c_[_A, b]
_step = 0
last_update_x_inx = -1
last_update_x = 0
while True:
i = np.nanargmax(d[:-1])
if d[i] <= 0:
break
# 利用高斯消元法求解
_res = A_hat[:, -1] / A_hat[:, i]
# 将忽略 divided by zero 的结果,系数小于等于0的也不能考虑在内
j = np.where(_res > 0, _res, np.inf).argmin()
if _res[j] <= 0: # 系数小于等于0的会违反了 >= 0 的基本约束条件
break
last_update_x_inx = i
last_update_x = _res[j]
# 下面计算y中除了y[i]之外的值
# 1.运用高斯消元法
A_hat[j, :] = A_hat[j, :] / A_hat[j, i] # A_hat[j,i] = 1
# for _row in range(A_hat.shape[0]):
# if _row != j:
# A_hat[_row,:] = A_hat[_row,:] - A_hat[_row,i] * A_hat[j,:]
# 下面四行等价于上述的for循环
_tmp = np.copy(A_hat[j, :])
_A = np.outer(A_hat[:, i], _tmp) # 列向量乘以行向量
A_hat -= _A
A_hat[j, :] = _tmp
d = d - d[i] * A_hat[j, :]
# 打印中间过程
_step += 1
# print('step:', _step)
# print('d = ', d)
# print('A_hat = ', A_hat)
# print('z = ', -d[-1])
z = -d[-1]
if last_update_x_inx == -1:
return None
return (z, (last_update_x_inx, last_update_x)) # return z