(Java)leetcode-78,90,46,47,39,40,131,22(回溯法题集)

文中题目还有许多其他解法,回溯法或许不是最优的,但是本文只对相关题目做回溯法的整理,方便对比学习

回溯法

回溯法又称试探法,类似于枚举法,但回溯法在搜索过程中可以根据约束条件进行剪枝(避免不必要的搜索),是一种较为通用的搜索算法。其采用深度优先(DFS)策略,从根节点出发,递归地搜索解空间树,直到找到解或者最后穷尽解空间树后返回。

本文整理了leetcode上用到回溯法的题目,并给出采用回溯法的思路与解法。
所有的解法都有一样的框架,只是在如何剪枝、去重等细节上略有不同。

leetcode 78. 子集

题目描述

给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。

说明:解集不能包含重复的子集。

示例:

输入: nums = [1,2,3]
输出:

[
  [3],
  [1],
  [2],
  [1,2,3],
  [1,3],
  [2,3],
  [1,2],
  []
]

思路

基础回溯题,用标准的回溯法进行一波深搜之后,产生的结果必然是没有重复的。
可以通过临时数组的变化来感受整个过程:

	[],
	[1],
	[1,2],
	[1,2,3],
	[1,3],
	[2],
	[2,3],
	[3]

理解这一题是理解后面题目的基础。

代码

public List<List<Integer>> subsets(int[] nums) {
    List<List<Integer>> list = new ArrayList<>();
    backtrack(list, new ArrayList<>(), nums, 0);
    return list;
}


private void backtrack(List<List<Integer>> list , List<Integer> tempList, int [] nums, int start){
    list.add(new ArrayList<>(tempList)); // 每次临时list元素变化,都添加到结果列表
    for(int i = start; i < nums.length; i++){
        tempList.add(nums[i]);
        backtrack(list, tempList, nums, i + 1); // start的增加推动搜索的展开
        tempList.remove(tempList.size() - 1); //回退,通过删除最后一个数字实现
    }
}

leetcode 90. 子集 II

题目描述

给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。

说明:解集不能包含重复的子集。

示例:

输入: [1,2,2]
输出:

[
  [2],
  [1],
  [1,2,2],
  [2,2],
  [1,2],
  []
]

思路

这题与上一题的区别是,待选数组nums中的元素可能出现重复,如果还照搬上面的方法,那么临时list的变化将如下:

	[],
	[1],
	[1,2],
	[1,2,2],
	[1,2], //重复
	[2],
	[2,2],
	[2] //重复

显然,解集也将出现重复。那么我们只需要想办法去重,那么这道题就能迎刃而解了。
第一感可能会写出下面的代码,也就是在将子集添加到结果列表前先判断,结果中是否已经存在这个子集。

代码1

public List<List<Integer>> subsetsWithDup(int[] nums) {
    List<List<Integer>> list = new ArrayList<>();
    backtrack(list, new ArrayList<>(), nums, 0);
    return list;
}

private void backtrack(List<List<Integer>> list , List<Integer> tempList, int [] nums, int start){
    list.add(new ArrayList<>(tempList));
    for(int i = start; i < nums.length; i++){
        tempList.add(nums[i]);
        if (!list.contains(tempList)) { //去重
            backtrack(list, tempList, nums, i + 1);	
        }
        tempList.remove(tempList.size() - 1);
    }
 }

然而因为contains()方法需要遍历整个list,这样判断的方法是比较低效的。
一种更好的判断方法是使用nums[i] == nums[i-1],结果为true说明这个分支前面已经走过一遍了,也就不需要再继续了,注意,为了这个判断生效,必须先对待选数组进行排序Arrays.sort(nums),以保证相同的数字必然处在相邻的位置,从而可以被如上语句判断出重复的分支。此外还要注意的是,为了防止数组越界,还必须加上i > start的条件,否则在第一个分支时就会被代码拦截,造成无法往下搜索的情况。
那么,注意到以上几个要点,就可以写出更好的代码了:

代码2

public List<List<Integer>> subsetsWithDup(int[] nums) {
    List<List<Integer>> list = new ArrayList<>();
    Arrays.sort(nums); // 排序是必要的
    backtrack(list, new ArrayList<>(), nums, 0);
    return list;
}

private void backtrack(List<List<Integer>> list, List<Integer> tempList, int [] nums, int start){
    list.add(new ArrayList<>(tempList));
    for(int i = start; i < nums.length; i++){
        if(i > start && nums[i] == nums[i-1]) continue; // skip duplicates
        tempList.add(nums[i]);
        backtrack(list, tempList, nums, i + 1);
        tempList.remove(tempList.size() - 1);
    }
} 

