动态规划C++

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/weixin_37895339/article/details/79801064

引入题目

给定数组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;
}

猜你喜欢

转载自blog.csdn.net/weixin_37895339/article/details/79801064
今日推荐