# 第19节 暴力递归到动态规划(二)
暴力递归改动态规划步骤:
- 一定要尝试出暴力递归版本,设计暴力递归:重要原则+4种常见尝试模型
从左往右的尝试模型
范围上的尝试模型
多样本位置全对应的尝试模型
寻找业务限制的尝试模型
- 分析有无重复解
- 用记忆化搜索实现
- 分析可变参数范围
- 写出basecase程式
- 分析普遍位置依赖
- 最终推出严格表结构实现动态规划
- 继续看严格表结构是否可再优化
- 若是普遍位置求解部分已经是O(1),不用再优化了
- 若是普遍位置求解部分是需要遍历的,则尝试使用斜率优化、临近位置观察法优化
设计暴力递归过程的原则:
1)每一个可变参数的类型,一定不要比int类型更加复杂
2)原则1)可以违反,让类型突破到一维线性结构,那必须是单一可变参数 例如:贴纸问题
3)如果发现原则1)被违反,但不违反原则2),只需要做到记忆化搜索即可
4)可变参数的个数,能少则少
# 题目一
背包问题
从左往右的尝试模型
给定两个长度都为N的数组weights和values, weights[i]和values[i]分别代表 i号物品的重量和价值。 给定一个正数bag,表示一个载重bag的袋子, 你装的物品不能超过这个重量。 返回你能装下最多的价值是多少?
暴力递归代码:
package class19;
/**
* 背包问题
* @author yangyanchao
*
*/
public class Test01_Knapsack {
/**
* 背包问题-暴力递归方法
* @param weights
* @param values
* @param bag
* @return
*/
public static int maxValue1(int[] weights, int[] values, int bag) {
if(weights == null || weights.length == 0 || values == null || values.length == 0
|| weights.length != values.length || bag < 0 ) {
// 无效解
return -1;
}
return process(weights, values, 0, bag);
}
/**
* 递归过程
* @param weights
* @param values
* @param index 当前来到哪个物品
* @param bag 剩余可装载的数量
* @return
*/
private static int process(int[] weights, int[] values, int index, int bag) {
if(bag < 0) {
// 背包容量不够了,那么就是无效的解 一定要放在最前面
return -1;
}
if(index == weights.length) {
// 装载到最后没有东西可装了,那么最多的价值就是0
return 0;
}
// 要么不装index位置物品
int p1 = process(weights, values, index + 1, bag);
int next = process(weights, values, index + 1, bag-weights[index]);
int p2 = 0;
if(next != -1) {
// 要么装index位置物品
p2 = values[index] + next;
}
return Math.max(p1, p2);
}
public static void main(String[] args) {
int[] weights = { 3, 2, 4, 7, 3, 1, 7 };
int[] values = { 5, 6, 3, 19, 12, 4, 2 };
int bag = 15;
System.out.println(maxValue1(weights, values, bag));
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
动态规划代码:
package class19;
/**
* 背包问题
* @author yangyanchao
*
*/
public class Test01_Knapsack {
/**
* 背包问题-动态规划方法
* @param weights
* @param values
* @param bag
* @return
*/
public static int maxValue2(int[] weights, int[] values, int bag) {
if(weights == null || weights.length == 0 || values == null || values.length == 0
|| weights.length != values.length || bag < 0 ) {
// 无效解
return -1;
}
int N = weights.length;
int[][] dp = new int[N + 1][bag + 1];
// 第N行 已经是0了 index == weights.length -> 0
for(int i = N - 1; i >= 0; i--) {
for(int j = 0; j <= bag; j++) {
int p1 = dp[i + 1][j];
int p2 = 0;
int next = j - weights[i] < 0 ? -1 : dp[i + 1][j - weights[i]];
if(next != -1) {
p2 = values[i] + next;
}
dp[i][j] = Math.max(p1, p2);
}
}
return dp[0][bag];
}
public static void main(String[] args) {
int[] weights = { 3, 2, 4, 7, 3, 1, 7 };
int[] values = { 5, 6, 3, 19, 12, 4, 2 };
int bag = 15;
System.out.println(maxValue2(weights, values, bag));
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# 题目二
数字串转化为字符串问题
从左往右的尝试模型
规定1和A对应、2和B对应、3和C对应...26和Z对应 那么一个数字字符串比如"111”就可以转化为: "AAA"、"KA"和"AK" 给定一个只有数字字符组成的字符串str,返回有多少种转化结果
暴力递归不优化版本代码:
package class19;
/**
* -数字串转化为字符串问题
* -规定1和A对应、2和B对应、3和C对应...26和Z对应
* -那么一个数字字符串比如"111”就可以转化为:
* -"AAA"、"KA"和"AK"
* -给定一个只有数字字符组成的字符串str,返回有多少种转化结果
* @author yangyanchao
*
*/
public class Test02_ConvertToLetterString {
/**
* -暴力递归不优化版本
* @param numStr
* @return
*/
public static int convertToLetterString1(String numStr) {
if(numStr == null || numStr.length() == 0) {
return 0;
}
char[] str = numStr.toCharArray();
return process(str, 0);
}
/**
* 递归过程
* @param str
* @param index 当前位置上考虑 返回转化结果数
* @return
*/
private static int process(char[] str, int index) {
if(index == str.length) {
// numStr所有字符都考察了一遍还没有违规的 就是一种转化结果
return 1;
}
if(str[index] == '0') {
// 该位置是0字符 无法转化为字母
return 0;
}
// 要么将index位置数字转化为字母进行转化
int ways = process(str, index + 1);
// 要么将index与index+1位置数字转化为字母进行转化
if(index + 1 < str.length && (str[index] - '0') * 10 + (str[index + 1] - '0') < 27) {
ways += process(str, index + 2);
}
return ways;
}
public static void main(String[] args) {
System.out.println(convertToLetterString1("305"));
System.out.println(convertToLetterString1("7210231231232031203123"));
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
动态规划代码:
package class19;
/**
* -数字串转化为字符串问题
* -规定1和A对应、2和B对应、3和C对应...26和Z对应
* -那么一个数字字符串比如"111”就可以转化为:
* -"AAA"、"KA"和"AK"
* -给定一个只有数字字符组成的字符串str,返回有多少种转化结果
* @author yangyanchao
*
*/
public class Test02_ConvertToLetterString {
/**
* -动态规划版本
* @param numStr
* @return
*/
public static int convertToLetterString2(String numStr) {
if(numStr == null || numStr.length() == 0) {
return 0;
}
char[] str = numStr.toCharArray();
int N = str.length;
int[] dp = new int[N + 1];
dp[N] = 1;
for(int index = N - 1; index >= 0; index--) {
if(str[index] != '0') {
int ways = dp[index + 1];
if(index + 1 < N && (str[index] - '0') * 10 + (str[index + 1] - '0') < 27) {
ways += dp[index + 2];
}
dp[index] = ways;
}
}
return dp[0];
}
public static void main(String[] args) {
System.out.println(convertToLetterString2("305"));
System.out.println(convertToLetterString2("7210231231232031203123"));
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# 题目三
贴纸问题
从左往右的尝试模型
给定一个字符串str,给定一个字符串类型的数组arr,出现的字符都是小写英文 arr每一个字符串,代表一张贴纸,你可以把单个字符剪开使用,目的是拼出str来 返回需要至少多少张贴纸可以完成这个任务。 例子:str= "babac",arr = {"ba","c","abcd"} ba + ba + c 3 abcd + abcd 2 abcd+ba 2 所以返回2
暴力递归不优化版本代码:
package class19;
/**
* -贴纸问题
* -给定一个字符串str,给定一个字符串类型的数组arr,出现的字符都是小写英文
* -arr每一个字符串,代表一张贴纸,你可以把单个字符剪开使用,目的是拼出str来
* -返回需要至少多少张贴纸可以完成这个任务。
* -例子:str= "babac",arr = {"ba","c","abcd"}
* -ba + ba + c 3 abcd + abcd 2 abcd+ba 2
* -所以返回2
* @author yangyanchao
*
*/
//本题测试链接:https://leetcode.com/problems/stickers-to-spell-word
public class Test03_StickersToSpellWord {
/**
* 贴纸问题-暴力递归不优化版本
* @param stickers
* @param target
* @return
*/
public static int minStickers1(String[] stickers, String target) {
if(stickers == null || stickers.length == 0 || target == null || target.length() == 0) {
return 0;
}
int ans = process1(stickers, target);
return ans == Integer.MAX_VALUE ? -1 : ans;
}
/**
* 递归过程
* @param stickers
* @param target 剩余的目标字符串
* @return
*/
private static int process1(String[] stickers, String target) {
if(target.length() == 0) {
// 目标字符串是空字符串我需要0张贴纸就搞定了
return 0;
}
// 目标字符串不为空,则我拿第一张贴纸来消除目标字符串
int min = Integer.MAX_VALUE;
for(int i = 0; i < stickers.length; i++) {
String rest = clearUp(stickers[i], target);
if(rest.length() != target.length()) {
// 消除了一些字符
min = Math.min(min, process1(stickers, rest));
}
}
return min + (min == Integer.MAX_VALUE ? 0 : 1);
}
/**
* 使用sticker的字符清除rest字符
* @param sticker
* @param rest
* @return
*/
private static String clearUp(String sticker, String target) {
char[] st = sticker.toCharArray();
char[] re = target.toCharArray();
int[] count = new int[26];
for(char ss : re) {
count[ss-'a']++;
}
for(char ss : st) {
count[ss-'a']--;
}
StringBuilder sb = new StringBuilder();
for(int i = 0; i < count.length; i++) {
if(count[i] > 0) {
for(int j = 0; j < count[i]; j++) {
sb.append((char) ('a' + i));
}
}
}
return sb.toString();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
暴力递归优化版本代码:
package class19;
/**
* -贴纸问题
* -给定一个字符串str,给定一个字符串类型的数组arr,出现的字符都是小写英文
* -arr每一个字符串,代表一张贴纸,你可以把单个字符剪开使用,目的是拼出str来
* -返回需要至少多少张贴纸可以完成这个任务。
* -例子:str= "babac",arr = {"ba","c","abcd"}
* -ba + ba + c 3 abcd + abcd 2 abcd+ba 2
* -所以返回2
* @author yangyanchao
*
*/
//本题测试链接:https://leetcode.com/problems/stickers-to-spell-word
public class Test03_StickersToSpellWord {
/**
* 贴纸问题-暴力递归优化版本
* 使用词频做stickers
* @param stickers
* @param target
* @return
*/
public static int minStickers2(String[] stickers, String target) {
if(stickers == null || stickers.length == 0 || target == null || target.length() == 0) {
return 0;
}
int N = stickers.length;
int[][] counts = new int[N][26];
for(int i = 0; i < N; i++) {
char[] chars = stickers[i].toCharArray();
for(int j = 0; j < chars.length; j++) {
counts[i][chars[j] - 'a']++;
}
}
int ans = process2(counts, target);
return ans == Integer.MAX_VALUE ? -1 : ans;
}
/**
* 使用counts除rest字符
* @param counts
* @param target
* @return
*/
private static int process2(int[][] counts, String target) {
if(target.length() == 0) {
return 0;
}
// target 做成-> 词频 targetCountcount 目标字符词频
char[] tarChars = target.toCharArray();
int[] targetCount = new int[26];
for(char cha : tarChars) {
targetCount[cha - 'a']++;
}
int min = Integer.MAX_VALUE;
for(int i = 0; i < counts.length; i++) {
// sticker-> 词频 stickerCount 贴纸词频
int[] stickerCount = counts[i];
if(stickerCount[tarChars[0] - 'a'] > 0) {
// 贴纸词频中有目标字符第一个字符 这步是重要优化与剪支
// 减小目标字符词频
for(int j = 0; j < stickerCount.length; j++) {
targetCount[j] -= stickerCount[j];
}
StringBuilder sb = new StringBuilder();
for(int j = 0; j < targetCount.length; j++) {
if(targetCount[j] > 0) {
for(int k = 0; k < targetCount[j]; k++) {
sb.append((char) ('a' + j));
}
}
}
min = Math.min(min, process2(counts, sb.toString()));
}
}
return min + (min == Integer.MAX_VALUE ? 0 : 1);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
记忆化搜索代码:
package class19;
import java.util.HashMap;
/**
* -贴纸问题
* -给定一个字符串str,给定一个字符串类型的数组arr,出现的字符都是小写英文
* -arr每一个字符串,代表一张贴纸,你可以把单个字符剪开使用,目的是拼出str来
* -返回需要至少多少张贴纸可以完成这个任务。
* -例子:str= "babac",arr = {"ba","c","abcd"}
* -ba + ba + c 3 abcd + abcd 2 abcd+ba 2
* -所以返回2
* @author yangyanchao
*
*/
//本题测试链接:https://leetcode.com/problems/stickers-to-spell-word
public class Test03_StickersToSpellWord {
/**
* 贴纸问题-记忆化搜索版本
* 使用词频做stickers
* @param stickers
* @param target
* @return
*/
public static int minStickers3(String[] stickers, String target) {
if(stickers == null || stickers.length == 0 || target == null || target.length() == 0) {
return 0;
}
int N = stickers.length;
int[][] counts = new int[N][26];
for(int i = 0; i < N; i++) {
char[] chars = stickers[i].toCharArray();
for(int j = 0; j < chars.length; j++) {
counts[i][chars[j] - 'a']++;
}
}
HashMap<String, Integer> dp = new HashMap<>();
dp.put("", 0);
int ans = process3(counts, target, dp);
return ans == Integer.MAX_VALUE ? -1 : ans;
}
/**
* 使用counts除rest字符
* @param counts
* @param target
* @return
*/
private static int process3(int[][] counts, String target, HashMap<String, Integer> dp) {
if(dp.containsKey(target)) {
return dp.get(target);
}
char[] tarChars = target.toCharArray();
int[] targetCount = new int[26];
for(char cha : tarChars) {
targetCount[cha - 'a']++;
}
int min = Integer.MAX_VALUE;
for(int i = 0; i < counts.length; i++) {
int[] stickerCount = counts[i];
if(stickerCount[tarChars[0] - 'a'] > 0) {
StringBuilder sb = new StringBuilder();
for(int j = 0; j < 26; j++) {
if(targetCount[j] > 0) {
int nums = targetCount[j] - stickerCount[j];
for(int k = 0; k < nums; k++) {
sb.append((char) ('a' + j));
}
}
}
min = Math.min(min, process3(counts, sb.toString(), dp));
}
}
int ans = min + (min == Integer.MAX_VALUE ? 0 : 1);
dp.put(target, ans);
return ans;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# 题目四
最长公共子序列问题
多样本位置全对应的尝试模型
给定两个字符串str1和str2, 返回这两个字符串的最长公共子序列长度
比如 : str1 = “a12b3c456d”,str2 = “1ef23ghi4j56k” 最长公共子序列是“123456”,所以返回长度6
暴力递归代码:
package class19;
/**
*- 最长公共子序列问题
*- 给定两个字符串str1和str2,
*- 返回这两个字符串的最长公共子序列长度
*- 比如 : str1 = “a12b3c456d”,str2 = “1ef23ghi4j56k”
*- 最长公共子序列是“123456”,所以返回长度6
* @author yangyanchao
*
*/
//链接:https://leetcode.com/problems/longest-common-subsequence/
public class Test04_LongestCommonSubsequence {
/**
* 最长公共子序列问题-暴力递归
* @param str1
* @param str2
* @return
*/
public static int longestCommonSubsequence1(String str1, String str2) {
if(str1 == null || str1.length() == 0 || str2 == null || str2.length() == 0) {
return 0;
}
int N = str1.length();
int M = str2.length();
char[] chars1 = str1.toCharArray();
char[] chars2 = str2.toCharArray();
return process1(chars1, chars2, N - 1, M - 1);
}
private static int process1(char[] chars1, char[] chars2, int n, int m) {
// n == 0 && m == 0 -> chars1[n] == chars2[m] ? 1 : 0
if(n == 0 && m == 0) {
return chars1[n] == chars2[m] ? 1 : 0;
}
if(n == 0) {
return chars1[n] == chars2[m] ? 1 : process1(chars1, chars2, n, m - 1);
}
if(m == 0) {
return chars1[n] == chars2[m] ? 1 : process1(chars1, chars2, n - 1, m);
}
// 要么最长公共子序列的结尾不是是chars1[n],但是可能是chars2[m]
int p1 = process1(chars1, chars2, n - 1, m);
int p2 = process1(chars1, chars2, n, m - 1);
int p3 = chars1[n] == chars2[m] ? 1 + process1(chars1, chars2, n - 1, m - 1) : 0;
return Math.max(p1, Math.max(p2, p3));
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
动态规划代码:
package class19;
/**
*- 最长公共子序列问题
*- 给定两个字符串str1和str2,
*- 返回这两个字符串的最长公共子序列长度
*- 比如 : str1 = “a12b3c456d”,str2 = “1ef23ghi4j56k”
*- 最长公共子序列是“123456”,所以返回长度6
* @author yangyanchao
*
*/
//链接:https://leetcode.com/problems/longest-common-subsequence/
public class Test04_LongestCommonSubsequence {
/**
* 最长公共子序列问题-动态规划
* @param str1
* @param str2
* @return
*/
public static int longestCommonSubsequence2(String str1, String str2) {
if(str1 == null || str1.length() == 0 || str2 == null || str2.length() == 0) {
return 0;
}
int N = str1.length();
int M = str2.length();
char[] chars1 = str1.toCharArray();
char[] chars2 = str2.toCharArray();
int [][] dp = new int[N][M];
dp[0][0] = chars1[0] == chars2[0] ? 1 : 0;
for(int i = 1; i < M; i++) {
dp[0][i] = chars1[0] == chars2[i] ? 1 : dp[0][i - 1];
}
for(int i = 1; i < N; i++) {
dp[i][0] = chars1[i] == chars2[0] ? 1 : dp[i - 1][0];
}
for(int n = 1; n < N; n++) {
for(int m = 1; m < M; m++) {
int p1 = dp[n - 1][m];
int p2 = dp[n][m - 1];
int p3 = chars1[n] == chars2[m] ? 1 + dp[n - 1][m - 1] : 0;
dp[n][m] = Math.max(p1, Math.max(p2, p3));
}
}
return dp[N - 1][M - 1];
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47