递归到二维动态规划

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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
题目一
package class067;

// 最小路径和
// 给定一个包含非负整数的 m x n 网格 grid
// 请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
// 说明:每次只能向下或者向右移动一步。
// 测试链接 : https://leetcode.cn/problems/minimum-path-sum/
public class Code01_MinimumPathSum {

// 暴力递归
public static int minPathSum1(int[][] grid) {
return f1(grid, grid.length - 1, grid[0].length - 1);
}

// 从(0,0)到(i,j)最小路径和
// 一定每次只能向右或者向下
public static int f1(int[][] grid, int i, int j) {
if (i == 0 && j == 0) {
return grid[0][0];
}
int up = Integer.MAX_VALUE;
int left = Integer.MAX_VALUE;
if (i - 1 >= 0) {
up = f1(grid, i - 1, j);
}
if (j - 1 >= 0) {
left = f1(grid, i, j - 1);
}
return grid[i][j] + Math.min(up, left);
}

// 记忆化搜索
public static int minPathSum2(int[][] grid) {
int n = grid.length;
int m = grid[0].length;
int[][] dp = new int[n][m];
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
dp[i][j] = -1;
}
}
return f2(grid, grid.length - 1, grid[0].length - 1, dp);
}

public static int f2(int[][] grid, int i, int j, int[][] dp) {
if (dp[i][j] != -1) {
return dp[i][j];
}
int ans;
if (i == 0 && j == 0) {
ans = grid[0][0];
} else {
int up = Integer.MAX_VALUE;
int left = Integer.MAX_VALUE;
if (i - 1 >= 0) {
up = f2(grid, i - 1, j, dp);
}
if (j - 1 >= 0) {
left = f2(grid, i, j - 1, dp);
}
ans = grid[i][j] + Math.min(up, left);
}
dp[i][j] = ans;
return ans;
}

// 严格位置依赖的动态规划
public static int minPathSum3(int[][] grid) {
int n = grid.length;
int m = grid[0].length;
int[][] dp = new int[n][m];
dp[0][0] = grid[0][0];
for (int i = 1; i < n; i++) {
dp[i][0] = dp[i - 1][0] + grid[i][0];
}
for (int j = 1; j < m; j++) {
dp[0][j] = dp[0][j - 1] + grid[0][j];
}
for (int i = 1; i < n; i++) {
for (int j = 1; j < m; j++) {
dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j];
}
}
return dp[n - 1][m - 1];
}

// 严格位置依赖的动态规划 + 空间压缩技巧
public static int minPathSum4(int[][] grid) {
int n = grid.length;
int m = grid[0].length;
// 先让dp表,变成想象中的表的第0行的数据
int[] dp = new int[m];
dp[0] = grid[0][0];
for (int j = 1; j < m; j++) {
dp[j] = dp[j - 1] + grid[0][j];
}
for (int i = 1; i < n; i++) {
// i = 1,dp表变成想象中二维表的第1行的数据
// i = 2,dp表变成想象中二维表的第2行的数据
// i = 3,dp表变成想象中二维表的第3行的数据
// ...
// i = n-1,dp表变成想象中二维表的第n-1行的数据
dp[0] += grid[i][0];
for (int j = 1; j < m; j++) {
dp[j] = Math.min(dp[j - 1], dp[j]) + grid[i][j];
}
}
return dp[m - 1];
}

}

暴力递归,因为只能向右或者向下。 所以就是i,j-1或者i-1,j 选取前面的最小值 到i,j

由2个可变参数可以完全决定返回值
能改成动态规划的递归,统一特征:
决定返回值的可变参数类型往往都比较简单,一般不会比int类型更复杂

从这个角度,可以解释带路径的递归(可变参数类型复杂),不适合或者说没有必要改成动态规划

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
题目2
package class067;


