1. 程式人生 > >教你徹底學會c語言動態規劃——進階篇

教你徹底學會c語言動態規劃——進階篇

今天小編給大家帶來了c語言動態規劃的進階篇。溫馨提示:亮點在最後!

教你徹底學會c語言動態規劃——進階篇

 

如果想學c++並想學好,可以加這個群,首先是玖四捌,中間是玖伍四,最後是四捌四,裡面有大量的學習資料可以下載。

在我的上一篇文章中已經詳細講解了動態規劃的原理和如何使用動態規劃解題。本篇文章,我將繼續通過例子來讓大家更加熟練地使用動態規劃演算法。

話不多說,來看如下例題,也是在動態規劃裡面遇到過的最頻繁的一個題,本題依然來自於北大POJ:

最長公共子序列(POJ1458)

給出兩個字串,求出這樣的一個最長的公共子序列的長度:子序列中的每個字元都能在兩個原串中找到, 而且每個字元的先後順序和原串中的先後順序一致。

Sample Input

abcfbc abfcab

programming contest

abcd mnp

Sample Output

4

2

0

解題思路:設輸入的兩個串為s1,s2, 設MaxLen(i,j)表示::s1的左邊i個字元形成的子串,與s2左邊的j個字元形成的子串的最長公共子序列的長度(i,j從0 開始算),則MaxLen(i,j) 就是本題的“狀態” (如果還不懂“狀態”是什麼意思,可以參考我上一篇文章)

假定 len1 = strlen(s1),len2 = strlen(s2),那麼題目就是要求 MaxLen(len1,len2)

顯然:

MaxLen(n,0) = 0 ( n= 0…len1)

MaxLen(0,n) = 0 ( n=0…len2)

於是,我們可以得到如下的遞推公式:

if ( s1[i-1] == s2[j-1] ) //s1的最左邊字元是s1[0] 
	MaxLen(i,j) = MaxLen(i-1,j-1) + 1; 
else 
	MaxLen(i,j) = Max(MaxLen(i,j-1),MaxLen(i-1,j) );

時間複雜度O(mn) m,n是兩個字串長度

 

教你徹底學會c語言動態規劃——進階篇

 

 

S1[i-1]!= s2[j-1]時,MaxLen(S1,S2)不會比MaxLen(S1,S2j-1) 和MaxLen(S1i-1,S2)兩者之中任何一個小,也不會比兩者都大。

通過上面的分析,我們很簡單就可以寫出如下的程式碼:

#include <iostream> 
#include <cstring> 
using namespace std; 
 
char sz1[1000]; 
char sz2[1000]; 
int maxLen[1000][1000]; 
int main(){ 
	while( cin >> sz1 >> sz2 ) { 
		int length1 = strlen( sz1); 
		int length2 = strlen( sz2); 
		int nTmp; 
		int i,j; 
		for( i = 0;i <= length1; i ++ ) 
			maxLen[i][0] = 0; 
		for( j = 0;j <= length2; j ++ ) 
			maxLen[0][j] = 0; 
 		for( i = 1;i <= length1;i ++ ) { 
			for( j = 1; j <= length2; j ++ ){ 
				if( sz1[i-1] == sz2[j-1] ) 
					maxLen[i][j] = maxLen[i-1][j-1] + 1; 
				else 
					maxLen[i][j] = max(maxLen[i][j-1],maxLen[i-1][j]); 
			} 
		} 
		cout << maxLen[length1][length2] << endl; 
	} 
	return 0; 
}

然後提交我們的程式碼,一次AC。

 

 

教你徹底學會c語言動態規劃——進階篇

 

如果想學c++並想學好,可以加這個群,首先是玖四捌,中間是玖伍四,最後是四捌四,裡面有大量的學習資料可以下載。

接下來我們再來看一道典型的例題:

最長上升子序列(百練2757)

一個數的序列ai,當a1 < a2 < ... < aS的時候,我們稱這個序列是上升的。對於給定的一個序列(a1, a2, ..., aN),我們可以得到一些上升的子序列(ai1, ai2, ..., aiK),這裡1 <= i1 < i2 < ... < iK <= N。比如,對於序列(1, 7, 3, 5, 9, 4, 8), 有它的一些上升子序列,如(1, 7), (3, 4, 8)等等。這些子序列中最長的長度是4,比如子序列(1, 3, 5, 8).。

你的任務,就是對於給定的序列,求出最長上升子序列的長度。

輸入資料

輸入的第一行是序列的長度N (1 <= N <= 1000)。第二行給出序列中的N個整數,這些整數的取值範圍都在0到10000。

輸出要求

最長上升子序列的長度。

輸入樣例

7

1 7 3 5 9 4 8

輸出樣例

4

解題思路

1.找子問題

“求序列的前n個元素的最長上升子序列的長度”是個子問題,但這樣分解子問題,不具有“無後效性”,因為假設F(n) = x,但可能有多個序列滿足F(n) = x。有的序列的最後一個元素比 an+1小,則加上an+1就能形成更長上 升子序列;有的序列最後一個元素不比an+1小……以後的事情受如何達到狀態n的影響,不符合“無後效性” ,因此我們必須換一種思路來解決此問題。

