详解动态规划法(包含完整可用的代码实例)

由于STL库更方便,本文用的是vector,没有用数组。

文中程序可以直接运行,可当做模板进行修改。

目录

一、动态规划算法思想

二、动态规划处理一维问题(以走台阶为例)

三、动态规划处理二维问题(以从矩阵左上角走到右下角最短路径问题为例)

四、动态规划求子序列(以求最长严格递增子序列长度为例)

五、最长公共子序列的长度

六、输出最长公共子序列


一、动态规划算法思想

  动态规划算法通常用于求解具有某种最优性质的问题。在这类问题中,可能会有许多可行解。每一个解都对应于一个值,我们希望找到具有最优值的解。动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。若用分治法来解这类问题,则分解得到的子问题数目太多,有些子问题被重复计算了很多次。如果我们能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,这样就可以避免大量的重复计算,节省时间。我们可以用一个表来记录所有已解的子问题的答案。不管该子问题以后是否被用到,只要它被计算过,就将其结果填入表中。这就是动态规划法的基本思路。具体的动态规划算法多种多样,但它们具有相同的填表格式。

分治法核心思想: 从上往下分析问题,大问题可以分解为子问题,子问题中还有更小的子问题

动态规划法思想:自底向上,推导递推公式,避免重复计算,降低计算量

二、动态规划处理一维问题(以走台阶为例)

问题描述:有10级台阶,一个人每次上一级或者两级,问有多少种走完10级台阶的方法。

解法:

因为问的是一共有多少种走法,则走到第n个台阶时的总走法应该是在第n-1级台阶时的总走法(再走一步一级跨越)加上在第n-2级台阶时的总走法(再走一步二级跨越),这里从上一步到第n台阶为什么不加1呢?因为从n-1台阶或者n-2台阶到n台阶就是一种走法。

1.设数组dp:每个位置存的值代表该台阶位置的总走法。(注意数组是从0开始的,即dp[0]代表台阶1)

2.分析边界条件:因为每次可以上一级或者两级,所以边界分析时需要考虑到两级台阶。当台阶数为1时走法为1(即走一级即毕),台阶数为2时走法为2(走两次一级和走一次二级)。

即:dp[0] = 1;    dp[1] = 2;

3.分析递归关系:对于任一台阶都可以分为通过两级或者一级到达。

即:dp[i] = dp[i - 1] + dp[i - 2];

4.遍历台阶:遍历台阶,数组的每个数值代表的是到该位置的总的走法,则数组最后一个位置的值就是总的走法。

#include <iostream>
#include<vector>
#include<string>
#include <unordered_map>
#include <unordered_set>
#include <queue>
#include <algorithm>//算法头文件
#include <numeric>
#include <stack>
#include<typeinfo>

using namespace std;

int getSteps(int n) {
	if (n < 1) return 0;
	if (n == 1) return 1;
	if (n == 2) return 2;
	vector<int> dp(n);
	dp[0] = 1;
	dp[1] = 2;
	for (int i = 2; i < n; i++) {
		dp[i] = dp[i - 1] + dp[i - 2];
	}
	return dp[n - 1];
}

int main()
{
	cout << getSteps(10) << endl;

	return 0;
}

三、动态规划处理二维问题(以从矩阵左上角走到右下角最短路径问题为例)

问题描述:给定一个矩阵m,从左上角开始每次只能向右走或者向下走,最后达到右下角的位置,路径中所有数字累加起来就是路径和,返回所有路径的最小路径和,如果给定的m如下,那么路径1,3,1,0,6,1,0就是最小路径和,返回11.

矩阵从左上角走到右下角
 01234
000000
101359
208135
305061
408840

 

解法:

对于矩阵中的每一个位置,由于要到达当前位置的方式只能是从左边或者是上边。所以当前位置的最小路径和应该是该位置左边或者上边中的路径和较小者加上当前位置的路径

1.设置数组:因为是二维矩阵,所以要设置二维数组,每个元素代表对应位置的路径和。

2.边界分析:因为当前位置只能通过其左边位置和上边位置达到,则最左边的一列(第1列)只能由上边位置到达;最上边行(第1行)只能由左边位置到达。故初始化边界条件:

	dp[0][0] = m[0][0];
	//初始化第一列
	for (int i = 1; i < row; i++) 
		dp[i][0] = dp[i - 1][0] + m[i][0];
	//初始化第一行
	for (int i = 1; i < col; i++)
		dp[0][i] = dp[0][i - 1] + m[0][i];

