我们从递归聊到动态规划,很显然,动态规划还没结束,今天继续来聊从暴力递归到动态规划的套路。
一、背包问题
1、题目描述
有n种物品,w数组表示物品重量、v数组表示物品的价值,物品 i 的重量为 w[i],价值为 v[i],假定所有物品的重量和价值都是非负的,背包所能承受的最大重量为bag,在背包承重范围内,如何挑选物品使得价值最大,返回最大的价值。
2、思路1 从尝试开始
对于每一个货物,我都可以选择要或不要,所以最大价值必定在其中一个分支下,因为我们把所有情况都枚举了。
/**
* @author Java和算法学习:周一
*/
public static int maxValue(int[] w, int[] v, int bag) {
if (w == null || v == null || w.length != v.length || w.length == 0) {
return 0;
}
return process1(w, v, 0, bag);
}
/**
* 当前考虑到了index位置的货物,index前的已经做好选择了不能改,index及以后的还能随意选择
* 返回不超过背包容量的最大价值
*/
public static int process1(int[] w, int[] v, int index, int bag) {
// 背包已经装不下了
if (bag < 0) {
return -1;
}
// 当前已经没有货物可以选择了
if (index == w.length) {
return 0;
}
// 不要当前位置的货物
int p1 = process1(w, v, index + 1, bag);
// 要当前位置的货物
int p2 = 0;
// 需要判断当前背包减去当前货物后的容量,小于0则也不能要当前货物
int next = process1(w, v, index + 1, bag - w[index]);
if (next != -1) {
p2 = v[index] + next;
}
return Math.max(p1, p2);
}
3、动态规划版本
首先分析有无重复调用的过程。例如w = 5、2、3、4……v = 10、4、6、8
(1)分析可变参数
(2)定义动态规划数组,边界赋值
(3)将递归过程转换为动态规划
/**
* @author Java和算法学习:周一
*/
public static int dp(int[] w, int[] v, int bag) {
if (w == null || v == null || w.length != v.length || w.length == 0) {
return 0;
}
int N = w.length;
// 根据可变参数index和bag定义动态规划数组
int[][] dp = new int[N + 1][bag + 1];
// bag < 0 时,返回值-1表示无效值可以不用管,因为bag最小值就是0,在数组下标就不会小于0
// index == w.length时,返回值0,因为int默认就是0,所以不用手动设置
// 根据递归过程可知,当前index位置的值总是依赖index+1(下一行)位置的值,所以行index从下往上
// 同一行的bag之间相互不依赖,所以从任意位置遍历bag(列)均可
for (int index = N - 1; index >= 0; index--) {
for (int restBag = 0; restBag <= bag; restBag++) {
// 以下直接由暴力递归修改即可
int p1 = dp[index + 1][restBag];
int p2 = 0;
int next = restBag - w[index] < 0 ? -1 : dp[index + 1][restBag - w[index]];
if (next != -1) {
p2 = v[index] + next;
}
dp[index][restBag] = Math.max(p1, p2);
}
}
// 根据暴力递归的主函数调用process1(w, v, 0, bag)可得返回(0, bag)位置的值
return dp[0][bag];
}
所有代码:
二、数字转字母
1、题目描述
规定1和A对应、2和B对应、3和C对应……26和Z对应。那么一个数字字符串比如"111”就可以转化为:“AAA"、 "KA"和"AK"。给定一个只有数字字符组成的字符串str,返回有多少种转化结果。
2、从尝试开始
(1)分析转换任意 i 位置的字符时,i 能够到最后位置,说明之前的转换有效,产生了一种转换方法;如果 i 位置单独为0字符,说明之前的转换方法无效
(2)i 位置的字符可以选择自己单转,也可以和后面的字符一起转,但是二者数字字符的值必须小于等于26才对
/**
* @author Java和算法学习:周一
*/
public static int number(String string) {
if (string == null || string.length() == 0) {
return 0;
}
return process1(string.toCharArray(), 0);
}
/**
* arr[0...i)上的字符已经转换完成,无需修改
* arr[i...)以后的字符还能自由转换
*
* @param arr 待转换的数组
* @param i 当前转换的位置
* @return arr的i位置及以后还有多少种转换方法
*/
private static int process1(char[] arr, int i) {
// i已经到了最后位置了,说明之前的转换有效,产生了一种转换方法
if (i == arr.length) {
return 1;
}
// 当前单独面对0字符,说明之前的选择不对
if (arr[i] == '0') {
return 0;
}
// i位置单独转换
int p1 = process1(arr, i + 1);
// i位置和i+1位置一起转换
// i+1存在,且i和i+1组成的数字字符小于27
int p2 = i + 1 < arr.length && (arr[i] - '0') * 10 + (arr[i + 1] - '0') < 27 ? process1(arr, i + 2) : 0;
return p1 + p2;
}
3、动态规划
(1)分析可变参数
暴力递归的可变参数只有每次转换的位置 i,i 的取值范围为[0, arr.length]
(2)定义动态规划数组,根据base case 进行边界赋值
1)由第(1)步可以定义动态规划数组为:
int n = arr.length;
int[] dp = new int[n + 1];
2)base case为
// i已经到了最后位置了,说明之前的转换有效,产生了一种转换方法
if (i == arr.length) {
return 1;
}
所以,边界赋值dp[n] = 1;
(3)将递归过程转换为动态规划
即将以下过程的process1过程转换为获取dp数组的值
// 当前单独面对0字符,说明之前的选择不对
if (arr[i] == '0') {
return 0;
}
// i位置单独转换
int p1 = process1(arr, i + 1);
// i位置和i+1位置一起转换
// i+1存在,且i和i+1组成的数字字符小于27
int p2 = i + 1 < arr.length && (arr[i] - '0') * 10 + (arr[i + 1] - '0') < 27 ? process1(arr, i + 2) : 0;
return p1 + p2;
转换后即为:
if (arr[i] != '0') {
int p1 = dp[i + 1];
int p2 = i + 1 < n && (arr[i] - '0') * 10 + (arr[i + 1] - '0') < 27 ? dp[i + 2] : 0;
dp[i] = p1 + p2;
}
(4)根据暴力递归主函数的调用,确定动态规划的返回值
暴力递归主函数的调用如下:
public static int number(String string) {
if (string == null || string.length() == 0) {
return 0;
}
return process1(string.toCharArray(), 0);
}
所以,动态规划的返回值为dp[0]
(5)最后整个动态规划的代码如下:
/**
* 直接由上面的暴力递归修改
*
* @author Java和算法学习:周一
*/
public static int dp(String s) {
if (s == null || s.length() == 0) {
return 0;
}
char[] arr = s.toCharArray();
// 1.根据暴力递归的可变参数i的取值范围,确定动态规划的数组大小
int n = arr.length;
int[] dp = new int[n + 1];
// 2.根据暴力递归的 base case 进行边界赋值
dp[n] = 1;
// 3.将暴力递归的递归过程转换为动态规划
for (int i = n - 1; i >= 0; i--) {
if (arr[i] != '0') {
int p1 = dp[i + 1];
int p2 = i + 1 < n && (arr[i] - '0') * 10 + (arr[i + 1] - '0') < 27 ? dp[i + 2] : 0;
dp[i] = p1 + p2;
}
}
// 4.根据暴力递归主函数的调用,确定动态规划的返回值
return dp[0];
}
所有代码: