回溯就是已经没什么别的办法了,回溯=暴力搜索,它相当于构建了一颗超大的树,然后每个叶子节点都是一种可能性,如果可能性符合条件,就要
回溯函数:
1.剪枝+如果到达最终状态,处理结果
2.元素进入与回退
比如说下面那个题,组合,给了一个k,k不确定,那就没法写for循环,只能回溯
回溯算法效率:就是拼谁剪枝剪得好,谁边界条件限制的好
第77题. 组合
给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。
思路:回溯
达到最终状态:那就是放元素的队列长度为k
剪枝:要达到不重复,需要在函数中有一个标识符,以及i不能选择太大,导致如果最后面剩余个数,每个都入队也不能达到k。这些都在for循环中限制了
注:我for循环中剪枝i<= n - (k - deque.size()) + 1 和不剪枝的速度差了22ms
class Solution {
List<List<Integer>> res = new LinkedList<>();
Deque<Integer> deque = new LinkedList<>();
public void backtracking(int n,int k,int startIndex){
if(deque.size()==k){
res.add(new LinkedList(deque));
return;
}
for(int i=startIndex;i<= n - (k - deque.size()) + 1;i++){
deque.offerLast(i);
backtracking(n,k,i+1);
deque.pollLast();
}
}
public List<List<Integer>> combine(int n, int k) {
backtracking(n,k,1);
return res;
}
}
第216题.组合总和III
回溯,就是要尽可能考虑全限制条件,剪枝条件,条件考虑的越全,算法性能越高
那么和上面的题相比,多了什么条件?
剪枝:如果deque长度达到n且和为k,加入结果集,如果和不为k,也要回退
如果长度未达到n且当前和超过k,剪枝
for循环中边界条件多了一个i<=9的限制
class Solution {
List<List<Integer>> res = new LinkedList<>();
Deque<Integer> deque = new LinkedList<>();
int count = 0;
public void backtracking(int n,int k,int startIndex){
if(deque.size()==k){
if(count==n) res.add(new LinkedList(deque));
return;
}
if(count>n-startIndex) return;
for(int i=startIndex;i<= Math.min(n - (k - deque.size()) + 1,9);i++){
deque.offerLast(i);
count+=i;
backtracking(n,k,i+1);
deque.pollLast();
count-=i;
}
}
public List<List<Integer>> combinationSum3(int k, int n) {
backtracking(n,k,1);
return res;
}
}
17.电话号码的字母组合
这个组合和最上面那个组合没有本质区别,只不过是组合怎么取值变了而已
class Solution {
List<String> res = new LinkedList<>();
Deque<Character> chars = new LinkedList<>();
public void backtracking(String digits,int index,Map<Character, String> phonemap){
if(digits.length()==0) return;
if(chars.size()==digits.length()){
StringBuilder str = new StringBuilder();
for(char c:chars){
str.append(c);
}
res.add(str.toString());
return;
}
String phonechar = phonemap.get(digits.charAt(index));
for(int i=0;i<phonechar.length();i++){
chars.offerLast(phonechar.charAt(i));
backtracking(digits,index+1,phonemap);
chars.pollLast();
}
}
public List<String> letterCombinations(String digits) {
Map<Character, String> phoneMap = new HashMap<Character, String>() {
{
put('2', "abc");
put('3', "def");
put('4', "ghi");
put('5', "jkl");
put('6', "mno");
put('7', "pqrs");
put('8', "tuv");
put('9', "wxyz");
}};
backtracking(digits,0,phoneMap);
return res;
}
}
第39题. 组合总和
给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的数字可以无限制重复被选取。
所以这个题剪枝条件就是count>target,因为没要求选择的数不超过几个
还有就是为了不重复,还是要记录数组选择到哪了
以及for循环中加入限制条件,count+当前数组最小值不能超过target
因为可重复选取,所以这里backtracking(candidates,target,i); 如果不可重复i要变成i+1
class Solution {
List<List<Integer>> result = new LinkedList<>();
Deque<Integer> list = new LinkedList<>();
int count=0;
public void backtracking(int[] candidates,int target,int index){
if(count==target){
result.add(new LinkedList(list));
return;
}
if(count>target) return;
for(int i=index;i<candidates.length && count+candidates[i]<=target ;i++){
list.offerLast(candidates[i]);
count+=candidates[i];
backtracking(candidates,target,i);
list.pollLast();
count-=candidates[i];
}
}
public List<List<Integer>> combinationSum(int[] candidates, int target) {
Arrays.sort(candidates);
backtracking(candidates,target,0);
return result;
}
}
以及就是什么时候要有startIndex,啥时候没有这个事,如果是要求无重复,一般是会有startIndex的
40.组合总和II
给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用一次。
这回的candidates 数组中有了一个新情况,就是candidates 里面是带有重复数字的
如果我们仅仅用startIndex,可能会出现这样的情况。两个数字是重复的,然后第一次我们选了第一个数字,第二次我们选了第二个数字,然后把这两次的结果都加入了结果集中,但是这样这两次结果是重复的,有人说那我们入结果集之前先依次比对没有重复的再加行不行?太麻烦,有一个简单的方法,引入used数组。
在used数组中,代表着上一个数是否被使用了,如果一个数的上一个数值与它相同,并且没有被使用,那么这个值也不应该被使用
其他的同上一题,这一会backtracking(candidates,target,i+1,used); 是i+1,因为只能用一次
class Solution {
List<List<Integer>> result = new LinkedList<>();
Deque<Integer> list = new LinkedList<>();
int count=0;
public void backtracking(int[] candidates,int target,int index,boolean[] used){
if(count==target){
result.add(new LinkedList(list));
return;
}
if(count>target) return;
for(int i=index;i<candidates.length && count+candidates[i]<=target ;i++){
if(i>0&&candidates[i]==candidates[i-1]&&used[i-1]==false){
continue;
}
list.offerLast(candidates[i]);
count+=candidates[i];
used[i]=true;
backtracking(candidates,target,i+1,used);
list.pollLast();
count-=candidates[i];
used[i]=false;
}
}
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
boolean[] used=new boolean[candidates.length];
Arrays.fill(used,false);
Arrays.sort(candidates);
backtracking(candidates,target,0,used);
return result;
}
}
131.分割回文串
(切割问题)
处理结果:如果startIndex等于字符串长度了,结束,并将结果加入队列
for循环中条件:长度不能超过字符串长度;以及如果截取到的串不为回文,继续下一次for循环
就完事了,没有相应的剪枝操作
class Solution {
List<List<String>> result = new LinkedList<>();
Deque<String> list = new LinkedList<>();
public String revString(String str){
return new StringBuilder(str).reverse().toString();
}
public void backtracking(String s,int startindex){
if(startindex==s.length()){
result.add(new LinkedList(list));
return;
}
for(int i=1;i+startindex<=s.length();i++){
String sub = s.substring(startindex,startindex+i);
if(!sub.equals(revString(sub))){
continue;
}
list.offerLast(sub);
backtracking(s,startindex+i);
list.pollLast();
}
}
public List<List<String>> partition(String s) {
backtracking(s,0);
return result;
}
}
93.复原IP地址
同样,也是字符串分割问题,不过这个题的限制条件就会增加很多,做这个题的目的,就是要看看各位提出限制条件的水平如何hhhh
处理结果部分:如果List集中长度为4,且startIndex等于字符串长度(也就是字符串完全分割),入结果集,即使startIndex等于字符串长度,也要直接return,这就相当于剪枝了
然后for循环中,判断首位是不是0且后面有数字,以及分割出来的字符是否大于255,以及长度不超过3
class Solution {
List<String> result = new LinkedList<>();
Deque<String> list = new LinkedList<>();
public void backtracking(String s,int startindex){
if(list.size()==4){
if(startindex==s.length()){
StringBuilder builder = new StringBuilder();
for(String i:list){
builder.append(i+".");
}
String str = builder.toString();
result.add(str.substring(0,str.length()-1));
return;
}
return;
}
for(int i=1;i+startindex<=s.length()&&i<=3;i++){
String sub = s.substring(startindex,startindex+i);
if(sub.charAt(0)=='0' && i!=1){
continue;
}
if(Integer.parseInt(sub)>255){
continue;
}
list.offerLast(sub);
backtracking(s,startindex+i);
list.pollLast();
}
}
public List<String> restoreIpAddresses(String s) {
backtracking(s,0);
return result;
}
}
第78题. 子集
1.数组中每个元素互不相同
所以结束条件是startIndex==数组长度
然后每一次进回溯函数,都要将当前List集合加入到结果集之中,轻松愉快
class Solution {
List<List<Integer>> result = new LinkedList<>();
Deque<Integer> deque = new LinkedList<>();
public void backtracking(int[] nums,int startindex){
result.add(new LinkedList(deque));
if(startindex==nums.length) return;
for(int i=startindex;i<nums.length;i++){
deque.offerLast(nums[i]);
backtracking(nums,i+1);
deque.pollLast();
}
}
public List<List<Integer>> subsets(int[] nums) {
backtracking(nums,0);
return result;
}
}
第90题.子集II
处理有重复的,使用used数组
在for循环里面加used判断呗
class Solution {
List<List<Integer>> result = new LinkedList<>();
Deque<Integer> deque = new LinkedList<>();
public void backtracking(int[] nums,int startindex,boolean[] used){
result.add(new LinkedList(deque));
if(startindex==nums.length) return;
for(int i=startindex;i<nums.length;i++){
if(i>0&&nums[i]==nums[i-1]&&used[i-1]==false) continue;
deque.offerLast(nums[i]);
used[i]=true;
backtracking(nums,i+1,used);
deque.pollLast();
used[i]=false;
}
}
public List<List<Integer>> subsetsWithDup(int[] nums) {
boolean[] used = new boolean[nums.length];
Arrays.fill(used,false);
Arrays.sort(nums);
backtracking(nums,0,used);
return result;
}
}
491.递增子序列
给定一个整型数组, 你的任务是找到所有该数组的递增子序列,递增子序列的长度至少是2。
输入: [4, 6, 7, 7] 输出: [[4, 6], [4, 7], [4, 6, 7], [4, 6, 7, 7], [6, 7], [6, 7, 7], [7,7], [4,7,7]]
这道题,可能大家第一想法是used数组,但是有一个问题,那就是used数组需要原数组是
有序的,但是一旦你去给数组排序,那就打乱了原有的数组顺序了,所以这道题我们只能使用set去去重。(注:used数组性能是最高的,如果可以用used数组,不要选择用set去去重)
可以看到这个set它是在函数内部的,每一个set只针对它对应的for循环生效,因此这个set的作用就是,在你选择当前值的时候(可能是第三个,可能是第四个,都有可能),保证这一次不会选择到重复的值
结果处理:如果list长度大于2,则加入结果集中
class Solution {
List<List<Integer>> result = new LinkedList<>();
Deque<Integer> list = new LinkedList<>();
public void backtracking(int[] nums,int startindex){
if(list.size()>=2){
result.add(new LinkedList(list));
}
if(startindex>=nums.length) return;
Set<Integer> set = new HashSet<>();
for(int i=startindex;i<nums.length;i++){
if((list.size()!=0&&nums[i]<list.getLast())||
(set.contains(nums[i]))){
continue;
}
list.offerLast(nums[i]);
set.add(nums[i]);
backtracking(nums,i+1);
list.pollLast();
}
}
public List<List<Integer>> findSubsequences(int[] nums) {
backtracking(nums,0);
return result;
}
}
46.全排列
一个没有重复的序列,打印其全排列
这一次,没有startIndex,且for循环内次都是从0开始,选择不选择的判断条件只根据used数组来
结束条件:list长度和nums数组长度相同,证明这是一次完整的排列
class Solution {
List<List<Integer>> result = new LinkedList<>();
Deque<Integer> list = new LinkedList<>();
public void backtracking(int[] nums,boolean[] used){
if(list.size()==nums.length){
result.add(new LinkedList(list));
return;
}
for(int i=0;i<nums.length;i++){
if(used[i]==true) continue;
list.offerLast(nums[i]);
used[i]=true;
backtracking(nums,used);
list.pollLast();
used[i]=false;
}
}
public List<List<Integer>> permute(int[] nums) {
boolean[] used = new boolean[nums.length];
Arrays.fill(used,false);
backtracking(nums,used);
return result;
}
}
47.全排列 II
简单,先给数组有序排列之后,与上一题相同呗,然后如果这个数的值遇上一个数相同,并且上一个数还没有被选择,该数也不能被选择
class Solution {
List<List<Integer>> result = new LinkedList<>();
Deque<Integer> list = new LinkedList<>();
public void backtracking(int[] nums,boolean[] used){
if(list.size()==nums.length){
result.add(new LinkedList(list));
return;
}
for(int i=0;i<nums.length;i++){
if(used[i]==true) continue;
if(i>0&&nums[i]==nums[i-1]&&used[i-1]==false) continue;
list.offerLast(nums[i]);
used[i]=true;
backtracking(nums,used);
list.pollLast();
used[i]=false;
}
}
public List<List<Integer>> permuteUnique(int[] nums) {
boolean[] used = new boolean[nums.length];
Arrays.fill(used,false);
Arrays.sort(nums);
backtracking(nums,used);
return result;
}
}
332.重新安排行程
我在自己的idea里运行加输出结果就没问题,到leetcode上就运行不了,无语
缘妙不可言
class Solution {
List<String> result = new LinkedList<>();
Deque<String> list = new LinkedList<>();
public void backtracting(List<List<String>> tickets,boolean[] used){
if(list.size() == tickets.size()+1){
List<String> newList = new LinkedList(list);
if(result.size()==0){
for(String s:newList){
result.add(new String(s));
}
return;
}
List<String> oldList = result;
for(int i=0;i<oldList.size();i++){
int compare = oldList.get(i).compareTo(newList.get(i));
if(compare > 0){
result.clear();
for(String s:newList){
result.add(new String(s));
}
break;
}
if(compare<0){
break;
}
}
return;
}
for(int i=0;i<tickets.size();i++){
if(used[i] == true){
continue;
}
List<String> tmp = tickets.get(i);
if(list.size()==0){
list.offerLast(tmp.get(0));
list.offerLast(tmp.get(1));
used[i] = true;
backtracting(tickets,used);
list.pollLast();
list.pollLast();
used[i] = false;
}
if(tmp.get(0)==list.peekLast()){
list.offerLast(tmp.get(1));
used[i] = true;
backtracting(tickets,used);
list.pollLast();
used[i] = false;
}
}
}
public List<String> findItinerary(List<List<String>> tickets) {
boolean[] used = new boolean[tickets.size()];
Arrays.fill(used,false);
backtracting(tickets,used);
return result;
}
}