1. 程式人生 > >動態規劃C++

動態規劃C++

引入題目

給定陣列arr,arr中所有的值都為正數且不重複。每個值代表一種面值的貨幣,每種面值的貨幣可以使用任意張,再給定一個整數aim代表要找的錢數,求找錢有多少種方法。
分析:可以使用,暴力搜尋方法、記憶搜尋方法、動態規劃方法、狀態繼續簡化後的動態規劃方法。

暴力搜尋方法

arr = {5,10,25,1},aim=1000.
1.用0張5元貨幣,讓[10,25,1]組成剩下的1000,最終方法數為res1.
2.用1張5元貨幣,讓[10,25,1]組成剩下的995,最終方法數為res2.

\ldots
201.用200張5元貨幣,讓[10,25,1]組成剩下的0,最終方法數為res201.
其中res1,…,res201還可以繼續遞迴分解。

int violentCoins(const int *arr,const int &length, int index, int aim) {
	int res = 0;
	if (index == length) {
		res = (aim == 0 ? 1 : 0);
	}
	else {
		for (int i = 0; arr[index] * i <= aim; ++i) {
			res += violentCoins(arr, length, index + 1, aim - arr[index] * i);
		}
	}
	return res;

}

缺點:暴力搜尋存在大量的重複計算,比如使用0張5元跟1張10元需要計算violentCoins(arr,length,2,990),而使用2張5元跟0張10元也需要計算violentCoins(arr,length,2,990)

記憶搜尋法

結合暴力搜尋法的缺點,可以設計一個map來儲存是否計算過violentCoins(arr,length,2,990)。如果計算過可以直接使用,如果沒有計算過則重新計算並將結果放入map中。

int memoryCoins(const int *arr, const int &length, int **map, int index, int aim) {//map == 0 表示沒計算過, ==-1表示為0次。
	int res = 0;
	if (index == length) {
		res = (aim == 0 ? 1 : 0);
	}
	else {
		if (map[index][aim] == 0) {
			for (int i = 0; arr[index] * i <= aim; ++i) {
				res += memoryCoins(arr, length, map, index + 1, aim - arr[index] * i);
			}
			map[index][aim] = res == 0 ? -1 : res;
		}
		else {
			res = map[index][aim] == -1 ? 0 : map[index][aim];
		}
	}
	return res;
}

動態規劃

如果arr長度為N,生成行數為N,列數為aim+1的矩陣dp。dp[i][j]的含義是在使用arr[0,…,i]貨幣的情況下,組成錢數j有多少種方法。
因此,第一列表示組成錢數為0的方法,第一列全為1。第一行表示arr[0]組成錢數j的方法,只有j=arr[0]的整數被時,dp[0][j]=1.
當要計算dp[i][j]時,考慮dp[i-1][j]+dp[i-1][j-arr[i]*1]+…+dp[i-1][j-arr[i]k].
依次計算每一行的值,最終,最右下角的值為所要求出的值。
求每一個位置的值時,都需要列舉上一行的值,時間複雜度為 O ( a i m ) O(aim) .dp中共有N
aim個位置,所以總體的時間複雜度為 O ( N a i m 2 ) O(N*aim^2) .

int dynamicProgram(const int *arr, const int &length, const int &aim) {
	int **dp = new int*[length];
	for (int i = 0; i < length; ++i) {
		dp[i] = new int[aim + 1];
		memset(dp[i], 0, (aim + 1) * sizeof(int));
		dp[i][0] = 1;
	}
	for (int i = 0; i < aim + 1; i+=arr[0]) {
		dp[0][i] = 1;
	}
	for (int i = 1; i < length; ++i) {
		for (int j = 1; j < aim + 1; ++j) {
			for (int k = 0; k <= j; k += arr[i]) {
				dp[i][j] += dp[i - 1][j - k];
			}
		}
	}
	int result = dp[length - 1][aim];
	delete[] dp;
	return result;
}

