前言
上一篇,介绍了动态规划DP的概念,以及暴力递归和动态规划的关系。这一篇,将介绍几道经典的动态规划题
1、台阶问题
题目
有n级台阶,一个人每次上一级或者两级,问有多少种可以走完n级台阶的方法
思路
想走到第n级台阶,有两种途径
1、在n-1级上一级
2、在n-2级上两级
因此,我们可以知道f(n) = f(n - 1) + f(n - 2) ,其实答案就是斐波那契数列
代码实现
递归版本
public static int step1(int n) {
// 对于第n级台阶方法 = f(n - 1) + f(n - 2)
if(n == 1 || n == 2) {
return n;
}
return step1(n - 1) + step1(n - 2);
}
动态规划版本
public static int step2(int n) {
if(n == 1 || n == 2) {
return n;
}
int[] dp = new int[1000];
dp[1] = 1;
dp[2] = 2;
for(int i = 3; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
2、最长递增子序列长度
题目
给定数组arr,返回arr的最长递增子序列长度。返回arr的最长递增子序列长度。
比如arr=[2,1,5,3,6,4,8,9,7],最长递增子序列为[1,3,4,8,9],
所以返回这个子序列的长度5
算法实现
我们构建一个以为数组dp,dp[i]的表示以arr[i]结尾时,最长递增子序列的长度。计算dp[i]时,先判断arr[i]是否大于arr[0..i],若大于,则找到dp[i..1]中最大的值+1,否则dp[i]置为1。最后返回dp数组中最大的数,即为最长递增子序列长度
public static int commonLongestAscSeq(int arr[]) {
if (arr == null) {
return 0;
}
if (arr.length == 1) {
return 1;
}
int max = 0;
// dp[i]代表以arr[i]为结尾的情况下,arr[0..i]的最大递增子序列长度
int[] dp = new int[arr.length];
dp[0] = 1;
for(int i = 1; i < arr.length; i++) {
// 用于标记arr[i]是否比arr[0~i-1]的数都小
boolean isSmallThanPre = true;
// 对于arr[i],我们要找0~i-1的最大递增子序列,然后在其基础上+1
for(int j = 0; j < i; j++) {
if(arr[i] > arr[j]) {
dp[i] = Math.max(dp[j] + 1, dp[i]);
isSmallThanPre = false;
}
}
// 如果都比i~1的数小,那么就置为1
if (isSmallThanPre) {
dp[i] = 1;
}
max = Math.max(dp[i], max);
}
return max;
}
3、01背包问题
题目
给定 n 种物品和一个容量为 bag的背包,物品 i 的重量是 weights[i],其价值为 values[i]
问:应该如何选择装入背包的物品,使得装入背包中的物品的总价值最大?
例如weights = { 2, 2, 6, 5, 4 },values = { 6, 3, 5, 4, 6 },bag=10,那么总价值最大为15,当选择v[0],v[1],v[4]时,6 + 3 + 6价值最大,重量为2 + 2 + 4 = 8。01背包问题就是每件物品最多拿一件,还有其延伸版本,完全背包问题等等,这里就不描述了。
思路
创建一个二维数组[物品的数量 + 1][背包的大小 + 1],dp[i][j]表示,在拿第i件物品时,j背包容量为j时,所能获得最大价值。那么对于dp[i][j],有两种情况
1、不拿这件物品时,dp[i][j]的价值更高 -> dp[i][j] = dp[i - 1][j]
2、拿这件物品时,dp[i][j]的价值更高,因此,我们在不拿i这件物品时(i - 1),且重量减去i物品重量( j - w[i])的最优解基础上 加上该物品的价值 -> dp[i][j] = v[i] + dp[i - 1][j - w[i - 1]]
得到了以上递推式后,我们就可以进行动态规划了
整个填表的过程如下所示:
算法实现
public static int maxValue2(int[] weights, int[] values, int bag) {
// dp[i][j]表示,在0~i件物品选择,j背包容量为j时,所能获得最大价值
int[][] dp = new int[weights.length + 1][bag + 1];
for(int i = 1; i < dp.length; i++) {
for(int j = 1; j <= bag; j++) {
// 容量不够放下i,所以不拿
if (j < weights[i - 1]) {
dp[i][j] = dp[i - 1][j];
}else {
int max1 = dp[i - 1][j];
int max2 = values[i - 1] + dp[i - 1][j - weights[i - 1]];
dp[i][j] = Math.max(max1, max2);
}
}
}
return dp[weights.length][bag];
}
4、最小代价
题目
给定两个字符str1和str2,再给定三个整数ic,dc和rc,分别代表插入、删除和替换一个
字符的代价。返回将str1编辑成str2的最小代价。
例如,str1="abc", str2="adc", ic=5,dc=3, rc=2。从"abc"->"adc",把"b"替换
成“d”的代价最小,所以返回2.
再比如str1="abc", str2="adc", ic=5,dc=3, rc=100。从"abc"->"adc",先删除"b",
然后插入"d"是代价最小的,所以返回8.
思路
创建一个二维数组dp[str1.len + 1][str2.len + 1],dp[i][j]代表str1[0..i] -> str2[0..j]的最小代价。
那么对于第一列代价都是删除代价
// 填写第一列,那么就是str1[0~i] -> ""
for(int i = 1; i <= chs1.length; i++) {
dp[i][0] = i * dc;
}
同理,对于第一行都是插入代价,那么就是"" -> str2[0~j]
// 填写第一行,那么就是"" -> str2[0~j]
for(int j = 1; j <= chs2.length; j++) {
dp[0][j] = j * ic;
}
那么而对于非第一行,第一列,dp[i][j]的值可能来自以下4中情况,注意,我们创建的dp数组是[str1.len + 1][str2.len + 1]的,因此求i,j的问题,我们需要回原数组,就需要i-1,j-1
1、【不操作】当str1[i - 1] == str2[i - 1],说明str1[0..i-1] -> str2[0..j-1]只需要str1[0..i-2] 编辑成 str2[0..j-2]的代价,也就是dp[i - 1][j - 1]
2、【删除】先把str1[0~i-2] 编辑成 str2[0~j-1],那么就删除str[i-1],因此此时花销为dp[i - 1][j] + dc
3、【插入】先把str1[0~i-1] 编辑成 str2[0~j-2],那么插入一个字符,使之相等,因此此时花销为ic + dp[i][j - 1]
4、【替换】先把str1[i - 2] 编辑成 str2[j - 2],那么直接替换str1[i - 1],使之相等,因此此时花销为dp[i - 1][j - 1] + rc
以str1 = "ab12cd3",str2 = "abcdf",ic=5,dc=3,rc=2为例,蓝色部分为初始的第一行,第一列的数字,整个填表的过程如下:
算法实现
public static int convertFirstToSecond(String str1, String str2, int ic, int dc, int rc) {
char[] chs1 = str1.toCharArray();
char[] chs2 = str2.toCharArray();
// dp[i][j]代表str1[0..i] -> str2[0..j]的最小代价
int[][] dp = new int[chs1.length + 1][chs2.length + 1];
// 填写第一列,那么就是str1[0~i] -> ""
for(int i = 1; i <= chs1.length; i++) {
dp[i][0] = i * dc;
}
// 填写第一行,那么就是"" -> str2[0~j]
for(int j = 1; j <= chs2.length; j++) {
dp[0][j] = j * ic;
}
for(int i = 1; i <= chs1.length; i++) {
for(int j = 1; j <= chs2.length; j++) {
if(chs1[i - 1] == chs2[j - 1]) {
// 说明str1[0~i-1] -> str2[0~j-1]只需要
// str1[0~i-2] -> str2[0~j-2]的代价,也就是dp[i - 1][j - 1]
dp[i][j] = dp[i - 1][j - 1];
}else {
// 若当前字符不等,则有三种情况
// 1、先把str1[0~i-2] 编辑成 str2[0~j-1],那么就删除str[i-1]
// 因此此时花销为dp[i - 1][j] + dc
int min1 = dc + dp[i - 1][j];
// 2、先把str1[0~i-1] 编辑成 str2[0~j-2],那么插入一个字符,使之相等
// 因此此时花销为ic + dp[i][j - 1]
int min2 = ic + dp[i][j - 1];
dp[i][j] = Math.min(min1, min2);
// 3、先把str1[i - 2] 编辑成 str2[j - 2],那么直接替换str1[i - 1],使之相等
// 因此此时花销为dp[i - 1][j - 1] + rc
int min3 = dp[i - 1][j - 1] + rc;
dp[i][j] = Math.min(dp[i][j], min3);
}
}
}
return dp[chs1.length][chs2.length];
}
总结
其实动态规划,也可以简单的认为是一个填表的过程,找出转换方程后,我们先填写显而易见的初始数值,然后再根据初始数字推出下一行的数据,直至整张表填完。然而有些动态规划的题目,不是那么直接的填表,例如,零钱问题的最优解。因此,下一篇就动态规划的读表展开~