算法笔记:DFS+Backtracking系列

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

例1:leetcode 78. Subset

solution-github

1. 题目分析

  1. 首先 ,这个题是NP问题,没有多项式时间内的算法,只能用搜索解决的问题
  2. 选择用DFS-backtracking 的递归方式解决
  3. 画递归树图
  4. 搜索问题中处理去重:选代表(改题我们遇到重复的,选择留下有序的),因为我们的递归helper是在index向后排的时候调用,(因为树没有重复,我们枚举元素都是按照顺序的,所以不会有答案出现重复)或者看本题的code中,start就是起到了去重作用的,每次都从start下一个开始
    (tips:
  5. 题目中说找出所有子集,所有可能这种,大部分都靠搜索解决
  6. 去重的误区:找到所有答案,再按要求去重,比如:[1,1,1,1,1,1]找所以子集,其实只有lengh+1个,但如果找所有子集,就太多了,
    )

2. 代码实现思路

  1. 先排除边界情况:判断输入的array nums 是不是有效的,这里做判断:
    if nums==null||nums.length==0{return results};(results是刚初始化的返回值)
  2. 写递归:
  3. 递归定义:以subset开头的所有自己放在result里(从nums里的startindex开始挑数)
  4. 递归的拆解
  5. 递归的出口、什么时候记录答案: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(n
s) s 是答案的个数

4. code

class Solution {
    /**
     * @param S: A set of numbers.
     * @return: A list of lists. All valid subsets.
     */
    public ArrayList<ArrayList<Integer>> subsets(int[] nums) { 
        if(nums==null||nums.length==0) return null;
        ArrayList<ArrayList<Integer>> results = new ArrayList<>();
        //把空集开头的所有集合放入result
        dfsHelper(nums,0,new ArrayList<Integer>(), results);
        return results;
    }

    private void dfsHelper(int[] nums, 
                            int startIndex, 
                            ArrayList<Integer> subset, 
                            ArrayList<ArrayList<Integer>> results){
        //deep copy,否则后面操作subset,subset的内容就改变了
        results.add(new ArrayList<Integer>(subset));
        for(int i = startIndex; i<nums.length;i++){
            subset.add(nums[i]);
            dfsHelper(nums,i+1,subset,results);
            subset.remove(subset.size()-1);
        }   
    }
}

5. 一些细节

  1. 库函数 Arrays.sort()是用的quick sort实现,可以认为是nlogn的复杂度
  2. 代码中写的deep copy,java默认都是pass by reference ,这里不new的话传递的是指向subset的reference,后面subset改变,results中加入的subset也会改变,最后返回就变成了[[],[],[],[]......]
  3. 这里不可以用results.addAll(xxx) ,因为addAll 表示,把xxx中的元素都加入到results中,我们是需要加入list,而不是list中的元素
  4. 为什么result.add 在for循环前面?
    因为答案不仅仅只存在于搜索树的叶子节点,每一个节点都是一个答案,所以进入这个搜索节点 就要add一下
  5. 要记得递归完成后subset.remove(subset.size() - 1),这就是BackTracking,把刚才加进去的那一个清除掉(add--remove) ,回到上一步,再继续向后进行,刚才添加进去的那个就是idx=subset.size() - 1,因为我们是往list添加元素,那么当前元素就是添加在list后面,我们回溯是一层一层上来,就是从后一层一层把元素remove掉,当前就remove目前的最后的元素
  6. DFS有两种实现方式,一种是递归,一种是非递归,递归只算作是一种实现的方式

例2 Leetcode 90. Subsets II

solution-github

1. 题目分析

  1. 出现了找出所有子集的关键字,所以还是用搜索
  2. 跟上题不一样的是 input数组中有重复元素

2. 代码实现思路

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

  1. 这里首先需要通过排序把相等的数凑在一起
  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<List<Integer>> subsetsWithDup(int[] nums) {
        List<List<Integer>> results = new ArrayList<>();
        if(nums==null||nums.length==0) return results;
        Arrays.sort(nums);
        ArrayList<Integer> subset = new ArrayList<>();
        dfsHelper(0,nums,subset,results);
        return results;
    }
    private void dfsHelper(int start, int[] nums, ArrayList<Integer> subset, List<List<Integer>> results){
        results.add(new ArrayList(subset));
        for(int i = start; i<nums.length; 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. 题目分析

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

2. 代码实现思路

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

3.时间复杂度

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

4. code

//按照subset的格式写的话是这样的
public class Solution {
    public List<List<Integer>> permute(int[] nums) {
        List<List<Integer>> results = new ArrayList<>();
        if(nums==null||nums.length==0) return results;
        List<Integer> subset = new ArrayList<>();
        helper(nums,subset,results);
        return results;
    }
    private void helper(int[] nums, List<Integer> subset, List<List<Integer>> results ){
        if(subset.size()==nums.length){
            results.add(new ArrayList<Integer>(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

47. Permutations II

1. 题目分析

  1. 有重复元素后得到的结果少多了。。ye
  2. 跟上题不一样的是 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<List<Integer>> permuteUnique(int[] nums) {
        
    List<List<Integer>> results = new ArrayList<>();
        if(nums==null||nums.length==0) return results;
        Arrays.sort(nums);
        boolean[] used = new boolean[nums.length];
        ArrayList<Integer> subset = new ArrayList<>();
        dfsHelper(nums,used,subset,results);
        return results;
    }
    private void dfsHelper( int[] nums, boolean[] used, ArrayList<Integer> subset, List<List<Integer>> results){
        if(subset.size()==nums.length){
            results.add(new ArrayList<Integer>(subset));
        }else{
            for(int i = 0; i<nums.length; 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<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);
        }
    }
}

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

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

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

Combination sum

  1. 结果里每种combination都只能出现一次,又每个数字使用次数不限,所以需要去重
  2. 因为每个数不限出现次数,所以for循环中每次dfs都传i进去,而不再是i+1
  3. for-loop里还有一点需要注意,target<nums[i] break;

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的

  1. 第一次for-loop,i=startIndex=0,combination先添加1,然后继续找,等所有情况找完之后(此时已递归好多次),
  2. 从下一个元素开始,发现又是1,因为刚才已经把包含1的组合都找过了(因为每个数只能用1次,所以可以和1组成combination的都用过了,所以第二个1没有数可以用了),所以这次不再找,continue;而且如果从这个1再找,可能就会找到跟之前那个1组成的一样的combination,但result里,combination不能重复的。总之就是要跳过了

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

  1. 不需要removeDuplicate(如果原题不removeDuplicate,也可直接排序一下,不过remove Diplicates应该复杂度低一些)
  2. 需要sort(第二题这个是必须sort,没有别的选择)
  3. 每次dfs开始从i+1,不再是i
  4. 多一次去重if(i!=startIdx && candidates[i]==candidates[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; // skip duplicates
            tempList.add(nums[i]);
            backtrack(list, tempList, nums, remain - nums[i], i + 1);
            tempList.remove(tempList.size() - 1); 
        }
    }
} 

例7:Palindrome Partitioning :

https://leetcode.com/problems/palindrome-partitioning/

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;
} 

模板

回溯:有过剪枝的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相关节点);                                                                      
}  
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,108评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,699评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,812评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,236评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,583评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,739评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,957评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,704评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,447评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,643评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,133评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,486评论 3 256
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,151评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,889评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,782评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,681评论 2 272

推荐阅读更多精彩内容