動態規劃 與兩道例題

現在要把這幾種常見的算法給理清弄明白了,要不然只能做個低級程序員了。


動態規劃DP是求解決策過程的最優化的數學方式。動態規劃一般分爲線性動規,區域動規,樹形動規,揹包動規。

動態規劃是一種方法,但不是一種算法,一般用於多決策中的最優化問題,具有遞推的思想。動態規劃與分治法類似,基本思想都是把待解問題分解成若干個子問題,先求解子問題,然後由這些子問題的解得到原問題的解。但分治法中分解得到的子問題是相互獨立的,但動態規劃中不是。動態規劃的基本思路與分治法相似,也是用一個表記錄所有已解子問題的答案,不管子問題以後是否被用到,都將其填入表中。多決策中,各個階段的決策依賴於當前狀態,又隨即引起了狀態的轉移,一個決策序列是在變化的狀態中產生出來的,故爲動態。

使用動態規劃解決問題:

1. 01揹包。

有一個包和n個物品,包的容量爲m,每個物品都有各自的體積和價值,問當從這n個物品中選擇多個物品放在包裏而物品體積總數不超過包的容量m時,能夠得到的最大價值是多少?[對於每個物品不可以取多次,最多隻能取一次,之所以叫做01揹包,0表示不取,1表示取]

先做簡單版本,即物品沒有價值,只計算能放入的最多物品時的總體積。

則很容易獲得一個簡單的遞歸版本:

int getMax(int* p ,int beglast,int arraylast){
	if(arraylast<1)return 0;
	int put,noput;
	noput = getMax(p+1,beglast,arraylast-1);
	if(*p <= beglast){
		put = *p + getMax(p+1,beglast - *p,arraylast-1);
	}else
		put = noput;
	if(put > noput)
		return put;
	else
		return noput;
}

int main(){
	int a[] = {3,3,5,1,1,2,5,4,3,3};
	cout<<getMax(a,10,10);
	system("pause");
}
這裏將問題化小,對第一個物品,假如放入,那計算得到放入第一個物品後的最大容量,加上第一個物品,與假設不放入第一個物品而獲得的最大容量進行比較,則最大值即爲當前揹包中放入的最多物品的總體積。這裏重要的是對任何一個物體,其加入與不加入揹包,會影響之後的物品最大容量的值,而動態規劃就體現在這裏,所以其實這裏會遍歷所有情況,即複雜度爲O(n * n)。實際上這就是這類問題的最簡單求解的方法,就是遍歷全部可能計算最大值,只是這裏使用遞歸,使其可讀性較高。

而要想使其效率更高,則要將遞歸改成循環,遞推儲存數據一般有兩種做法,

一種是使用Data1和Data2兩個數組,一個記錄上一階段的結果,然後根據Data1推出對應的Data2,然後再將Data2複製給Data1,然後繼續,這樣如果每次都是簡單的遞推一級時比較適合,但頻繁賦值比較麻煩,可以使用STL中的deque, 而且這種方法不適合遞推與之前一級以上的階段有聯繫的問題。

另一種是對於與之前N階相關的問題,建立數組Data[0...N],記錄最近N階的保存數據,而噹噹前階段需要方法 階段k的數據時,訪問 data[ k MOD(N+1) ] 即可。


嘗試自己用自己的想法將這個遞歸過程改爲循環過程:

int getMaxBegA(int obj[], int const objn,int value[],int begsize){
	//對於這個填表,填的應該是剩下揹包量以及對於第n個石頭時的
	int** p =  new int* [++begsize];
	int** v = new int* [begsize];
	for(int i=0; i< begsize;i++){
		p[i] = new int[objn];
		v[i] = new int[objn];
	}
	for(int i = 0; i < begsize;i++){
		if( i >= obj[objn-1]) {
			v[i][objn-1] = value[objn-1];
			p[i][objn-1] = obj[objn-1];
		}
		else {
			v[i][objn-1] = 0;
			p[i][objn-1] = 0;
		}
	}
	int a,b;
	for(int i = objn -2; i > -1 ;i--){
		for(int j = 0 ; j <begsize;j++){
			if(j < obj[i]){
				p[j][i] = p[j][i+1];
				v[j][i] = v[j][i+1];
			}
			else{
				a = value[i] + v[j-obj[i]][i+1];	
				b = v[j][i+1];
				if(a > b){
					v[j][i] = a;
					p[j][i] = obj[i] + p[j-obj[i]][i+1];	
				}else{
					p[j][i] = p[j][i+1];
					v[j][i] = v[j][i+1];
				}
			}
		}
	}
	for(int i = 0 ; i < objn;i++){
		cout<<i<<":  ";
		for(int j = 0 ; j < begsize;j++){
			cout<<p[j][i]<<"  ";
		}
		cout<<endl;
	}

	a =  v[begsize-1][0];
	for(int i=0;i<begsize;i++){
		delete[] p[i];
		delete[] v[i];
	}
	delete[] p;
	delete[] v;
	return a;
}
而事實上動態規劃最好的填表大致如此,建一個二維表,行表示決策的階段,而列表示決策的狀態,只不過我這裏剛好反了。通過填表,對於重複問題就不需要重複計算,但是需要大量的空間。如果想要更好的空間複雜度的話,進行之前所說的優化,即這裏每一個新的階段都取決與上一個階段的 不同狀態的結果,而在從後倒推到第一階段時,其只需要之後的第二階段的數據即可,則不需要保存多餘的數據,但每次轉向新的階段都要重新賦值之前的結果,時間上會更加複雜一點。

