系列文章目录
目录
39.组合总和
回溯法
剪枝优化:
-
在下一层递归中返回:对于
target<0
的情况,其实是依然进入了下一层递归,只是下一层递归剪枝时候,会判断target<0
的话就返回。 -
在本层递归中返回:如果已经知道下一层的
target<0
,就没有必要进入下一层递归。在for循环的搜索范围上,对总集合排序之后,如果下一层的target(就是本层的target - candidates[i]
)已经<0
,就可以结束本轮for
循环的遍历。public List<List<Integer>> combinationSum(int[] candidates, int target) { Arrays.sort(candidates); ... }
public void backTracking(int[] candidates, int target, int startIndex) { ... //单层循环逻辑 for (int i = startIndex; i < candidates.length && target - candidates[i] >= 0; i++) { ... } } }
import java.util.Arrays;
import java.util.LinkedList;
class Solution {
List<Integer> path = new LinkedList<>();
List<List<Integer>> res = new LinkedList<>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
//Arrays.sort(candidates);//对总集合排序
backTracking(candidates, target, 0);
return res;
}
public void backTracking(int[] candidates, int target, int startIndex) {
//剪枝
if (target < 0) return;
//终止条件
if (target == 0) {
res.add(new LinkedList<>(path));
return;
}
//单层循环逻辑
for (int i = startIndex; i < candidates.length /*&& target - candidates[i] >= 0*/; i++) {
path.add(candidates[i]);
target -= candidates[i];//处理节点
backTracking(candidates, target, i);// 关键点:不用i+1了,表示可以重复读取当前的数
path.remove(path.size() - 1);// 回溯
target += candidates[i];// 回溯
}
}
}
40.组合总和II
回溯法
使用标记数组去重
- 判断同一树层上元素(相同的元素)是否使用过:
如果candidates[i] == candidates[i - 1]
并且used[i - 1] == false
,就说明:前一个树枝,使用了candidates[i - 1]
,也就是说同一树层使用过candidates[i - 1]
,此时for
循环里就应该做continue
的操作。 - 在
candidates[i] == candidates[i - 1]
相同的情况下:
used[i - 1] == true
,说明同一树枝candidates[i - 1]
使用过。
used[i - 1] == false
,说明同一树层candidates[i - 1]
使用过。
同一树层,used[i - 1] == false
才能表示,当前取的candidates[i]
是从candidates[i - 1]
回溯而来的。而used[i - 1] == true
,说明是进入下一层递归,去下一个数,所以是树枝上。
import java.util.Arrays;
import java.util.LinkedList;
class Solution {
List<Integer> path = new LinkedList<>();
List<List<Integer>> res = new LinkedList<>();
boolean[] used;
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
// 为了将重复的数字都放到一起,所以先进行排序
Arrays.sort(candidates);
used = new boolean[candidates.length];
// 加标志数组,用来辅助判断同层节点是否已经遍历
Arrays.fill(used, false);
backTracking(candidates,target,0);
return res;
}
public void backTracking(int[] candidates, int target, int startIndex) {
if (target == 0) {
res.add(new LinkedList<>(path));
return;
}
for (int i = startIndex; i < candidates.length && target - candidates[i] >= 0; i++) {//剪枝
// 出现重复节点,同层的第一个节点已经被访问过,所以直接跳过
if (i > 0 && candidates[i] == candidates[i - 1] && !used[i - 1]) {
continue;
}
//处理节点
used[i] = true;
path.add(candidates[i]);
target -= candidates[i];
// 每个节点仅能选择一次,所以从下一位开始
backTracking(candidates, target, i + 1);//递归
used[i] = false;
path.remove(path.size() - 1);
target += candidates[i];
}
}
}
不使用标记数组去重(①双指针:pre
记录前一个元素。②直接用startIndex
来去重)
import java.util.Arrays;
import java.util.LinkedList;
class Solution {
List<Integer> path = new LinkedList<>();
List<List<Integer>> res = new LinkedList<>();
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates);//排序
backTracking(candidates, target, 0);
return res;
}
public void backTracking(int[] candidates, int target, int startIndex) {
//终止条件
if (target == 0) {
res.add(new LinkedList<>(path));
return;
}
//int pre = 0;
//单层循环逻辑
for (int i = startIndex; i < candidates.length && target - candidates[i] >= 0; i++) {//剪枝target<0则不进入循环(break)
//当前元素和前一个元素相等,则该元素跳过
//if (candidates[i] == pre) continue;
//pre = candidates[i];
//正确剔除重复解的办法,跳过同一树层使用过的元素(startIndex控制每一层的元素)
if (i > startIndex && candidates[i] == candidates[i - 1]) continue;
path.add(candidates[i]);//处理节点
target -= candidates[i];
backTracking(candidates, target, i + 1);
path.remove(path.size() - 1);//回溯
target += candidates[i];//回溯
}
}
}
131.分割回文串
回溯法
- 切割线:
本题递归函数参数还需要startIndex
,因为切割过的地方,不能重复切割,和组合问题也是保持一致的。在处理组合问题的时候,递归参数需要传入startIndex
,表示下一轮递归遍历的起始位置,这个startIndex
就是切割线。 - 判断回文子串:
使用双指针法,一个指针从前向后,一个指针从后向前,如果前后指针所指向的元素是相等的,就是回文字符串了。
在终止条件判断是否为回文子串:
import java.util.LinkedList;
class Solution {
List<String> path = new LinkedList<>();
List<List<String>> res = new LinkedList<>();
public List<List<String>> partition(String s) {
backTracking(s, 0);
return res;
}
public void backTracking(String s, int startIndex) {
//终止条件
if (startIndex == s.length()) {
//判断path中每个字符串是否是回文串,不是就退出
for (String str : path) {
int left = 0;
int right = str.length() - 1;
while (left < right) {
if (str.charAt(left) != str.charAt(right)) {
return;
}
left++;
right--;
}
}
res.add(new LinkedList<>(path));
return;
}
for (int i = startIndex; i < s.length(); i++) {
//截取子串
path.add(s.substring(startIndex, i + 1));
//起始位置后移,保证不重复
backTracking(s, i + 1);
path.remove(path.size() - 1);
}
}
}
在加入path
时判断是否为回文子串,是就加入path
,否则continue
:
import java.util.LinkedList;
class Solution {
List<String> path = new LinkedList<>();
List<List<String>> res = new LinkedList<>();
public List<List<String>> partition(String s) {
backTracking(s,0);
return res;
}
public void backTracking(String s, int startIndex) {
//终止条件
if (startIndex == s.length()) {
res.add(new LinkedList<>(path));
return;
}
for (int i = startIndex; i < s.length(); i++) {
String subS = s.substring(startIndex, i + 1);
//如果是回文子串,则记录
if (isPalindrome(subS)) {
path.add(subS);
} else {
continue;
}
//起始位置后移,保证不重复
backTracking(s, i + 1);
path.remove(path.size() - 1);//回溯
}
}
//判断是否是回文串
public boolean isPalindrome(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;
}
}