【数据结构与算法】动态规划|经典题目|算法模板|详细解释

动态规划Dynamic Programming

基本思想

把原始问题划分成一系列子问题,问题的最优解由子问题的最优解推导得到

算法特点
  • 整体问题最优解取决于子问题的最优解(状态转移方程)
  • 可分为多个相关子问题,子问题的解被重复使用,求解每个子问题仅一次,并将其结果保存在一个表中,以后用到时直接存取,不重复计算,节省计算时间
  • 自底向上地计算。

动态规划最关键的需要明确一下几个问题(以Fibonacci数列为例):

dp实例
dp数组所代表的意义dp[i]第i个Fibonacci数列数字
基础实例dp[0] = 0; dp[1] = 1
状态转移方程dp[i] = dp[i-1] + dp[i-2]
计算顺序递增i
所求目标dp[n]

递归与动态规划对比
在这里插入图片描述

经典题目

在这里插入图片描述

  • 问题描述
    从1号格子跳到n号格子。一次跳跃可以跳1~k个格子。每个格子有不同数量的金币,问如何跳跃才能使金币总数达到最大。(不能后跳)
  • 问题分析
    当前状态最后一步只能由它前面的1至k个格子所跳到,所以取前1~k个格子的最大值,依次往前推到起点。
    动态规划:不管过程怎么来,只看当前状态。
    (第i个格子的来源只能是第i-1,i-2,…i-k个格子,每步都取最大值)
dp实例
dp数组所代表的意义跳到第i个格子所能拿到的最大金币数
基础实例dp[1] = a[1];
状态转移方程dp[i]=max{dp[i-1], dp[i-1],…dp[i-k],if i-k>0}
计算顺序i++
所求目标dp[n]
  • AC代码
#include <iostream>
#include <cmath>
#include <algorithm>
using namespace std;
int n, k;
int a[10001], dp[10001];
int main() {
	cin >> n >> k;
	for (int i = 1; i < n - 1; i++) {
		cin >> a[i];
	}
	memset(dp, -10001, sizeof(dp));
	dp[0] = 0;
	for (int i = 1; i < n; i++) {
		int j = 1;
		while (j <= k && (i - j) >= 0) {
			dp[i] = max(dp[i], dp[i - j] + a[i]);
			j++;
		}
	}
	cout << dp[n - 1] << endl;
}

在这里插入图片描述

  • 问题描述
    在上一题的基础上,需要将最优路径打印出来
  • 问题分析
    用数组record[N]保存记录每个点之前最好的点,最终由最后一个格子倒推回起点。
  • AC代码
#include <iostream>
#include <cmath>
#include <algorithm>
using namespace std;
int n, k;
int a[10001], dp[10001];
int record[10001]; // //记录每个点之前最好的点
int main() {
	cin >> n >> k;
	for (int i = 1; i < n - 1; i++) {
		cin >> a[i];
	}
	memset(dp, -10001, sizeof(dp));
	dp[0] = 0;
	for (int i = 1; i < n; i++) {
		int j = 1;
		while (j <= k && (i - j) >= 0) {
			if (dp[i - j] + a[i] >= dp[i]) {
				dp[i] = dp[i - j] + a[i]; // 0 1 2 3 4
				record[i] = i-j;  // 0 0 1 1 3
			}
			j++;
		}
	}
	int i=n - 1;
	int result[10001];
	memset(result, 0, sizeof(result));
	int q = 1;
	result[0] = n-1;
	while (i > 0) {
		i = record[i];
		result[q++] = i; // //由最后一个点出发 向前依次找到最好的路径
	}
	cout << dp[n - 1] << endl;
	cout << q-1 << endl;
	for (i = q-1; i >= 0; i--) {
		cout << result[i]+1<<" ";
	}
}

在这里插入图片描述

  • 问题描述
    在NxM的棋盘上,从起点(1,1)跳到终点(N,M),每次只能向下或者向右跳,一共有多少种路径。
    在这里插入图片描述
  • 问题分析
    动态规划,只关注当前状态,当前最后一步只能由上边和左边的格子跳到。