3.递推关系:对于每一个位置,可由其左边或上边的位置达到。

dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + m[i][j];

4.遍历矩阵:矩阵每个位置的数值代表的就是到该位置的最小路径和,则右下角的值就是最终的结果。

#include <iostream>
#include<vector>
#include<string>
#include <unordered_map>
#include <unordered_set>
#include <queue>
#include <algorithm>//算法头文件
#include <numeric>
#include <stack>
#include<typeinfo>

using namespace std;

int const x_length = 5, y_length = 5;
vector<vector<int>> m = {
	{0, 0, 0, 0, 0},
	{0, 1, 3, 5, 9},
	{0, 8, 1, 3, 5},
	{0, 5, 0, 6, 1},
	{0, 8, 8, 4, 0}
};

int getSteps(vector<vector<int>> &m) {
	if (m.empty()) return 0;
	int row = m.size();
	int col = m[0].size();
	vector<vector<int>> dp(row, vector<int>(col));
	/******边界初始化******/
	dp[0][0] = m[0][0];
	//初始化第一列
	for (int i = 1; i < row; i++) 
		dp[i][0] = dp[i - 1][0] + m[i][0];
	//初始化第一行
	for (int i = 1; i < col; i++)
		dp[0][i] = dp[0][i - 1] + m[0][i];

	/******递推关系********/
	for (int i = 1; i < row; i++) {
		for (int j = 1; j < col; j++) {
			dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + m[i][j];
		}
	}
	return dp[row - 1][col - 1];
}
int main()
{
	cout << getSteps(m) << endl;

	return 0;
}

四、动态规划求子序列(以求最长严格递增子序列长度为例)

问题描述:

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。子序列是由数组派生而来的序列。例如,nums = [10,9,2,5,3,7,101,18] 最长递增子序列是 [2,3,7,101],因此长度为 4 。

这里的「上升」是「严格上升」,例如: [2, 3, 3, 6, 7] 这样的子序列是不符合要求的。

题目只问最长上升子序列的长度,没有问最长上升子序列是什么,因此考虑使用动态规划。

第 1 步:状态定义。dp[i] 表示以 nums[i] 结尾的最长上升子序列的长度。即:在 [0, ..., i] 的范围内,选择以数字 nums[i] 结尾可以获得的最长上升子序列的长度。

说明:以 nums[i] 结尾,是子序列动态规划问题的经典设计状态思路,思想是动态规划的无后效性(定义得越具体,状态转移方程越好推导)。

第 2 步:推导状态转移方程:遍历到 nums[i] 的时候,我们应该把下标区间 [0, ... ,i - 1] 的 dp 值都看一遍,如果当前的数 nums[i] 大于之前的某个数,那么 nums[i] 就可以接在这个数后面形成一个更长的上升子序列。把前面的数都看了, dp[i] 就是它们的最大值加1。即比当前数要小的那些里头,找最大的,然后加 1。

状态转移方程即:dp[i] = max(1 + dp[j] if j < i and nums[j] < nums[i])

第 3 步:初始化。单独一个数是子序列,初始化的值为 1;

第 4 步:输出。应该扫描这个 dp 数组,其中最大值的就是题目要求的最长上升子序列的长度。

#include <iostream>
#include<vector>
#include<string>
#include <unordered_map>
#include <unordered_set>
#include <queue>
#include <algorithm>//算法头文件
#include <numeric>
#include <stack>
#include<typeinfo>

using namespace std;

vector<int> nums = { 10,9,2,5,3,7,101,18};

int getSteps(vector<int> nums) {
	int len = nums.size();
	if (len == 0) return 0;
	if (len < 2) return  nums[0];
	vector<int> dp(len, 1);//已经包含了初始边界条件
	for (int i = 1; i < len; i++) {
		int tmp = INT_MIN;
		for (int j = 0; j < i; j++) {
			if (nums[i] > nums[j])
				dp[i] = max(dp[j] + 1, dp[i]);
		}
	}
	return *max_element(dp.begin(), dp.end());
}
int main()
{
	cout << getSteps(nums) << endl;

	return 0;
}

 

五、最长公共子序列的长度

题目描述:

给定两个字符串str1和str2,返回两个字符串的最长公共子序列,例如:str1="1A2C3D4B56",str2="B1D23CA45B6A","123456"和"12C4B6"都是最长公共子序列,返回哪一个的长度都行。