// 单词搜索(无法改成动态规划)
// 给定一个 m x n 二维字符网格 board 和一个字符串单词 word
// 如果 word 存在于网格中,返回 true ;否则,返回 false 。
// 单词必须按照字母顺序,通过相邻的单元格内的字母构成
// 其中"相邻"单元格是那些水平相邻或垂直相邻的单元格
// 同一个单元格内的字母不允许被重复使用
// 测试链接 : https://leetcode.cn/problems/word-search/
public class Code02_WordSearch {

public static boolean exist(char[][] board, String word) {
char[] w = word.toCharArray();
for (int i = 0; i < board.length; i++) {
for (int j = 0; j < board[0].length; j++) {
if (f(board, i, j, w, 0)) {
return true;
}
}
}
return false;
}

// 因为board会改其中的字符
// 用来标记哪些字符无法再用
// 带路径的递归无法改成动态规划或者说没必要
// 从(i,j)出发,来到w[k],请问后续能不能把word走出来w[k...]
public static boolean f(char[][] b, int i, int j, char[] w, int k) {
if (k == w.length) {
return true;
}
if (i < 0 || i == b.length || j < 0 || j == b[0].length || b[i][j] != w[k]) {
return false;
}
// 不越界,b[i][j] == w[k]
char tmp = b[i][j];
b[i][j] = 0;
boolean ans = f(b, i - 1, j, w, k + 1)
|| f(b, i + 1, j, w, k + 1)
|| f(b, i, j - 1, w, k + 1)
|| f(b, i, j + 1, w, k + 1);
b[i][j] = tmp;//回溯啊 这个也很关键的
return ans;
}
}
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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
题目三
package class067;