leetcode 46. 全排列

题目描述

给定一个没有重复数字的序列,返回其所有可能的全排列。

示例:

输入: [1,2,3]
输出:

[
  [1,2,3],
  [1,3,2],
  [2,1,3],
  [2,3,1],
  [3,1,2],
  [3,2,1]
]

思路

这道题目对比以上两道题目,明显的区别是:

  • 解集的元素大小都是一致的(全排列),并且临时list不再是每变化一次就添加到解集中,而是满了再添加。
  • 前面的题目,还有个特点是“好马不吃回头草”,例如[1,2,3]之后是[1,3],“2”不必再理会;这道题中必须一个不落,也就是还要把2给添加到临时list中,如何做到一个不落?就是每次都遍历nums[]中的所有元素,并且用一个boolean数组去标记每个位置上的元素是否已经被添加,若没有,则需要添加到临时list中

代码

class Solution {
	public List<List<Integer>> permuteUnique(int[] nums) {
	   List<List<Integer>> list = new ArrayList<>();
	    Arrays.sort(nums); 
	   backtrack(list, new ArrayList<>(), new boolean[nums.length], nums);
	   return list;
	}

	private void backtrack(List<List<Integer>> list, List<Integer> tempList, boolean[] used, int [] nums){
	   if(tempList.size() == nums.length){ //满了再添加到结集
	      list.add(new ArrayList<>(tempList));
	   } else {
	      for(int i = 0; i < nums.length; i++){ 
	      	if((i > 0 && nums[i] == nums[i-1]) || used[i]) continue;
	      	 used[i] = true;
	         tempList.add(nums[i]);
	         backtrack(list, tempList, used, nums);
	         used[i] = false;
	         tempList.remove(tempList.size() - 1);
	      }
	   }
	} 
}

leetcode 47. 全排列 II

题目描述

给定一个可包含重复数字的序列,返回所有不重复的全排列。

示例:

输入: [1,1,2]
输出:

[
  [1,1,2],
  [1,2,1],
  [2,1,1]
]

思路

显然,这题与46的区别就是:给定序列可能重复。要对解集进行去重,关键是used[i - 1]为false,意味着进入到了重复的分支,那么应该continue。
比如[1,1,2]的分支已经搜索过,剪枝后的[1]就会触发!used[i - 1],避免继续往下搜索产生[1,1,2]的重复解。(注意两个1的区别)

代码

public List<List<Integer>> permuteUnique(int[] nums) {
    List<List<Integer>> list = new ArrayList<>();
    Arrays.sort(nums);
    backtrack(list, new ArrayList<>(), nums, new boolean[nums.length]);
    return list;
}

private void backtrack(List<List<Integer>> list, List<Integer> tempList, int [] nums, boolean [] used){
    if(tempList.size() == nums.length){
        list.add(new ArrayList<>(tempList));
    } else{
        for(int i = 0; i < nums.length; i++){
            if(used[i] || i > 0 && nums[i] == nums[i-1] && !used[i - 1]) continue;
            used[i] = true; 
            tempList.add(nums[i]);
            backtrack(list, tempList, nums, used);
            used[i] = false; 
            tempList.remove(tempList.size() - 1);
        }
    }
}

leetcode 39.组合总和

题目描述

给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。

candidates 中的数字可以无限制重复被选取。

说明:

所有数字(包括 target)都是正整数。
解集不能包含重复的组合。
示例 1:

输入: candidates = [2,3,6,7], target = 7,
所求解集为:

[
  [7],
  [2,2,3]
]

示例 2:

输入: candidates = [2,3,5], target = 8,
所求解集为:

[
  [2,2,2,2],
  [2,3,3],
  [3,5]
]

思路

这题的特点是

  • 给出的数字可以复用
  • 添加到解集的条件是,数字之和=目标值

除此之外和第一题(leetcode 78. 子集)如出一辙。
复用的方法是调用递归函数时,int start传的不是i+1而是i。
判定条件就维护一个remain值即可。
解决以上两点,照着第一题画葫芦就能写出代码了。(排序不是必须的)

代码

public List<List<Integer>> combinationSum(int[] nums, int target) {
    List<List<Integer>> list = new ArrayList<>();
    Arrays.sort(nums);
    backtrack(list, new ArrayList<>(), nums, target, 0);
    return list;
}

