回溯 皇后 算法笔记_算法笔记:DFS+Backtracking系列

subset-DFS+Backtracking系列,有模板方法可以记

1. 题目分析

首先 ,这个题是NP问题,没有多项式时间内的算法,只能用搜索解决的问题

选择用DFS-backtracking 的递归方式解决

画递归树图

搜索问题中处理去重:选代表(改题我们遇到重复的,选择留下有序的),因为我们的递归helper是在index向后排的时候调用,(因为树没有重复,我们枚举元素都是按照顺序的,所以不会有答案出现重复)或者看本题的code中,start就是起到了去重作用的,每次都从start下一个开始

(tips:

题目中说找出所有子集,所有可能这种,大部分都靠搜索解决

去重的误区:找到所有答案,再按要求去重,比如:[1,1,1,1,1,1]找所以子集,其实只有lengh+1个,但如果找所有子集,就太多了,

)

2. 代码实现思路

先排除边界情况:判断输入的array nums 是不是有效的,这里做判断:

if nums==null||nums.length==0{return results};(results是刚初始化的返回值)

写递归:

递归定义:以subset开头的所有自己放在result里(从nums里的startindex开始挑数)

递归的拆解

递归的出口、什么时候记录答案:if(){return;},这里不需要出口,要一直做到没有就退出

3. 时间复杂度分析

通用时间复杂度:O(解的个数 * 每个解产生的复杂度),数学推导有兴趣的话 可以看这个帖子,http://www.jiuzhang.com/qa/1601/

subset

=O(2^n * n) 构造每个答案的时间是1,2,。。。n平均就是n/2,就是n

permutation

=O(nn!) n!个答案

n queens 不知道有几个答案

=O(ns) s 是答案的个数

4. code

class Solution {

/**

* @param S: A set of numbers.

* @return: A list of lists. All valid subsets.

*/

public ArrayList> subsets(int[] nums) {

if(nums==null||nums.length==0) return null;

ArrayList> results = new ArrayList<>();

//把空集开头的所有集合放入result

dfsHelper(nums,0,new ArrayList(), results);

return results;

}

private void dfsHelper(int[] nums,

int startIndex,

ArrayList subset,

ArrayList> results){

//deep copy,否则后面操作subset,subset的内容就改变了

results.add(new ArrayList(subset));

for(int i = startIndex; i

subset.add(nums[i]);

dfsHelper(nums,i+1,subset,results);

subset.remove(subset.size()-1);

}

}

}

5. 一些细节

库函数 Arrays.sort()是用的quick sort实现,可以认为是nlogn的复杂度

代码中写的deep copy,java默认都是pass by reference ,这里不new的话传递的是指向subset的reference,后面subset改变,results中加入的subset也会改变,最后返回就变成了[[],[],[],[]......]

这里不可以用results.addAll(xxx) ,因为addAll 表示,把xxx中的元素都加入到results中,我们是需要加入list,而不是list中的元素

为什么result.add 在for循环前面?

因为答案不仅仅只存在于搜索树的叶子节点,每一个节点都是一个答案,所以进入这个搜索节点 就要add一下

要记得递归完成后subset.remove(subset.size() - 1),这就是BackTracking,把刚才加进去的那一个清除掉(add--remove) ,回到上一步,再继续向后进行,刚才添加进去的那个就是idx=subset.size() - 1,因为我们是往list添加元素,那么当前元素就是添加在list后面,我们回溯是一层一层上来,就是从后一层一层把元素remove掉,当前就remove目前的最后的元素

DFS有两种实现方式,一种是递归,一种是非递归,递归只算作是一种实现的方式

1. 题目分析

出现了找出所有子集的关键字,所以还是用搜索

跟上题不一样的是 input数组中有重复元素

2. 代码实现思路

跟上题一样,除了:

如何去重,比如输入[1,2,2] list长=1的只有两个了[1],[2]。我们可以通过选代表的方式来决定,放入第一个2,所以我们要做的就是在放入第一个2之前,不能放第二个2,

这里首先需要通过排序把相等的数凑在一起

