二叉树 暂略,已经练习很多了
见 二叉树 中包含所有二叉树
回溯
回溯有一个增量构造答案的过程,这个过程通常使用递归来实现。选 “a”, “b”, “c” 选 “ad” …
递归: 考虑好边界条件以及和非边界条件写对即可。剩下交给数学归纳法
回溯/动归三问:-> 主要是为了写对 边界条件以及和非边界条件
当前操作是什么?
子问题是什么?
下一个子问题是什么?
子集型 思路 可以有两种思路:
站在输入的角度:
枚举第 i 个元素 你是选/不选
ps: 01背包与此相似
每个数都可以在子集中,也可以不在子集中
此时:叶子结点是答案
回溯三问:
当前操作
枚举第 i 个数选/不选
子问题
从下标 >= i 的数字中构造子集
下一个子问题
从下标 >= i + 1的数字中构造子集
站在答案 角度:
枚举第一个数选择谁,第二个数选择谁,
此时:每个节点都是答案
回溯三问:
当前操作
枚举答案的第一个数选什么第二个数选什么…
子问题
从下标 >= i 的数字中构造子集
下一个子问题
从下标 >= j + 1的数字中构造子集
组合型 思路 子集 + 剪枝 = 组合型问题
下图(左选两个数,右选三个数)
为什么从大到小枚举呢?
假设我们需要选 3 个数,现在已经选了1个了,即 k = 3, m = 1; 我们还需要选择 d = k - m -> 3 - 1 = 2 个数
由于是从大到小枚举,那么我们如果 i < d; 即要选的数为[1,1] 也就是1的话那么是无论如何都没办法选出来两个数 k = 2 的(这是因为题目的范围是[1,n])所以从大到小会比较容易剪枝
正序枚举怎么做呢?
k - path.size()
是 我们还需要几个
我们要判断的就是还需要的能否被正确的提供,当前我们剩余的个数是 n - i + 1
因此如果需要的不能被满足,直接提前截止即可 if (k - path.size() > n - cur + 1) return;
总结 对于组合型和子集型回溯有两种思考路径:
排列型
和组合的区别就在于[2, 1]
和 [1, 2]
在组合中被认定为一种,但是排列中则是两种不同的
DP
主要还是 状态定义 + 状态转移
可以借用子集型回溯中的
选或不选 / 选哪个
从上至下 - 记忆化搜索
从下而上 - 递推
回溯例题 子集型 1 2 3 4 5 6 7 8 17. 电话号码的字母组合 https://leetcode.cn/problems/letter-combinations-of-a-phone-number/solutions/2059416/hui-su-bu-hui-xie-tao-lu-zai-ci-pythonja-3orv/ 78. 子集 https://leetcode.cn/problems/subsets/solutions/2059409/hui-su-bu-hui-xie-tao-lu-zai-ci-pythonja-8tkl/ 131. 分割回文串 https://leetcode.cn/problems/palindrome-partitioning/solutions/2059414/hui-su-bu-hui-xie-tao-lu-zai-ci-pythonja-fues/ 784. 字母大小写全排列 https://leetcode.cn/problems/letter-case-permutation/ 1601. 最多可达成的换楼请求数目 https://leetcode.cn/problems/maximum-number-of-achievable-transfer-requests/ 2397. 被列覆盖的最多行数 https://leetcode.cn/problems/maximum-rows-covered-by-columns/ 306. 累加数 https://leetcode.cn/problems/additive-number/ 2698. 求一个整数的惩罚数 https://leetcode.cn/problems/find-the-punishment-number-of-an-integer/
组合型 1 2 3 4 77. 组合 https://leetcode.cn/problems/combinations/solutions/2071017/hui-su-bu-hui-xie-tao-lu-zai-ci-pythonja-65lh/ 216. 组合总和 III https://leetcode.cn/problems/combination-sum-iii/solutions/2071013/hui-su-bu-hui-xie-tao-lu-zai-ci-pythonja-feme/ 22. 括号生成 https://leetcode.cn/problems/generate-parentheses/solutions/2071015/hui-su-bu-hui-xie-tao-lu-zai-ci-pythonja-wcdw/ 301. 删除无效的括号 https://leetcode.cn/problems/remove-invalid-parentheses/
排列型 1 2 3 46. 全排列 https://leetcode.cn/problems/permutations/solutions/2079585/hui-su-bu-hui-xie-tao-lu-zai-ci-jing-que-6hrh/ 51. N 皇后 https://leetcode.cn/problems/n-queens/solutions/2079586/hui-su-tao-lu-miao-sha-nhuang-hou-shi-pi-mljv/ 52. N 皇后 II(直接用 51 题代码搞定)https://leetcode.cn/problems/n-queens-ii/solution/hui-su-miao-sha-nhuang-hou-yi-ge-shi-pin-l41
子集型: 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 class Solution { String[] hm = new String [] { "" , "" , "abc" , "def" , "ghi" , "jkl" , "mno" , "pqrs" , "tuv" , "wxyz" }; int n; char [] digitCharArr; List<String> res = new ArrayList <>(); StringBuilder sb = new StringBuilder (); public List<String> letterCombinations (String digits) { digitCharArr = digits.toCharArray(); n = digitCharArr.length; backtrack(0 ); return res; } private void backtrack (int curIdx) { if (curIdx == n) { String innerRes = sb.toString(); if (innerRes.length() != 0 ) res.add(innerRes); return ; } int idx = digitCharArr[curIdx] - '0' ; for (int i = 0 ; i < hm[idx].length(); i++) { sb.append(hm[idx].charAt(i)); backtrack(curIdx + 1 ); sb.deleteCharAt(sb.length() - 1 ); } return ; } }
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 class Solution { int [] nums; int n; List<List<Integer>> paths = new ArrayList <>(); LinkedList<Integer> path = new LinkedList <>(); public List<List<Integer>> subsets (int [] nums) { this .nums = nums; n = nums.length; if (n == 0 ) return paths; backtrackSelectOrNot(0 ); backtrackChooseFromEnumeration(0 ); return paths; } private void backtrackSelectOrNot (int curIdx) { if (curIdx == n) { paths.add(new LinkedList <>(path)); return ; } path.addLast(nums[curIdx]); backtrackSelectOrNot(curIdx + 1 ); path.removeLast(); backtrackSelectOrNot(curIdx + 1 ); return ; } private void backtrackChooseFromEnumeration (int curIdx) { paths.add(new LinkedList <>(path)); if (curIdx == n) return ; for (int innerIdx = curIdx; innerIdx < n; innerIdx++) { path.addLast(nums[innerIdx]); backtrackChooseFromEnumeration(innerIdx + 1 ); path.removeLast(); } return ; } }
选或不选:
每一次抉择(backtrackSelectOrNot function中)都只有两种选择,选或者不选,并且这种情况下只有叶子结点为答案,因此需要判断是否为叶子结点,如果是的话才加入答案中
枚举选哪个:
每一次枚举(backtrackChooseFromEnumeration中)都可以选择curIdx之后的数,且所有的节点都为答案,因此无需判断直接加入,只需要做好递归的边界即可
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 class Solution { int n; List<List<String>> paths = new ArrayList <>(); LinkedList<String> path = new LinkedList <>(); char [] sCharArr; String s; public List<List<String>> partition (String s) { n = s.length(); sCharArr = s.toCharArray(); this .s = s; backtrackSelectOrNot(0 , 0 ); return paths; } private void backtrackSelectOrNot (int start, int i) { if (start == n) { paths.add(new LinkedList <>(path)); return ; } if (i < n - 1 ) { backtrackSelectOrNot(start, i + 1 ); } if (isPalindrome(start, i)) { path.addLast(s.substring(start, i + 1 )); backtrackSelectOrNot(i + 1 , i + 1 ); path.removeLast(); } return ; } private void backtrackEnumerate (int start) { if (start == n) { paths.add(new LinkedList <>(path)); return ; } for (int i = start; i < n; i++) { if (isPalindrome(start, i)) { path.addLast(s.substring(start, i + 1 )); backtrackEnumerate(i + 1 ); path.removeLast(); } } return ; } private boolean isPalindrome (int left, int right) { while (left < right) { if (sCharArr[left] == sCharArr[right]) { left++; right--; } else { return false ; } } return true ; } }
两个难点:
决策树如何画
如何将char 小写变大写,大写变小写
问题1:
问题2:
Java碎碎念
char[idx] ^= 1 << 5
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 class Solution { int n; List<String> paths = new ArrayList <>(); char [] cArr; public List<String> letterCasePermutation (String s) { n = s.length(); cArr = s.toCharArray(); backtrack(0 ); return paths; } private void backtrack (int startIdx) { if (startIdx == n) { paths.add(new String (cArr)); return ; } if (Character.isDigit(cArr[startIdx])) { backtrack(startIdx + 1 ); return ; } cArr[startIdx] ^= 1 << 5 ; backtrack(startIdx + 1 ); cArr[startIdx] ^= 1 << 5 ; backtrack(startIdx + 1 ); return ; } }
这道题其实可以不用恢复现场,因为我们需要的是叶子结点;但是我们需要先处理 不转换当前char
再处理转换当前char
由于回溯发生在不转换,即没有变化,那也就不需要回溯现场了:
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 class Solution { int n; List<String> paths = new ArrayList <>(); char [] cArr; public List<String> letterCasePermutation (String s) { n = s.length(); cArr = s.toCharArray(); backtrack(0 ); return paths; } private void backtrack (int startIdx) { if (startIdx == n) { paths.add(new String (cArr)); return ; } if (Character.isDigit(cArr[startIdx])) { backtrack(startIdx + 1 ); return ; } backtrack(startIdx + 1 ); cArr[startIdx] ^= 1 << 5 ; backtrack(startIdx + 1 ); return ; } }
这道题关键在于枚举列然后计算覆盖的行然后统计最大即可
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 class Solution { int max = -1 ; int m, n; int [][] matrix, matrixCopy; public int maximumRows (int [][] matrix, int numSelect) { m = matrix.length; n = matrix[0 ].length; this .matrix = matrix; matrixCopy = new int [m][]; for (int i = 0 ; i < m; i++) { matrixCopy[i] = matrix[i].clone(); } backtrack(0 , numSelect); return max; } private void backtrack (int startIdx, int numSelect) { if (n - startIdx < numSelect) return ; if (numSelect == 0 ) { max = Math.max(max, countCovers()); return ; } for (int i = startIdx; i < n; i++) { for (int r = 0 ; r < m; r++) { matrix[r][i] = 0 ; } backtrack(i + 1 , numSelect - 1 ); for (int r = 0 ; r < m; r++) { matrix[r][i] = matrixCopy[r][i]; } } } private int countCovers () { int ret = 0 ; for (int i = 0 ; i < m; i++) { int count = 0 ; for (int j = 0 ; j < n; j++) { if (matrix[i][j] == 0 ) count++; } if (count == n) ret++; } return ret; } private void printMatrix () { for (int i = 0 ; i < m; i++) { for (int j = 0 ; j < n; j++) { System.out.print(matrix[i][j] + " " ); } System.out.println(); } System.out.println(); } }
这道题自己的做法是枚举切割的起始点,能做但是比较复杂;
如果回溯返回boolean想要记录结果的话:
if (backtrack()) return true
代码:
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 import java.math.BigInteger;class Solution { int numSelected = 0 ; String num; int n; LinkedList<BigInteger> path = new LinkedList <>(); public boolean isAdditiveNumber (String num) { this .num = num; n = num.length(); return backtrack(0 ); } private boolean backtrack (int cutIdx) { if (cutIdx == n) { return path.size() >= 3 && check(path.get(path.size() - 3 ), path.get(path.size() - 2 ), path.get(path.size() - 1 )); } if (num.charAt(cutIdx) == '0' ) { if (path.size() >= 2 ) { if (check(path.get(path.size() - 2 ), path.get(path.size() - 1 ), BigInteger.ZERO)) { path.addLast(BigInteger.ZERO); if (backtrack(cutIdx + 1 )) return true ; path.removeLast(); } else { return false ; } } else { path.addLast(BigInteger.ZERO); if (backtrack(cutIdx + 1 )) return true ; path.removeLast(); } } else { for (int i = cutIdx; i < n; i++) { BigInteger current = new BigInteger (num.substring(cutIdx, i + 1 )); path.addLast(current); if (path.size() >= 3 && !check(path.get(path.size() - 3 ), path.get(path.size() - 2 ), current)) { path.removeLast(); continue ; } if (backtrack(i + 1 )) return true ; path.removeLast(); } } return false ; } private boolean check (BigInteger a, BigInteger b, BigInteger c) { BigInteger sum = a.add(b); return sum.equals(c); } }
三叶的解法 是枚举结束点
并且这里教了如何使用高精度加法:
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 class Solution { String num; int n; List<List<Integer>> list = new ArrayList <>(); public boolean isAdditiveNumber (String _num) { num = _num; n = num.length(); return dfs(0 ); } boolean dfs (int u) { int m = list.size(); if (u == n) return m >= 3 ; int max = num.charAt(u) == '0' ? u + 1 : n; List<Integer> cur = new ArrayList <>(); for (int i = u; i < max; i++) { cur.add(0 , num.charAt(i) - '0' ); if (m < 2 || check(list.get(m - 2 ), list.get(m - 1 ), cur)) { list.add(cur); if (dfs(i + 1 )) return true ; list.remove(list.size() - 1 ); } } return false ; } boolean check (List<Integer> a, List<Integer> b, List<Integer> c) { List<Integer> ans = new ArrayList <>(); int t = 0 ; for (int i = 0 ; i < a.size() || i < b.size(); i++) { if (i < a.size()) t += a.get(i); if (i < b.size()) t += b.get(i); ans.add(t % 10 ); t /= 10 ; } if (t > 0 ) ans.add(t); boolean ok = c.size() == ans.size(); for (int i = 0 ; i < c.size() && ok; i++) { if (c.get(i) != ans.get(i)) ok = false ; } return ok; } } 作者:宫水三叶 链接:https: 来源:力扣(LeetCode) 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
这道题非常有意思,可以用两个指针来做到选或不选的思路:
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 class Solution { int sum = 0 ; public int punishmentNumber (int n) { int res = 0 ; for (int i = 1 ; i <= n; i++) { sum = 0 ; char [] square = Integer.toString(i * i).toCharArray(); if (backtrack(square, 0 , 0 , i)) { res += i * i; } } return res; } private boolean backtrack (char [] square, int start, int end, int i) { if (end == square.length) { return i == sum; } if (sum > i) return false ; if (end < square.length - 1 ) { if (backtrack(square, start, end + 1 , i)) return true ; } sum += sumUp(square, start, end); if (backtrack(square, end + 1 , end + 1 , i)) return true ; sum -= sumUp(square, start, end); return false ; } private int sumUp (char [] square, int start, int end) { int x = 0 ; for (int i = start; i <= end; i++){ x = x * 10 + square[i] - '0' ; } return x; } }
从答案的视角枚举
其实就是枚举逗号位置
在start = 0 时候 i尝试 0, 1, 2, 3 也就是[0,0],[0, 1], [0,2],[0,3] 即temp = 1, 12, 129, 1296 sum = 此时的 temp
在start = 1 时候 i尝试 1, 2, 3 也就是[1, 1], [1,2],[1,3] 即temp = 2, 29, 296; sum = start = 0 时候的temp
+ 此时的 temp
在start = 2 时候 i尝试 2, 3 也就是[2,2],[2,3] 即temp = 9, 96; sum = start = 0 时候的temp
+ start = 1 时候的temp
+ 此时的 temp
在start = 3 时候 i尝试 3 也就是[3,3] 即temp = 6; sum = start = 0 时候的temp
+ start = 1 时候的temp
+ start = 2 时候的temp
+ 此时的 temp
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 class Solution { int sum = 0 ; public int punishmentNumber (int n) { int res = 0 ; for (int i = 1 ; i <= n; i++) { sum = 0 ; char [] square = Integer.toString(i * i).toCharArray(); if (backtrackEnumeration(square, 0 , i)) { res += i * i; } } return res; } private boolean backtrackEnumeration (char [] square, int start, int target) { if (start == square.length) return target == sum; if (sum > target) return false ; int temp = 0 ; for (int i = start; i < square.length; i++) { temp = temp * 10 + square[i] - '0' ; sum = sum + temp; if (backtrackEnumeration(square, i + 1 , target)) { return true ; } sum = sum - temp; } return false ; } }
子集型总结 一般可以从选和不选以及枚举选哪个来做;
碰到需要选择切点的,类似于306, 以及2698题,我们可以先通过把一个数变成一个string或者char[]来进行枚举。
另外枚举状态下的回溯就是无非是当前层然后深入下一层然后碰到递归终点返回之后会继续尝试
其达成的效果就是当前层穷举,下一层穷举…
所以这就是所谓的递归不要管他是如何深入的。只要知道做了什么事,怎么出去,完成当前scope下的任务即可
组合型 无优化剪枝:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class Solution { LinkedList<Integer> path = new LinkedList <>(); List<List<Integer>> paths = new ArrayList <>(); int n, k; public List<List<Integer>> combine (int n, int k) { this .n = n; this .k = k; backtrack(1 ); return paths; } private void backtrack (int cur) { if (path.size() == k) { paths.add(new LinkedList <>(path)); return ; } for (int i = cur; i <= n; i++) { path.addLast(i); backtrack(i + 1 ); path.removeLast(); } } }
从大到小枚举剪枝:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class Solution { LinkedList<Integer> path = new LinkedList <>(); List<List<Integer>> paths = new ArrayList <>(); int n, k; public List<List<Integer>> combine (int n, int k) { this .n = n; this .k = k; backtrack(n); return paths; } private void backtrack (int cur) { if (path.size() == k) { paths.add(new LinkedList <>(path)); return ; } for (int i = cur; i >= 1 ; i--) { if (i < k - path.size()) return ; path.addLast(i); backtrack(i - 1 ); path.removeLast(); } } }
从小到大枚举
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 class Solution { LinkedList<Integer> path = new LinkedList <>(); List<List<Integer>> paths = new ArrayList <>(); int n, k; public List<List<Integer>> combine (int n, int k) { this .n = n; this .k = k; backtrack(1 ); return paths; } private void backtrack (int cur) { if (k - path.size() > n - cur + 1 ) return ; if (path.size() == k) { paths.add(new LinkedList <>(path)); return ; } for (int i = cur; i <= n; i++) { path.addLast(i); backtrack(i + 1 ); path.removeLast(); } } }
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 class Solution { LinkedList<Integer> path = new LinkedList <>(); List<List<Integer>> paths = new ArrayList <>(); int k, n; public List<List<Integer>> combinationSum3 (int k, int n) { this .k = k; this .n = n; backtrackSelectOrNot(1 ); return paths; } private void backtrackEnumeration (int start) { if (path.size() == k) { int sum = 0 ; for (int i = 0 ; i < path.size(); i++) { sum += path.get(i); } if (sum == n) paths.add(new LinkedList <>(path)); return ; } for (int i = start; i <= 9 ; i++) { path.addLast(i); backtrackEnumeration(i + 1 ); path.removeLast(); } return ; } private void backtrackSelectOrNot (int cur) { if (cur > 9 ) { if (path.size() != k) return ; } if (path.size() == k) { int sum = 0 ; for (int i = 0 ; i < path.size(); i++) { sum += path.get(i); } if (sum == n) paths.add(new LinkedList <>(path)); return ; } backtrackSelectOrNot(cur + 1 ); path.addLast(cur); backtrackSelectOrNot(cur + 1 ); path.removeLast(); return ; } }
依旧是使用了选和不选的操作:
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 class Solution { LinkedList<Character> path = new LinkedList <>(); List<String> paths = new ArrayList <>(); int m; int n; public List<String> generateParenthesis (int n) { this .n = n; m = 2 * n; backtrackSelectOrNot(0 , 0 ); return paths; } private void backtrackSelectOrNot (int parenthesisSum, int leftParenthesisCount) { if (parenthesisSum == m) { StringBuilder sb = new StringBuilder (); for (char c : path) { sb.append(c); } paths.add(sb.toString()); return ; } if (leftParenthesisCount < n) { path.addLast('(' ); backtrackSelectOrNot(parenthesisSum + 1 , leftParenthesisCount + 1 ); path.removeLast(); } if (parenthesisSum - leftParenthesisCount < leftParenthesisCount) { path.addLast(')' ); backtrackSelectOrNot(parenthesisSum + 1 , leftParenthesisCount); path.removeLast(); } return ; } }
需要注意的是这个树的构成是要计算左右可选的括号数量而不是盲目的选或者不选。
比如:我们一开始会选择左括号,当不选择左括号的时候(左括号数量满足数量),我们会去选择有括号,这时候是不选左括号的下分情况的选择右括号的情况:(右括号的数量 < 左括号的数量)
暴力的穷举每一个括号看是选择还是不选择
答案加入到数组里后从中选择最长的就是满足要求的,将最长的加入答案
另外判断一个字符串的括号是否合法的函数也很有意思,可以多学习一下, 思路是用代表左括号的指针移动,碰到左括号右移,右括号左移,如果left == 0证明合法,否则比如左括号的idx < 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 class Solution { StringBuilder sb = new StringBuilder (); List<String> paths = new ArrayList <>(); String s; int n; public List<String> removeInvalidParentheses (String s) { this .s = s; n = s.length(); backtrack(0 ); return filterResult(); } private List<String> filterResult () { int maxLen = 0 ; for (String str : paths) { maxLen = Math.max(maxLen, str.length()); } HashSet<String> set = new HashSet <>(); for (String str : paths) { if (str.length() == maxLen) { set.add(str); } } return new LinkedList <>(set); } private void backtrack (int idx) { if (idx == n) { if (isValid(sb.toString())) { paths.add(sb.toString()); } return ; } char c = s.charAt(idx); if (c != '(' && c != ')' ) { sb.append(c); backtrack(idx + 1 ); sb.deleteCharAt(sb.length() - 1 ); } else { sb.append(c); backtrack(idx + 1 ); sb.deleteCharAt(sb.length() - 1 ); backtrack(idx + 1 ); } return ; } private boolean isValid (String sb) { int left = 0 ; for (char c : sb.toCharArray()) { if (c == '(' ) { left++; } else if (c == ')' ) { left--; if (left < 0 ) { return false ; } } } return left == 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 class Solution { LinkedList<Integer> path = new LinkedList <>(); List<List<Integer>> paths = new ArrayList <>(); int [] nums; boolean [] numAppeared; int n; public List<List<Integer>> permute (int [] nums) { this .nums = nums; n = nums.length; numAppeared = new boolean [n]; backtrack(0 ); return paths; } private void backtrack (int idx) { System.out.println(idx + " " + path); if (idx == n) { paths.add(new LinkedList <>(path)); return ; } for (int i = 0 ; i < n; i++) { if (!numAppeared[i]) { numAppeared[i] = true ; path.addLast(nums[i]); backtrack(idx + 1 ); path.removeLast(); numAppeared[i] = false ; } } return ; } }
这里需要注意的有两个点:
backtrack中的for loop 是 i: [0, n)
因为我们每一次都要看从 0 开始的然后通过boolean[] 来判断是否加入答案
注意递归时候是 (idx + 1)
而不是 (i + 1)
idx
应该表示的是当前的排列中已经放置了多少数字,或者说正在为哪个位置选择数字。然后选择的是 [0, n]
N皇后问题 这道题有两种思维方式,首先是 labuladong 的比较好理解的,其次是灵神 的通过抽屉原理证明的全排列
其方法核心大差不差,主要是在构造上
核心:枚举每一行上的皇后的全排列,在不冲突的前提下进行构造
labuladong版本:
整体比较长,每一次都需要判断左上和右上角是否冲突
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 class Solution { List<List<String>> res = new ArrayList <>(); public List<List<String>> solveNQueens (int n) { char [][] chessBoard = new char [n][n]; for (char [] chessRow : chessBoard) { Arrays.fill(chessRow, '.' ); } int curRow = 0 ; backtracking(chessBoard, curRow, n); return res; } private void backtracking (char [][] chessBoard, int curRow, int n) { if (curRow == n) { res.add(array2List(chessBoard)); return ; } for (int curCol = 0 ; curCol < n; curCol++) { if (isValid(chessBoard, curRow, curCol)) { chessBoard[curRow][curCol] = 'Q' ; backtracking(chessBoard, curRow + 1 , n); chessBoard[curRow][curCol] = '.' ; } } } private boolean isValid (char [][] chessBoard, int curRow, int curCol) { for (int r = 0 ; r < curRow; r++) { if (chessBoard[r][curCol] == 'Q' ) return false ; } for (int r = curRow - 1 , c = curCol - 1 ; r >= 0 && c >= 0 ; r--, c--) { if (chessBoard[r][c] == 'Q' ) return false ; } for (int r = curRow - 1 , c = curCol + 1 ; r >= 0 && c < chessBoard[0 ].length; r--, c++) { if (chessBoard[r][c] == 'Q' ) return false ; } return true ; } private List<String> array2List (char [][] chessBoard) { List<String> transformedArray = new ArrayList <>(); int r = chessBoard.length, c = chessBoard[0 ].length; for (char [] chars : chessBoard) { StringBuilder sb = new StringBuilder (); for (int j = 0 ; j < c; j++) { sb.append(chars[j]); } transformedArray.add(sb.toString()); } return transformedArray; } }
灵神的第一个版本也需要判断:
但是加入了一点数学证明:即右上方的 r + c == 当前的 r + c 左上方的 r - c == 当前的 r - c:
从而发现我们可以通过开辟两个boolean[]
来看r + c是否之前出现过如果出现过就说明当前位置无法放置皇后因为会冲突
修改过后:
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 class Solution { List<List<String>> paths = new ArrayList <>(); int [] col; boolean [] onPath, diagRPlusC, diagRMinusC; int m, n; public List<List<String>> solveNQueens (int n) { this .n = n; this .m = 2 * n - 1 ; col = new int [n]; onPath = new boolean [m]; diagRMinusC = new boolean [m]; diagRPlusC = new boolean [m]; backtrack(0 ); return paths; } private void backtrack (int r) { if (r == n) { List<String> board = new ArrayList <>(n); for (int c : col) { char [] row = new char [n]; Arrays.fill(row, '.' ); row[c] = 'Q' ; board.add(new String (row)); } paths.add(board); return ; } for (int c = 0 ; c < n; c++) { int rc = r - c + n - 1 ; if (!onPath[c] && !diagRPlusC[r + c] && !diagRMinusC[rc]) { col[r] = c; onPath[c] = diagRPlusC[r + c] = diagRMinusC[rc] = true ; backtrack(r + 1 ); onPath[c] = diagRPlusC[r + c] = diagRMinusC[rc] = false ; } } } }
只需要稍微更改下逻辑即可:这里我们返回计数而不是构造了一个答案
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 class Solution { int res; int [] col; boolean [] onPath, diagRPlusC, diagRMinusC; int m, n; public int totalNQueens (int n) { this .n = n; this .m = 2 * n - 1 ; col = new int [n]; onPath = new boolean [m]; diagRMinusC = new boolean [m]; diagRPlusC = new boolean [m]; backtrack(0 ); return res; } private void backtrack (int r) { if (r == n) { res++; return ; } for (int c = 0 ; c < n; c++) { int rc = r - c + n - 1 ; if (!onPath[c] && !diagRPlusC[r + c] && !diagRMinusC[rc]) { col[r] = c; onPath[c] = diagRPlusC[r + c] = diagRMinusC[rc] = true ; backtrack(r + 1 ); onPath[c] = diagRPlusC[r + c] = diagRMinusC[rc] = false ; } } } }
DP例题 DP 1 2 3 4 5 6 198. 打家劫舍 https://leetcode.cn/problems/house-robber/solutions/2102725/ru-he-xiang-chu-zhuang-tai-ding-yi-he-zh-1wt1/ 70. 爬楼梯 https://leetcode.cn/problems/climbing-stairs/ 746. 使用最小花费爬楼梯 https://leetcode.cn/problems/min-cost-climbing-stairs/ 2466. 统计构造好字符串的方案数 https://leetcode.cn/problems/count-ways-to-build-good-strings/ 213. 打家劫舍 II https://leetcode.cn/problems/house-robber-ii/ 213. 打家劫舍 II 题解 https://leetcode.cn/problems/house-robber-ii/solution/jian-ji-xie-fa-zhi-jie-diao-yong-198-ti-qhvri/
按照思路一步步来:
回溯怎么写:
当前操作:选或者不选当前这家偷
子问题和下一个子问题:
如果之前偷了,那就是从 n - 2开始
如果之前不偷,那就是从 n - 1开始
回溯代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Solution { int [] nums; int n; public int rob (int [] nums) { this .nums = nums; n = nums.length; return backtrack(n - 1 ); } private int backtrack (int i) { if (i < 0 ) { return 0 ; } int res = Math.max(backtrack(i - 1 ), backtrack(i - 2 ) + nums[i]); return res; } }
注意这里的回溯定义,返回了从 n - 1开始之前的打家劫舍的最大值
加入记忆化搜索:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Solution { int [] nums; int n; int [] cache; public int rob (int [] nums) { this .nums = nums; n = nums.length; cache = new int [n]; Arrays.fill(cache, -1 ); return backtrack(n - 1 ); } private int backtrack (int i) { if (i < 0 ) { return 0 ; } if (cache[i] != -1 ) return cache[i]; int res = Math.max(backtrack(i - 1 ), backtrack(i - 2 ) + nums[i]); cache[i] = res; return res; } }
改造为递推
1 2 3 4 5 6 7 8 9 10 11 12 13 class Solution { public int rob (int [] nums) { int n = nums.length; int [] dp = new int [n + 2 ]; dp[n + 1 ] = 0 ; dp[n] = 0 ; for (int i = 0 ; i < n; i++) { dp[i + 2 ] = Math.max(dp[i + 1 ], dp[i] + nums[i]); } return dp[n + 1 ]; } }
空间优化(由于 i + 2 只和i + 1 以及 i 有关)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Solution { public int rob (int [] nums) { int n = nums.length; int f = 0 ; int f1 = 0 ; int f0 = 0 ; for (int i = 0 ; i < n; i++) { f = Math.max(f1, f0 + nums[i]); f0 = f1; f1 = f; } return f1; } }
由于现在是环形那么我们就是在原版的198中增加了一些条件限制:
如果选nums[0]
进行偷窃,我们下一个偷的就从2
开始;因为是环形,偷了第i = 0
个意味着不能偷i = n - 1
,也就是 偷n - 2
因此问题变成从nums[2]
到 nums[n−2]
的非环形版本,调用 198 题的代码解决
如果不
选nums[0]
进行偷窃,我们下一个偷的就从1
开始;因为是环形,偷了第 i = 1
个意味着不能偷i = n - 2
,也就是 偷n - 1
因此问题变成从nums[1]
到 nums[n−1]
的非环形版本,调用 198 题的代码解决
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Solution { public int rob (int [] nums) { int n = nums.length; return Math.max(robFromLC198(nums, 2 , n - 2 ) + nums[0 ], robFromLC198(nums, 1 , n - 1 )); } public int robFromLC198 (int [] nums, int start, int end) { int n = nums.length; int f = 0 ; int f1 = 0 ; int f0 = 0 ; for (int i = start; i <= end; i++) { f = Math.max(f1, f0 + nums[i]); f0 = f1; f1 = f; } return f1; } }
这是一道树形DP:
这个考点叫树上最大独立集
最大独立集:从图中选出尽量多的点,是的这些点互不相邻;它的变形是最大化点权之和
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 class Solution { Map<TreeNode, Integer> memo = new HashMap <>(); public int rob (TreeNode root) { int [] res = dfs2(root); return Math.max(res[0 ], res[1 ]); } private int dfs (TreeNode node) { if (node == null ) return 0 ; if (memo.containsKey(node)) { return memo.get(node); } int rob_result = node.val + (node.left == null ? 0 : rob(node.left.left) + rob(node.left.right)) + (node.right == null ? 0 : rob(node.right.left) + rob(node.right.right)); int not_rob_result = rob(node.left) + rob(node.right); int result = Math.max(rob_result, not_rob_result); memo.put(node, result); return result; } private int [] dfs2(TreeNode node) { if (node == null ) return new int [] {0 , 0 }; int [] leftResult = dfs2(node.left); int [] rightResult = dfs2(node.right); int resRob = leftResult[0 ] + rightResult[0 ] + node.val; int resNotRob = Math.max(leftResult[0 ], leftResult[1 ]) + Math.max(rightResult[0 ], rightResult[1 ]); return new int [] {resNotRob, resRob}; } }
分成多个子问题,爬第n阶楼梯的方法数量,等于 2 部分之和
爬上 n−1 阶楼梯的方法数量。因为再爬1阶就能到第n阶
爬上 n−2 阶楼梯的方法数量,因为再爬2阶就能到第n阶
所以我们得到公式 dp[n]=dp[n−1]+dp[n−2] 同时需要初始化 dp[0]=1 和 dp[1]=1 时间复杂度:O(n)
1 2 3 4 5 6 7 8 9 10 11 12 13 class Solution { public int climbStairs (int n) { if (n == 1 ) return 1 ; int f1 = 1 ; int f2 = 2 ; for (int i = 3 ; i <= n; i++) { int newF = f1 + f2; f1 = f2; f2 = newF; } return f2; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 class Solution { public int minCostClimbingStairs (int [] cost) { int [] dp = new int [cost.length + 1 ]; dp[0 ] = 0 ; dp[1 ] = 0 ; for (int i = 2 ; i < dp.length; i++) { dp[i] = Math.min(dp[i - 1 ] + cost[i - 1 ], dp[i - 2 ] + cost[i - 2 ]); } Arrays.stream(dp).forEach(e -> System.out.print(e + " " )); return dp[dp.length - 1 ]; } }
然后是基于dp数组的空间优化:
1 2 3 4 5 6 7 8 9 10 11 12 13 class Solution { public int minCostClimbingStairs (int [] cost) { int dpi1 = 0 , dpi2 = 0 ; int minCost = 0 ; for (int i = 2 ; i < cost.length + 1 ; i++) { minCost = Math.min(dpi1 + cost[i - 1 ], dpi2 + cost[i - 2 ]); dpi2 = dpi1; dpi1 = minCost; } return minCost; } }
最后是这次写出来的版本:根本区别是我定义是到达这个位置的最小成本(初始化f0 = cost[0] f1 = cost[1])而之前的是初始化为0
1 2 3 4 5 6 7 8 9 10 11 class Solution { public int minCostClimbingStairs (int [] cost) { int f0 = cost[0 ], f1 = cost[1 ]; for (int i = 2 ; i < cost.length; i++) { int newF = Math.min(f0, f1) + cost[i]; f0 = f1; f1 = newF; } return Math.min(f0, f1); } }
题目中:请你返回满足以上要求的 不同 好字符串数目。由于答案可能很大,请将结果对 109 + 7
取余 后返回。
范围比较大,就是大概率dp问题
这道题和 70 题类似思路是:
dp定义:dp[i] 是当前长度为i的字符串数目
所以答案是 dp[row] + … + dp[high]
状态转移:和 70 题类似:
70 是要么一次爬一级楼梯要么爬两级所以转移从 i - 1 以及 i - 2 得到
这道题是 转移从 zero 或者 one 得到
i - zero 为末尾加了 zero 个 零
i - one 为 末尾 加了 one 个 一
初始化 技巧:涉及到方案数我们可以列举Corner Case e.g.: 如果是空串,那么就是 f[0] = 1
空串的字符串个数为1
取MOD
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Solution { static final int MOD = (int ) 1e9 + 7 ; public int countGoodStrings (int low, int high, int zero, int one) { int [] dp = new int [high + 1 ]; dp[0 ] = 1 ; for (int i = 1 ; i <= high; i++) { if (i >= zero) dp[i] = (dp[i] + dp[i - zero]) % MOD ; if (i >= one) dp[i] = (dp[i] + dp[i - one]) % MOD; } int ans = 0 ; for (int i = low; i <= high; i++) { ans = (ans + dp[i]) % MOD; } return ans; } }
例子:
1 2 输入:low = 3, high = 3, zero = 1, one = 1 输出:8
我们有dp数组:1 2 4 8
dp[1]
= 2 (可以由1个1组成 或者 由1个0)
dp[2]
= 4 (1
0, 1
1,0
1,0
0)
dp[3]
= 8 (10
0,10
1,11
0,11
1,01
0,01
1,00
0,00
1)
所以就是每一次都将添加1或0后的形成的方案数
+ 之前的结果转移
得到
背包问题 0-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 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 class Solution { int [] nums; int n; int [][] memo; public int findTargetSumWays (int [] nums, int target) { this .nums = nums; this .n = this .nums.length; int s = Arrays.stream(nums).sum(); int p = target + s; if (p % 2 != 0 || s < target || p < 0 ) return 0 ; p /= 2 ; int [] dp = new int [p + 1 ]; dp[0 ] = 1 ; for (int i = 0 ; i < n; i++) { int num = nums[i]; for (int j = p; j >= num; j--) { dp[j] = dp[j] + dp[j - num]; } } return dp[p]; } private int dp (int idx, int target) { if (idx < 0 ) { return target == 0 ? 1 : 0 ; } if (memo[idx][target] != -1 ) return memo[idx][target]; if (target < nums[idx]) { memo[idx][target] = dp(idx - 1 , target); return memo[idx][target]; } int tot = dp(idx - 1 , target) + dp(idx - 1 , target - nums[idx]); memo[idx][target] = tot; return tot; } }
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 class Solution { int [] coins; int n; int [][] memo; public int coinChange (int [] coins, int amount) { this .n = coins.length; this .coins = coins; int [] dp = new int [amount + 1 ]; Arrays.fill(dp, Integer.MAX_VALUE / 2 ); dp[0 ] = 0 ; for (int coin : coins) { for (int capacity = coin; capacity <= amount; capacity++) { dp[capacity] = Math.min(dp[capacity], dp[capacity - coin] + 1 ); } } int ans = dp[amount]; return ans < Integer.MAX_VALUE / 2 ? ans : -1 ; } private int dp (int idx, int amount) { if (idx == n) { return amount == 0 ? 0 : Integer.MAX_VALUE / 2 ; } if (memo[idx][amount] != -1 ) return memo[idx][amount]; if (coins[idx] > amount) { memo[idx][amount] = dp(idx + 1 , amount); return memo[idx][amount]; } int res = Math.min(dp(idx + 1 , amount), dp(idx, amount - coins[idx]) + 1 ); memo[idx][amount] = res; return res; } }