private void backtrack(List<List<Integer>> list, List<Integer> tempList, int [] nums, int remain, int start){
    if(remain < 0) return;
    else if(remain == 0) list.add(new ArrayList<>(tempList));
    else{ 
        for(int i = start; i < nums.length; i++){
            tempList.add(nums[i]);
            backtrack(list, tempList, nums, remain - nums[i], i); // not i + 1 because we can reuse same elements
            tempList.remove(tempList.size() - 1);
        }
    }
}

leetcode 40. 组合总和 II

题目描述

给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。

candidates 中的每个数字在每个组合中只能使用一次。

说明:

所有数字(包括目标数)都是正整数。
解集不能包含重复的组合。
示例 1:

输入: candidates = [10,1,2,7,6,1,5], target = 8,
所求解集为:

[
  [1, 7],
  [1, 2, 5],
  [2, 6],
  [1, 1, 6]
]

示例 2:

输入: candidates = [2,5,2,1,2], target = 5,
所求解集为:

[
  [1,2,2],
  [5]
]

思路

此题与上一题的区别是,candidates中的元素有重复,但是只能使用一次。
只能使用一次则int start传i+1即可,有重复则用if(i > start && nums[i] == nums[i-1] ) continue去重。

代码

public List<List<Integer>> combinationSum2(int[] nums, int target) {
    List<List<Integer>> list = new ArrayList<>();
    Arrays.sort(nums);
    backtrack(list, new ArrayList<>(), nums, target, 0);
    return list;
}

private void backtrack(List<List<Integer>> list, List<Integer> tempList, int [] nums, int remain, int start){
    if(remain < 0) return;
    else if(remain == 0) list.add(new ArrayList<>(tempList));
    else{ 
        for(int i = start; i < nums.length; i++) {
        	if(i > start &&  nums[i] == nums[i-1] ) continue;
            tempList.add(nums[i]);
            backtrack(list, tempList, nums, remain - nums[i], i + 1);
            tempList.remove(tempList.size() - 1); 
        }
    }
}

leetcode 131. 分割回文串

题目描述

给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。

返回 s 所有可能的分割方案。

示例:

输入: “aab”
输出:

[
  ["aa","b"],
  ["a","a","b"]
]

思路

此题特殊之处是,待选为一个字符串,可以通过substring()来推进搜索。
最为关键的是通过回文串判断这个门槛后才将子串添加到临时list中。

代码

class Solution {
	public List<List<String>> partition(String s) {
	   List<List<String>> list = new ArrayList<>();
	   backtrack(list, new ArrayList<>(), s, 0);
	   return list;
	}

	public void backtrack(List<List<String>> list, List<String> tempList, String s, int start){
	   if(start == s.length())
	      list.add(new ArrayList<>(tempList));
	   else{
	      for(int i = start; i < s.length(); i++){
	         if(isPalindrome(s, start, i)){
	            tempList.add(s.substring(start, i + 1));
	            backtrack(list, tempList, s, i + 1);
	            tempList.remove(tempList.size() - 1);
	         }
	      }
	   }
	}

	public boolean isPalindrome(String s, int low, int high){
	   while(low < high)
	      if(s.charAt(low++) != s.charAt(high--)) return false;
	   return true;
	} 

}

leetcode 22. 括号生成

题目描述

给出 n 代表生成括号的对数,请你写出一个函数,使其能够生成所有可能的并且有效的括号组合。

例如,给出 n = 3,生成结果为:

[
“((()))”,
“(()())”,
“(())()”,
“()(())”,
“()()()”
]

思路

此题特点是搜索时【每个结点】只有两种选择,要么添加左括号,要么右括号,这样就产生了两条搜索路径。
我们可以优先尝试添加左括号,在达到上限时添加右括号,通过right < left保证没有违背括号原则,通过回溯保证搜索到所有的结果。

代码

class Solution {
 	public List<String> generateParenthesis(int n) {
        List<String> list = new ArrayList<String>();
        backtrack(list, "", 0, 0, n);
        return list;
    }
    
    public void backtrack(List<String> list, String str, int left, int close, int n){
        
        if(str.length() == max*2){
            list.add(str);
            return;
        }
        
        if(left < n)
            backtrack(list, str+"(", left+1, right, n);
        if(right < left)
            backtrack(list, str+")", left, right+1, n);
    }
}
发布了143 篇原创文章 · 获赞 45 · 访问量 7万+

猜你喜欢

转载自blog.csdn.net/z714405489/article/details/103125023
今日推荐