39. 组合总和
思路
数字重复
- 不同于之前不允许重复数字的组合题,本题允许重复的数字,那对于组合而言,只需要允许当前节点的数值在下一层级也允许被选到就行
输入源的有序性对剪枝优化的差异
- 如果有序,则剪枝时根据有序性,可以推断右边及右下的所有节点可选数值都大于当前节点值,因此直接裁掉
- 如果无序,则无法如上推断,只能裁掉当前节点及其子树,裁剪空间有限
注意:
- 排序耗费的时间和剪枝优化的时间要对比
解题方法
复杂度
- 时间复杂度:
添加时间复杂度, 示例: O ( n ) O(n) O(n)
- 空间复杂度:
添加空间复杂度, 示例: O ( n ) O(n) O(n)
Code
class Solution {
private List<List<Integer>> result = new ArrayList<>();
private LinkedList<Integer> path = new LinkedList<>();
private int sum = 0;
public List<List<Integer>> combinationSum(int[] candidates, int target) {
// 先将数组排序,这样剪枝优化时可以直接裁掉树的右下角
// 但实际运行起来,似乎这个排序的时间大于裁剪优化的时间
// Arrays.sort(candidates);
backTracking(candidates, target, 0);
return result;
}
private void backTracking(int[] candidates, int target, int startIndex) {
// 终止条件
if (sum == target) {
result.add(new ArrayList<>(path));
return;
}
// 单层操作 -- 横向遍历
// 注意:这里无法通过 path 的个数来剪枝
for (int i = startIndex; i < candidates.length; i++) {
if (sum + candidates[i] > target) {
// 如果是有序
// return;
// 如果是无序,只能裁掉当前节点的子树
continue;
}
path.add(candidates[i]);
sum += candidates[i];
// 纵向遍历
backTracking(candidates, target, i); // startIndex 不加一,表示下一层可以与当前的一致
path.removeLast();
sum -= candidates[i];
}
return;
}
}
40.组合总和II
思路
注意:不能使用 map、set 去重,会超时
所谓去重,其实就是使用过的元素不能重复选取
组合问题可以抽象为树形结构,那么“使用过”在这个树形结构上是有两个维度的
- 一个维度是同一树枝上使用过(纵向)
- 一个维度是同一树层上使用过(横向)
本题中,元素在同一个组合内是可以重复的,怎么重复都没事,但两个组合不能相同。
所以我们要去重的是同一树层上的“使用过”(横向),同一树枝上的都是一个组合里的元素(纵向),不用去重。
注意:树层去重的话,需要对数组排序,然后用类似双指针就能做对比
解题方法
描述你的解题方法
复杂度
- 时间复杂度:
添加时间复杂度, 示例: O ( n ) O(n) O(n)
- 空间复杂度:
添加空间复杂度, 示例: O ( n ) O(n) O(n)
Code
class Solution {
// 不能使用map、set 去重,会超时
// 所谓去重,其实就是使用过的元素不能重复选取
// 都知道组合问题可以抽象为树形结构,那么“使用过”在这个树形结构上是有两个维度的,一个维度是**同一树枝上使用过(纵向)**,**一个维度是同一树层上使用过(横向)**
// 本题中,元素在同一个组合内是可以重复的,怎么重复都没事,但两个组合不能相同。
// 所以我们要去重的是**同一树层上的“使用过”(横向)**,**同一树枝上的都是一个组合里的元素(纵向),不用去重**。
// 注意:树层去重的话,需要对数组排序,然后用类似双指针就能做对比
private List<List<Integer>> result = new ArrayList<>();
private LinkedList<Integer> path = new LinkedList<>();
private int sum = 0;
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates);
backTracking(candidates, target, 0);
return result;
}
private void backTracking(int[] candidates, int target, int startIndex) {
// 终止条件
if (sum == target) {
result.add(new ArrayList<>(path));
return;
}
for (int i = startIndex; i < candidates.length && sum + candidates[i] <= target; 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];
}
return;
}
}
131.分割回文串
思路
通过回溯来解决分割问题,将分割问题转换成逻辑树,在每一层中,通过 startIndex
控制截取的位置,每个节点往下是纵向的递归遍历,注意,传入的是 i + 1
,也即,切割过的地方不能再切割
回文判断
- 反转后判断字符串是否相等
- 双指针对向遍历,判断是否相等
解题方法
描述你的解题方法
复杂度
- 时间复杂度:
添加时间复杂度, 示例: O ( n ) O(n) O(n)
- 空间复杂度:
添加空间复杂度, 示例: O ( n ) O(n) O(n)
Code
class Solution {
private List<List<String>> result = new ArrayList<>();
private LinkedList<String> path = new LinkedList<>();
public List<List<String>> partition(String s) {
backTracking(s, 0);
return result;
}
private void backTracking(String s, int startIndex) {
// 终止条件
// 能走到叶子节点的,说明 path 内的都是符合回文定义的字符串,如果不符合,会在之前节点中被终止
if (s.length() <= startIndex) {
result.add(new ArrayList<>(path));
return;
}
// 横向遍历
// 通过 startIndex 控制切割字符串 s 的位置
for (int i = startIndex; i < s.length(); i++) {
// 当前节点处理
// 判断切割出来的字符串 [startIndex, i] 是不是回文,如果是,则加入到 path 中,如果不是,则当前节点及其子树都丢弃
if (!isPalindrome(s, startIndex, i)) {
continue;
}
path.add(s.substring(startIndex, i + 1));
// 纵向遍历
backTracking(s, i + 1);
path.removeLast();
}
return;
}
private boolean isPalindrome(String s, int start, int end) {
for (int i = start, j = end; i < j; i++, j--) {
if (s.charAt(i) != s.charAt(j)) {
return false;
}
}
return true;
}
}