記憶搜尋方法與動態規劃方法的聯絡

  1. 記憶化搜尋方法就是某種形態的動態規劃方法。
  2. 記憶化方法不關心到達某一遞迴過程的路徑,只是單純的對計算過的遞迴過程進行記錄,避免重複的遞迴過程。
  3. 動態規劃法則是規定好每一個遞迴過程的計算順序,依次進行計算,後面的計算過程嚴格依賴前面的計算過程。
  4. 兩者都是空間換時間的方法,也都有列舉的過程,區別就在於動態規劃規定計算順序,而記憶搜尋不用規定。

什麼是動態規劃

  1. 其本質是利用申請的空間來記錄每一個暴力搜尋的計算結果,下次要用結果的時候直接使用,而不再進行重複的遞迴過程。
  2. 動態規劃規定每一種遞迴狀態的計算順序,從簡單的基本的狀態出發,順序的計算出所有的狀態,最終獲得結果的過程。
  3. 動態規劃與記憶搜尋本質上是相同的,但是動態規劃嚴格規定計算順序,而記憶搜尋非常功利,因此動態規劃具有進一步優化的可能。

針對上面的問題,dp[i][j] = dp[i][j-arr[i]] + dp[i-1][j],從而省略掉列舉的過程。因此時間複雜度降為 O ( N a i m ) O(N*aim)

int dynamicProgram(const int *arr, const int &length, const int &aim) {
	int **dp = new int*[length];
	for (int i = 0; i < length; ++i) {
		dp[i] = new int[aim + 1];
		memset(dp[i], 0, (aim + 1) * sizeof(int));
		dp[i][0] = 1;
	}
	for (int i = 0; i < aim + 1; i += arr[0]) {
		dp[0][i] = 1;
	}
	for (int i = 1; i < length; ++i) {
		for (int j = 1; j < aim + 1; ++j) {
			if (j - arr[i] >= 0) {
				dp[i][j] = dp[i][j - arr[i]] + dp[i - 1][j];
			}
			else {
				for (int k = 0; k <= j; k += arr[i]) {
					dp[i][j] += dp[i - 1][j - k];
				}
			}
		}
	}
	int result = dp[length - 1][aim];
	delete[] dp;
	return result;
}

暴力遞迴題目優化成動態規劃方法的大體過程

  1. 首先寫出暴力遞迴的方法
  2. 然後尋找哪些引數可以代表一個遞迴過程。
  3. 找到遞迴過程的引數後,記憶化搜尋的方法可以容易寫出。
  4. 通過分析記憶化搜尋的依賴路徑,進而實現動態規劃。簡單的可以直接得到的狀態先計算,例如二維中的第一行與第一列,依賴簡單狀態的結果,進行復雜過程的後續計算。
  5. 然後觀察動態規劃的計算過程能否實現簡化,得到更簡單的狀態方程。

動態規劃方法的關鍵點

  1. 最優化原理,也就是最優子結構性質。指一個最優化策略,不論過去狀態與決策如何,對前面的決策所形成的狀態而言,餘下的諸決策必須構成最優策略。簡單來說就是最優化策略的子策略總是最優的。
  2. 無後效性,指的是某狀態下決策的收益,只與狀態和決策有關,與到達該狀態的方式無關。
  3. 子問題的重疊性,動態規劃將原來具有指數級時間複雜度的暴力搜尋演算法改進成了具有多項式時間複雜度的演算法,其中關鍵在於解決冗餘,這是動態規劃演算法的根本目的。

例題一

有n級臺階,一個人每次上一級或者兩級,問有多少種走完n級臺階的方法?
分析:f(n) = f(n-1) + f(n-2)
f(1) = 1, f(2) = 2, f(3) = 3,…,f(n) = f(n-1) + f(n-2)

int upTheSteps(const int &n) {
	int f1 = 1;
	int f2 = 2;
	int f3 = 0;
	if (n <= 1) {
		return 1;
	}
	if (n == 2) {
		return f2;
	}
	for (int i = 0; i < n - 2; ++i) {
		f3 = f1 + f2;
		f1 = f2;
		f2 = f3;
	}
	return f3;
}

例題二

給定一個矩陣m,從左上角開始每次只能向右或者向下走,最後到達右下角的位置,路徑上所有的數字累加起來就是路徑和,返回所有的路徑中最小的路徑和,如果給定的m如下,路徑1,3,1,0,6,1,0是所有路徑中路徑和最小的,所以返回12.
1 3 5 9
8 1 3 4
5 0 6 1
8 8 4 0
分析:生成一個與m相同維度的矩陣dp,表示走到當前位置的最小路徑和。可以寫出第一行與第一列
1 4 9 18
9
14
22
然後每一步就是m矩陣當前值加上當前位置左或上最小的數,可以得到整個dp矩陣。右下角的數為最小路徑。