再在helper中,for loop中,if(i>start&&nums[i]==nums[i-1]) continue;的那句话,i>start 说明start这个元素在i前面,应该被先加入子集,但却没有加入。进入递归前,加入的元素是i,传入的start=i+1, 递归中的i==start,刚好加入subset,如果i>start,说明I前面的还没有加入subset

3. 时间复杂度分析

O(n^2)

4. code

public class Solution {

public List> subsetsWithDup(int[] nums) {

List> results = new ArrayList<>();

if(nums==null||nums.length==0) return results;

Arrays.sort(nums);

ArrayList subset = new ArrayList<>();

dfsHelper(0,nums,subset,results);

return results;

}

private void dfsHelper(int start, int[] nums, ArrayList subset, List> results){

results.add(new ArrayList(subset));

for(int i = start; i

if(i>start&&nums[i]==nums[i-1]) continue;

subset.add(nums[i]);

dfsHelper(i+1,nums,subset,results);

subset.remove(subset.size()-1);

}

}

}

5. 一些细节

subset2 需要注意。。

continu用法?这个太初级了吧,最需要注意的就是 for循环递归调用前的if判断

例3: permutation

46. Permutations

(先是一段碎碎念:背会了subset也不会写permutation,好烦躁。这个题可以看出和subset的区别是 拼完之后才放入result中,所以就想helper之后再results.add,但走了条错路,正确的思路是,跟以前一样开始helper的时候result.add不同的是只有当subset==nums.length才添加这个subset. 又挣扎了一会儿发现就算写在后面也可以,写在后面跑一遍发现还是添加了subset那么多东西,就想到要要等三个了在加入results,再想到判断subset大小满足条件才添加,一样可以ac。。。一直觉得递归很难想可能就在于,我怎么都想不到在helper刚进入的时候添加上一次的subset,顶多能看懂,自己想总是把添加操作想在后面,sign。。)

1. 题目分析

如果把所有的recursion问题都想象成一棵树。subset上面每一个树的节点都是结果。而permutation上面只有叶子节点才是结果

2. 代码实现思路

跟subset其实很像,能想到if那句,这个题就解了

3.时间复杂度

recursive的复杂度都不低

这个应该比subset快一点,上面有计算通式,因为解的个数变少了,每个解产生的复杂度是一样的

4. code

//按照subset的格式写的话是这样的

public class Solution {

public List> permute(int[] nums) {

List> results = new ArrayList<>();

if(nums==null||nums.length==0) return results;

List subset = new ArrayList<>();

helper(nums,subset,results);

return results;

}

private void helper(int[] nums, List subset, List> results ){

if(subset.size()==nums.length){

results.add(new ArrayList(subset));

}else{

for(int i = 0; i< nums.length; i++){

if(subset.contains(nums[i])) continue;

subset.add(nums[i]);

helper(nums,subset,results);

subset.remove(subset.size() - 1);

}

}

}

}

5. 一些细节:

这里舍弃了index的方法,个人觉得不太好想,除非很好的理解搜索和去重,subset中的index是帮助去重的,随着index一直向后推移才保证了不会重复添加之前的子集,而这个题不需要去重,所以根据画递归树的过程可以大致想到,每次都要从头遍历的,但我没想到用size和length比较来判断,想的是result.add放在helper后面不就是等3个都齐了才添加么,由此可见还是对递归了解不够。递归方法完成后会一步一步回到之前的调用者函数。要搞明白流程,画图或者写流程很有帮助

例4:permutation2

1. 题目分析

有重复元素后得到的结果少多了。。ye

跟上题不一样的是 input数组中有重复元素

2. 代码实现思路

跟上题一样,除了:如何去重,

if(i>0 &&nums[i-1]==nums[i] && !used[i-1]) continue;这个判断判断主要是为了去除重复元素影响。

比如,给出一个排好序的数组,[1,2,2],那么第一个2和第二2如果在结果中互换位置,我们也认为是同一种方案,所以我们强制要求相同的数字,原来排在前面的,在结果当中也应该排在前面,这样就保证了唯一性。所以当前面的2还没有使用的时候,就不应该让后面的2使用。

如果看不懂这句话,找一张大点的纸,按照程序写一下执行流程,执行到第二次放入第一个元素1的时候就明白了。

3. 时间复杂度分析

O(n^2)

4. code