“求以ak(k=1, 2, 3…N)為終點的最長上升子序列的長度”,一個上升子序列中最右邊的那個數,稱為該子序列的 “終點”。雖然這個子問題和原問題形式上並不完全一樣,但是隻要這N個子問題都解決了,那麼這N個子問題的解中, 最大的那個就是整個問題的解。

2.確定狀態

子問題只和一個變數—— 數字的位置相關。因此序列中數的位置k就是“狀態”,而狀態 k 對應的“值”,就是以ak做為“終點”的最長上升子序列的長度。 狀態一共有N個。

3.找出狀態轉移方程

maxLen (k)表示以ak做為“終點”的

最長上升子序列的長度那麼:

初始狀態:maxLen (1) = 1

maxLen (k) = max { maxLen (i):1<=i < k 且 ai < ak且 k≠1 } + 1 若找不到這樣的i,則maxLen(k) = 1

maxLen(k)的值,就是在ak左邊,“終點”數值小於ak ,且長度最大的那個上升子序列的長度再加1。因為ak左邊任何“終點”小於ak的子序列,加上ak後就能形成一個更長的上升子序列。

有了這個思路,我們就可以很輕鬆地寫出程式碼了。然而,即使到了這裡,我們依然還能從兩個方向解決這道題,我們可以將它們分別稱為“人人為我”遞推型動歸和“我為人人”遞推型動歸 。請看下面的講解:

“人人為我”遞推型動歸

狀態i的值Fi由若干個值 已知的狀態值Fk,Fm,..Fy 推出,如求和,取最大值

 

教你徹底學會c語言動態規劃——進階篇

 

 

根據這個方向,我們不難寫出如下程式碼:

#include <iostream> 
#include <cstring> 
#include <algorithm> 
using namespace std; 
 
const int MAXN =1010; 
int a[MAXN]; int maxLen[MAXN]; 
 
int main(){ 
	int N; 
	cin >> N; 
	for( int i = 1;i <= N;++i){ 
		cin >> a[i]; 
		maxLen[i] = 1; 
	} 
	for( int i = 2; i <= N; ++i){ //每次求以第i個數為終點的最長上升子序列的長度 
		for( int j = 1; j < i; ++j) //察看以第j個數為終點的最長上升子序列 
			if( a[i] > a[j] ) 
				maxLen[i] = max(maxLen[i],maxLen[j]+1); 
	} 
	cout << * max_element(maxLen+1,maxLen + N + 1 ); 
	return 0; 
} //時間複雜度O(N2)

下面是我這段程式碼的提交結果:

 

教你徹底學會c語言動態規劃——進階篇

 

 

“我為人人”遞推型動歸

狀態i的值Fi在被更新(不一定是 最終求出)的時候,依據Fi去更 新(不一定是最終求出)和狀態i 相關的其他一些狀態的值 Fk,Fm,..Fy

 

教你徹底學會c語言動態規劃——進階篇

 

 

根據這個方向,我們又可以寫出如下程式碼:

#include <iostream> 
#include <cstring> 
#include <algorithm> 
using namespace std; 
 
const int MAXN =1010; 
int a[MAXN]; 
int maxLen[MAXN]; 
 
int main(){ 
	int N; 
	cin >> N; 
	for( int i = 1;i <= N;++i){ 
		cin >> a[i]; 
		maxLen[i] = 1; 
	} 
	for( int i = 1; i <= N; ++i) 
		for( int j = i + 1; j <= N; ++j ) //看看能更新哪些狀態的值 
			if( a[j] > a[i] ) 
				maxLen[j] = max(maxLen[j],maxLen[i]+1); 
	cout << * max_element(maxLen+1,maxLen + N + 1 ); 
	return 0;
} //時間複雜度O(N2)

下面是我這段程式碼的提交結果:

 

 

 

接下來,就要進行一個總結了:

動規的三種形式

1)記憶遞迴型

優點:只經過有用的狀態,沒有浪費。遞推型會檢視一些 沒用的狀態,有浪費。

缺點:可能會因遞迴層數太深導致棧溢位,函式呼叫帶來額外時間開銷。總體來說,比遞推型慢。

2) “我為人人”遞推型

沒有什麼明顯的優勢,有時比較符合思考的習慣。個別特殊題目中會比“人人為我”型節省空間。

3)“人人為我”遞推型

在選取最優備選狀態的值Fm,Fn,…Fy時, 有可能有好的演算法或資料結構可以用來顯 著降低時間複雜度。

寫在最後

關於怎麼快速學C/C++,可以加下小編的C/C++學習群:,,邀請碼:CSDN,不管你是小白還是大牛,小編我都歡迎,不定期分享乾貨,歡迎初學和進階中的小夥伴。

 

每天晚上19:30都會開直播給大家分享C/C++遊戲程式設計學習知識和路線方法,群裡會不定期更新最新的教程和學習方法,最後祝所有程式設計師都能夠走上人生巔峰,讓程式碼將夢想照進現實