彻底搞懂“动态规划”

动态规划

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
      2321
      201310
      7121010
      45265
    • 该方法采用双重循环即可完成,注意从最后一行开始计算,采用二维数组存放每个数字到底边最大距离的值。

    • 空间优化

      • 空间优化一:采用一维数组存放结果,观察上表可以发现在计算完倒数第二行的第一个元素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,s2MaxLen(i,j)表示s1左边i个字符形成的子串与s2左边j个字符形成的子串的最长公共子序列的长度,MaxLen(i,j)即为本题的“状态”。 本题即求:MaxLen(len(s1),len(s2))

  • 状态转移方程的确定

    • 边界条件:MaxLen(0,n)=0MaxLen(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:滑雪

**问题描述:**滑雪区域由一个二维数组给出,数组中的每个数字代表点的高度。一个人可以从某个点滑向上下左右相邻的四个点之一,当且仅当高度小于该点时。问最长的滑雪路径长度为多少。输入有两部分组成,第一行输入两个整数RC,分别表示该二维数组的行数和列数,下面是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...anJohn可以从这些物品中选择一些,如果选出的物体总体积是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;
}
  • 程序优化:该程序中采用了二维数组,当NM非常大的时候,空间消耗会是巨大的,会出现超内存的情况。在递推的时候我们发现,求解第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] 的含义是由你来定义的,你想求什么,就定义它是什么,这样,这道题也就解出来了。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

steven_moyu

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

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

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

打赏作者

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

抵扣说明:

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

余额充值