一个大问题可以分解为许多小问题,小问题一步步的推导出大问题,且小问题会有很多重复的。
- DP问题通常可以先使用暴力递归的形式求解。
- 在暴力递归的规程中,对小问题的答案进行缓存,防止重复计算,成为记忆化搜索
- 根据暴力递归的控制参数决定dp数组含义,bad case 观察dp数组转移规律,使用具体例子来观察规律
- 构建dp数组,首先根据bad case构建已知的位置,其他位置转移时根据暴力递归的逻辑,将递归改为从dp数组中拿值
机器人路径
给定一个n,范围为0 ~ n-1,机器人一开始处于start位置,经过step步到达end位置的方法有几种?
- 暴力递归尝试
首先定义递归函数,当前处于cur位置,如果能经过step步到达end,则对count进行自增。
如果cur在两端,则只能向另一侧移动,否则可以向两边移动。
public void testRobotTrace() {int i = robotTrace(6, 2, 4, 4);System.out.println(i);}/*** 范围0 ~ n-1, 从start开始走step步到达end的路径数*/public int robotTrace(int n,int start,int step,int end){int trace = trace(n, end, step, start);System.out.println(trace == 4);return trace;}/*** @param cur 当前位置* @return 方法数*/private int trace(int n,int end,int step,int cur){if (step == 0){return cur == end ? 1 : 0;}// 左端点必须走右边if (cur == 0){return trace(n,end,step-1,1);}else// 右端点必须走左边if (cur == n-1){return trace(n,end,step-1,n-2);}else {int right = trace(n,end,step-1,cur+1);int left = trace(n,end,step-1,cur-1);return left + right;}}
- 分析包里递归是否包含重复子问题
我们举例n=6,start=2,end=4,step=4。
路径1:2-3-4-5-4
路径2:2-3-4-3-4
我们看到从4到达4,step=2时的子问题重复出现,因此可以使用缓存进行优化
- 基于缓存优化暴力递归子问题
cache数组为二维数组,cache[i][j]代表在i位置还剩j步能到达end的步数,初始的时候全部初始化为-1.
容量为n和step+1,因为我们的范围是0~n-1,step取值0~step
public void testRobotTrace() {int i = robotTrace(6, 2, 4, 4);System.out.println(i);}/*** 范围0 ~ n-1, 从start开始走step步到达end的路径数*/public int robotTrace(int n, int start, int step, int end) {int[][] cache = new int[n][step+1];for (int i = 0; i < cache.length; i++) {Arrays.fill(cache[i],-1);}int trace = traceCache(n, end, step, start,cache);System.out.println(trace == 4);return trace;}private int traceCache(int n, int end, int step, int cur, int[][] cache) {if (cache[cur][step] >= 0) {return cache[cur][step];}if (step == 0) {int count = cur == end ? 1 : 0;cache[cur][step] = count;return count;}int count = 0;if (cur == 0) {count = trace(n, end, step - 1, 1);} else if (cur == n - 1) {count = trace(n, end, step - 1, n - 2);} else {int right = trace(n, end, step - 1, cur + 1);int left = trace(n, end, step - 1, cur - 1);count = left + right;}cache[cur][step] = count;return count;}
- cache数组规律查找,构建dp数组dp[cur][step]
举例n=6,start=2,end=4,step=4
首先根据bad case设置对应值
if (step == 0) {int count = cur == end ? 1 : 0;cache[cur][step] = count;return count;}
| dp | 0 | 1 | 2 | 3 | 4 |
|---|---|---|---|---|---|
| 0 | 0 | ||||
| 1 | 0 | ||||
| 2 | 0 | ||||
| 3 | 0 | ||||
| 4 | 1 | ||||
| 5 | 0 |
然后根据我们的逻辑可以看出,转移过程是从step-1 到 step的,因此我们可以直接推出整个dp数组。
int count = 0;if (cur == 0) {count = trace(n, end, step - 1, 1);} else if (cur == n - 1) {count = trace(n, end, step - 1, n - 2);} else {int right = trace(n, end, step - 1, cur + 1);int left = trace(n, end, step - 1, cur - 1);count = left + right;}cache[cur][step] = count;return count;
| dp | 0 | 1 | 2 | 3 | 4 |
|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 | 1 |
| 1 | 0 | 0 | 0 | 1 | 0 |
| 2 | 0 | 0 | 1 | 0 | 4 |
| 3 | 0 | 1 | 0 | 3 | 0 |
| 4 | 1 | 0 | 2 | 0 | 5 |
| 5 | 0 | 1 | 0 | 2 | 0 |
public int robotTraceDp(int n, int start, int step, int end) {int[][] dp = new int[n][step+1];dp[end][0] = 1;for (int i = 1; i <= step; i++) {dp[0][i] = dp[1][i-1];for (int j = 1; j < n-1; j++) {dp[j][i] = dp[j+1][i-1] + dp[j-1][i-1];}dp[n-1][i] = dp[n-2][i-1];}return dp[start][step];}
我们返回值直接取我们的递归方法调用的形式,即返回dp[start][step]即可
游戏博弈
背包问题
有数组weight[]和数组value[],分别代表货物的重量和价值,现有容量为bag的背包,求背包能装货物的最大价值 ps: 0 < weight.length = value.length <1000 weight[i] >= 0 , value[i] >=0 bag >= 0
- 暴力递归尝试
朴素的自左向右/自顶向下的暴力递归方法,就是对条件数组进行遍历决策。背包问题只有两个决策,要或者不要,因此从左至右进行遍历货物选择要或者不要
/**** @param wight 货物重量数组* @param value 货物价值数组* @param bag 背包最大容量* @return 背包能装货物的最大价值*/public int maxValue(int[] wight, int[] value, int bag){return maxValue(wight,value,0,bag);}/**** @param wight 货物重量数组* @param value 货物价值数组* @param index 当前货物索引* @param bag 背包最大容量* @return 背包能装货物的最大价值*/public int maxValue(int[] wight, int[] value, int index, int bag){if (bag < 0){return 0;}if (index == wight.length){return 0;}// index货物不要int v1 = maxValue(wight,value,index+1,bag);// index货物要,但是不能超过背包容量// 如果超过了背包容量,这个选择是非法的,所以只能选择不要// 这里让v2==0,其实只要让v2<v1即可int v2 = bag-wight[index] >= 0 ?value[index] + maxValue(wight, value, index+1, bag-wight[index]): 0;// 选最大的return Math.max(v1,v2);}public static void main(String[] args) {Solution solution = new Solution();int[] wight = {3,2,4,7};int[] value = {5,6,3,19};int bag = 11;int ans = solution.maxValue(wight, value, bag);System.out.println(ans == 25);}
- 分析暴力递归重复子问题
我们观察到暴力递归的每次递归的控制参数是index和bag,即index和bag组成了一个个的子问题。
我们举例有wight=[4,5,9,1], bag = 12.
当选择了wight[0],wight[1],没有选择wight[2]时,有子问题index=3,bag = 3
当没有选择wight[0],wight[1],选择wight[2]时,有子问题index=3,bag = 3
即有重复子问题,可以使用缓存避免重复子问题求解。
- 基于缓存优化暴力递归
基于缓存优化的思路很简单,进入递归,通过bad case后,先看缓存中有没有,有直接返回。
没有缓存,计算出结果后,返回之前先存到缓存里。
这里缓存为了后续的方便dp数组创建,直接用数组了,单纯的缓存话由于bag可能很大,可以用map代替
cache缓存是个二维数组,行为index范围为[0,weight.length],列为bag范围为[负数,bag]
因此cache的大小也可以为wight.length+1和bag+1
注意:wight.length+1貌似是可以替换为wight.length,因为在bad case里如果index == wight.length 会直接返回。这是正确的,这里定义为wight.length+1的目的是因为在递归的过程中,index是可以等于wight.length的,虽然在bad case里直接返回了,但是我们可以多定义一行以包含参数中出现的边界。后面可以看到多出一行后在找出dp数组的规律时更直观
/**** @param wight 货物重量数组* @param value 货物价值数组* @param bag 背包最大容量* @return 背包能装货物的最大价值*/public int maxValue(int[] wight, int[] value, int bag){int[][] cache = new int[wight.length+1][bag+1];for (int i = 0; i < cache.length; i++) {Arrays.fill(cache[i],-1);}return maxValue(wight,value,0,bag,cache);}/**** @param wight 货物重量数组* @param value 货物价值数组* @param index 当前货物索引* @param bag 背包最大容量* @return 背包能装货物的最大价值*/public int maxValue(int[] wight, int[] value, int index, int bag, int[][] cache){if (bag < 0){return 0;}if (index == wight.length){return 0;}if (cache[index][bag] >= 0){return cache[index][bag];}// index货物不要int v1 = maxValue(wight,value,index+1,bag,cache);// index货物要,但是不能超过背包容量// 如果超过了背包容量,这个选择是非法的,所以只能选择不要,这里让v2==0,后面选最大值自然选了v1int v2 = bag-wight[index] >= 0 ?value[index] + maxValue(wight, value, index+1, bag-wight[index],cache): 0;// 选最大的int ans = Math.max(v1,v2);cache[index][bag] = ans;return ans;}public static void main(String[] args) {Solution solution = new Solution();int[] wight = {3,2,4,7};int[] value = {5,6,3,19};int bag = 11;int ans = solution.maxValue(wight, value, bag);System.out.println(ans == 25);}
- 根据cache数组的构建规则推导dp转移数组
其实cache数组就是dp数组,我们先根据一个实际的例子来看cache数组的规律,如下是cache数组,我们换个名字叫dp数组,根据主函数的调用可知我们的目标答案是dp[0][11]
return maxValue(wight,value,0,bag,cache);
| dp | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | ※ | |||||||||||
| 1 | ||||||||||||
| 2 | ||||||||||||
| 3 | ||||||||||||
| 4 |
if (index == wight.length){return 0;}
| dp | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | ||||||||||||
| 1 | ||||||||||||
| 2 | ||||||||||||
| 3 | ||||||||||||
| 4 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
// index货物不要int v1 = maxValue(wight,value,index+1,bag,cache);// index货物要,但是不能超过背包容量// 如果超过了背包容量,这个选择是非法的,所以只能选择不要,这里让v2==0,后面选最大值自然选了v1int v2 = bag-wight[index] >= 0 ?value[index] + maxValue(wight, value, index+1, bag-wight[index],cache): 0;// 选最大的int ans = Math.max(v1,v2);
从代码可以看出,index行的值是基于index+1行转移出来的,因此我们能够从index=4的值推出index=3,2,1,0的值,这个规律就是转移方程,因此从该规律我们可以直接构造dp数组,将递归过程直接换成从dp数组取值即可。
| dp | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 0 | 0 | 6 | 6 | 6 | 11 | 11 | 19 | 19 | 25 | 25 | 25 |
| 1 | 0 | 0 | 6 | 6 | 6 | 6 | 9 | 19 | 19 | 25 | 25 | 25 |
| 2 | 0 | 0 | 0 | 0 | 3 | 3 | 3 | 19 | 19 | 19 | 19 | 22 |
| 3 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 19 | 19 | 19 | 19 | 19 |
| 4 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
public int dp(int[] wight, int[] value, int bag){int[][] dp = new int[wight.length+1][bag+1];for (int i = wight.length-1; i >= 0; i--) {for (int j = 0; j <= bag; j++) {int v1 = dp[i+1][j];int v2 = j - wight[i] >= 0 ? (value[i] + dp[i+1][j-wight[i]]) : 0;dp[i][j] = Math.max(v1,v2);}}return dp[0][bag];}
最长公共子序列
暴力递归
// 以下为暴力递归部分代码// text1[0....i] 和 text2[0....j]的最长公共子序列private int run(String text1, String text2,int i, int j){// 如果两个字符串都只有1位,相等代表有最长为1的公共子序列if (i == 0 && j == 0){return text1.charAt(0) == text2.charAt(0) ? 1 : 0;}else if (i == 0){// 如果text1的长度为1,那么如果text2当前j位置与其相等,则代表有1位,否则,当前j位可以舍弃,用j-1位再试return text1.charAt(0) == text2.charAt(j) ? 1 : run(text1,text2,i,j-1);}else if (j == 0){// 与上面的情况相似return text1.charAt(i) == text2.charAt(0) ? 1 : run(text1,text2,i-1,j);}else {// 如果text1和text2的长度都大于1,则有三种情况// 如果公共子序列 可能以text1的i位置为结尾,一定不以text2的j位置为尾int v1 = run(text1,text2,i,j-1);// 如果公共子序列 可能以text2的j位置为结尾,一定不以text1的i位置为尾int v2 = run(text1,text2,i-1,j);// 如果公共子序列 一定以text1的i位置为结尾,一定以text2的j位置为尾int v3 = text1.charAt(i) == text2.charAt(j) ? 1+run(text1,text2,i-1,j-1) : 0;return Math.max(v1,Math.max(v2,v3));}}
dp改造
public int longestCommonSubsequence(String text1, String text2) {if (text1 == null || text1.length() == 0 || text2 == null || text2.length() == 0) return 0;int[][] dp = new int[text1.length()][text2.length()];dp[0][0] = text1.charAt(0) == text2.charAt(0) ? 1 : 0;// 注意j == 0时,dp[0][0]已经有值了 从1开始,下同for (int j = 1; j < text2.length(); j++) {dp[0][j] = text1.charAt(0) == text2.charAt(j) ? 1 : dp[0][j-1];}for (int i = 1; i < text1.length(); i++) {dp[i][0] = text1.charAt(i) == text2.charAt(0) ? 1 : dp[i-1][0];}for (int i = 1; i < text1.length(); i++) {for (int j = 1; j < text2.length(); j++) {int v1 = dp[i][j-1];int v2 = dp[i-1][j];int v3 = text1.charAt(i) == text2.charAt(j) ? 1+dp[i-1][j-1] : 0;dp[i][j] = Math.max(Math.max(v1,v2),v3);}}return dp[text1.length()-1][text2.length()-1];}
最长回文子序列
- 暴力递归
```java
public int longestPalindromeSubseq(String s) {
}return run(s.toCharArray(),0,s.length()-1);
// str在[l...r]范围内的最长回文子序列长度private int run(char[] str,int l,int r){if (l == r){return 1;}if (l == r-1){return str[l] == str[r] ? 2 : 1;}// 一共四种情况// 一定不以l人作为左右两端int v1 = run(str,l+1,r-1);// 一定不以l做左端点,但可能以r为右端点int v2 = run(str,l+1,r);// 一定不以r做右端点,但可能以l为左端点int v3 = run(str,l,r-1);// 一定以lr为端点,判断相等序列长度+2并判断不以lr为端点情况,否则直接0int v4 = str[l] == str[r] ? 2 + v1 : 0;return Math.max(Math.max(v1,v2),Math.max(v3,v4));}
2. dp改造```javapublic int longestPalindromeSubseq(String s) {int[][] dp = new int[s.length()][s.length()];for (int l = 0; l < dp.length; l++) {for (int r = 0; r < dp.length; r++) {if (l == r){dp[l][r] = 1;}if (l == r-1){dp[l][r] = s.charAt(l) == s.charAt(r) ? 2 : 1;}}}for (int l = s.length()-3; l >=0; l--) {for (int r = 2+l; r <s.length(); r++) {// 一共四种情况// 一定不以l人作为左右两端int v1 = dp[l+1][r-1];// 一定不以l做左端点,但可能以r为右端点int v2 = dp[l+1][r];// 一定不以r做右端点,但可能以l为左端点int v3 = dp[l][r-1];// 一定以lr为端点,判断相等序列长度+2并判断不以lr为端点情况,否则直接0int v4 = s.charAt(l) == s.charAt(r) ? 2 + v1 : 0;dp[l][r] = Math.max(Math.max(v1,v2),Math.max(v3,v4));}}return dp[0][s.length()-1];}
