动态规划
1 问题导入
7
3
8
8
1
0
2
7
4
4
4
5
2
6
5
7\\ 3\space8\\ 8\space1\space0\\ 2\space7\space4\space4\\ 4\space5\space2\space6\space5\\
73 88 1 02 7 4 44 5 2 6 5
在上面的数字三角形总寻找一条从顶部到底边的路径,使得路径上所经过的数字之和最大。路径上的每一步都只能往下或者往右。只需要求出这个最大和即可,不必给出具体路径。三角形的行数大于1小于等于100,数字为0~99。
**求解思路:**使用二维数组存放数字三角形,D(r,j)
表示第r
行第j
个数字,MaxSum(r,j)
表示从D(r,j)
到底边的各条路径中,最佳路径的数字之和。
-
解法一:递归求解
-
从
D(r,j)
出发,下一步只能走D(r+1,j)
或者D(r+1,j+1)
。故对于N
行的三角形有if(r==N) { MaxSum(r,j) = D(r,j); } else { MaxSum(r,j) = Max(MaxSum(r+1,j),MaxSum(r+1,j+1)) + D(r,j); }
-
可以求解但是有大量的重复计算,会超时。
-
-
解法二:去除重复计算,使递归具有记忆功能
- 在上面的递归程序中,
MaxSum(i,j)
会被重复计算很多次,这也是导致程序超时的主要原因。如果每算出一个MaxSum(r,j)
就保存起来,下次使用时直接访问该值即可,就可以避免重复计算,此时可在 O ( n 2 ) O(n^2) O(n2)时间内完成
- 在上面的递归程序中,
-
解法三:递推
-
容易知道最后一行数字到底边的最大距离之和就等于该数字本身,倒数第二行数字到底边最大距离等于该数字本身+在最后一行中,该数字正下方的数字或该数字右下方的数字的最大值,这样即可算出倒数第二行数字到底边距离的最大值…一直递推即可得到
MaxSum(1,1)
。如下表所示30 23 21 20 13 10 7 12 10 10 4 5 2 6 5 -
该方法采用双重循环即可完成,注意从最后一行开始计算,采用二维数组存放每个数字到底边最大距离的值。
-
空间优化
- 空间优化一:采用一维数组存放结果,观察上表可以发现在计算完倒数第二行的第一个元素
7
后,可以直接将其存放在最后一行元素4
的位置,因为4
之后再也不会被用到,不会对结果产生影响。 - 空间优化二:一维数组都不用使用,使用一个指针,指向矩阵
D
的最后一行,直接将结果存放在矩阵D
的最后一行即可
- 空间优化一:采用一维数组存放结果,观察上表可以发现在计算完倒数第二行的第一个元素
-
示例代码
-
递归
#include <iostream> #include <algorithm> #define MAX 101 using namespace std; int D[MAX][MAX]; int n; int MaxSum(int i, int j) { if(i == n) { return D[i][j]; } int x = MaxSum(i+1,j); int y = MaxSum(i+1,j+1); return max(x,y) + D[i][j]; } int main() { int i,j; cin >> n; for(i=1; i<=n; i++) { for(j=1; j<=i; j++) { cin >> D[i][j]; } } cout<< MaxSum(1,1) <<endl; return 0; }
-
递归记忆型
#include <iostream> #include <algorithm> #define MAX 101 using namespace std; int D[MAX][MAX]; int n; int maxSum[MAX][MAX]; int MaxSum(int i, int j) { if(maxSum[i][j] != -1) { return maxSum[i][j]; } if(i == n) { maxSum[i][j] = D[i][j]; } else { int x = MaxSum(i+1,j); int y = MaxSum(i+1,j+1); maxSum[i][j] = max(x,y) + D[i][j]; } return maxSum[i][j]; } int main() { int i,j; cin >> n; for(i=1; i<=n; i++) { for(j=1; j<=i; j++) { cin >> D[i][j]; maxSum[i][j] = -1; } } cout<< MaxSum(1,1) <<endl; return 0; }
-
动态规划
#include <iostream> #include <algorithm> using namespace std; #define MAX 101 int D[MAX][MAX]; int n; int maxSum[MAX][MAX]; int main() { int i,j; cin >> n; for(i=1; i<=n; i++) { for(j=1; j<=i; j++) { cin >> D[i][j]; } } for(i=1; i<=n; i++) { maxSum[n][i] = D[n][i]; } for(i=n-1; i>=1; i--) { for(int j=1; j<=i; j++) { maxSum[i][j] = max(maxSum[i+1][j],maxSum[i+1][j+1]) + D[i][j]; } } cout<< maxSum[1][1] <<endl; }
-
空间优化
#include <iostream> #include <algorithm> using namespace std; #define MAX 101 int D[MAX][MAX]; int n; int * maxSum; int main() { int i,j; cin >> n; for(i=1; i<=n; i++) { for(j=1; j<=i; j++) { cin >> D[i][j]; } } maxSum = D[n]; // maxSum指向第n行 for(int i=n-1; i>=1; i--) { for(int j=1; j<=i; j++) { maxSum[j] = max(maxSum[j],maxSum[j+1]) + D[i][j]; } } cout<< maxSum[1] <<endl; return 0; }
2 动态规划解题思路
2.1 递归到动规的转化
一般来说,递归有n
个参数,就定义一个n
维的数组,数组的下标是递归函数参数的取值范围,数组元素的值是递归函数的返回值,这样就可以从边界值开始,逐步填充数组,相当于计算递归函数值的逆过程。
2.2 动规解题思路
- 将原问题分解为子问题
- 把原问题分解为若干子问题,子问题和原问题形式相同或类似,只不过规模变小了。子问题都解决,原问题即解决。
- 子问题的解一旦求出来就会被保存,所以每个子问题只需求解一次
- 确定状态
- 在用动态规划解题时,我们往往将和子问题相关的各个变量的一组取值,称之为一个“状态”。一个“状态”对应于一个或多个子问题,所谓的某个“状态”下的值,就是这个“状态”所对应的子问题的解。整个问题的时间复杂度是状态数目乘以计算每个状态所需的时间。
- 用动态规划解题时,经常碰到的情况是,
K
个整型变量能够构成一个状态。如果K
个整型变量的取值范围分别是N1,N2...Nk
,那么就可以使用一个K
维数组array[N1][N2]...[Nk]
来存储各个状态的“值”。这个“值”未必就是一个整数或者浮点数,可能是需要一个结构才能表示的,那么array
就可以是一个结构数组。一个“状态”下的“值”通常会是一个或多个子问题的解。
- 确定一些初始状态(边界状态)的值
- 确定状态转移方程
- 定义完什么是“状态”后,以及在该“状态”下的“值”后,就要找出不同状态之间是如何迁移(即如何从一个或多个“值”已知的“状态”,求另一个“状态”的“值”)。状态的迁移可以用递推公式表示,也称为“状态转移方程”。
2.3 能用动规解决的问题的特点
- 问题具有最优子结构的性质。如果问题的最优解所包含的子问题的解也是最优的,我们就称该问题具有最优子结构性质。
- 无后效型。当前的若干个状态一旦确定,则此后过程的演变就只和这若干个状态的值有关,和之前是采用哪种手段或经过哪条路径演变到当前的这若干个状态,没有关系。
3 基本例题
case 1:最长上升子序列
**定义:**最长上升子序列(Longest Increasing Subsequence),简称LIS,也有些情况求的是最长非降序子序列,二者区别就是序列中是否可以有相等的数。假设我们有一个序列 b i b_i bi,当 b 1 < b 2 < … < b n b_1 < b_2 < … < b_n b1<b2<…<bn的时候,我们称这个序列是上升的。对于给定的一个序列 ( a 1 , a 2 , … , a n ) (a_1, a_2, …, a_n) (a1,a2,…,an),我们也可以从中得到一些上升的子序列 ( a i 1 , a i 2 , … , a i k ) (a_{i1}, a_{i2}, …, a_{ik}) (ai1,ai2,…,aik),这里 1 < = i 1 < i 2 < … < i k < = N 1 <= i1 < i2 < … < ik <= N 1<=i1<i2<…<ik<=N,但必须按照从前到后的顺序。比如,对于序列(1, 7, 3, 5, 9, 4, 8),我们就会得到一些上升的子序列,如(1, 7, 9), (3, 4, 8), (1, 3, 5, 8)等等,而这些子序列中最长的(如子序列(1, 3, 5, 8) ),它的长度为4,因此该序列的最长上升子序列长度为4。
**问题描述:**对于给定的序列,求出最长上升子序列的长度
求解思路
- 首先寻找子问题:求以
a
k
(
k
=
1
,
2
,
3...
N
)
a_k(k=1,2,3...N)
ak(k=1,2,3...N)为终点的最长上升子序列的长度,一个上升子序列中左右边的那个数,称为该子序列的“终点”。虽然这个子问题和原问题的形式并不完全一样,但是只要这
N
个子问题都解决了,那么这N
个子问题的解中,最大的那个就是整个问题的解。 - **确定状态:**子问题之和一个变量–数字的位置
k
有关。因此序列中数字的位置k
就是状态,而状态k
对应的“值”,就是以a_k
为“终点”的最长上升子序列的长度 - 确定状态转移方程:
maxLen(k)
表示以a_k
作为终点的最长上升子序列的长度。maxLen(k)
的值,就是在a_k
左边,“终点”数值小于a_k
,且长度最大的那个上升子序列的长度再加1
。因为a_k
左边任何“终点”小于a_k
的子序列,加上a_k
后就能形成一个更长的上升子序列。- 初始状态:
maxLen(1)=1
- 转移方程:
maxLen(k)=max{maxLen(i) if 1<=i<k && a_i<a_k && k!=1}+1
- 初始状态:
示例代码
#include <iostream>
#include <algorithm>
using namespace std;
const int MAXN = 1010;
int a[MAXN];
int maxLen[MAXN];
int main() {
int N;
cin >> N;
for(int i=1;i<=N;i++) {
cin>>a[i];
maxLen[i] = 1;
}
for(int i=2; i<=N; i++){ // 每次求以第i个数为终点的最长上升子序列
for(int j=1; j<i; j++) {// 查看以第j个数为终点的最长上升子序列
if(a[j] < a[i]) {
maxLen[i] = max(maxLen[i],maxLen[j]+1);
}
}
}
cout<< *max_element(maxLen+1,maxLen+N+1);
return 0;
}
case 2:最长公共子序列
**题目描述:**给出两个字符串,求出这样的一个最长的公共子序列的长度:子序列中的每个字符都能在两个原串中找到,而且每个字符的先后顺序和原串中的先后顺序一致
**求解思路:**设输入的两个串s1,s2
,MaxLen(i,j)
表示s1
左边i
个字符形成的子串与s2
左边j
个字符形成的子串的最长公共子序列的长度,MaxLen(i,j)
即为本题的“状态”。 本题即求:MaxLen(len(s1),len(s2))
-
状态转移方程的确定
-
边界条件:
MaxLen(0,n)=0
、MaxLen(n,0)=0
-
递推公式:
if (s1[i-1] == s2[j-1]) MaxLen(i,j) = MaxLen(i-1,j-1) + 1 else MaxLen(i,j) = max{MaxLen(i,j-1),MaxLen(i-1,j)}
**注意:**当
s1[i-1] != s2[j-1]
时,MaxLen(s1,s2)
不会比MaxLen(s1,s2_j-1),MaxLen(s1_i-1,s2)
两者之中的任何一个小,也不会比两者大
-
示例代码
#include <iostream>
#include <cstring>
using namespace std;
char sz1[1000];
char sz2[1000];
int maxLen[1000][1000];
int main() {
while(cin >> sz1 >> sz2) {
int length1 = strlen(sz1);
int length2 = strlen(sz2);
int nTmp;
int i,j;
// 边界条件的确定
for(i=0; i<=length1; i++) {
maxLen[i][0] = 0;
}
for(j=0; j<=length2; j++) {
maxLen[0][j] = 0;
}
for(i=1; i<=length1; i++) {
for(j=1; j<=length2; j++) {
if(sz1[i-1] == sz2[j-1]) {
maxLen[i][j] = maxLen[i-1][j-1] + 1;
} else {
maxLen[i][j] = max(maxLen[i-1][j],maxLen[i][j-1]);
}
}
}
cout<< maxLen[length1][length2] <<endl;
}
return 0;
}
case 3:最佳加法表达式
**题目描述:**有一个由1,2...9
组成的数字串,问如果将m
个加号插入到这个数字串中,在各种可能形成的表达式中,值最小的那个表达式的值是多少?
**解题思路:**假定数字串长度是n
,添完加号后,表达式的最后一个加号添加在第i
个数字后面,那么整个表达式的最小值,就等于在前i
个数字中插入m-1
个加号所能形成的最小值,加号第i+1
到第n
个数字所组成的数的值。
- 该求解方法将问题分解为了规模更小的子问题,并且形式和原问题的形式相同,可以采用带记忆功能的递归算法进行求解
- 总的时间复杂度: O ( m n 2 ) O(mn^2) O(mn2)
示例代码
#include <iostream>
#include <string>
#include <cstdio>
#include <cstdlib>
#define MAX 10
using namespace std;
string str;
int Number[MAX][MAX]; // 存储从第i个数字到第j个数字所组成的数
// 在n个数字中插入m个+号所能形成的表达式最小值
int V(int m, int n,string s) {
if(m == 0) {
return atoi(s.c_str()); // atoi():将字符串转化为整型
} else if(n < m+1) {
return -1;
} else {
int tmp = 100000;
for(int i=m; i<n; i++) {
s = str.substr(0,i); // 截取字符串
tmp = min(tmp,V(m-1,i,s)+Number[i][n-1]);
}
return tmp;
}
}
// 初始化Number矩阵,避免重复计算
void Init() {
for(int i=0; i<str.length(); i++) {
for(int j=i+1; j<=str.length(); j++) {
string s = str.substr(i,j-i);
int value = atoi(s.c_str());
Number[i][j-1] = value;
}
}
}
int main ()
{
getline(cin,str);
Init();
int m;
cin>>m;
cout<<V(m,str.length(),str);
}
case 4:滑雪
**问题描述:**滑雪区域由一个二维数组给出,数组中的每个数字代表点的高度。一个人可以从某个点滑向上下左右相邻的四个点之一,当且仅当高度小于该点时。问最长的滑雪路径长度为多少。输入有两部分组成,第一行输入两个整数R
和C
,分别表示该二维数组的行数和列数,下面是R
行,每行有C
个整数,代表高度h
,输出最长区域的长度
**解题思路:**使用递归的方式求解,L(i,j)
代表从点(i,j)
出发的最长滑行长度。一个点(i,j)
,如果周围没有比它低的点,则L(i,j)=1
;否则,找出递推公式,L(i,j)
等于(i,j)
周围四个点中,比(i,j)
低,且L
值最大的那个点的L
值+1
示例代码
#include <iostream>
#include <cmath>
#include <algorithm>
using namespace std;
const int MAX = 101;
int Road[MAX][MAX];
// 判断(i,j)周围是否存在比其小的点
bool HasLower(int i, int j, int R, int C) {
while(i!=0) {
if(Road[i-1][j] < Road[i][j]) {
return true;
} else {
break;
}
}
while(i!=R-1) {
if(Road[i+1][j] < Road[i][j]) {
return true;
} else {
break;
}
}
while(j!=0) {
if(Road[i][j-1] < Road[i][j]) {
return true;
} else {
break;
}
}
while(j!=C-1) {
if(Road[i][j+1] < Road[i][j]) {
return true;
} else {
break;
}
}
return false;
}
// 递归求解最长滑雪路径
int LongestPath(int i, int j, int R, int C) {
if(!HasLower(i,j,R,C)) {
return 1;
} else {
if(i == 0) {
if(j == 0) {
int tmp = 0;
if(Road[i][j] > Road[i][j+1]) {
tmp = max(LongestPath(i,j+1,R,C),tmp);
}
if(Road[i][j] > Road[i+1][j]) {
tmp = max(LongestPath(i+1,j,R,C),tmp);
}
return tmp + 1;
}else if(j == C-1) {
int tmp = 0;
if(Road[i][j] > Road[i][j-1]) {
tmp = max(LongestPath(i,j-1,R,C),tmp);
}
if(Road[i][j] > Road[i+1][j]) {
tmp = max(LongestPath(i+1,j,R,C),tmp);
}
return tmp + 1;
} else {
int tmp = 0;
if(Road[i][j] > Road[i][j+1]) {
tmp = max(LongestPath(i,j+1,R,C),tmp);
}
if(Road[i][j] > Road[i][j-1]) {
tmp = max(LongestPath(i,j-1,R,C),tmp);
}
if(Road[i][j] > Road[i+1][j]) {
tmp = max(LongestPath(i+1,j,R,C),tmp);
}
return tmp + 1;
}
} else if(i == R-1) {
if(j == 0) {
int tmp = 0;
if(Road[i][j] > Road[i][j+1]) {
tmp = max(LongestPath(i,j+1,R,C),tmp);
}
if(Road[i][j] > Road[i-1][j]) {
tmp = max(LongestPath(i-1,j,R,C),tmp);
}
return tmp + 1;
}else if(j == C-1) {
int tmp = 0;
if(Road[i][j] > Road[i][j-1]) {
tmp = max(LongestPath(i,j-1,R,C),tmp);
}
if(Road[i][j] > Road[i-1][j]) {
tmp = max(LongestPath(i-1,j,R,C),tmp);
}
return tmp + 1;
} else {
int tmp = 0;
if(Road[i][j] > Road[i][j+1]) {
tmp = max(LongestPath(i,j+1,R,C),tmp);
}
if(Road[i][j] > Road[i][j-1]) {
tmp = max(LongestPath(i,j-1,R,C),tmp);
}
if(Road[i][j] > Road[i-1][j]) {
tmp = max(LongestPath(i-1,j,R,C),tmp);
}
return tmp + 1;
}
} else if(j == 0) {
int tmp = 0;
if(Road[i][j] > Road[i-1][j]) {
tmp = max(LongestPath(i-1,j,R,C),tmp);
}
if(Road[i][j] > Road[i+1][j]) {
tmp = max(LongestPath(i+1,j,R,C),tmp);
}
if(Road[i][j] > Road[i][j+1]) {
tmp = max(LongestPath(i,j+1,R,C),tmp);
}
return tmp + 1;
} else if(j == C-1) {
int tmp = 0;
if(Road[i][j] > Road[i-1][j]) {
tmp = max(LongestPath(i-1,j,R,C),tmp);
}
if(Road[i][j] > Road[i+1][j]) {
tmp = max(LongestPath(i+1,j,R,C),tmp);
}
if(Road[i][j] > Road[i][j-1]) {
tmp = max(LongestPath(i,j-1,R,C),tmp);
}
return tmp + 1;
} else {
int tmp = 0;
if(Road[i][j] > Road[i-1][j]) {
tmp = max(LongestPath(i-1,j,R,C),tmp);
}
if(Road[i][j] > Road[i+1][j]) {
tmp = max(LongestPath(i+1,j,R,C),tmp);
}
if(Road[i][j] > Road[i][j-1]) {
tmp = max(LongestPath(i,j-1,R,C),tmp);
}
if(Road[i][j] > Road[i][j+1]) {
tmp = max(LongestPath(i,j+1,R,C),tmp);
}
return tmp + 1;
}
}
}
int main() {
int R,C;
cin>>R>>C;
for(int i=0; i<R; i++) {
for(int j=0; j<C; j++) {
cin >> Road[i][j];
}
}
int longestpath = 1;
for(int i=0; i<R; i++) {
for( int j=0; j<C; j++) {
longestpath = max(LongestPath(i,j,R,C), longestpath);
}
}
cout<<"最长滑雪路径的长度 : "<<longestpath<<endl;
}
case 4:神奇的口袋
**问题描述:**有一个神奇的口袋,总的容积是40,用这个口袋可以变出一些物品,这些物品的总体积必须是40。John
现在有n
个想要得到的物品,每个物品的体积分别是a1,a2...an
。John
可以从这些物品中选择一些,如果选出的物体总体积是40,那么利用这个神奇的口袋,John
就可以得到这些物品。现在的问题是,John
有多少种不同的选择物品的方式
求解思路
- 解法一:枚举每个物品是选还是不选,共有 2 20 2^{20} 220种情况
- 解法二:递归求解。将问题分解为从前
k
种物品中选择一些,凑成体积w
的做法,这是面临第k
个物品,就有两种选择:选还是不选。 - 解法三:动态规划。利用二维数组
Ways[i][j]
表示从前j
种物品中凑出体积i
的方法数目。首先对边界条件进行初始化,接着采用双重循环,结合之前子问题的解,填充二维数组Ways[i][j]
的的值。
示例代码
-
递归
#include <iostream> using namespace std; int a[30]; int N; int Ways(int w, int k) { //示从前k种物品里凑出体积w的方法数 if(w == 0) { // 体积为0,什么都不选,也是一种方法 return 1; } if(k <= 0) { // 没有物品 return 0; } return Ways(w,k-1) + Ways(w-a[k],k-1); } int main() { cin >> N; for(int i=1; i<=N; i++) { cin >> a[i]; } cout<<"共有 "<<Ways(40,N)<<" 种方法"<<endl; return 0; }
-
动态规划
#include <iostream> #include<string.h> using namespace std; int a[30]; int N; int Ways[40][30]; // Ways[i][j]表示从前j种物品里凑出体积i的方法数 int main() { cin >> N; memset(Ways,0,sizeof(Ways)); for(int i=1; i<=N; i++) { cin>>a[i]; Ways[0][i] = 1; } Ways[0][0] = 1; for(int w=1; w<=40; w++) { for(int k=1; k<=N; k++) { Ways[w][k] = Ways[w][k-1]; //不拿第k个物体 if(w-a[k] >= 0) { Ways[w][k] += Ways[w-a[k]][k-1]; // 拿第k个物体 } } } cout<<"共有 : "<<Ways[40][N]<<" 种方法"<<endl; return 0; }
case 5:背包问题
**问题描述:**有N
件物品和一个容积为M
的背包。第i
件物品的体积为w[i]
,价值是v[i]
。求解将哪些物品装入背包可使价值总和最大。每种物品只有一件,可以选择放或者不放。
**求解思路:**用F[i][j]
表示取前i
种物品,使它们总体积不超过j
的最优取法取得的价值总和。即要求F[N][M]
。将问题分解为子问题,用二维数组存储每一个子问题的解,以便后续直接使用,避免了重复计算。
-
边界条件:只有一个物体的情况下,如果物体体积小于最大容积,则装进去,此时最大价值就是该物体的价值;否则最大价值为0。在选择后面的物体时,可以用之前的数据结合递推式求解。
if(w[1] <= j) { F[1][j] = d[1]; } else { F[1][j] = 0; }
-
递推公式:下式表示在面对第
i
个物体时,有两种选择:装或者不装,F[i-1][j]
代表的是不装的时候的最大价值总和,F[i-1][j-w[i]]+d[i]
表示的是装的时候的最大价值总和if(j-w[i] >= 0) { F[i][j] = max(F[i-1][j],F[i-1][j-w[i]]+d[i]); }
示例代码
#include<iostream>
#include <algorithm>
using namespace std;
int main()
{
int w[5] = { 0 , 2 , 3 , 4 , 5 }; //商品的体积2、3、4、5
int v[5] = { 0 , 3 , 4 , 5 , 6 }; //商品的价值3、4、5、6
int bagV = 8; //背包大小
int dp[5][9] = { { 0 } }; //动态规划表
for (int i = 1; i <= 4; i++) {
for (int j = 1; j <= bagV; j++) {
if (j < w[i])
dp[i][j] = dp[i - 1][j];
else
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - w[i]] + v[i]);
}
}
//动态规划表的输出
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 9; j++) {
cout << dp[i][j] << ' ';
}
cout << endl;
}
return 0;
}
- 程序优化:该程序中采用了二维数组,当
N
和M
非常大的时候,空间消耗会是巨大的,会出现超内存的情况。在递推的时候我们发现,求解第i
行的数据时,只用到了第i-1
换行的数据,与其他数据无关。因此,我们可以采取一维数组进行存储,需要注意的一点是,每次求解需要从右往左进行,因为用到的两个数据存在于上一行的正上方和左边,如果从左往右,则会提前将这两个数据覆盖。
反思与总结
- 动态规划的思想来源于递归,在采用动态规划算法时,没有统一的方法,通常要根据具体问题具体分析。动态规划常用的两种形式如下
- 递归型:直观,容易编写代码,但是可能会因为递归层数太深导致爆栈,函数调用带来额外时间开销。无法使用滚动数组节省空间。总的来说,比递推型慢。
- 递推型:从已知推未知。效率高,有可能使用滚动数组节省空间。
- 不管采用上述两种方法中的哪一种,有两个条件是必须的,分别为边界条件和状态转移方程
- 动态规划,无非就是利用历史记录,来避免我们的重复计算。而这些历史记录,我们得需要一些变量来保存,一般是用一维数组或者二维数组来保存。动态规划解题的三大步骤
- 第一步骤:定义数组元素的含义,上面说了,我们会用一个数组,来保存历史数组,假设用一维数组
dp[]
吧。这个时候有一个非常非常重要的点,就是规定你这个数组元素的含义,例如你的dp[i]
是代表什么意思? - 第二步骤:找出数组元素之间的关系式,我觉得动态规划,还是有一点类似于我们高中学习时的归纳法的,当我们要计算
dp[n]
时,是可以利用dp[n-1],dp[n-2]…..dp[1]
,来推出dp[n]
的,也就是可以利用历史数据来推出新的元素值,所以我们要找出数组元素之间的关系式,例如dp[n] = dp[n-1] + dp[n-2]
,这个就是他们的关系式了。而这一步,也是最难的一步. - 第三步骤:找出初始值。学过数学归纳法的都知道,虽然我们知道了数组元素之间的关系式,例如
dp[n] = dp[n-1] + dp[n-2]
,我们可以通过dp[n-1]
和dp[n-2]
来计算dp[n]
,但是,我们得知道初始值啊,例如一直推下去的话,会由dp[3] = dp[2] + dp[1]
。而dp[2]
和dp[1]
是不能再分解的了,所以我们必须要能够直接获得dp[2]
和dp[1]
的值,而这,就是所谓的初始值。 - 由了初始值,并且有了数组元素之间的关系式,那么我们就可以得到
dp[n]
的值了,而dp[n]
的含义是由你来定义的,你想求什么,就定义它是什么,这样,这道题也就解出来了。
- 第一步骤:定义数组元素的含义,上面说了,我们会用一个数组,来保存历史数组,假设用一维数组