什么是动态规划方法?
- 其本质是利用申请的空间来记录每一个暴力搜索的计算结果,下次要使用结果的时候直接使用,而不在进行重复的递归过程。
- 动态规划规定每一种递归状态的计算顺序,依次进行计算。(而记忆搜索方法不规定计算顺序)
面试中遇到的暴力递归题目可以优化成动态规划方法的大体过程:
- 实现暴力递归方法。
- 在暴力搜索方法的函数中看看那些参数可以代表递归过程。
- 找到代表递归过程的参数之后,记忆化搜索的方法非常容易实现。
- 通过分析记忆化搜索方法的依赖路径,进而实现动态规划。
- 根据记忆化搜索方法改出动态规划方法,进而看看是否能化简,如果能化简,还能实现时间复杂度更低的动态规划方法。
动态规划方法的关键点:
- 最优子结构性质。
- 无后效性。指的是某状态下决策的受益,只与状态和决策相关,与到达该状态的方式无关。
- 子问题的重叠性。解决在暴力搜索过程中冗余计算的问题,这是动态规划算法的根本目的。
PS:之所以认为动态规划比较难,是因为对于暴力搜索不了解,导致对其优化过程不了解,找不出状态转移方程,画不出状态转移表,最终也无法写出动态规划。
此外:对于经典的动态规划问题如:硬币找零问题、0-1背包问题、LCS问题,直接记住动态规划的方法是什么样的,而不是从暴力过程快慢慢整理出来,因为这些经典问题是教材级别的问题,面试官希望面试者能直接说动态规划的过程。
1.硬币找零问题
题干:
有数组penny,penny中所有的值都为正数且不重复。每个值代表一种面值的货币,每种面值的货币可以使用任意张,再给定一个整数aim(小于等于1000)代表要找的钱数,求换钱有多少种方法。
给定数组penny及它的大小(小于等于50),同时给定一个整数aim,请返回有多少种方法可以凑成aim。
解答:申请一个数组dp[n][m],其中n代表货币的面值种类,m代表要找的钱数,则dp[i][j] 为使用一共i种货币,要找零钱为j的情况下换钱的方法,其状态转移方程为:dp[i][j] = dp[i - 1][j] + dp[i][j - penny[i],即是使用第i种货币和不使用第i种货币的情况总和。
1 int countWays(vector<int> penny, int n, int aim) { 2 // write code here 3 int dp[50][1001]; 4 for(int i = 0; i < n; i++){ 5 dp[i][0] = 1; 6 } 7 for(int j = 0; j <= aim; j++){ 8 if(j % penny[0] == 0) 9 dp[0][j] = 1; 10 else 11 dp[0][j] = 0; 12 } 13 14 for(int i = 1; i < n; i++){ 15 for(int j = 1; j <= aim; j++) 16 if(j >= penny[i]) 17 dp[i][j] = dp[i - 1][j] + dp[i][j - penny[i]]; 18 else 19 dp[i][j] = dp[i - 1][j]; 20 } 21 22 return dp[n - 1][aim]; 23 }
2.矩阵最小路径和
题干:
有一个矩阵map,它每个格子有一个权值。从左上角的格子开始每次只能向右或者向下走,最后到达右下角的位置,路径上所有的数字累加起来就是路径和,返回所有的路径中最小的路径和。
给定一个矩阵map及它的行数n和列数m,请返回最小路径和。保证行列数均小于等于100.
1 int getMin(vector<vector<int> > map, int n, int m) { 2 // write code here 3 //dp[i][j] 表示从左上角走到坐标为i,j的位置所经过的最短路径 4 int dp[100][100]; 5 dp[0][0] = map[0][0]; 6 for(int i = 1; i < m; i++) 7 dp[0][i] = dp[0][i - 1] + map[0][i]; 8 for(int i = 1; i < n; i++) 9 dp[i][0] = dp[i - 1][0] + map[i][0]; 10 11 for(int i = 1; i < n; i++){ 12 for(int j = 1; j < m; j++){ 13 dp[i][j] = min(dp[i - 1][j],dp[i][j - 1]) + map[i][j]; 14 } 15 } 16 17 return dp[n - 1][m - 1]; 18 } 19 20 /* 21 int min(int a,int b){ 22 return a < b ? a : b; 23 } 24 */
3.LIS
题干:
这是一个经典的LIS(即最长上升子序列)问题,请设计一个尽量优的解法求出序列的最长上升子序列的长度。
给定一个序列A及它的长度n(长度小于等于500),请返回LIS的长度。
1 int getLIS(vector<int> A, int n) { 2 // write code here 3 //dp[i]表示为以A[i]结尾的最长上升子序列的长度,最后再找出dp[i]的最大值 4 //dp[i] = dp[j] + 1(j >= 0 && j < i),且dp[j]为max 5 int dp[500] = {1}; 6 7 for(int i = 1; i < n; i++){ 8 //找出dp[j](j < i && j >= 0)中的最大值且满足A[j] < A[i](满足上升特性); 9 int curMax = 0; 10 for(int j = 0; j < i; j++){ 11 if(dp[j] > curMax && A[j] < A[i]) 12 curMax = dp[j]; 13 } 14 dp[i] = curMax + 1; 15 16 } 17 18 //找出dp数组中的最大值 19 int max = 0; 20 for(int i = 0; i < n; i++){ 21 if(dp[i] > max) 22 max = dp[i]; 23 } 24 25 return max; 26 }
4.LCS(最长公共子序列)
题干:
给定两个字符串A和B,返回两个字符串的最长公共子序列的长度。例如,A="1A2C3D4B56”,B="B1D23CA45B6A”,”123456"或者"12C4B6"都是最长公共子序列。
给定两个字符串A和B,同时给定两个串的长度n和m,请返回最长公共子序列的长度。保证两串长度均小于等于300。
1 int findLCS(string A, int n, string B, int m) { 2 // write code here 3 //dp[i][j]表示str1[0,1,...,i]和str2[0,1,...,j]的最长公共子序列长度 4 int dp[300][300] = {0}; 5 // 注意它们的初始化 6 for(int j = 0; j < m; j++){ 7 if(A[0] == B[j]){ 8 dp[0][j] = 1; 9 while(j++ < m) 10 dp[0][j] = 1; 11 break; 12 } 13 } 14 15 for(int i = 0; i < n; i++){ 16 if(A[i] == B[0]){ 17 dp[i][0] = 1; 18 while(i++ < n) 19 dp[i][0] = 1; 20 break; 21 } 22 } 23 24 for(int i = 1; i < n; i++){ 25 for(int j = 1; j < m; j++){ 26 if(A[i] == B[j]){ 27 dp[i][j] = dp[i - 1][j - 1] + 1; 28 } 29 else 30 dp[i][j] = max(dp[i - 1][j],dp[i][j - 1]); 31 } 32 } 33 34 return dp[n - 1][m - 1]; 35 36 }
或者这样,初始化上更简单
1 int findLCS(string A, int n, string B, int m) { 2 // write code here 3 //dp[i][j]表示str1[0,1,...,i]和str2[0,1,...,j]的最长公共子序列长度 4 int dp[301][301] = {0}; 5 // 注意它们的初始化,这样更简单 6 for(int j = 0; j < m; j++){ 7 dp[0][j] = 0; 8 } 9 10 for(int i = 0; i < n; i++){ 11 dp[i][0] = 0; 12 } 13 14 for(int i = 1; i <= n; i++){ 15 for(int j = 1; j <= m; j++){ 16 if(A[i] == B[j]){ 17 dp[i][j] = dp[i - 1][j - 1] + 1; 18 } 19 else 20 dp[i][j] = max(dp[i - 1][j],dp[i][j - 1]); 21 } 22 } 23 24 return dp[n - 1][m - 1]; 25 26 }
5.0-1背包问题
题干:
一个背包有一定的承重cap,有N件物品,每件都有自己的价值,记录在数组v中,也都有自己的重量,记录在数组w中,每件物品只能选择要装入背包还是不装入背包,要求在不超过背包承重的前提下,选出物品的总价值最大。
给定物品的重量w价值v及物品数n和承重cap。请返回最大总价值。
1 int maxValue(vector<int> w, vector<int> v, int n, int cap) { 2 // write code here 3 //dp[i][j]表示放前i件物品,背包容量为j时的最大价值 4 5 int dp[n][cap + 1]; //背包容量可以为cap 6 for(int i = 0; i < n; i++) 7 dp[i][0] = 0; 8 for(int j = 1; j <= cap; j++){ 9 if(j >= w[0]) 10 dp[0][j] = v[0]; 11 else 12 dp[0][j] = 0; 13 } 14 15 for(int i = 1; i < n; i++){ 16 for(int j = 1; j <= cap; j++){ //注意下标 17 if(j < w[i]) 18 dp[i][j] = dp[i - 1][j]; 19 else 20 dp[i][j] = max(dp[i - 1][j],dp[i - 1][j - w[i]] + v[i]); 21 } 22 } 23 24 25 return dp[n - 1][cap];
6.最优编辑
题干:
对于两个字符串A和B,我们需要进行插入、删除和修改操作将A串变为B串,定义c0,c1,c2分别为三种操作的代价,请设计一个高效算法,求出将A串变为B串所需要的最少代价。
给定两个字符串A和B,及它们的长度和三种操作代价,请返回将A串变为B串所需要的最小代价。保证两串长度均小于等于300,且三种代价值均小于等于100。
1 int findMinCost(string A, int n, string B, int m, int c0, int c1, int c2) { 2 // write code here 3 //dp[i][j]代表将str1(0,1,...,i-1)变为str2(0,1,2,...,j-1)的最小代价,注意有空字符串 4 int dp[n + 1][m + 1]; 5 dp[0][0] = 0; 6 for(int i = 1; i <= n; i++) //表示第一列 7 dp[i][0] = c1 * i; 8 for(int j = 1; j <= m; j++) //表示第一行 9 dp[0][j] = c0 * j; 10 for(int i = 1; i <= n; i++){ 11 for(int j = 1; j <= m; j++){ 12 if(A[i - 1] == B[j - 1]) 13 dp[i][j] = dp[i - 1][j - 1]; 14 else { 15 int min1 = min(dp[i - 1][j] + c1,dp[i][j - 1] + c0); 16 dp[i][j] = min(min1,dp[i - 1][j - 1] + c2); 17 } 18 } 19 } 20 21 return dp[n][m]; 22 23 }