力扣周结如期而至,本周开刷了贪心模块,贪心没有套路这五个字属实有点把我折磨的不轻,先将回溯模块整理一下
77. 组合
解题思路
回溯三部曲
- 递归函数的返回值以及参数
并不需要对返回节点进行操作我们只是需要将符合条件的放到结果集中即可
- 回溯函数终止条件
终止条件就是path集合达到k就可以将节点回收
- 单层的搜索逻辑
剪枝优化
回溯法是一种暴力检索方式,只能对其进行剪枝优化,对于本题的剪枝操作如果n = 4,k = 4的话,那么第一层for循环的时候,从元素2开始的遍历都没有意义了。 在第二层for循环,从元素3开始的遍历都没有意义了。所以我们可以基于此来进行剪枝操作
k - list.size()
表示还要添加的元素
n - (k - list.size()) + 1
表示单层遍历中还可以添加的元素
核心代码
class Solution {
List<List<Integer>> result = new LinkedList<List<Integer>>();
public List<List<Integer>> combine(int n, int k) {
LinkedList<Integer> list = new LinkedList<>();
backTracking(list, n, k, 1);
return result;
}
public void backTracking(LinkedList<Integer> list, int n, int k, int index) {
if(list.size() == k) {
result.add(new LinkedList<Integer>(list));
return;
}
for(int i = index; i <= n - (k - list.size()) + 1; i++) {
list.add(i);
//递归
backTracking(list, n, k, i + 1);
//回溯
list.removeLast();
}
}
}
216. 组合总和 III
解题思路
递归三部曲
- 确认函数的返回值和参数类型,本道题是对符合条件的节点进行收割并不需要对节点进行操作所以返回值是void
- 回溯的终止条件,这道题需要两个条件判断终止,1.sum的和大于n如果不做这一步就会一直递归下去,2.sum==n并且集合容量等于k
- 单层循环,单层循环就是将节点添加
剪枝优化
我们要优化的点就是对单层循环进行优化将一个没必要的递归删去
核心代码
class Solution {
List<List<Integer>> result = new LinkedList<>();
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> combinationSum3(int k, int n) {
backTracking(k, n, 1, 0);
return result;
}
public void backTracking(int k, int n, int index, int sum) {
//这道题的并不是像77.组合那样在叶子结点收割,而是可能在任意结点收割,所以这一步剪枝尤其重要否则会一直递归下去
if(sum > n) {
return;
}
//递归结束的条件path的容量为k并且sum的和为n
if(path.size() == k && sum == n) {
result.add(new LinkedList<>(path));
return;
}
//剪枝
//逐步分析剪枝函数
//k - path.size()代表还需要添加多少元素
//其实将函数移项一下很容易理解 9 - i + 1 代表的是范围内剩余多少元素
//k - path.size() <= 9 - i + 1
//上面的这一个不等式表示的是还需要添加多少元素的个数要小于等于范围内的元素
for(int i = index; i <= 9 - (k - path.size()) + 1; i++) {
path.add(i);
sum += i;
backTracking(k, n, i + 1, sum);
//回溯
sum -= i;
path.removeLast();
}
}
}
17. 电话号码的字母组合
解题思路
哈希打表法先将每个数字的字母存储到哈希表中
递归三部曲
- 确认函数返回值和参数类型,仍然是收割节点所以对于返回的节点不需要做处理所以返回类型为void
- 回溯的终止条件,当path的容量和字符串的长度相等的时候就表示可以收割了
- 这里的单层逻辑是个问题,也是本题的难点,对于每个数字都对应三到四个字母,我们需要对这每个字母都进行组合,所以需要两层for循环,外层循环即是从哈希表中取出需要添加的数字,然后对数字对应的字母进行递归回溯操作
核心代码
class Solution {
List<String> result = new LinkedList<>();
StringBuffer path = new StringBuffer();
public List<String> letterCombinations(String digits) {
if(digits.length() == 0) {
return result;
}
HashMap<Character, String> map = new HashMap<>();
map.put('2',"abc");
map.put('3',"def");
map.put('4',"ghi");
map.put('5',"jkl");
map.put('6',"mno");
map.put('7',"pqrs");
map.put('8',"tuv");
map.put('9',"wxyz");
backTracking(digits, map, 0);
return result;
}
public void backTracking(String digits, HashMap<Character, String> map, int startIndex) {
if(path.length() == digits.length()) {
result.add(path.toString());
return;
}
for(int i = startIndex; i < digits.length(); i++) {
String temp = map.get(digits.charAt(startIndex));
for(int j = 0; j < temp.length(); j++) {
path.append(temp.charAt(j));
backTracking(digits, map, i + 1);
path.deleteCharAt(path.length() - 1);
}
}
}
}
39. 组合总和
解题思路
这道题和216很像不同于216需要对组合的个数进行限制而且这道题是可以反复取数字的
核心代码
class Solution {
List<List<Integer>> result = new LinkedList<>();
LinkedList<Integer> path = new LinkedList<>();
int sum = 0;
public List<List<Integer>> combinationSum(int[] candidates, int target) {
backTracking(candidates, target, 0);
return result;
}
public void backTracking(int[] candidates, int target, int startIndex) {
//因为不是叶子结点收割
if(sum > target) {
return;
}
if(sum == target) {
result.add(new LinkedList<Integer>(path));
return;
}
for(int i = startIndex; i < candidates.length; i++) {
path.add(candidates[i]);
sum += candidates[i];
backTracking(candidates, target, i);//注意这里并不是i+1对于下一层的回溯我们仍然会用到上层的数
path.removeLast();
sum -= candidates[i];
}
}
}
40. 组合总和 II
解题思路
有别于39题的地方是数组里包含了重复的元素如果不进行去重的话会造成相同的组合所以我们需要排序和对上一个节点的判断进行去重操作
核心代码
class Solution {
List<List<Integer>> result = new LinkedList<>();
LinkedList<Integer> path = new LinkedList<>();
int sum = 0;
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates);
backTracking(candidates, target, 0);
return result;
}
//难点在于去重!如何去重
public void backTracking(int[] candidates, int target, int startIndex) {
//因为不是叶子结点收割
if(sum > target) {
return;
}
if(sum == target) {
result.add(new LinkedList<Integer>(path));
return;
}
for(int i = startIndex; i < candidates.length; i++) {
//首个元素是默认添加的,重点在于层序遍历如果第二个元素和第一个元素相同那么就没有添加的必要了
if(i > startIndex && candidates[i] == candidates[i - 1]) {
continue;
}
path.add(candidates[i]);
sum += candidates[i];
backTracking(candidates, target, i + 1);
path.removeLast();
sum -= candidates[i];
}
}
}
以上五道题都是组合类型的问题
131. 分割回文串
非常经典的一道题也是回溯问题中分割字符串的代表题目
解题思路
递归三部曲
- 确认函数返回值和参数类型本题并不需要对节点进行操作所以参数的返回类型为void
- 回溯的终止条件本题是将分割到最后一个字符的时候就停止也就是叶子节点的时候进行收割
- 单层循环,因为要保证path集合中每一个字符串都是回文串所以我们需要在添加的时候进行判断
核心代码
class Solution {
//结果集
List<List<String>> result = new LinkedList<>();
//路径
LinkedList<String> path = new LinkedList<>();
public List<List<String>> partition(String s) {
backTracking(s, 0);
return result;
}
public void backTracking(String s, int startIndex) {
if(startIndex == s.length()) {
result.add(new LinkedList<>(path));
return;
}
for(int i = startIndex; i < s.length(); i++) {
//分割字符串
String temp = s.substring(startIndex, i + 1);
//如果是回文串那么就将回文串添加进来否则直接进行下一循环
if(compare(temp)) {
path.add(temp);
backTracking(s, i + 1);
path.removeLast();
}
}
}
//判断是否是回文串
public boolean compare(String s) {
int left = 0;
int right = s.length() - 1;
while(left < right) {
if(s.charAt(left) != s.charAt(right)) {
return false;
}
left++;
right--;
}
return true;
}
}
93. 复原 IP 地址
解题思路
判断分割的字符串是否符合IP地址
//判断是否含有前导零
//判断是否在范围内
public boolean compare(String s) {
//前导0的判断,字符串长度大于1且第一个元素为0
if(s.length() > 1 && s.charAt(0) == '0') {
return false;
}
//如果字符串长度大于3直接返回false
if(s.length() > 3) {
return false;
}
int num = Integer.parseInt(s);
return num <= 255 ? true : false;
}
复原ip地址我的思路和分割回文串基本类似,因为都是要在分割到字符串末尾的时候path集合中每个元素都符合条件,这道题当时一次很顺利的过了很开心
核心代码
class Solution {
List<String> result = new LinkedList<String>();
StringBuffer sb = new StringBuffer();
//记录.的个数
int count = 0;
public List<String> restoreIpAddresses(String s) {
backTracking(s, 0);
return result;
}
public void backTracking(String s, int index) {
if(index == s.length() && count == 4) {
//消除最后一个.
String str = sb.toString();
str = str.substring(0, str.length() - 1);
result.add(str);
}
//不同于分割回文串的地方当.的个数已经超过了3并且还没分割到字符串的末尾那么接下里的操作就没有意义了直接回溯就可以了
for(int i = index; i < s.length() && count <= 3; i++) {
String temp = s.substring(index, i + 1);
//判断分割的字符串是否符合条件
if(compare(temp)) {
sb.append(temp);
sb.append(".");
count++;
backTracking(s, i + 1);
sb.delete(sb.length() - temp.length() - 1, sb.length());
count--;
}
}
}
//判断是否含有前导零
//判断是否在范围内
public boolean compare(String s) {
//前导0的判断,字符串长度大于1且第一个元素为0
if(s.length() > 1 && s.charAt(0) == '0') {
return false;
}
//如果字符串长度大于3直接返回false
if(s.length() > 3) {
return false;
}
int num = Integer.parseInt(s);
return num <= 255 ? true : false;
}
}
回溯算法的分割字符串的问题接下来就是子集问题
78. 子集
解题思路
对于本题的子集问题无脑的将节点添加就可以了,相信做到这里二叉树的模型已经很清晰了,我们需要对添加的每一个节点进行添加
核心代码
class Solution {
List<List<Integer>> result = new LinkedList<>();
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> subsets(int[] nums) {
backTracking(nums, 0);
return result;
}
public void backTracking(int[] nums, int index) {
result.add(new LinkedList<>(path));
for(int i = index; i < nums.length; i++) {
path.add(nums[i]);
backTracking(nums, i + 1);
path.removeLast();
}
}
}
90. 子集 II
解题思路
有别于78需要排序去重
核心代码
class Solution {
List<List<Integer>> result = new LinkedList<>();
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> subsetsWithDup(int[] nums) {
Arrays.sort(nums);
backTracking(nums, 0);
return result;
}
public void backTracking(int[] nums, int index) {
result.add(new LinkedList<Integer>(path));
for(int i = index; i < nums.length; i++) {
//排序去重,因为for循环相当于时单层遍历,第一个元素肯定不存在重复,但是从第二个元素起如果和它前一个元素一致就重复了
if(i > index && nums[i] == nums[i - 1]) {
continue;
}
path.add(nums[i]);
backTracking(nums, i + 1);
path.removeLast();
}
}
}
46. 全排列
解题思路
回溯三部曲
- 确认函数的返回类型和参数,本题不需要对节点进行操作所以函数的返回类型为void
- 确认递归结束的条件当path集合的容量和数组长度相同的时候就表示为一种组合方式
- 确认单层循环的逻辑,对于已经添加的节点不需要再添加,然后每次添加从数组的第一个元素开始添加
核心代码
class Solution {
List<List<Integer>> result = new ArrayList<>();
ArrayList<Integer> path = new ArrayList<>();
public List<List<Integer>> permute(int[] nums) {
backTracking(nums);
return result;
}
public void backTracking(int[] nums) {
if(path.size() == nums.length) {
result.add(new ArrayList<>(path));
return;
}
for(int i = 0; i < nums.length; i++) {
if(!path.isEmpty() && path.contains(nums[i])) {
continue;
}
path.add(nums[i]);
backTracking(nums);
path.remove(path.size() - 1);
}
}
}
47. 全排列 II
核心思路
有别于46题的地方在于对单层逻辑的判断例如1,1,2这个序列由于第一个元素为1会出现1,1,2的结果但是由于第二个元素为1如果不进行判断会导致重复的情况发生,所以我们额外使用一个Boolean数组来判断每个元素的使用情况,对于递归的逻辑如果这个元素使用过了直接跳到下一层循环即可(和46的全排序一致),重要的就是对于去重的判断,因为我们只需要判定第一层循环中是否选择这个数字所以我们可以添加这个每一行限制条件i > 0 && !used[i - 1] && nums[i] == nums[i - 1]
确保了我们不会选择相同的节点
核心代码
class Solution {
List<List<Integer>> result = new ArrayList<>();
ArrayList<Integer> path = new ArrayList<>();
//记录每个元素的使用情况
boolean[] used = null;
public List<List<Integer>> permuteUnique(int[] nums) {
used = new boolean[nums.length];
//排序去重
Arrays.sort(nums);
backTracking(nums);
return result;
}
public void backTracking(int[] nums) {
if(path.size() == nums.length) {
result.add(new ArrayList<>(path));
return;
}
for(int i = 0; i < nums.length; i++) {
//如果元素使用过直接跳过当前循环
if(used[i]) {
continue;
}
//去重,从数组第二个元素开始判断,如果第一个元素没有使用过并且当前元素和上一个元素相同直接跳过元素
//!used[i - 1]前一个元素没有使用过确保了不影响递归只是影响单层逻辑
if(i > 0 && !used[i - 1] && nums[i] == nums[i - 1]) {
continue;
}
path.add(nums[i]);
used[i] = true;
backTracking(nums);
path.remove(path.size() - 1);
used[i] = false;
}
}
}
回溯模块基本就刷完了对于有代表性的Hard题我后面会单独领出来再做细致的讲解比如说N皇后解数独这类型棋盘的回溯问题
接下来就是贪心的模块刚开始刷很模糊想两三个小时也想不明白一道题而且现状还在持续所以我需要特别理解一下什么是局部最优推出全局最优。继续加油吧