首页 > 技术文章 > 动态规划

coding-wtf 2016-08-23 18:57 原文

什么是动态规划方法?

  1. 其本质是利用申请的空间来记录每一个暴力搜索的计算结果,下次要使用结果的时候直接使用,而不在进行重复的递归过程。
  2. 动态规划规定每一种递归状态的计算顺序,依次进行计算。(而记忆搜索方法不规定计算顺序)

面试中遇到的暴力递归题目可以优化成动态规划方法的大体过程:

  1. 实现暴力递归方法。
  2. 在暴力搜索方法的函数中看看那些参数可以代表递归过程。
  3. 找到代表递归过程的参数之后,记忆化搜索的方法非常容易实现。
  4. 通过分析记忆化搜索方法的依赖路径,进而实现动态规划。
  5. 根据记忆化搜索方法改出动态规划方法,进而看看是否能化简,如果能化简,还能实现时间复杂度更低的动态规划方法。

动态规划方法的关键点:

  1. 最优子结构性质。
  2. 无后效性。指的是某状态下决策的受益,只与状态和决策相关,与到达该状态的方式无关。
  3. 子问题的重叠性。解决在暴力搜索过程中冗余计算的问题,这是动态规划算法的根本目的。

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"都是最长公共子序列。

给定两个字符串AB,同时给定两个串的长度nm,请返回最长公共子序列的长度。保证两串长度均小于等于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串所需要的最少代价。

给定两个字符串AB,及它们的长度和三种操作代价,请返回将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     }

 

推荐阅读