首先此处我们可以与组合问题进行对比:
组合问题:
组合总和问题||:
与上一题的区别在于:
1)组合总和问题中的数组元素是无重复的并且每一个元素可以被重复选取
2)组合II中的数组元素是有重复的,但是每一个元素只能被选去一次。
所以问题在于去重:
去重分为树枝去重和树层去重,此处只进行树层去重。难点在于如何只进行树层去重而不进行树枝去重,所以此处引入新的数组,used数组,如果当前元素使用过就标位1,然后再根据逻辑进行树层去重。
回溯三部曲:
1)输入和输出
组合问题是:输入(数字的数量n,组合的数目k,startindex)输出void
组合总和问题:输入(数组,总和,startindex)输出void
组合总和问题:输入(数组,总和,startindex,used数组)输出void
2)终止条件:
组合问题是:path的大小==组合的数目k,就添加到路径并且return
组合之和:
组合数字之和等于总和就添加到路径并且return,数字之和大于总和直接return
组合之和||:
组合中的数字之和等于总和就添加到路径并且return,数字之和大于总和直接return
3)单层回溯逻辑
此处对比电话号码的字母组合问题,如果是在一个集合中挑选数字就需要startindex,如果不是在一个集合中挑选数字就不需要startindex。
组合问题:startindex = i+1,为了避免重复
组合之和问题: startindex= i,单个数字在组合中可以重复
组合之和||问题:树枝不去重,树层去重。(一定要剪枝不然会超出时间限制)
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();
}
4)剪枝优化:
组合问题:
在集合n中至多要从该起始位置 : i <= n - (k - path.size()) + 1,开始遍历
组合之和问题:
对总集合排序之后,如果下一层的sum(就是本层的 sum + candidates[i])已经大于target,就可以结束本轮for循环的遍历。
for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++)
回溯三部曲:
1)输入和输出
输入:字符串s,startindex(因为考虑到字符串也是一个集合,所以此处也应使用startindex)
输出:void
2)结束的逻辑
在结束的逻辑中这个集合(字符串)需要全部遍历一遍,所以startindex=s.length()时将path装入result(这个装入过程中涉及到对回文串的判断),并且renturn。
if(startindex==s.length()){
for(String str:path){
StringBuilder sb = new StringBuilder(str);
String s2 = sb.reverse().toString();
if(!str.equals(s2)){
return;
}
}
result.add(new ArrayList(path));
return;
}
3)单层递归逻辑
可以先按照树的结构理解一下,path第一次装入a,下一个叶子就是ab,path第一次装入aa,下一个叶子节点就是b,path第一次装入aab,就直接结束了。
所以我们可以发现下一次的startindex是本层的endindex,正好s.substring中涉及到start和结束,所以每次循环的i的意思就变了,不再是集合中的下标而是,切割的结束节点,同时作为下一次切割的开始,为保证每次最少可以切到1个字符,所以i=startindex+1.
for(int i = startindex+1;i<=s.length();i++){
path.add(s.substring(startindex,i));
backtracking(s,i);
path.remove(path.size()-1);
}
1)递归函数参数;
void backtracking (const string& s, int startIndex) {
2)终止条件:
切割线切到了字符串最后面,说明找到了一种切割方法,此时就是本层递归的终止条件。
那么在代码里什么是切割线呢?
在处理组合问题的时候,递归参数需要传入startIndex,表示下一轮递归遍历的起始位置,这个startIndex就是切割线。
void backtracking (const string& s, int startIndex) {
// 如果起始位置已经大于s的大小,说明已经找到了一组分割方案了
if (startIndex >= s.size()) {
result.push_back(path);
return;
}
}
3)
在for (int i = startIndex; i < s.size(); i++)
循环中,我们 定义了起始位置startIndex,那么 [startIndex, i] 就是要截取的子串。
首先判断这个子串是不是回文,如果是回文,就加入在vector
中,path用来记录切割过的回文子串。
代码如下:
for (int i = startIndex; i < s.length(); i++) {
//如果是回文子串,则记录
if (isPalindrome(s, startIndex, i)) {
String str = s.substring(startIndex, i + 1);
deque.addLast(str);
} else {
continue;
}
//起始位置后移,保证不重复
backTracking(s, i + 1);
deque.removeLast();
}
注意切割过的位置,不能重复切割,所以,backtracking(s, i + 1); 传入下一层的起始位置为i + 1。
对比发现,我是在for循环中进行加1,答案是在后续中进行加1,效果是相同的。
补充一下对于回文子串的判断:
//判断是否是回文串
private boolean isPalindrome(String s, int startIndex, int end) {
for (int i = startIndex, j = end; i < j; i++, j--) {
if (s.charAt(i) != s.charAt(j)) {
return false;
}
}
return true;
}