39. 组合总和
给你一个 无重复元素 的整数数组
candidates和一个目标整数target,找出candidates中可以使数字和为目标数target的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为target的不同组合数少于150个。
思路
这道题给定的数组中元素是不重复的,但每个元素可以不限次数拿取,因此本题的难点在于如何去重。
去重的问题问题是在回溯过程中取值时产生的,如当 target = 7时,取值顺序可以是2 -> 2 -> 3,也可能是2 -> 3 -> 2 和 3 -> 2 -> 2 ,在取值时没有约束,导致最终结果相同。
为了解决这个问题,我们可以首先将给定数组 candidates 进行排序(如自然排序),然后在取值时只允许从同一个方向进行取值,即如果当前取到值的下标为startIndex,那么下一次的取值范围为[startIndex, candidates.length - 1],这样使得取值结果中所有的元素都是不递减的方式取得的,因此只会取一次,避免了重复。
当当前取值的所有元素的和total与将要取值的值的和大于target时,不用继续取值,可以进行剪枝。
代码
class Solution {
public List<List<Integer>> combinationSum(int[] candidates, int target) {
List<Integer> combination = new ArrayList<>();
List<List<Integer>> res = new ArrayList<>();
Arrays.sort(candidates);
backTracking(candidates, target, combination, 0, 0,res);
return res;
}
private void backTracking(int[] candidates, int target,List<Integer> combination, int total, int startIndex, List<List<Integer>> res){
if(total == target){
res.add(new ArrayList<Integer>(combination));
return;
}
for(int i = startIndex; i < candidates.length && (total + candidates[i]) <= target; i++){
combination.add(candidates[i]);
backTracking(candidates, target, combination, total + candidates[i], i, res);
combination.remove(combination.size() - 1);
}
}
}
40.组合总和II
给定一个候选人编号的集合
candidates和一个目标数target,找出candidates中所有可以使数字和为target的组合。
candidates中的每个数字在每个组合中只能使用 一次 。
注意: 解集不能包含重复的组合。
思路
与 39.组合总和 相比,本题的 candidates中的元素是可重复的,并且每个元素只能使用 1 次。
由于元素是可重复的,在回溯树中,对于同一层的两个相同元素,之后的遍历路径是相同的,结果会存在重复。因此,在某一层中,与当前元素相同的元素已经进行遍历时,当前元素不进行遍历,可以达到去重的效果。
为了方便判断相同元素已经进行过遍历,将 candidates先行进行排序,这样,相同的元素将会相邻排布,只需判断当前元素与前一元素是否相同即可。
代码
class Solution {
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
List<List<Integer>> res = new ArrayList<>();
List<Integer> combination = new ArrayList<>();
Arrays.sort(candidates);
backTracking(candidates, target, res, combination, 0, 0);
return res;
}
private void backTracking(int[] candidates, int target, List<List<Integer>> res, List<Integer> combination, int total, int startIndex){
if(total == target){
res.add(new ArrayList<>(combination));
return;
}
for(int i = startIndex; i < candidates.length && total + candidates[i] <= target; i++){
// 同一层中相同元素后续的遍历结果相同,此判断为去重操作
if(i > startIndex && candidates[i - 1] == candidates[i]){
continue;
}
combination.add(candidates[i]);
backTracking(candidates, target, res, combination, total + candidates[i], i + 1);
combination.remove(combination.size() - 1);
}
}
}
131.分割回文串
给你一个字符串
s,请你将 **s**分割成一些子串,使每个子串都是 回文串 。返回s所有可能的分割方案。
回文串 是正着读和反着读都一样的字符串。
思路
这道题主要问题有2个:
- 如何判断回文?
- 如何分隔字符串?
判断回文的方法 : 双指针法
- 对于字符串
s, 使用i和j作为两个指针分别指向字符串的两个字符,每次判断s[i, j]是否是回文。 - 由上可知,
i <= j。 - 对于字符串
s[i, j],有以下三种情况:i == j=>s[i] == s[j],字符串是回文;i + 1 == j=>s[i]和s[j]相邻,字符串长度为 2 。s[i] == s[j]=> 字符串是回文- 否则, 字符串不是回文。
- 否则,
s[i]和s[j]之间还有其他字符,这两个字符为字符串的两端。当s[i] == s[j]并且 二者之间的字符串s[i + 1, j - 1]也是回文时,s[i, j]是回文;否则,它不是回文。
- 由上述描述可知,要知道字符串
s[i, j]是不是回文,需要知道字符串s[i + 1, j - 1]是不是回文。 - 也就是说,当
i在遍历时,要先遍历i + 1再遍历i(反方向),当遍历j时,要先遍历j - 1再遍历j(正方向)。 - 因此,
i遍历的下标是从s.length() - 1到0; - 由于
i <= j, 要判断的字符串范围为s[i, i]到s[i, s.length() - 1]。 - 因此,相应地,对于确定的每一个
i来说,j遍历的下标是从i到s.length() - 1。
分割字符串:回溯法
- 对于字符串
s,使用splitIndex来标识下一刀开始分割的位置,即此字符串中,下标为[0, splitIndex - 1]的子串已经分割完毕,待分割的字符串下标范围为[splitIndex, s.length() - 1]。 - 在初始条件中,待分割的字符串下标范围为
[0, s.length() - 1],即splitIndex为0。 - 在每一次分割中,用
i标识分割结束的下标,切出来的子串下标范围为[splitIndex, i](左闭右闭区间);- 如果该子串是回文,则加入到当前结果集中,并递归分割剩余字符串
[i + 1, s.length() -1],即下一次分割的splitIndex为i + 1;递归结束,不要忘记回退,不然会影响之后的递归结果。 - 否则,这种分割方法达不到目标,不必继续分割剩余字符串(剪枝)。
- 如果该子串是回文,则加入到当前结果集中,并递归分割剩余字符串
- 当
splitIndex == s.length()时,整个字符串都分割完成,这意味着之前的每一次分割得到的字串都是回文,将当前结束集加入到最终结果集中即可以。
代码
class Solution {
public List<List<String>> partition(String s) {
boolean[][] isPalindrome = new boolean[s.length()][s.length()];
List<List<String>> res = new ArrayList<>();
List<String> path = new ArrayList<>();
getPalindrome(isPalindrome, s);
backTracking(s, isPalindrome, 0, res, path);
return res;
}
private void getPalindrome(boolean[][] isPalindrome, String s){
for(int i = s.length() - 1; i >= 0; i--){
for(int j = i; j < s.length(); j++){
if(i == j){
// s[i] == s[j]
isPalindrome[i][j] = true;
continue;
}
if(j - i == 1){
// s[i] 与 s[j] 相邻
isPalindrome[i][j] = s.charAt(i) == s.charAt(j) ? true : false;
continue;
}
if(s.charAt(i) == s.charAt(j) && isPalindrome[i+1][j-1]){
isPalindrome[i][j] = true;
}else{
isPalindrome[i][j] = false;
}
}
}
}
private void backTracking(String s, boolean[][] isPalindrome, int splitIndex, List<List<String>> res, List<String> path){
if(s.length() == splitIndex){
res.add(new ArrayList<String>(path));
return;
}
for(int i = splitIndex; i < s.length(); i++){
// 只有s[splitIndex, i]是回文的时候,才需要继续分隔字符串
// tips :左闭右闭 区间
// 否则,不符合要求,可以剪枝
if(isPalindrome[splitIndex][i]){
// 将字符串s[splitIndex, i] 加入 path
path.add(new String(s.subSequence(splitIndex, i + 1).toString()));
// 分隔剩余部分 s[i + 1,s.length() - 1]
backTracking(s, isPalindrome, i + 1, res, path);
// 回退
path.remove(path.size() - 1);
}
}
}
}
如果想让i正方向地进行遍历,也是可以的,此时,j >= i,子字符串为s[j, i]。代码如下:
private void getPalindrome(boolean[][] isPalindrome, String s){
for(int i = 0; i < s.length(); i++){
for(int j = i; j >= 0; j--){
if(i == j){
// s[i] == s[j]
isPalindrome[j][i] = true;
continue;
}
if(i - j == 1){
// s[i] 与 s[j] 相邻
isPalindrome[j][i] = s.charAt(i) == s.charAt(j) ? true : false;
continue;
}
if(s.charAt(i) == s.charAt(j) && isPalindrome[j + 1][i - 1]){
isPalindrome[j][i] = true;
}else{
isPalindrome[j][i] = false;
}
}
}
}