7.组合总和
例题39:给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target 的不同组合数少于 150 个。
1.题目与之前的组合总和有所区别,数字可以重复取,注意回溯中的i;
2.本题抽象为树形结构搜索过程如图所示:
3.Java中排序函数是Arrays.sort();
class Solution {
List<List<Integer>> res=new ArrayList<>();
LinkedList<Integer> path=new LinkedList<>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
Arrays.sort(candidates);//为什么排序之后就没有重复结果了?
backtracking(candidates,target,0,0);
return res;
}
public void backtracking(int[] candidates,int target,int sum,int startIndex){
if(sum>target){
return;
}
if(sum==target){
res.add(new ArrayList<>(path));
return;
}
for(int i=startIndex;i<candidates.length;i++){
/* if(sum+candidates[i]>target){
break;不让大于target的进入递归,如果去掉就在if那里判断
}*/
path.add(candidates[i]);
sum+=candidates[i];
backtracking(candidates,target,sum,i);//只能取当前i与之后的i,不会取到之前的i
//backtracking(candidates,target,sum,startIndex);这个会遍历重复的结果,如2,2,3和2,3,2
path.removeLast();
sum-=candidates[i];
}
}
}
8.组合总和||
例题40:给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用 一次 。
注意:解集不能包含重复的组合。
1.本题难点在于数组中有重复元素,却不能有重复的组合。如果把所有结果找出来,再用set或者map去重很可能会超时,所以要在搜索时就去重。
2.这里的去重指的到底是哪一种?在同树层上去重和在树枝上去重。要搞清楚因为数组中有重复元素,所以树枝递归中是允许有重复元素的。而不允许有相同组合,也就是说for循环中,不能有相同的元素开始。
3.在同层上去重有两种方法,一种使用used数组判定,一种判断当前开始递归的i与上次递归的index是否相等,如果相等就跳过。
class Solution {
List<List<Integer>> res=new ArrayList<>();
LinkedList<Integer> path=new LinkedList<>();
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates);
backtracking(candidates,target,0,0);
return res;
}
public void backtracking(int[] candidates,int target,int sum,int index){
if(sum==target){
res.add(new ArrayList<>(path));
return;
}
for(int i=index;i<candidates.length && sum+candidates[i]<=target;i++){
if(i>index && candidates[i]==candidates[i-1] ){//为什么用i>index可以在同层去重
continue;
}
path.add(candidates[i]);
sum+=candidates[i];
backtracking(candidates,target,sum,i+1);
sum-=candidates[i];
path.removeLast();
}
}
}
9.分割回文串
例题131:给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。
回文串 是正着读和反着读都一样的字符串。
切割问题也可以抽象为树形结构的搜索过程,如下图所示:
Java中获取字符串某个字符用charAt()函数;
Java中提取一个字符串某个子串,用substring(start,end);注意这里是左开右闭区间。
该题的难点:将切割问题抽象为树型结构的组合问题,搞清楚横向for循环是从哪里开始,纵向递归是从哪里分割。
如何模拟切割线?startIndex就是开始位置,i就是结束的切割位置,其中就是分割的子串。
在递归中如何切割子串?用的substring()这个函数。
如何判断回文字符串?普通做法就是双指针往中间夹,可以优化。
class Solution{
List<List<String>> res=new ArrayList<>();
LinkedList<String> path=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 ArrayList<>(path));
return;
}
for(int i=startIndex;i<s.length();i++){
//从startIndex开始到i的子串
if(isHw(s,startIndex,i)){
String str=s.substring(startIndex,i+1);
path.add(str);
}
else{
continue;
}
backtracking(s,i+1);
path.removeLast();
}
}
public boolean isHw(String s,int start,int end){
while(start<=end){
if(s.charAt(start)!=s.charAt(end)){
return false;
}
else{
start++;
end--;
}
}
return true;
}
}
10.复原IP地址
例题93:有效 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 ‘.’ 分隔。
例如:“0.1.2.201” 和 “192.168.1.1” 是 有效 IP 地址,但是 “0.011.255.245”、“192.168.1.312” 和 “192.168@1.1” 是 无效 IP 地址。
给定一个只包含数字的字符串 s ,用以表示一个 IP 地址,返回所有可能的有效 IP 地址,这些地址可以通过在 s 中插入 ‘.’ 来形成。你 不能 重新排序或删除 s 中的任何数字。你可以按 任何 顺序返回答案。
class Solution{
List<String> res=new ArrayList<>();
String path;
public List<String> restoreIpAddresses(String s){
backtracking(s,0,0);
return res;
}
public void backtracking(String s,int startIndex,int putDh){
if(putDh==3){
if(isR(s,startIndex,s.length()-1)){
res.add(s);
}
return;
}
//这个题不能用开始位置作为终止条件,而是分割成4段,也就是说有3个逗号
for(int i=startIndex;i<s.length();i++){
if(isR(s,startIndex,i)){
s=s.substring(0,i+1)+"."+s.substring(i+1);
putDh++;
backtracking(s,i+2,putDh);//注意这里是插入逗号之后的位置
putDh--;
s=s.substring(0,i+1)+s.substring(i+2);//删掉添加的逗号
}
else{
break;//直接退出这次分割
}
}
}
public boolean isR(String s,int start,int end){
if(start>end){
return false;
}
if(s.charAt(start)=='0' && start!=end){
return false;
}
int num=0;
for(int i=start;i<=end;i++){
if(s.charAt(i)>'9' || s.charAt(i)<'0'){
return false;
}
//计算该字符串对应的数
num=num*10+(s.charAt(i)-'0');
if(num>255 || num<0){
return false;
}
}
return true;
}
}
注意Java中的substring(start,end),也可以直接写开始substring(start),表示从这开始到末尾的子串。
添加逗号之后,递归之后去掉逗号的字符下标要注意是除掉逗号那个下标。
从右到左拿到一个数,计算整个数的大小的方法是在for循环中对每位数10*num+num;
11.子集问题
例题78:给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
这个题和之前的分割组合都不同,组合和分割是收集树的叶子节点,而子集问题是收集树的所有节点。
其实子集问题也可以看作组合问题,但是是无序的,子集{1,2}和{2,1}是一样的。既然无序,取过的元素不会重复,写回溯的时候,for循环从startIndex开始,而不是从0开始。
什么时候for循环是从0开始,排列问题就是从0开始,因为集合是有序的,集合{1,2}和{2,1}是两个集合。
从上图可以看出,子集问题在遍历的时候是把所有节点都记录下来。
class Solution{
List<List<Integer>> res=new ArrayList<>();
LinkedList<Integer> path=new LinkedList<>();
public List<List<Integer>> subsets(int[] nums){
backtracking(nums,0);
return res;
}
public void backtracking(int[] nums,int startIndex){
if(startIndex<=nums.length){ //注意这里是<=,因为每次传path进来i+1了
res.add(new ArrayList<>(path));
}
if(startIndex>nums.length){
return;
}
for(int i=startIndex;i<nums.length;i++){
path.add(nums[i]);
backtracking(nums,i+1);
path.removeLast();
}
}
}
这个题不需要任何剪枝,因为要保存所有的节点。是标准的模板
区分组合、分割、子集问题。组合和分割是求树的叶子节点,子集问题是求树的所有节点。
12.周末总结
1.回溯的组合总和|| 和第一周的组合问题不同。这题没有数量要求,可以无限重复,但是有总和的限制,所以间接也是个数的限制。
2.组合总和||| 依旧是元素重复,但解集不能有重复的组合。难点就在于去重。去重分为树枝去重和树层去重。
都知道组合问题可以抽象为树形结构,那么“使用过”在这个树形结构上是有两个维度的,一个维度是同一树枝上“使用过”,一个维度是同一树层上“使用过”。没有理解这两个层面上的“使用过” 是造成大家没有彻底理解去重的根本原因。
我在图中将used的变化用橘黄色标注上,可以看出在candidates[i] == candidates[i - 1]相同的情况下:
used[i - 1] == true,说明同一树枝candidates[i - 1]使用过
used[i - 1] == false,说明同一树层candidates[i - 1]使用过
3.分割回文串难点:
- 切割问题类似于组合问题
- 如何模拟切割线
- 切割问题中递归如何终止
- 在递归循环中如何截取子串
- 如何判断回文
如果想到了用求解组合问题的思路来解决 切割问题本题就成功一大半了,接下来就可以对着模板照葫芦画瓢。
4.复原IP地址:比分割回文串多了限制,如只能分成4段,直接更改字符串插入逗号。
5.求子集问题,在树形结构中收集所有的节点。
这个题可以作为子集问题的模板。