动态规划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] << " ";
}
}
}