39. 组合总和
此处的去重略有不同:元素可以重复取,只不过最后的集合结果仍旧不可重复。
因此,需要先对数组进行排序。
没必要剪枝了,只能判断下一层递归的。因此区别不大。
class Solution {
public List<List<Integer>> combinationSum(int[] candidates, int target) {
result = new ArrayList<>();
path = new LinkedList<>();
Arrays.sort(candidates);
backTracking(candidates, target, 0);
return result;
}
private List<List<Integer>> result;
private LinkedList<Integer> path;
private void backTracking(int[] candidates, int targetSum, int startIndex){ // 如果至少一个数字的被选数量不同,则两种组合是不同的。需要去重——只能选择比自己大的。
// 终止条件
if(targetSum<0){
return;
}else if(targetSum==0){
result.add(new ArrayList<Integer>(path));
return;
}
// 回溯
for(int i=startIndex; i<candidates.length; i++){
path.add(candidates[i]);
backTracking(candidates, targetSum-candidates[i], i);
path.removeLast();
}
}
}
40.组合总和II
本来还想改改上一题的代码,发现不太行。
这题又是新花样:取元素的组合里有重复的,但是不能重复取。
因为是同层之间的重复,所以在for
循环内解决。
class Solution {
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
result = new ArrayList<>();
path = new LinkedList<>();
Arrays.sort(candidates);
backTracking(candidates, target, 0);
return result;
}
private List<List<Integer>> result;
private LinkedList<Integer> path;
private void backTracking(int[] candidates, int targetSum, int startIndex){
// 终止条件
if(targetSum<0){
return;
}else if(targetSum==0){
result.add(new ArrayList<Integer>(path));
return;
}
// 回溯
int pre = 0;
for(int i=startIndex; i<candidates.length; i++){
if(pre==candidates[i]) continue;
path.add(candidates[i]);
backTracking(candidates, targetSum-candidates[i], i+1);
path.removeLast();
pre = candidates[i];
}
}
}
代码随想录解法:使用boolean[] used
数组,用来记录同一树枝上的元素是否使用
如果candidates[i] == candidates[i - 1]
并且 used[i - 1] == false
,就说明:前一个树枝,使用了candidates[i - 1]
,也就是说同一树层使用过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]
for
循环而来的。
不得不吐槽一下, 缩进为2太难看了。
class Solution {
LinkedList<Integer> path = new LinkedList<>();
List<List<Integer>> ans = new ArrayList<>();
boolean[] used;
int sum = 0;
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
used = new boolean[candidates.length];
// 加标志数组,用来辅助判断同层节点是否已经遍历
Arrays.fill(used, false);
// 为了将重复的数字都放到一起,所以先进行排序
Arrays.sort(candidates);
backTracking(candidates, target, 0);
return ans;
}
private void backTracking(int[] candidates, int target, int startIndex) {
if (sum == target) {
ans.add(new ArrayList(path));
}
for (int i = startIndex; i < candidates.length; i++) {
if (sum + candidates[i] > target) {
break;
}
// 出现重复节点,同层的第一个节点已经被访问过,所以直接跳过
if (i > 0 && candidates[i] == candidates[i - 1] && !used[i - 1]) {
continue;
}
used[i] = true;
sum += candidates[i];
path.add(candidates[i]);
// 每个节点仅能选择一次,所以从下一位开始
backTracking(candidates, target, i + 1);
used[i] = false;
sum -= candidates[i];
path.removeLast();
}
}
}
组合总和总结
共同点:解集不能包含重复的组合 —— 为了解决该问题,有的用了startIndex,有的用了排序,有的用了used数组。
区别:
-
216.组合总和III
candidates不能重复,也不能重复取。
使用startIndex。 -
39.组合总和
candidates不能重复,但可以重复取。
使用startIndex,并且排序。 -
40.组合总和II
candidates可以重复,但不能重复取。
使用startIndex,并且排序,并且使用used数组。
131.分割回文串
要求每个子串都是回文串
如何切割?
切割问题类似于组合问题,需要用到startIndex:一旦切割了,只需要继续切割后半部分即可。
直到切割完毕:最后没有后半部分了。
组合问题:选取一个a之后,在bcdef中再去选取第二个,选取b之后在cdef中再选取第三个…。
切割问题:切割一个a之后,在bcdef中再去切割第二段,切割b之后在cdef中再切割第三段…。
注意:
- substring中的字母都是小写……
class Solution {
public List<List<String>> partition(String s) {
result = new ArrayList<>();
path = new LinkedList<>();
backTracking(s, 0);
return result;
}
private List<List<String>> result;
private List<String> path;
private void backTracking(String s, int startIndex){ // 分割,左闭右开
// 判断是否回文
if(path.size()>0){
String temp = path.getLast();
for(int i=0, j=temp.length()-1; i<j; i++, j--){
if(temp.charAt(i)!=temp.charAt(j)) return;
}
}
if(startIndex==s.length()){
result.add(new LinkedList<String>(path));
return;
}
// 回溯
for(int i=startIndex + 1; i<=s.length(); i++){
path.add(s.substring(startIndex, i));
backTracking(s, i);
path.removeLast();
}
}
}
代码随想录解法:
- 使用动态规划判断回文串。
- 将回文判断放在了回溯时:我个人是不喜欢这种写法,容易把回溯弄得复杂了。
动规判断回文串:
给定一个字符串s, 长度为n, 它成为回文字串的充分必要条件是
s[0] == s[n-1]
且s[1:n-1]
是回文字串。
可以高效地事先一次性计算出, 针对一个字符串s, 它的任何子串是否是回文字串, 然后在我们的回溯函数中直接查询即可, 省去了双指针移动判定这一步骤。
dp 用二维数组表示start与end。
class Solution {
List<List<String>> result;
LinkedList<String> path;
boolean[][] dp;
public List<List<String>> partition(String s) {
result = new ArrayList<>();
char[] str = s.toCharArray();
path = new LinkedList<>();
dp = new boolean[str.length + 1][str.length + 1];
isPalindrome(str);
backtracking(s, 0);
return result;
}
public void backtracking(String str, int startIndex) {
if (startIndex >= str.length()) {
//如果起始位置大于s的大小,说明找到了一组分割方案
result.add(new ArrayList<>(path));
} else {
for (int i = startIndex; i < str.length(); ++i) {
if (dp[startIndex][i]) {
//是回文子串,进入下一步递归
//先将当前子串保存入path
path.addLast(str.substring(startIndex, i + 1));
//起始位置后移,保证不重复
backtracking(str, i + 1);
path.pollLast();
} else {
//不是回文子串,跳过
continue;
}
}
}
}
//通过动态规划判断是否是回文串,参考动态规划篇 52 回文子串
public void isPalindrome(char[] str) {
for (int i = 0; i <= str.length; ++i) {
dp[i][i] = true;
}
for (int i = 1; i < str.length; ++i) {
for (int j = i; j >= 0; --j) {
if (str[j] == str[i]) {
if (i - j <= 1) {
dp[j][i] = true;
} else if (dp[j + 1][i - 1]) {
dp[j][i] = true;
}
}
}
}
}
}