1. 程式人生 > >學習筆記第三十二節:線性規劃與單純形

學習筆記第三十二節:線性規劃與單純形

正題

      我們今天講一下線性規劃,以這一道題為例:#179. 線性規劃

      首先面對一堆小於等於的約束,我們應該怎麼做?

      我們以樣例來解釋:

      有四條約束,要同時滿足:st.\begin{Bmatrix} 2x_1+x_2\leqslant 6\\ -x_1+2x_2 \leqslant 3 \\ x_1,x_2\geqslant 0\end{Bmatrix}

      同時,我們要使得x_1+x_2最大。

      首先,我們可以把可行解在一個二維平面上表示出來。

      如圖,可行解就是藍色部分,我們要使得x_1+x_2最大,那麼我們就用一條k=-1

的直線,從上往下掃,然後,碰到的第一個點就是答案。

      這就是圖解法。如果有三個變數就是一個立體空間裡面,用一個平面從上往下掃就可以了。但是4個變數的時候就難以構建了。

      這就是單純形的應用。

      首先我們可以多設兩個變數,把小於等於號去掉。

      st.\begin{Bmatrix} 2x_1+x_2+x_3 = 6\\ -x_1+2x_2+x_4 = 3 \\ x_1,x_2,x_3,x_4\geqslant 0\end{Bmatrix}

      這個也是沒有錯的,對吧。

     我們把答案也寫進去,就變成了。

     \\ans=x_1+x_2\\2x_1+x_2+x_3 = 6\\ -x_1+2x_2+x_4 = 3 \\ x_1,x_2,x_3,x_4\geqslant 0

      再變形一下,就變成:

      \\ans=x_1+x_2\\x_3 = 6-2x_1-x_2\\ x_4 = 3+x_1-2x_2\\ x_1,x_2,x_3,x_4\geqslant 0

      當出現這樣的,常數項是非負的,所有的等式右邊的變數都是一樣的,我們把它叫做基可行解形式

      左邊的變數叫做基變數,右邊的變數叫做非基變數

      在這個時候,我們構造一組答案是極其簡單的,就是讓右邊的變數全部為0,然後左邊的變數就等於右邊的常數項,向上面的方程組,有一個顯然的解就是x_1=0,x_2=0,x_3=6,x_4=3,ans=0

       這組解肯定是可行的,而且我們可以保證這組解一定在集合上的多邊形的頂點上。(具體為什麼請先繼續往下看

      我們考慮怎麼將答案增大?

      我們可以看到,x_1是非基變數,也就是說,他現在取0.

      但是他其實可以取到更大,那它到底能取到多大呢?

      就要看下面的約束了,對於第一個約束,他最大能取到3(6/2),對於第二個約束,這個約束對x_1是沒有約束的,因為在第二個約束中,x_1不管取到多大,約束都是成立的。

      那麼我們只能選第一個約束。

      那如果我們讓x_1為3,其他變數最好的情況就是取0。

      所以,我們讓x_1作為基變數,讓這個約束原來的基變數換做為非基變數。

      \\ans=x_1+x_2\\x_1 = 3-0.5x_3-0.5x_2\\ x_4 = 3+x_1-2x_2\\ x_1,x_2,x_3,x_4\geqslant 0

      顯然這樣子還不是基可行解形式,因為x_1在兩邊都出現了。

      那我們做一遍高斯消元就可以了!(意思就是把x_1=balabala帶進其他有x_1的地方。

      就變成了

      \\ans=3-0.5x_3+0.5x_2\\x_1 = 3-0.5x_3-0.5x_2\\ x_4 = 6-0.5x_3-2.5x_2\\ x_1,x_2,x_3,x_4\geqslant 0

      那我們就得到了一個更大的解x_1=3,x_2=0,x_3=0,x_4=6,ans=3

      回想一下,我們剛剛乾了些什麼? 

      首先,我們在答案裡面,找出了一個係數大於0的非基變數,這個非基變數,我們把它叫做這一次操作的換入變數,接著,我們又找到一個對這個換入變數約束最緊的約束。將它與這條約束原來的基變數進行一個交換,我們把這個基變數叫做換出變數,這個交換叫做轉軸。接著,我們用這個式子對每一個式子進行高斯消元

      我們就可以得到一組更大的解。當答案式子中非基變數都小於0了。我們就可以斷定答案無法增大了。

      如果我們找到了一個大於0的非基變數,但是我們找不到一個約束這個非基變數的約束(係數大於0)。

      那麼這個約束就可以無限增大,那麼就是無最大解(Unbounded)的情況。

      我們已經解決了如果有一個基可行解的時候,怎麼去找到一個更優的解。

      如果我們一開始沒有一個基可行解呢?也就是說,如果我們一開始的約束不滿足基可行解形式呢?

      就像第二個樣例:

      \\ans=x_1-x_2 \\x_3=4-x_1-x_2\\x_4=-2+x_1+2x_2 \\x_1,x_2,x_3,x_4\geq 0

       是不滿足基可行解的形式的。

       存在一種解法,可以檢驗是否存在一個初始解。這個方法就是給每一條約束的右邊都加上一個人工變數A,其中A大於等於0.

       然後我們儘量使A小,也就是說當A=0時,才存在一個初始解。

       因為給每一條約束右邊都加上一個人工變數A就相當於x_1+...+x_n<=S_1+A,那麼當A=0時,就是原來的約束。

       顯然,我們只要把答案設為-A就可以了,因為可以使得-A最大,從而使得A最小。

       \\ans=-A \\x_3=4-x_1-x_2+A\\x_4=-2+x_1+2x_2+A \\x_1,x_2,x_3,x_4,A\geq 0

       但是,現在還是不滿足基可行解形式的,怎麼辦呢?

       我們選出常數項最小的那一行。將那一行的A轉軸成一個基變數。

       像上面,我們選出第二個約束。

       \\ans=-2+x_1+2x_2-x_4 \\x_3=6-2x_1-3x_2+x_4\\A=2-x_1-2x_2+x_4 \\x_1,x_2,x_3,x_4,A\geq 0

       現在肯定可以保證所有的約束都滿足基可行解形式了。

       用單純形演算法來求出A的最小值(-A的最大值)就可以了。

       若A=0,那麼就有一個初始解,我們將原來的答案序列代入,如果存在一個變數是基變數,那麼就先換成非基變數,最後得出第一組解。

       若A不等於0,那麼說明不存在第一組解,程式結束。

       剩下的再交給單純形演算法就可以了。

       這樣的時間複雜度是很優秀的(隨機情況下)。

       但是程式碼量大,不容易實現。

       有一種十分玄學的隨機做法。

       就是對於一個約束,如果常數項小於0,那麼我們找到一個正係數的非基變數,將這個非基變數轉軸,常數項就大於0了。

       如果找不到正係數的非基變數,那麼說明就不存在一個解,因為x_i = S-x_j (S<0)\to x_i\ngeqslant 0

       那麼就很好的完美的解決這類的約束最值問題。

程式碼

        如果真的要把每一條約束的左右兩邊的存下來,那無疑是非常麻煩的。

        其實每一條約束都可以寫成S = \sum x_i 其中S是一個常數。

        那麼我們就可以寫成一個矩陣的形式。\bigl(\begin{smallmatrix} S & x_1 & x_2 &... & x_n \end{smallmatrix}\bigr)

        每一行的基變數用一個數組存下來就可以了。

        當我們要找一個換入變數的時候,我們還是在第0行找一個正係數的非基變數x。

        但是在找一個換出變數的時候,我們要找一行,使得這一行的非基變數x大於0,而不是小於0的。因為相當於一個移項。

        轉軸,就相當於給這一行都除以這個非基變數。再給其他行做高斯消元。

        注意,這個時候,第0行(答案行)的常數項是答案的相反數。

        因為每一次轉軸和高斯消元的過程,就變成了給答案減去一個數。

        另外,我們注意到基變數的係數肯定為1,而非基變數只有n個,我們只要每行開n個空間,存下非基變數的係數就可以了。

        id表示第i個非基變數存的是幾號變數,id[n+i]表示第i行的基變數是幾號變數。

uoj資料太噁心。97分過不了(其實沒有人過得了

#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<iostream>
using namespace std;

int n,m,t;
double a[30][25];
double eps=1e-5;
int id[45];
double ans[25];

void pivot(int x,int y){
	swap(id[n+x],id[y]);
	double temp=a[x][y];
	for(int i=0;i<=n;i++) a[x][i]/=temp;
	a[x][y]=1/temp;
	for(int i=0;i<=m;i++) if(x!=i){
		temp=a[i][y];a[i][y]=0;
		for(int j=0;j<=n;j++) a[i][j]-=temp*a[x][j];
	}
}

bool prepare(){
	int x,y;
	while(1){
		x=y=0;
		for(int i=1;i<=m;i++) if(a[i][0]<-eps) {x=i;if(rand()%3!=0) break;}
		if(!x) break;
		for(int i=1;i<=n;i++) if(a[x][i]<-eps) {y=i;if(rand()%3!=0)break;}
		if(!y){
			printf("Infeasible");
			return false;
		}
		pivot(x,y);
	}
	return true;
}

bool simplex(){
	int x,y;
	double mmin;
	while(1){
		x=y=0;
		for(int i=1;i<=n;i++) if(a[0][i]>eps) {y=i;break;}
		if(!y) break;
		mmin=(double)1e18;
		for(int i=1;i<=m;i++) if(a[i][y]>eps && a[i][0]/a[i][y]<mmin) {x=i;mmin=a[i][0]/a[i][y];}
		if(!x) {
			printf("Unbounded");
			return false;
		}
		pivot(x,y);
	}
	return true;
}

int main(){
	srand(23333);
	scanf("%d %d %d",&n,&m,&t);
	for(int i=1;i<=n;i++) scanf("%lf",&a[0][i]);
	for(int i=1;i<=m;i++){
		for(int j=1;j<=n;j++) scanf("%lf",&a[i][j]);
		scanf("%lf",&a[i][0]);
	}
	for(int i=1;i<=n;i++) id[i]=i;
	if(prepare() && simplex()){
		printf("%.8lf\n",-a[0][0]);
		if(t){
			for(int i=1;i<=m;i++) if(id[n+i]>=1)ans[id[n+i]]=a[i][0];
			for(int i=1;i<=n;i++) printf("%.8lf ",ans[i]);
		}
	}
}

         關於對偶:

        前面我們研究的問題都是“約束為小於等於,求最大值的問題”,然而現在要求解得問題是"約束為大於等於,求最小值的問題"。

        首先符號不是問題,我們只要將兩邊同時乘上-1,就可以變號了。

        最小值也不是問題,那我們在單純形演算法的時候,在答案式拿出一個係數為負的就可以了。

        但是這樣使得在找初始解的時候可能會變慢許多,所以,有人可以證明上面的兩個問題是一個對偶問題,也就是答案相同。

        那麼怎麼個對偶法呢?

        對於標準型線性規劃問題:

        \\maximize\ c*x \\s.t. \ A_i*x<=b_i\ \ (i\in[1,m])

        它的對偶問題是:

        \\minimize\ b*x \\s.t.\ A^T_i*x>=c_i\ \ (i\in[1,n])

       相關證明請翻閱演算法導論。

       其中A是一個係數矩陣,A^T是A的轉置。A_i表示的是第i行的A。

       A>=0

       另外的,對於A的任意一個n*n的矩陣行列式都為-1,0,1,那麼這個線性規劃問題的最優解是整數。當然,還要求b,c都為整數矩陣。