dp实例
dp数组所代表的意义dp[i][j] 跳到(i,j)一共有多少种路径
基础实例dp[1][1] = 1; 起点到起点只有一种走法
状态转移方程dp[i]=max{dp[i-1], dp[i-1],…dp[i-k],if i-k>0}
每个格子只能由它左边或者上边的格子跳到。
所以跳到当前格子的路径数等于跳到左边的格子的路径数加上跳到上边的格子的路径数。
计算顺序i++,j++
所求目标dp[n][m]
  • AC代码
#include <iostream>
using namespace std;
int dp[1001][1001];
int main() {
	int n, m;
	cin >> n >> m;
	for (int i = 1; i <= n; i++) {
		for (int j = 1; j <= m; j++) {
			if (i == 1 && j == 1) {
				dp[i][j] = 1; //最左边和最上边的格子只有一种跳法
			}
			else {
				dp[i][j] = (dp[i - 1][j] + dp[i][j - 1]) % int(1e6 + 7); //防止溢出
			}
		}
	}
	cout << dp[n][m] << endl;
	return 0;
}

在这里插入图片描述

  • 问题描述
    在NxM的棋盘上,从起点(1,1)跳到终点(N,M),每次只能向下或者向右跳,每个格子有不同的金币数,问怎么跳能使跳到终点时拿到最多的金币 ,同时输出该路径(R为向右跳,D为向左跳)。
    在这里插入图片描述
  • 问题分析
    动态规划,只关注当前状态,当前最后一步只能由上边和左边的格子跳到,所以能拿到的最大值为两者中间的最大值加上自身的金币,并用record[i][j]数组记录跳到[i][j]前面的最好的点。
dp实例
dp数组所代表的意义dp[i][j] 跳到(i,j)可以拿到的最大金币数
基础实例dp[1][1] =a[1][1]; 起点只能拿到起点的金币
状态转移方程dp[i][j]=max(dp[i-1][j]+dp[i][j-1])+a[i][j]
每个格子只能由它左边或者上边的格子跳到。
所以跳到当前格子所能拿到的最大金币数等于跳到左边的格子所能拿到的最大金币数和跳到上边的格子所能拿到的最大金币数其中的最大值,在加上自己格子的金币数。
计算顺序i++,j++
所求目标dp[n][m]
  • AC代码
#include <iostream>
using namespace std;
int dp[1001][1001];
int a[1001][1001];
char record[1001][1001];
char result[100000];
int ans=0;
int main() {
	int n, m;
	cin >> n >> m;

	for (int i = 1; i <= n; i++) {
		for (int j = 1; j <= m; j++) {
			cin >> a[i][j];
		}
	}
	for (int i = 1; i <=n; i++) {
		for (int j = 1; j <=m; j++) {
			if (i == 1 && j == 1) {
				dp[i][j] = a[i][j];
				record[i][j] = '0';
			}
			else if (i == 1) {
				dp[i][j] = dp[i][j - 1] + a[i][j];
				record[i][j] = 'R';
			}
			else if (j == 1) {
				dp[i][j] = dp[i-1][j] + a[i][j];
				record[i][j] = 'D';
			}
			else {
				if (dp[i - 1][j] >=dp[i][j - 1]) {
					dp[i][j] = dp[i - 1][j]+a[i][j];
					record[i][j] = 'D';
				}
				else {
					dp[i][j] = dp[i][j-1] + a[i][j];
					record[i][j] = 'R';
				}
			}
			
		}
	}
	cout << (dp[n][m])<< endl;
	
	int l = 0;
	int a = n;
	int b = m;
	while (record[a][b]!='0') {/ //由最后一个点出发 向前依次找到最好的路径
		result[l++] = record[a][b];
		if (record[a][b] == 'R') {
			b -= 1;
		}
		else {
			a -= 1;
		}
	}
	for (int i = n + m - 3; i >= 0; i--) {
		cout << result[i];
	}
	return 0;
}

在这里插入图片描述

  • 问题描述
    寻找最长上升子序列Longest Increasing Sequence(LIS)
    a[1],a[2] ……a[n-1],a[n]
    动态规划:减而治之,只关注当前状态
  • 问题分析
