Day22--回溯--77. 组合,216. 组合总和 III,17. 电话号码的字母组合
Day22–回溯–77. 组合,216. 组合总和 III,17. 电话号码的字母组合
回溯理论知识
《代码随想录》:
- 什么是回溯法?
- 回溯法也可以叫做回溯搜索法,它是一种搜索的方式。
- 回溯是递归的副产品,只要有递归就会有回溯。
- 所以以下讲解中,回溯函数也就是递归函数,指的都是一个函数。
- 回溯法的效率?
- 回溯法的性能如何呢,这里要和大家说清楚了,虽然回溯法很难,很不好理解,但是回溯法并不是什么高效的算法。
- 因为回溯的本质是穷举,穷举所有可能,然后选出我们想要的答案,如果想让回溯法高效一些,可以加一些剪枝的操作,但也改不了回溯法就是穷举的本质。
- 那么既然回溯法并不高效为什么还要用它呢?
- 因为没得选,一些问题能暴力搜出来就不错了,撑死了再剪枝一下,还没有更高效的解法。
- 回溯法,一般可以解决如下几种问题:
- 组合问题:N个数里面按一定规则找出k个数的集合
- 切割问题:一个字符串按一定规则有几种切割方式
- 子集问题:一个N个数的集合里有多少符合条件的子集
- 排列问题:N个数按一定规则全排列,有几种排列方式
- 棋盘问题:N皇后,解数独等等
- 如何理解回溯法?
- 回溯法解决的问题都可以抽象为树形结构,是的,我指的是所有回溯法的问题都可以抽象为树形结构!
- 因为回溯法解决的都是在集合中递归查找子集,集合的大小就构成了树的宽度,递归的深度就构成了树的深度。
- 递归就要有终止条件,所以必然是一棵高度有限的树(N叉树)。
摘抄:大家可以从图中看出for循环可以理解是横向遍历,backtracking(递归)就是纵向遍历,这样就把这棵树全遍历完了,一般来说,搜索叶子节点就是找的其中一个结果了。
// 回溯算法模板框架
void backtracking(参数) {if (终止条件) {存放结果;return;}for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {处理节点;backtracking(路径,选择列表); // 递归回溯,撤销处理结果}
}
77. 组合
思路:
回溯法backtracking。时间复杂度: O(n * 2^n)
// 递归法,使用startIndex作为重要递归参数
class Solution {List<List<Integer>> res = new ArrayList<>();List<Integer> path = new ArrayList<>();public List<List<Integer>> combine(int n, int k) {backtracking(n, k, 1);return res;}private void backtracking(int n, int k, int startIndex) {// 当够k个了,就加到resif (path.size() == k) {res.add(new ArrayList(path));return;}for (int i = startIndex; i <= n; i++) {// path加入本层元素path.add(i);// 下一层从i+1开始backtracking(n, k, i + 1);path.remove(path.size() - 1);}}
}
思路:
剪枝优化
// 递归法,使用startIndex作为重要递归参数
class Solution {List<List<Integer>> res = new ArrayList<>();List<Integer> path = new ArrayList<>();public List<List<Integer>> combine(int n, int k) {backtracking(n, k, 1);return res;}private void backtracking(int n, int k, int startIndex) {// 剪枝;剩余的元素(n - startIndex + 1)加上已有的元素path.size()都不够k个了,就返回if ((n - startIndex + 1) + path.size() < k) {return;}// 当够k个了,就加到resif (path.size() == k) {res.add(new ArrayList(path));return;}for (int i = startIndex; i <= n; i++) {// path加入本层元素path.add(i);// 下一层从i+1开始backtracking(n, k, i + 1);path.remove(path.size() - 1);}}
}
216. 组合总和 III
题目翻译:k个数相加等于n,注意数字只能1->9不是1->n
思路:
递归法。使用startIndex和target作为重要递归参数
- 写在递归函数中的,+1或者-1,就是自带“回溯”。
- 先手动+1,递归后再手动-1的效果是一样的
// 递归法。使用startIndex和target作为重要递归参数
class Solution {List<List<Integer>> res = new ArrayList<>();List<Integer> path = new ArrayList<>();public List<List<Integer>> combinationSum3(int k, int n) {backtracking(k, 1, n);return res;}private void backtracking(int k, int startIndex, int target) {// 剪枝,不然会超时。// 当target<0,证明和过大了,当path.size()超过了k个,证明和过小了(需要的数字过多)if (target < 0 || path.size() > k) {return;}// 个数够了 且 target==0if (path.size() == k && target == 0) {res.add(new ArrayList(path));}// 注意这里是小于9不是小于nfor (int i = startIndex; i <= 9; i++) {path.add(i);// 下一层遍历从i+1开始,同时,仍需要的target,要减去本层已有的i// 写在递归函数中的,+1/-1,就是自带“回溯”。// 先手动+1,递归后再手动-1的效果是一样的backtracking(k, i + 1, target - i);path.remove(path.size() - 1);}}
}
17. 电话号码的字母组合
思路:
组合问题。就是要将按键转为字符数组。比如按键”23“,就是['a','b','c']和['d','e','f']
的组合问题。
class Solution {// 结果集List<String> res = new ArrayList<>();// 路径上的元素集StringBuilder path = new StringBuilder();public List<String> letterCombinations(String digits) {backtracking(digits, 0);return res;}private void backtracking(String digits, int index) {// 如果index=n,证明越界了,已经遍历完了if (index == digits.length()) {return;}// 获取当前循环的按键所拥有的字母char[] ch = getChars(digits.charAt(index));// 遍历这个按键的字母// 如果遍历到了最后一个按键,要做收尾工作,把path转为字符串记录到resif (index == digits.length() - 1) {for (int i = 0; i < ch.length; i++) {path.append(ch[i]);res.add(new String(path));path.deleteCharAt(path.length() - 1);}} else {// 如果不是最后一个按键,递归遍历下一个按键(index+1)for (int i = 0; i < ch.length; i++) {path.append(ch[i]);backtracking(digits, index + 1);// 记得path要做回溯操作path.deleteCharAt(path.length() - 1);}}}// 获取这个按键所拥有的字母private char[] getChars(char number) {switch (number) {case '2': {return new char[] { 'a', 'b', 'c' };}case '3': {return new char[] { 'd', 'e', 'f' };}case '4': {return new char[] { 'g', 'h', 'i' };}case '5': {return new char[] { 'j', 'k', 'l' };}case '6': {return new char[] { 'm', 'n', 'o' };}case '7': {return new char[] { 'p', 'q', 'r', 's' };}case '8': {return new char[] { 't', 'u', 'v' };}case '9': {return new char[] { 'w', 'x', 'y', 'z' };}}return null;}
}
其中,获取这个按键所拥有的字母可以做优化。遍历到了最底层,拼接的时机可以优化。
class Solution {// 结果集List<String> res = new ArrayList<>();// 路径上的元素集StringBuilder path = new StringBuilder();// 按键所拥有的字母,其中"0","1"是空串String[] numString = { "", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz" };public List<String> letterCombinations(String digits) {if (digits == null || digits.length() == 0) {return res;}backtracking(digits, 0);return res;}private void backtracking(String digits, int index) {// 如果index=n,证明越界了,已经遍历完了if (index == digits.length()) {// 要做收尾工作,把path转为字符串记录到resres.add(new String(path));return;}// 获取当前循环的按键所拥有的字母String letters = numString[digits.charAt(index) - '0'];// 遍历这个按键的字母for (int i = 0; i < letters.length(); i++) {path.append(letters.charAt(i));// 如果不是最后一个按键,递归遍历下一个按键(index+1)backtracking(digits, index + 1);// 记得path要做回溯操作path.deleteCharAt(path.length() - 1);}}}