做这种题,我们要用一个二维数组(dp[MAX_N][MAX_N])来存放每一个状态的值。那么,每一个网格的值是怎么来的呢。在这里我们把每一个状态即dp[i][j] 看做最长公共子序列的长度。由此我们,s1 … s(i+1) 和 t1 … t(j+1) 对应的公共子列长度可能是:

当s(i+1) == t(j+1),在 s1 … si 和 t1 … tj 的公共子列末尾追加上s(i+1) 。

否则则可能是 s1 … si 和 t1 … t(j+1) 的公共子列或者 s1 … s(i+1) 和 t1 … tj 的公共子列最大值。

对应以下一个公式:

#include <iostream>
#include<vector>
#include<string>
#include <unordered_map>
#include <unordered_set>
#include <queue>
#include <algorithm>//算法头文件
#include <numeric>
#include <stack>
#include<typeinfo>

using namespace std;

vector<int> nums = { 10,9,2,5,3,7,101,18};
string str1 = "asdf";
string str2 = "adfsd";

int getSteps(string &str1, string &str2) {
	int len1 = str1.size();
	int len2 = str2.size();
	vector<vector<int>> dp(len1 + 1, vector<int>(len2 + 1, 0));
	for (int i = 0; i < len1; i++) {
		for (int j = 0; j < len2; j++) {
			if (str2[j] == str1[i] )
				dp[i+1][j+1] = dp[i][j] + 1;
			else
				dp[i + 1][j + 1] = max(dp[i + 1][j], dp[i][j + 1]);
		}
	}
	return dp[len1][len2];

}
int main()
{
	cout << getSteps(str1, str2) << endl;

	return 0;
}

六、输出最长公共子序列

#include <string>
#include <iostream>
#ifndef MAX
#define MAX(X,Y) ((X>=Y)? X:Y)
#endif
using namespace std;
int **Lcs_length(string X, string Y, int **B)
{
	int x_len = X.length();
	int y_len = Y.length();
	int **C = new int *[x_len + 1];
	for (int i = 0; i <= x_len; i++)
	{
		C[i] = new int[y_len + 1];        //定义一个存放最优解的值的表;
	}
	for (int i = 0; i <= x_len; i++)
	{
		C[i][0] = 0;
		B[i][0] = -2;                     //-2表示没有方向
	}
	for (int j = 0; j <= y_len; j++)
	{
		C[0][j] = 0;
		B[0][j] = -2;
	}
	for (int i = 1; i <= x_len; i++)
	{
		for (int j = 1; j <= y_len; j++)
		{

			if (X[i - 1] == Y[j - 1])
			{
				C[i][j] = C[i - 1][j - 1] + 1;

				B[i][j] = 0;             //0表示斜向左上
			}
			else
			{
				if (C[i - 1][j] >= C[i][j - 1])
				{
					C[i][j] = C[i - 1][j];
					B[i][j] = -1;       //-1表示竖直向上;
				}
				else
				{
					C[i][j] = C[i][j - 1];
					B[i][j] = 1;        //1表示横向左
				}
			}

		}
	}
	return C;
}

void OutPutLCS(int **B, string X, int str1_len, int str2_len)
{
	if (str1_len == 0 || str2_len == 0)
	{
		return;
	}
	if (B[str1_len][str2_len] == 0)   //箭头斜向左上
	{
		OutPutLCS(B, X, str1_len - 1, str2_len - 1);
		cout << X[str1_len - 1] << endl;
	}
	else if (B[str1_len][str2_len] == -1)
	{
		OutPutLCS(B, X, str1_len - 1, str2_len);
	}
	else
	{
		OutPutLCS(B, X, str1_len, str2_len - 1);
	}
}

int main()
{
	string X = "1A2C3D4B56";
	string Y = "B1D23CA45B6A";

	int x_len = X.length();
	int y_len = Y.length();

	int **C;//定义一个二维数组

	int **B = new int *[x_len + 1];
	for (int i = 0; i <= x_len; i++)
	{
		B[i] = new int[y_len + 1];
	}
	C = Lcs_length(X, Y, B);
	for (int i = 0; i <= x_len; i++)
	{
		for (int j = 0; j <= y_len; j++)
		{
			cout << C[i][j] << " ";
		}
		cout << endl;
	}
	cout << endl;
	for (int i = 0; i <= x_len; i++)
	{
		for (int j = 0; j <= y_len; j++)
		{
			cout << B[i][j] << " ";
		}
		cout << endl;
	}
	OutPutLCS(B, X, x_len, y_len);//构造最优解
	system("pause");
	return 0;
}

 

  • 3
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

子木呀

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值