int minPath(int **arr, const int &row, const int &col) {
	if (row == 0 || col == 0) {
		return -1;
	}
	if (row == 1 && col == 1) {
		return arr[0][0];
	}
	int **pathMatrix = new int *[row];
	for (int i = 0; i < row; ++i) {
		pathMatrix[i] = new int [col];
		memset(pathMatrix[i], 0, sizeof(int)*col);
	}
	pathMatrix[0][0] = arr[0][0];
	for (int i = 1; i < row; ++i) {
		pathMatrix[i][0] = arr[i][0] + pathMatrix[i - 1][0];
	}
	for (int i = 1; i < col; ++i) {
		pathMatrix[0][i] = arr[0][i] + pathMatrix[0][i - 1];
	}
	for (int i = 1; i < row; ++i) {
		for (int j = 1; j < col; ++j) {
			if (pathMatrix[i][j - 1] < pathMatrix[i - 1][j]) {
				pathMatrix[i][j] = pathMatrix[i][j - 1] + arr[i][j];
				cout << pathMatrix[i][j] << "  ";
			}
			else {
				pathMatrix[i][j] = pathMatrix[i - 1][j] + arr[i][j];
				cout << pathMatrix[i][j] << "  ";
			}
		}
		cout << endl;
	}
	int result = pathMatrix[row - 1][col - 1];
	delete[] pathMatrix;
	return result;
}

例題三

給定一個數組arr,返回arr的最長遞增子序列長度,比如arr=[2,1,5,3,6,4,8,9,7],最長遞增子序列為[1,3,4,8,9],所以返回這個子序列的長度為5.
分析:定義一個數組dp,dp[i]表示必須在以arr[i]這個數結尾的情況下,arr[0,…,i]中的最大遞增子序列長度。

int maxIncreasingSubsequence(int *arr, const int &length) {
	if (length == 0) {
		return 0;
	}
	if (length == 1) {
		return 1;
	}
	int *dp = new int[length];
	dp[0] = 1;
	int result = 1;
	for (int i = 1; i < length; ++i) {
		int maxLength = 0;
		for (int j = 0; j < i; ++j) {
			if (arr[i] > arr[j]) {
				if (dp[j] > maxLength) {
					maxLength = dp[j];
				}
			}
		}
		dp[i] = maxLength + 1;
		cout << dp[i] << endl;
		if (dp[i] > result) {
			result = dp[i];
		}
	}
	delete[] dp;
	return result;
}

例題四

給定兩個字串str1與str2,返回兩個字串的最長公共子序列。例如,str1=“1A2C3D4B56”,str2=“B1D23CA45B6A”,“123456”或者“12C4B6”都是最長公共子序列,返回哪一個都行。
分析:假設str1的長度為M,str2的長度為N,生成一個大小為M*N的矩陣dp。dp[i][j]的含義是str1[0…i]與str2[0…j]的最長公共子序列的長度。
dp求法如下:

  1. dp的第一行,如果str2[i]==str1[0],則dp[i,i+1,…,M]=1;
  2. dp的第一列,如果str2[0]==str1[i],則dp[0][i,i+1,…,N]=1;
  3. dp[i][j]的值可能來源於三種情況:
    情況一:dp[i][j-1]
    情況二:dp[i-1][j]
    情況三:如果str1[i]=str[j], 則dp[i-1][j-1]+1
    選取上述三種情況最大的數。
