单纯形法求解简易线性规划初步(C++实现)(一)

最近诸事不顺,夜夜难眠。上午在朋友圈里面看到这样一段话


……
故事的最后
讨厌英语的人出国留学,热爱国学的人学习金融
不愿远行的人去了西北东南,志在四方的人留在了省内
亲昵的情侣异地而恋,内向的女生走进师范
爱好止步爱好,愿意止步愿意,美好的憧憬晒干在烈日下,再也爬不上现实的岸
……
我们都成了体面的懦夫

线性规划初步

发了一顿牢骚接着开始正题
上个月折腾了一阵线性方程组的LUP分解求解,这几天又研究了一下线性规划(LP,Linear Programming),线性规划实际上用到的还是非常多的,从一般的理论数学到工程管理运筹学再到某些电路设计的前期优化,这些问题的约束条件都非常复杂。对于一个只有两个变量的线性规划图解法当然是非常不错的,而两个以上变量求解就不是非常容易了。在某些情况下三个变量确实可以利用空间坐标系解决,但是面与面的重叠相交这是要几核的大脑才能想得清楚啊T^T,四个变量有哪个大神可以画出来?这些复杂的问题需要一种巧妙地算法完成计算,这个就是这篇文章讲到地单纯形法。

1.从任意线性规划到标准型

我们熟知线性规划有两部分构成,一个是目标函数,另一个是约束条件,例如