public class Solution {

public List> permuteUnique(int[] nums) {

List> results = new ArrayList<>();

if(nums==null||nums.length==0) return results;

Arrays.sort(nums);

boolean[] used = new boolean[nums.length];

ArrayList subset = new ArrayList<>();

dfsHelper(nums,used,subset,results);

return results;

}

private void dfsHelper( int[] nums, boolean[] used, ArrayList subset, List> results){

if(subset.size()==nums.length){

results.add(new ArrayList(subset));

}else{

for(int i = 0; i

if(used[i]) continue;

if(i>0 &&nums[i-1]==nums[i] && !used[i-1]) continue;

used[i] = true;

subset.add(nums[i]);

dfsHelper(nums,used,subset,results);

used[i] = false;

subset.remove(subset.size()-1);

}

}

}

}

例5:Combination Sum :

https://leetcode.com/problems/combination-sum/

结果里每种combination都只能出现一次,又每个数字使用次数不限,所以需要去重,(如果不removeDuplicate,也可直接排序一下,不过remove Diplicates应该复杂度低一些)

public List> combinationSum(int[] nums, int target) {

List> list = new ArrayList<>();

Arrays.sort(nums);

backtrack(list, new ArrayList<>(), nums, target, 0);

return list;

}

private void backtrack(List> list, List 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);

}

}

}

例6:Combination Sum II (can't reuse same element) :

这题和上题的题目区别在于

Combination sum

结果里每种combination都只能出现一次,又每个数字使用次数不限,所以需要去重

因为每个数不限出现次数,所以for循环中每次dfs都传i进去,而不再是i+1

for-loop里还有一点需要注意,target

Combination sum2

给定数组中的数字出现几次,combination中总共只能出现几次。也就是每个数只能用一次,当然combinations里还是不能有重复combination。 比如输入是[7,1,2,5,1,6,10], 8,排序后是[1,1,2,5,6,7,10],输出里可以有116,但如果input:1,1,1.target:1, result: 1 不能有三个1的

第一次for-loop,i=startIndex=0,combination先添加1,然后继续找,等所有情况找完之后(此时已递归好多次),

从下一个元素开始,发现又是1,因为刚才已经把包含1的组合都找过了(因为每个数只能用1次,所以可以和1组成combination的都用过了,所以第二个1没有数可以用了),所以这次不再找,continue;而且如果从这个1再找,可能就会找到跟之前那个1组成的一样的combination,但result里,combination不能重复的。总之就是要跳过了

两道题代码写法的区别有四点:

不需要removeDuplicate(如果原题不removeDuplicate,也可直接排序一下,不过remove Diplicates应该复杂度低一些)

需要sort(第二题这个是必须sort,没有别的选择)

每次dfs开始从i+1,不再是i

多一次去重if(i!=startIdx && candidates[i]==candidates[i-1]) continue;

public List> combinationSum2(int[] nums, int target) {

List> list = new ArrayList<>();

Arrays.sort(nums);

backtrack(list, new ArrayList<>(), nums, target, 0);

return list;

}

private void backtrack(List> list, List 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; // skip duplicates

tempList.add(nums[i]);

backtrack(list, tempList, nums, remain - nums[i], i + 1);

tempList.remove(tempList.size() - 1);

}

}

}

例7:Palindrome Partitioning :

public List> partition(String s) {

List> list = new ArrayList<>();

backtrack(list, new ArrayList<>(), s, 0);

return list;

}

public void backtrack(List> list, List 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;

}

模板

回溯:有过剪枝的DFS过程。比如上面的subset

解答树角度:带回溯的dfs遍历一棵解答树

回溯的一般结构:

void dfs(int 当前状态)

{

if(当前状态为边界状态)

{

记录或输出

return;

}

for(i=0;i

{

//获取各种子状态。

修改全局变量

if(子状态满足条件)

{

dfs(子状态)

}

恢复全局变量--回溯

}

}

BFS:

//将首节点加入队列:

q.push(head);

//标记首节点已访问:

isvisited[head]=true;

while(!q.empty()) {

int temp=q.pop();

//访问temp,并标记temp已被访问过,

//将temp的子相关节点加入队列

q.push(temp相关节点);

}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值