dp实例
dp数组所代表的意义dp[i] 表示以a[i]结尾的LIS长度
基础实例dp[1] =1; 只有一个字符
状态转移方程dp[i] = max(dp[j] + 1,1) if a[j]<a[i] 且j<i
当它前面的元素a[j]<a[i]时,则以a[i]结尾的LIS的长度必定等于以a[j]结尾的LIS的长度加一(即把a[i]算入),而a[i]前面可以有多个元素小于a[i],所以取这之间的最大值.
计算顺序i++
所求目标Max(dp[n])
把以每一个元素结尾的LIS长度都表示出来 取最大值即为这个序列的LIS
  • AC代码
#include <algorithm>
#define N 1000
using namespace std;
int a[N];
int dp[N];
int main() {
	int n;
	cin >> n;
	for (int i = 0; i < n; i++) {
		cin >> a[i];
	}
	dp[0] = 1;
	for (int i = 1; i < n; i++) {
		dp[i] = 1;
		for (int j = 0; j < i; j++) {
			if(a[i]>a[j]) dp[i] = max(dp[i],dp[j]+1);
		}
		
	}
	sort(dp,dp+n);
	cout << dp[n - 1] << endl;

}

在这里插入图片描述

  • 问题描述
    寻找最长公共子序列Longest Common Sequence(LCS)
    同样需要将子序列输出–record数组记录
  • 问题分析
    要找X 和 Y的LCS,首先考虑X的最后一个元素Xn和Y的最后一个元素Ym。
    1)如果 xn=ym,即X的最后一个元素与Y的最后一个元素相同,这说明该元素一定位于公共子序列中。因此,现在只需要找:LCS(Xn-1,Ym-1) (子问题)
    2)如果xn != ym,即序列X 和 序列Y 的最后一个元素不相等,说明最后一个元素不可能是最长公共子序列中的元素,则LCS中的最后一个元素可能则变为Xn-1或Ym-1。产两个子问题:LCS(Xn-1,Ym) 和 LCS(Xn,Ym-1)
    LCS(Xn-1,Ym)表示:最长公共序列可以在(x1,x2,…x(n-1)) 和 (y1,y2,…yn)中找。
    LCS(Xn,Ym-1)表示:最长公共序列可以在(x1,x2,…xn) 和 (y1,y2,…y(n-1))中找。
    3)求解上面两个子问题,得到的公共子序列谁最长,那谁就是 LCS(X,Y),
    4)不断将问题分解到基础实例。
dp实例
dp数组所代表的意义dp[i][j] X[1,2…i] 和 Y[1,2…j] 的最长公共子序列
基础实例dp[1] =1; 只有一个字符
状态转移方程dp[i][j] = 0 (i=0或j=0)
dp[i][j] =dp[i-1][j-1] +1 (x[i] = y[i])
dp[i][j] = max(dp[i-1][j] , dp[i][j-1]) (x[i]!=y[i])
计算顺序i++,j++
所求目标dp[N][M]
  • AC代码
#include <iostream>
#include <algorithm>
#define N 1005
using namespace std;
int a[N];
int b[N];
int dp[N][N];
char record[N][N];
int result[10000];
int n;
int m;
int main() {
	cin >> n;
	for (int i = 0; i < n; i++) {
		cin >> a[i];
	}
	cin >> m;
	for (int i = 0; i < m; i++) {
		cin >> b[i];
	}

	for (int i = 1; i <= n; i++) {
		for (int j = 1; j <= m; j++) {
			if (a[i-1] == b[j-1]) {
				dp[i][j] = dp[i - 1][j - 1] + 1;
				record[i][j] = 'A';
			}
			else {
				if (dp[i - 1][j] >= dp[i][j - 1]) {//向下走

					record[i][j] = 'D';
					dp[i][j] = dp[i - 1][j];
				}
				else {
					dp[i][j] = dp[i][j - 1]; //向右走

					record[i][j] = 'R';
				}
				
			}
		}
	}
	cout << dp[n][m] << endl;
	int l = 0;
	int i = n;
	int j = m;
//由最后一个点出发 向前依次找到最好的路径 取出对应的字符
	while (i>=1 &&j>=1) {
		if (record[i][j] =='A') {
			result[l++] = a[i - 1];
			i -= 1;
			j -= 1;
		}
		else {
			if (record[i][j] == 'D') { i -= 1; }
			else j -= 1;
		}
	}
	if (dp[n][m] == 0) ;
	else {
		for (int i = dp[n][m]-1; i>=0; i--) {
			cout << result[i] << " ";
		}
	}

}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值