m i n : 3 x 1 + 7 x 2 { x 1 + x 2 = 11 x 1 4 x 2 3 x 1 0

这个线性规划中的约束条件包含一个等式两个不等式,其中对变量的约束只有一个不等式,对于这种样子的线性规划我们称其非标准型。
标准型与非标准型的判定非常简单:

  1. 目标函数必须最大化
  2. 约束条件必须为不等式约束,并且符号为小于等于号
  3. 变量必须为非负约束

根据以上三点我们可以对一个标准型有一个大概的印象,标准型肯定是长这样的

m a x : c 1 x 1 + c 2 x 2 . . . c n x n { a 11 x 1 + a 12 x 2 . . . a 1 n x n b 1 a 21 x 1 + a 22 x 2 . . . a 2 n x n b 2 a m 1 x 1 + a m 2 x 2 . . . a m n x n b m x 1 , x 2 . . . x n 0

嗯这样看起来很复杂,我们把它写成矩阵向量式,我们定义一个n位的列向量c为目标函数的系数向量,一个n*m的矩阵A为约束条件的系数矩阵,一个m维的列向量b为不等号后面那个常数,还有一个描述x地n维向量。于是我们可以非常容易地表达这个标准型

m a x : c T x A x b x 0

是不是很符合极简主义的理念
回到上面那个例子,我们看到这个例子非常糟糕几乎没有一点符合,我们需要一点一点把它修改为标准型:
对于目标函数 m i n : 3 x 1 + 7 x 2 ,他要求解最小值,我们就把它取负,这样很容易就变成求最大值 m a x : 3 x 1 7 x 2 很方便
对于约束条件,第一行 x 1 + x 2 = 11 这玩意是个等号。我们注意到如果有这样两个式子

{ x 1 + x 2 11 x 1 + x 2 11

如果要同时满足这两个式子则必然取等,因此可以利用这组式子代替等式作为约束条件,是不是很妙?同样的对于大于等于号我们都要变成小于等于号
对于 x 1 4 x 2 3 变成标准型同样很方便,只要同乘以一个-1令不等号变向就可以了 x 1 + 4 x 2 3
剩下问题只有变量的约束,这个例子里面一看只有 x 1 为非负约束, x 2 只字未提,也就是说 x 2 的正负式任意的,这个不可以有。我们需要将 x 2 等价地替换为两个非负变量即

x 2 = x 2 x 2
这里虽然 x 2 x 2 均非负,两者作差实际可以取遍实数集 R ,我们将其作为一个等价替代,代替目标函数,约束条件中的 x 2 可以得到

m a x : 3 x 1 7 ( x 2 x 2 ) { x 1 + ( x 2 x 2 ) 11 x 1 ( x 2 x 2 ) 11 x 1 + 4 ( x 2 x 2 ) 3 x 1 , x 2 , x 2 0

这样标准型就出来了

2.松弛

看到松弛首先想到了老太太岁月沧桑的脸,俺以后估计也那样了T^T,在线性规划里,松弛很简单就是为了把不等号变成等号。对于一个标准型,式子里只有小于等于号,处理就非常方便。以上一节得到的标准型为例

x 1 + ( x 2 x 2 ) 11

由于是小于等于号,我们在不等号的右边减去一个非负的值,这个值恰到好处地使得不等号变为等号

x 1 + ( x 2 x 2 ) = 11 x 3

对于余下地式子同样可以找到几个恰到好处的值使得不等号变为等号

m a x : 3 x 1 7 ( x 2 x 2 ) { x 1 + ( x 2 x 2 ) = 11 x 3 x 1 ( x 2 x 2 ) = 11 x 4 x 1 + 4 ( x 2 x 2 ) = 3 x 5 x 1 , x 2 , x 2 , x 3 , x 4 , x 5 0

现在变量一下子变成了六个。有时候数学就是这么讨厌,把一道高中数学题一下子变成了想都想不明白的一堆公式。上面那个式子带着括号还有带撇号的变量,我们把它展开重新命名得到一个好看的松弛型

m a x : 3 x 1 7 x 2 + 7 x 3 { x 1 + x 2 x 3 + x 4 = 11 x 1 x 2 + x 3 + x 5 = 11 x 1 + 4 x 2 4 x 3 + x 6 = 3 x 1 , x 2 , x 3 , x 4 , x 5 , x 6 0


刚刚我们提到松弛是为了将不等号变为等号,对于包含等号的是不是可以不用进行松弛?答案是可以的!!前面大于等于号变成小于等于是否多此一举?这个每个人都有自己的看法。在程序中这一步确实是可以省去的,因为每多几行代码就要额外消耗运算力,在实际的项目或者是玩ACM中是得不偿失的!!不过很多情况下用到线性规划的时候变量都是由非负约束的,比如最大流问。当然这些问题本身就有自己特定的一类算法,线性规划仅仅是其中的一种而已。但是在下文以及下一篇博客我们会看到标准型将在某些方面发挥很大用途

为了演示博文中的这个过程,文中的代码依然按照 求标准型==>松弛==>单纯形求解的顺序进行。

单纯形法

1.求解过程

在大多数教材中单纯形法都是以单纯形表的形式出现的,其背后的意思较为隐晦。博文里还是使用式子来展现单纯形法的运算过程
首先上例子:

m a x : 5 x 1 + 2 x 2 { 30 x 1 + 20 x 2 + x 3 = 160 5 x 1 + x 2 + x 4 = 15 x 1 + x 5 = 4 x 1 , x 2 , x 3 , x 4 , x 5 0

首先我们确定一组基变量,这组基变量将构成一组基本解,例如我们选择 x 3 x 4 x 5 为基变量,我们可以这么写,为了好看俺省去了大括号

m a x : 5 x 1 + 2 x 2 x 3 = 160 30 x 1 20 x 2 x 4 = 15 5 x 1 x 2 x 5 = 4 x 1

我们令右边的所有非基变量为零,可以得到一组基本解 ( 0 , 0 , 160 , 15 , 4 ) T

{ x 1 = 0 x 2 = 0 x 3 = 160 x 4 = 15 x 5 = 4
这一组解满足变量非负的要求,可以认为这一组 基本解基本可行解 此时目标函数的值就可以把这组基本可行解带进去,一看就是0


注:之所以俺重新举了一个例子不沿用上文中得到的那个松弛型,大家不妨将 x 4 x 5 x 6 作为一组基变量试试,得到的基本解 ( 0 , 0 , 0 , 11 , 11 , 3 ) T 他不满足变量非负的要求( x 5 = 11 , x 6 = 3 ),对于找不到基本可行解的线性规划的解法将在下文中介绍


有了一个基本可行解也有了一个小的可怜的目标值,我们需要寻找更优解。
我们看到现在目标函数为 5 x 1 + 2 x 2 如果能适量增加 x 1 x 2 任意一个的值,那么目标函数就不会为零0,因为在 x 1 x 2 前面的系数都是正的。这里我们选择 x 1

我们让 x 1 增加,但是又要保证另外的变量非负,因此我们需要找到一组最紧约束(最小)的 x 1 ,这个就是我们选定的转入基变量

{ 0 = 160 30 x 1 0 = 15 5 x 1 0 = 4 x 1  

很显然第二个式子得到的最小,我们认为该组拥有最紧约束,因此我们就拿第二组开刀,把 x 4 替换掉,这个 x 4 就是被替换的转出基变量,替换以后他就不是基变量了!!
原式 x 4 = 15 5 x 1 x 2 x 1 反解出来得到 x 1 = 3 1 5 x 2 1 5 x 4
相应地我们将目标函数、约束条件中的 x 1 统统换掉,得到一个新的式子

m a x : 15 + 2 x 2 x 4 x 3 = 70 14 x 2 + 6 x 4 x 1 = 3 1 5 x 2 1 5 x 4 x 5 = 1 + 1 5 x 2 + 1 5 x 4

如果我们零右边的非基变量为零碰巧又有一组基本可行解出来 ( 3 , 0 , 70 , 0 , 1 ) T 随着基本可行解的变化,目标函数的值提高为15,很开心,这个很开心的操作成为转动操作。
现在为了能让目标函数的最大值继续变大,我们只能增加 x 2 的值,因为当前的目标函数里只有他的系数式正的,如果增大 x 4 …….那不是变小了么???
因此我们将 x 2 作为转入基变量,找到约束最紧的那一个式子,显然就是第一个,我们重复转动操作,得到新的式子

m a x : 20 1 14 2 x 3 4 7 x 4 x 2 = 5 1 14 x 3 + 3 7 x 4 x 1 = 2 + 1 70 x 3 2 7 x 4 x 5 = 2 1 70 x 3 + 2 7 x 4

运气太好了现在我们又有一组可行解 ( 2 , 5 , 0 , 0 , 2 ) T 现在目标值变为20,到此为止我们 不能使用任何操作使目标函数的值继续增大,因为 x 3 x 4 的系数都是负的,到此我们认为取得了最优解,即所求线性规划的最大值目标值为20
实际上这一过程就是单纯形法的计算过程,在每一次迭代中我们都能找到一个更优的基本可行解,继而找到最优解。而我们能够证明,只要找到第一个基本可行解,并且初始状态下非基变量前面的系数为负数,我们就能一直持续转,这个要求相当苛刻!实际使用中只有为数不多的集中情况满足这个要求,在下一篇博文中我们将讨论求解任意一个线性规划(只要他有解)

2.单纯形法的循环问题

我们注意到单纯形法是一个不断迭代的过程,如果其有解算法应该能在有限次内结束,遗憾的是利用计算机我们可以轻易(其实有点难的)构造出一种线性规划,他在某一次转动操作以后目标函数的值依然不变,这个现象称为退化,有时候严重地退化会导致程序发生死循环。退化问题曾一度让单纯形法陷入窘境。1955年E.Beale曾举出一个非常震撼的例子

m a x : 3 4 x 4 + 20 x 5 1 2 x 6 + 6 x 7 { x 1 + 1 4 x 4 8 x 5 x 6 + 9 x 7 = 0 x 2 + 1 2 x 4 12 x 5 1 2 x 6 + 3 x 7 = 1 x 3 + x 6 = 0 x j 0 ( j = 1 , 2...7 )

这是一个非常棘手的线性规划我们看到第一行第二行约束条件的b(b是约束条件的那个常数向量),对第一行与第二行的任意一次转动都不会让目标函数更大。为了解决这个问题人们对输入进行微幅扰动,一定程度上克服了退化的问题,只要扰动适当解的精确度也可以接受。这个问题真正被克服是在1974,Bland提出了一个叫做Bland法则的东东,非常简单只需要如下操作

1.选择目标函数中系数非负的(必须的)且下标最小的变量为转入基变量;
2.有若干个相同的最紧约束选择下标最小的那个作为转出基变量;
3.没了

有了勃兰特法则(Bland Rule)我们只需要小小的改动一下代码就可以顺利地避免退化导致的死循环。其实退化带来的死循环非常罕见,一般情况下退化都是暂时的,除了降低一点效率并不会带来多大影响。
但是我们不能忽略另外两种可能导致死循环的情况:无解以及无限解,这两种情况发生的实在是太多了,为了避免出现这样的情况我们需要程序在合适的时候终止。
我们能证明,每一组确定的基变量对应一个唯一的松弛型
我们同样可以证明,对于标准型有约束条件就有m个松弛变量
这两句话一结合,实际上告诉我们基变量的组合方式是有限的,正常情况下循环的次数也是有限的,这个上限为 C m + n m 即从松弛后的所有变量中选出m个有几种,这个其实也暗含了一个信息:单纯形法是一个非多项式时间的算法 并且我们能够构造出这样的一个线性规划让这个算法不能再多项式时间内结束。这听起来很恐怖其实大多数时候单纯形法都是很高效的不信你试试看?当然现在存在能在多项时间内求解的算法:椭球算法和内点法。前者是第一个多项式时间算法,但是时间复杂度太高运行跟蜗牛一样,后者当输入规模巨大时跟单纯形法体量相当。但是线性规划还有一个分支就是整数规划,这个是一个NP-Hard问题,至今没有多项式时间算法可以解决。

简易单纯形法的实现

由于变量是浮点型,对于浮点型变量比较大小还是要注意一下的

#define f0 0.00000001
void solve()
{
    int i, j, k, u, Cycle = 0bool flag ;
    double temp = 0, Prebase, Pretar;
    int n = 100;

    for (i = 0; i < cons; i++)
        Main_Matrix[0][i] *= -1; //数组中同时存储了A和b的内容

    while (n > 0)
    {
        flag =false;
        Cycle++;//迭代次数
        n = 0;
        for (i = 1; i < var + 1; i++)
        {
            if (tar[i] > f0)
                n++;
        }

        for (i = 1; i < var + 1; i++)//遵循勃兰特规则,选择下标最小的作为转入变量
        {
            if (tar[i] > f0)
            {

                flag = true;
                k = get_min(i); //获取最紧约束,遵循勃兰特法则
                base[k] = i - 1; 

                for (j = 0; j < var + 1; j++)
                {
                    if (j != base[k] + 1)
                        Main_Matrix[j][k] /= Main_Matrix[base[k] + 1][k]; 
                }

                //高斯消元
                for (j = 0; j < cons; j++)
                {
                    Prebase = Main_Matrix[base[k] + 1][j];
                    if (j != k)
                    {
                        for (u = 0; u < var + 1; u++)
                            Main_Matrix[u][j] -= Prebase* Main_Matrix[u][k];
                    }

                }
                Pretar = tar[base[k] + 1];
                for (u = 0; u < var + 1; u++)
                    tar[u] -= Pretar*Main_Matrix[u][k];
            }
            if (flag)
                break;
        }
        if (Cycle>comb(cons,cons+var))//判断是否为无限循环,数据量大时这一句计算很耗时,可定义变量优化不必每次计算
            break;
    }
}

该函数由输入的松弛型开始求解,数组Main_Matrix中同时存储了变量系数A和常数b,存储格式为

[ b 1 a 11 a 12 a 1 n b 2 a 21 a 22 a 2 n b m a m 1 a m 2 a m n ]

程序中的下标从0开始。

int get_min(int s)
{
    int i, mincons= 0;
    double j = 100000000;
    for (i = 0; i < cons; i++)
    {
        if (Main_Matrix[0][i] * Main_Matrix[s][i] < f0 && (-1 * Main_Matrix[0][i] / Main_Matrix[s][i]) < j-f0)
        {
            j = -1 * Main_Matrix[0][i] / Main_Matrix[s][i];
            mincon = i;
        }
    }
    return mincons;
}

这个函数将返回约束最紧的那个转出变量,同时遵循勃兰特法则

void read_file()
{
    int i, j;
    char Type[4];
    int symb[100], var_symb[100];
    memset(Main_Matrix, 0.0, sizeof(Main_Matrix));
    memset(tar, 0.0, sizeof(tar));
    memset(base, -1, sizeof(base));
    ifstream ReadFile;
    ReadFile.open("input.txt");
    ReadFile >> Type;
    ReadFile >> var;
    ReadFile >> cons;

    while (!ReadFile.eof())
    {
        //目标函数系数
        if (Type[1] == 'i'&&Type[2] == 'n')
        {
            for (i = 0; i < var + 1; i++)
            {//目标函数第一个数为常数
                ReadFile >> tar[i];
                tar[i] *= -1;
            }
        }
        else
        {
            for (i = 0; i < var + 1; i++)
                ReadFile >> tar[i];
        }
        //主矩阵
        for (j = 0; j < cons; j++) //第一个是等号右边的系数,数量要加1
        {
            for (i = 0; i < var + 1; i++)
            {
                ReadFile >> Main_Matrix[i][j];
            }
        }

        for (i = 0; i < cons; i++)
            ReadFile >> symb[i];   //约束符号=0;>2 < 1
        for (i = 0; i < var; i++)
            ReadFile >> var_symb[i];//约束符号 无0;>2 < 1
        ReadFile.close();


        //处理变量系数为负或为无的情况
        for (i = 0; i < var; i++)
        {
            switch (var_symb[i])
            {
            case 0:
            {
                      tar[var + 1] = -tar[i + 1]; //无约束则用x`-x``,系数只需要拷贝过去,增加一个变量
                      for (j = 0; j < cons; j++)
                      {
                          Main_Matrix[var + 1][j] = -Main_Matrix[i + 1][j]; //把增加的变量系数取负
                      }
                      var_symb[var] = 2; //把符号变成大于
                      var_symb[i] = 2;
                      var++; //变量数加一
                      break;
            }
            case 1:
            {
                      tar[i + 1] *= -1; //把目标函数的那个变量取反
                      for (j = 0; j < cons; j++)
                      {
                          Main_Matrix[i + 1][j] *= -1; //这里所有变量系数取反
                      }
                      var_symb[i] = 2; //变符号
                      break;
            }
            }

        }

        //松弛

        for (i = 0; i < cons; i++)
        {
            switch (symb[i])
            {
            case 0: //处理约束相等的情况
            {
                        for (j = 0; j < var + 1; j++)
                            Main_Matrix[j][cons] = Main_Matrix[j][i]; //增加一个,并且约束符号相反
                        //对增加的等式与原等式进行松弛
                        //原式默认大于
                        Main_Matrix[var + 1][i] = 1; //右边减一个松弛变量,移过去为正
                        base[i] = var;
                        var_symb[var] = 2;
                        var++;
                        for (j = 0; j < var + 1; j++)
                            Main_Matrix[j][cons] *= -1; //取反,令不等号反向
                        Main_Matrix[var + 1][cons] = 1; //加一个松弛变量,移过去为负
                        var_symb[var] = 2;
                        base[cons] = var;
                        var++;
                        cons++;
                        break;
            }
            case 2: //小于零
            {
                        for (j = 0; j < var + 1; j++)
                            Main_Matrix[j][cons] *= -1; //取反,令不等号反向
                        Main_Matrix[var + 1][i] = 1;    //加一个松弛变量,移过去为负
                        var_symb[var] = 2;
                        base[i] = var;
                        var++;
                        break;
            }
            case 1: //大于零
            {
                        Main_Matrix[var + 1][i] = 1; //加一个松弛变量,移过去为负
                        var_symb[var] = 2;
                        base[i] = var;
                        var++;
                        break;
            }
            }
        }

    }

}

输入数据测试

max
2 3
0 5 2
160 30 20
15 5 1
4 1 1
1 1 1
2 2 2

输出

20  -11.4286  -13  -0.0714286  -0.571429  0  -0.0714286  -0.571429  0

-5  -8.57143  14  0.0714286  -0.428571  0  0.0714286  -0.428571  0
-2  6.71429  -2.6  -0.0142857  0.285714  0  -0.0142857  0.285714  0
-2  -5.71429  2.6  0.0142857  -0.285714  1  0.0142857  -0.285714  1

1  0  7  -1  -1  -1  -1  -1

可以看到准确输出了最大值20,最优解 ( 2 , 5 , 0 , 0 , 0 , 0 , 0 , 2 ) T (程序里将b取反因此这里为负数)
完整的VS2013工程评论里会补充,貌似CSDN下载必须要收资源分

猜你喜欢

转载自blog.csdn.net/little_cats/article/details/81189794