// 最长公共子序列
// 给定两个字符串text1和text2
// 返回这两个字符串的最长 公共子序列 的长度
// 如果不存在公共子序列,返回0
// 两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列
// 测试链接 : https://leetcode.cn/problems/longest-common-subsequence/
public class Code03_LongestCommonSubsequence {

public static int longestCommonSubsequence1(String str1, String str2) {
char[] s1 = str1.toCharArray();
char[] s2 = str2.toCharArray();
int n = s1.length;
int m = s2.length;
return f1(s1, s2, n - 1, m - 1);
}

// s1[0....i1]与s2[0....i2]最长公共子序列长度
public static int f1(char[] s1, char[] s2, int i1, int i2) {
if (i1 < 0 || i2 < 0) {
return 0;
}
int p1 = f1(s1, s2, i1 - 1, i2 - 1);
int p2 = f1(s1, s2, i1 - 1, i2);
int p3 = f1(s1, s2, i1, i2 - 1);
int p4 = s1[i1] == s2[i2] ? (p1 + 1) : 0;
return Math.max(Math.max(p1, p2), Math.max(p3, p4));
}

// 为了避免很多边界讨论
// 很多时候往往不用下标来定义尝试,而是用长度来定义尝试
// 因为长度最短是0,而下标越界的话讨论起来就比较麻烦
public static int longestCommonSubsequence2(String str1, String str2) {
char[] s1 = str1.toCharArray();
char[] s2 = str2.toCharArray();
int n = s1.length;
int m = s2.length;
return f2(s1, s2, n, m);
}

// s1[前缀长度为len1]对应s2[前缀长度为len2]
// 最长公共子序列长度
public static int f2(char[] s1, char[] s2, int len1, int len2) {
if (len1 == 0 || len2 == 0) {
return 0;
}
int ans;
if (s1[len1 - 1] == s2[len2 - 1]) {
ans = f2(s1, s2, len1 - 1, len2 - 1) + 1;
} else {
ans = Math.max(f2(s1, s2, len1 - 1, len2), f2(s1, s2, len1, len2 - 1));
}
return ans;
}

// 记忆化搜索
public static int longestCommonSubsequence3(String str1, String str2) {
char[] s1 = str1.toCharArray();
char[] s2 = str2.toCharArray();
int n = s1.length;
int m = s2.length;
int[][] dp = new int[n + 1][m + 1];
for (int i = 0; i <= n; i++) {
for (int j = 0; j <= m; j++) {
dp[i][j] = -1;
}
}
return f3(s1, s2, n, m, dp);
}

public static int f3(char[] s1, char[] s2, int len1, int len2, int[][] dp) {
if (len1 == 0 || len2 == 0) {
return 0;
}
if (dp[len1][len2] != -1) {
return dp[len1][len2];
}
int ans;
if (s1[len1 - 1] == s2[len2 - 1]) {
ans = f3(s1, s2, len1 - 1, len2 - 1, dp) + 1;
} else {
ans = Math.max(f3(s1, s2, len1 - 1, len2, dp), f3(s1, s2, len1, len2 - 1, dp));
}
dp[len1][len2] = ans;
return ans;
}

// 严格位置依赖的动态规划
public static int longestCommonSubsequence4(String str1, String str2) {
char[] s1 = str1.toCharArray();
char[] s2 = str2.toCharArray();
int n = s1.length;
int m = s2.length;
int[][] dp = new int[n + 1][m + 1];
for (int len1 = 1; len1 <= n; len1++) {
for (int len2 = 1; len2 <= m; len2++) {
if (s1[len1 - 1] == s2[len2 - 1]) {
dp[len1][len2] = 1 + dp[len1 - 1][len2 - 1];
} else {
dp[len1][len2] = Math.max(dp[len1 - 1][len2], dp[len1][len2 - 1]);
}
}
}
return dp[n][m];
}

// 严格位置依赖的动态规划 + 空间压缩
public static int longestCommonSubsequence5(String str1, String str2) {
char[] s1, s2;
if (str1.length() >= str2.length()) {
s1 = str1.toCharArray();
s2 = str2.toCharArray();
} else {
s1 = str2.toCharArray();
s2 = str1.toCharArray();
}
int n = s1.length;
int m = s2.length;
int[] dp = new int[m + 1];
for (int len1 = 1; len1 <= n; len1++) {
int leftUp = 0, backup;
for (int len2 = 1; len2 <= m; len2++) {
backup = dp[len2];
if (s1[len1 - 1] == s2[len2 - 1]) {
dp[len2] = 1 + leftUp;
} else {
dp[len2] = Math.max(dp[len2], dp[len2 - 1]);
}
leftUp = backup;
}
}
return dp[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
// 最长回文子序列
// 给你一个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度
// 测试链接 : https://leetcode.cn/problems/longest-palindromic-subsequence/
public class Code04_LongestPalindromicSubsequence {
public static int longestPalindromeSubseq3(String str) {
char[] s = str.toCharArray();
int n = s.length;
int[][] dp = new int[n][n];
for (int l = n - 1; l >= 0; l--) {
dp[l][l] = 1;
if (l + 1 < n) {
dp[l][l + 1] = s[l] == s[l + 1] ? 2 : 1;
}
for (int r = l + 2; r < n; r++) {
if (s[l] == s[r]) {
dp[l][r] = 2 + dp[l + 1][r - 1];
} else {
dp[l][r] = Math.max(dp[l + 1][r], dp[l][r - 1]);
}
}
}
return dp[0][n - 1];
}
}

题目五

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
// 严格位置依赖的动态规划
// 节点数为n高度不大于m的二叉树个数
// 现在有n个节点,计算出有多少个不同结构的二叉树
// 满足节点个数为n且树的高度不超过m的方案
// 因为答案很大,所以答案需要模上1000000007后输出
// 测试链接 : https://www.nowcoder.com/practice/aaefe5896cce4204b276e213e725f3ea
// 请同学们务必参考如下代码中关于输入、输出的处理
// 这是输入输出处理效率很高的写法
// 提交以下所有代码,把主类名改成Main,可以直接通过

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StreamTokenizer;

public class Code05_NodenHeightNotLargerThanm {

public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StreamTokenizer in = new StreamTokenizer(br);
PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
while (in.nextToken() != StreamTokenizer.TT_EOF) {
int n = (int) in.nval;
in.nextToken();
int m = (int) in.nval;
out.println(compute2(n, m));
}
out.flush();
out.close();
br.close();
}

public static int MAXN = 51;

public static int MOD = 1000000007;
public static long[][] dp2 = new long[MAXN][MAXN];

public static int compute2(int n, int m) {
for (int j = 0; j <= m; j++) {
dp2[0][j] = 1;
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
dp2[i][j] = 0;
for (int k = 0; k < i; k++) {
// 一共i个节点,头节点已经占用了1个名额
// 如果左树占用k个,那么右树就占用i-k-1个
dp2[i][j] = (dp2[i][j] + dp2[k][j - 1] * dp2[i - k - 1][j - 1] % MOD) % MOD;
}
}
}
return (int) dp2[n][m];
}
}