int maxPublicSubsequence(std::string &str1, std::string &str2, std::vector<char> &ans) { //ans倒敘放著相同子序列
	int **dp = new int *[str1.length()];
	for (int i = 0; i < str1.length(); ++i) {
		dp[i] = new int[str2.length()];
		memset(dp[i], 0, sizeof(int)*str2.length());
	}
	for (int i = 0; i < str1.length(); ++i) {
		dp[i][0] = 0;
		if (str1[i] == str2[0]) {
			for (int j = i; j < str1.length(); ++j) {
				dp[j][0] = 1;
			}
			break;
		}
	}
	for (int i = 0; i < str2.length(); ++i) {
		dp[0][i] = 0;
		if (str2[i] == str1[0]) {
			for (int j = i; j < str2.length(); ++j) {
				dp[0][j] = 1;
			}
			break;
		}
	}
	for (int i = 1; i < str1.length(); ++i) {
		for (int j = 1; j < str2.length(); ++j) {
			int tem = dp[i - 1][j];
			if (tem < dp[i][j - 1]) {
				tem = dp[i][j - 1];
			}
			if (str1[i] == str2[j]) {
				if (tem < dp[i - 1][j - 1] + 1) {
					tem = dp[i - 1][j - 1] + 1;
				}
			}
			dp[i][j] = tem;
		}
	}
	int i = str1.length() - 1;
	int j = str2.length() - 1;
	while (i > 0 && j > 0) {
		if (dp[i][j] == dp[i - 1][j]) {
			--i;
		}
		else if (dp[i][j] == dp[i][j - 1]) {
			--j;
		}
		else if (dp[i][j] == dp[i - 1][j - 1] + 1) {
			ans.push_back(str1[i]);
			--j;
			--i;
		}
	}
	if (dp[i][j] > 0) {
		if (j == 0) {
			ans.push_back(str2[0]);
		}
		if (i == 0) {
			ans.push_back(str1[0]);
		}
	}


	int result = dp[str1.length() - 1][str2.length() - 1];
	delete[] dp;
	return result;
}

題目五

一個揹包有一定的承重W,有N件物品,每件都有自己的價值,記錄在陣列v中,也都有自己的重量,記錄在陣列w中,每件物品只能選擇要裝入揹包還是不裝入揹包,要求在不超過揹包承重的前提下,選出物品的總價值最大。
假設dp[x][y]表示前x件物品,不超過重量y的時候的最大價值。則第x件物品的情況如下:
第一種情況:如果選擇第x件物品,則前x-1件物品得到的重量不能超過y-w[x]。dp[x][y]=dp[x-1][y-w[x]] + v[x]。
第二種情況:如果不選擇第x件物品,則前x-1件物品得到的重量不能超過y。dp[x][y]=dp[x-1][y]。
則比較dp[x-1][y-w[x]]+v[x]與dp[x-1][y]的大小,來判斷是否放入第x件物品。

逆向回推判斷裝入物品種類:

	int j = maxWeight;
	for (int i = objectNumber - 1; i > 0; --i) {
		if (dp[i][j] > dp[i - 1][j]) {
			count[i] += 1;
			j -= weights[i];
		}
	}
	if (dp[0][j] > 0) {
		count[0] = 1;
	}

完整程式碼

int knapsackProblem(const int &maxWeight, const int &objectNumber, const int *weights, const int *values, int *count) {//count 表示物品是否被裝入,0表示沒裝,1表示裝入
	if (maxWeight == 0) {
		return 1;
	}
	if (objectNumber == 0) {
		return 0;
	}
	int **dp = new int *[objectNumber];
	for (int i = 0; i < objectNumber; ++i) {
		dp[i] = new int[maxWeight + 1];
		memset(dp[i], 0, (maxWeight + 1) * sizeof(int));
	}
	for (int i = 0; i <= maxWeight + 1; ++i) {
		if (i / weights[0] > 0) {
			dp[0][i] = values[0];

		}
		else {
			dp[0][i] = 0;
		}
	}
	for (int i = 1; i < objectNumber; ++i) {
		for (int j = 0; j < maxWeight + 1; ++j) {
			if (j - weights[i] >= 0) {
				if (dp[i - 1][j] > dp[i - 1][j - weights[i]] + values[i]) {
					dp[i][j] = dp[i - 1][j];
				}
				else {
					dp[i][j] = dp[i - 1][j - weights[i]] + values[i];
				}
			}
			else {
				dp[i][j] = dp[i - 1][j];
			}
		}
	}
	int j = maxWeight;
	for (int i = objectNumber - 1; i > 0; --i) {
		if (dp[i][j] > dp[i - 1][j]) {
			count[i] += 1;
			j -= weights[i];
		}
	}
	if (dp[0][j] > 0) {
		count[0] = 1;
	}
	
	for (int i = 0; i < objectNumber; ++i) {
		for (int j = 0; j < maxWeight + 1; ++j) {
			cout << dp[i][j] << " ";
		}
		cout << endl;
	}
	int result = dp[objectNumber - 1][maxWeight];
	delete[] dp;
	return result;
}