在看了編程之美的關於最長子序列的算法後,嘗試寫一個類似思路的解法:

int L10(int obj[],int value[],int maxbeg,int size){
	int* LW = new int [size];
	int* LV = new int[size];
	for(int i = 0 ; i < size;i++){
		if(obj[i] <= maxbeg){
			LW[i] = obj[i];
			LV[i] = value[i];
		}else{
			LW[i] = 0;
			LV[i] = 0;
		}
		for(int j = 0 ; j < i;j++){
			if(LV[j]+ value[i] > LV[i] && obj[i]+LW[j] <= maxbeg){
				LV[i] = LV[j] + value[i];
				LW[i] = LW[j] + obj[i];
			}
		}
	}
	int MAX = 0;
	for(int i =0; i < size;i++){
		if(LV[i] > MAX)
			MAX = LV[i];
	}
	return MAX;
}
發現這樣也可以獲得正確的結果,難道這種方法就是解決大部分的動態規劃的常用方式?



然後在進行考慮一個動態規劃的常見題目:

設L=<a1,a2,…,an>是n個不同的實數的序列,L的遞增子序列是這樣一個子序列Lin=<aK1,ak2,…,akm>,其中k1<k2<…<km且aK1<ak2<…<akm。求最大的m值。


簡單的遞歸算法:

int getMaxLengthArray(int stack[],int stacktop,int array[],int arraysize){
	int length = 0,mis,re ;
	if(arraysize<1)return 0;
	if( *array > stack[stacktop] ){
		mis = getMaxLengthArray(stack,stacktop,array+1,arraysize-1);
		stack[stacktop+1] = *array++;
		re = 1+getMaxLengthArray(stack,stacktop+1,array,arraysize-1);
		if(mis > re)return mis;
		else return re;

	}
	if( *array == stack[stacktop]){
		return getMaxLengthArray(stack,stacktop,array+1,arraysize-1);
	}
	if(*array < stack[stacktop]){
		mis = getMaxLengthArray(stack,stacktop,array+1,arraysize-1);
		int n = 0;
		while( *array <= stack[stacktop] ){
			stacktop--;
			n++;
		}
		stack[++stacktop] = *array;
		re = getMaxLengthArray(stack,stacktop,array+1,arraysize-1) - n ;
		if(re>mis)return re;
		else return mis;
	}

}
int getMax(int array[],int arraysize){
	int* stack = new int[arraysize+1];
	stack[0] = 0xFFFFFFFF;
	return getMaxLengthArray(stack,0,array,arraysize);
}
這裏還需要用到回溯,由於我暫時還沒有研究回溯,只能用一種簡單的方法來實現回溯,即用棧來記錄計算的過程,然後使用出棧來模擬回溯。

這裏的算法是不夠合理和簡潔的,編程之美中如此寫:

int LISA(int array[],int size){
	int* LIS = new int[size];
	int MAX,i,j;
	for(i= 0; i < size;i++){
		LIS[i] = 1;
		for(j = 0; j < i;j++){
			//這裏對於遍歷到第i個元素,遍歷其前i個元素,若當前第i個元素加在之前的第j個元素後能導致其子
			//串長度增加,則記錄這個新的第i個節點當前最長字段數量。
			if(array[j] < array[i] && LIS[j] + 1 >LIS[i])
				LIS[i] = LIS[j] + 1;
		}

	}
	MAX = 1;
	for(i = 0 ; i < size;i++){
		if(LIS[i] > MAX )
			MAX = LIS[i];
	}
	delete[] LIS;
	return MAX;
}

這樣已經很簡潔了,但是編程之美表示還不夠,編程之美是如此做的,使用一個新的數組來儲存當前最長子序列以及最長子序列時對應的最大值的最小值,如果用一個數組MaxV表示,則MAXV[i] 的值爲當前遍歷到第j個字符串時,之前長度爲i的子段的最大值即最右點 的最小值。由於對於點j,其尋找適合自己的子序列時必定從之前最長子序列開始找起,當最長子序列的最大值 小於 j的值時,就表示j點可以接在這個最長子序列之後,然後更新信息,將續表的最長子序列的最右點值改爲j的值。這個記錄還有一個要更新的地方是當j點並不比當前最大子序列長時,即在得到的長度依然是之前已存在的子序列長度,這時要更新這個長度對應的最大值的最小值,即判斷j點的值釋放比這個長度的當前最大值小。由於這個最長子序列的當前最大長度是一級一級增加的,所以這個記錄當前子序列長度的數組是連續,記錄當前最大值,而查找時只要從最大值開始向下查找。但這樣依然是o(N*N),。

而編程之美的進一步優化是將這個向下的 o(N)的查找改爲二分查找,這樣這個查找的複雜度就成了 O(log2 N)  。而整個過程的複雜度也變成了 O(N*log2 N)了,這裏太麻煩,我就不實現了。




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