題目六

給定兩個字串str1和str2,再給定三個整數ic,dc,rc分別表示插入,刪除和替換一個字元的代價。返回將str1編輯成str2的最小代價。比如,str1=“abc”,str2=“adc”,ic=5,dc=3,rc=2,最小代價為替換,2.
分析:str1長度為M,str2長度為N,生成一個dp[M+1][N+1]的矩陣,dp[i][j]表示將str1[0,…,i-1]編輯成str2[0,…,j-1]的最小代價。比如str1=“ab12cd3”,str2=“abcdf”。dp矩陣為
這裡寫圖片描述
第0行,表示依次插入的代價,第0列表示依次刪除的代價。dp[i][j]有以下四種情況。
情況一:dp[i-1][j]+dc=dp[i][j],刪除一個元素。
情況二:dp[i][j-1]+ic=dp[i][j],插入一個元素。
情況三:當str1[i]==str2[j]時,dp[i-1][j-1]=dp[i][j]。不用插入元素就能相等。
情況四:當str[i]!=str2[j]時,dp[i-1][j-1]+rc=dp[i][j],替換一個元素就相等。
比較上述四個情況,選取最小值。

int minCost(std::string str1, std::string str2, const int *cost, std::vector<std::string> &ans) { //cost依次為插入,刪除,替換 ,ans裡倒敘儲存操作
	int ic = cost[0];
	int dc = cost[1];
	int rc = cost[2];
	int **dp = new int *[str1.length()+1];
	for (int i = 0; i < str1.length()+1; ++i) {
		dp[i] = new int[str2.length()+1];
		memset(dp[i], 0, sizeof(int)*(str2.length()+1));
	}
	for (int i = 0; i < str2.length() + 1; ++i) {
		dp[0][i] = i * ic;
	}
	for (int j = 0; j < str1.length() + 1; ++j) {
		dp[j][0] = j * dc;
	}
	for (int i = 1; i < str1.length() + 1; ++i) {
		for (int j = 1; j < str2.length() + 1; ++j) {
			if (dp[i][j - 1] + ic < dp[i - 1][j] + dc) {
				dp[i][j] = dp[i][j - 1] + ic;
			}
			else {
				dp[i][j] = dp[i - 1][j] + dc;
			}
			if (str1[i-1] == str2[j-1]) {         //此處注意,因為dp[0][0]表示兩個字串都為空時,因此字串下表應當-1
				if (dp[i - 1][j - 1] < dp[i][j]) {
					dp[i][j] = dp[i - 1][j - 1];
				}
			}
			else {
				if (dp[i - 1][j - 1] + rc < dp[i][j]) {
					dp[i][j] = dp[i - 1][j - 1] + rc;
				}
			}
		}
	}
	for (int i = 0; i < str1.length() + 1;++i) {
		for (int j = 0; j < str2.length() + 1; ++j) {
			cout << dp[i][j] << " ";
		}
		cout << endl;
	}
	int i = str1.length();
	int j = str2.length();
	while (i > 0 && j > 0) {
		if (dp[i][j] == dp[i][j - 1] + ic) {
			ans.push_back("ic");
			--j;
		}
		else if (dp[i][i] == dp[j - 1][i] + dc) {
			ans.push_back("dc");
			--i;
		}
		else if (dp[i][j] == dp[i-1][j-1]){
			ans.push_back("pass");
			--i;
			--j;
		}
		else if(dp[i][j] == dp[i-1][j-1] + rc){
			ans.push_back("rc");
			--i;
			--j;
		}
	}
	if (dp[i][j] > 0) {
		while (i){
			ans.push_back("dc");
			--i;
		}
		while (j) {
			ans.push_back("ic");
			--j;
		}
	}
	int result = dp[str1.length()][str2.length()];
	delete[] dp;
	return result;
}