搜索回溯

239 阅读5分钟

概述

  • 回溯

    • 采用试错的思想,尝试分步去解决问题,
    • 分步过程中,若现有分步答案不能得到正确的答案,则将取消上一步或几步的计算,再通过其他的可能去分步寻求正确的答案
      • 找到一个可能存在的正确答案
      • 尝试过所有的分步后都没有找到答案,宣布此题无解
    • 递归实现
    • 又称暴力解法,搜索一个问题的所有解,通过深度优先搜索实现
  • 深度优先搜索 dfs

    • 用于遍历树和图
    • 这个算法会尽可能深的搜索树的分支。当结点 v 的所在边都己被探寻过,搜索将 回溯 到发现结点 v 的那条边的起始结点。这一过程一直进行到已发现从源结点可达的所有结点为止。如果还存在未被发现的结点,则选择其中一个作为源结点并重复以上过程,整个进程反复进行直到所有结点都被访问为止。
  • 理解

    • 回溯算法和深度优先搜索算法都有不撞南墙不回头的意思,回溯强调深度搜索的用途,在深度搜索过程中不断尝试,寻求想要的结果。回溯强调搜索的合理性,深度搜索强调遍历的思想。
  • 回溯与动态规划的区别

    • 相同
      • 分步骤求解
      • 每一步有多个选择
    • 不同
      • 回溯强调所有解是什么,本质上就是一个遍历算法,时间复杂度较高
      • 动态规划只需要求我们评估最优解是多少,最优解的对应的具体不做要求,因此很适合评估一个方案的效果
  • 49全排列之回溯经典例题

    • 总结搜索的方法:按顺序枚举每一位可能出现的情况,已经选择的数字在 当前 要选择的数字中不能出现。按照这种策略搜索就能够做到 不重不漏。这样的思路,可以用一个树形结构表示。
      • 每一个结点表示了求解全排列问题的不同的阶段,这些阶段通过变量的「不同的值」体现,这些变量的不同的值,称之为「状态」;
      • 使用深度优先遍历有「回头」的过程,在「回头」以后, 状态变量需要设置成为和先前一样 ,因此在回到上一层结点的过程中,需要撤销上一次的选择,这个操作称之为「状态重置」;
      • 深度优先遍历,借助系统栈空间,保存所需要的状态变量,在编码中只需要注意遍历到相应的结点的时候,状态变量的值是正确的,具体的做法是:往下走一层的时候,path 变量在尾部追加,而往回走的时候,需要撤销上一次的选择,也是在尾部操作,因此 path 变量是一个栈;
      • 深度优先遍历通过「回溯」操作,实现了全局使用一份状态变量的效果

image.png

image.png

//大佬
public class Solution {

    public List<List<Integer>> permute(int[] nums) {
        int len = nums.length;
        // 使用一个动态数组保存所有可能的全排列
        List<List<Integer>> res = new ArrayList<>();
        if (len == 0) {
            return res;
        }

        boolean[] used = new boolean[len];
        List<Integer> path = new ArrayList<>();

        dfs(nums, len, 0, path, used, res);
        return res;
    }

    private void dfs(int[] nums, int len, int depth,
                     List<Integer> path, boolean[] used,
                     List<List<Integer>> res) {
        if (depth == len) {
            //res.add(path);出现全为空的列表
            //变量 path 所指向的列表 在深度优先遍历的过程中只有一份 ,深度优先遍历完成以后,回到了根结点,成为空列表。在 Java 中,参数传递是 值传递,对象类型变量在传参的过程中,复制的是变量的地址。这些地址被添加到 res 变量,但实际上指向的是同一块内存地址,因此我们会看到 6 个空的列表对象。解决的方法很简单,在 res.add(path); 这里做一次拷贝即可。

            res.add(new ArrayList<>(path);
            return;
        }

        // 在非叶子结点处,产生不同的分支,这一操作的语义是:在还未选择的数中依次选择一个元素作为下一个位置的元素,这显然得通过一个循环实现。
        for (int i = 0; i < len; i++) {
            if (!used[i]) {
                path.add(nums[i]);
                used[i] = true;

                dfs(nums, len, depth + 1, path, used, res);
                // 注意:下面这两行代码发生 「回溯」,回溯发生在从 深层结点 回到 浅层结点 的过程,代码在形式上和递归之前是对称的
                used[i] = false;
                path.remove(path.size() - 1);
            }
        }
    }

    public static void main(String[] args) {
        int[] nums = {1, 2, 3};
        Solution solution = new Solution();
        List<List<Integer>> lists = solution.permute(nums);
        System.out.println(lists);
    }
}

注意:深度优先搜索的for循环中,如果答案的列表是无序之分,若元素不得重复则下一次搜索从i+1开始,若元素可以重复则从i开始。若答案的列表有序之分则从0开始,但是要去除重复元素(方法一是用数组标记,回溯时记得重置。方法二是判断列表中是否已经有当前值,有则跳过本次搜索)

46全排列

//自己
class Solution {
    List<List<Integer>> ans = new ArrayList<>();
    int capacity;

    public List<List<Integer>> permute(int[] nums) {
        capacity=nums.length;
        List<Integer> temp=new ArrayList<>();
        if(capacity==0){
            return ans;
        }
        dfs(nums,0,temp);
        return ans;
    }

    void dfs(int [] nums,int begin,List<Integer> temp){
        if(temp.size()==capacity){
            ans.add(new ArrayList<>(temp));
            return;
        }
        for(int i=begin;i<capacity;i++){
            //去除重复的元素
            if(temp.contains(nums[i])){
                continue;
            }
            temp.add(nums[i]);
            dfs(nums,begin,temp);
            temp.remove(temp.size()-1);
        }
    } 
}

39组合总和

  • 思路分析
    • 根据示例 1:输入: candidates = [2, 3, 6, 7]target = 7
    • 候选数组里有 2,如果找到了组合总和为 7 - 2 = 5 的所有组合,再在之前加上 2 ,就是 7 的所有组合;
    • 同理考虑 3,如果找到了组合总和为 7 - 3 = 4 的所有组合,再在之前加上 3 ,就是 7 的所有组合,依次这样找下去。
    • 这一类问题都需要先画出树形图,然后编码实现。编码通过 深度优先遍历 实现
    • 这样得到的答案带有重复的组合,因此去重,观察该树可知道,第二层开始遍历的节点从当前节点开始。

image.png

image.png

class Solution {
    List<List<Integer>> ans=new ArrayList<>();
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        int len=candidates.length;
        if(len==0){
            return ans;
        }
        dfs(candidates,0,len,target,new ArrayList<>());
        return ans;
    }
    void dfs(int[] candidates,int begin,int len,int target,List<Integer> route){
        if(target<0){
            return;
        }
        if(target==0){
            ans.add(new ArrayList<>(route));
            return;
        }
        for(int i=begin;i<len;i++){
            route.add(candidates[i]);
            //当前下标开始
            dfs(candidates,i,len,target-candidates[i],route);
            route.remove(route.size()-1);
        }
    }
}

78子集

class Solution {
    List<List<Integer>> ans=new ArrayList<>();
    int len=0;
    public List<List<Integer>> subsets(int[] nums) {
        len=nums.length;
        List<Integer> temp=new ArrayList<>();

        dfs(nums,0,temp);
        return ans;
    }
    private void dfs(int[] nums,int begin,List<Integer> temp){
       
        ans.add(new ArrayList<>(temp));
          
        for(int i=begin;i<len;i++){
            temp.add(nums[i]);
            dfs(nums,i+1,temp);
            temp.remove(temp.size()-1);
        }
        
    }
}

子集2(数层去重)

class Solution {
    List<List<Integer>> ans = new ArrayList<>();
    List<Integer> temp = new ArrayList<>();
    int len;
    //用以树层去重的boolean数组
    Boolean[] used;
    public List<List<Integer>> subsetsWithDup(int[] nums) {
        len=nums.length;
        Arrays.sort(nums);
        used=new Boolean[len]; 
        dfs(len,0,nums);
        return ans;
    }
    void dfs(int len,int begin,int[] nums){
        ans.add(new ArrayList<>(temp));
        for(int i=begin;i<len;i++){
            if(i>0  && nums[i]==nums[i-1] && !used[i-1]){
                continue;
            }
            temp.add(nums[i]);
            used[i]=true;
            dfs(len,i+1,nums);
            temp.remove(temp.size()-1);
            used[i]=false;
        }
    }
   
}

全排列2(树层去重+树枝去重)

class Solution {
    int len;
    List<List<Integer>> ans=new ArrayList<>();
    List<Integer> path=new ArrayList<>();
    boolean[] used;
    public List<List<Integer>> permuteUnique(int[] nums) {
        Arrays.sort(nums);
        len=nums.length;
        used=new boolean[len];
        dfs(nums);
        return ans;
    }
    void dfs(int[] nums){
        if(path.size()==len){
            ans.add(new ArrayList<>(path));
            return;
        }
        for(int i=0;i<len;i++){
            //        树层                                   树枝
            if((i>0 && nums[i]==nums[i-1] && !used[i-1]) || used[i]){
                continue;
            }
            path.add(nums[i]);
            used[i]=true;
            dfs(nums);
            used[i]=false;
            path.remove(path.size()-1) ;
        }
    }
}

22括号生成(全排列)

回溯要明确DFS的三个要素,以及剪枝这两个部分。

DFS部分:

  1. 状态:括号不断叠加后的字符串,可能需要参量辅助记录有几个左括号,几个右括号
  2. 子状态:每一层的可选状态,两个,左括号,右括号
  3. 结束态:左括号用完,右括号用完

剪枝部分:

  1. 添加右括号不得超过左括号的数目
  2. 左括号最多只有n个
class Solution {
    List<String> ans=new ArrayList<>();
    int N;
    public List<String> generateParenthesis(int n) {
        StringBuilder sb = new StringBuilder();
        N=n;
        dfs(sb,0,0);
        return ans;
    }
    void dfs(StringBuilder sb,int open,int close){
        if(sb.length()==2*N){
            ans.add(sb.toString());
            return;
        }
        if(open<N){
            sb.append("(");
            dfs(sb,open+1,close);
            sb.deleteCharAt(sb.length()-1);
        }
        if(open>close){
            sb.append(")");
            dfs(sb,open,close+1);
            sb.deleteCharAt(sb.length()-1);
        }
    }
}

131分割回文串(动态规划(用以判断是否为回文字符串)+回溯)

class Solution {
    List<List<String>> ans=new ArrayList<>();
    int len;
    public List<List<String>> partition(String s) {
        len=s.length()-1;
        char[] charArray=s.toCharArray();
        List<String> path = new ArrayList<>();
        // 预处理 
        // 状态:dp[i][j] 表示 s[i][j] 是否是回文
        boolean[][] dp=new boolean[len+1][len+1];
        // 状态转移方程:在 s[i] == s[j] 的时候,dp[i][j] 参考 dp[i + 1][j - 1]
        for(int right=0;right<=len;right++){
            for(int left=0;left<=right;left++){
                if(charArray[left]==charArray[right] && (right-left<=2 || dp[left+1][right-1])){
                    dp[left][right]=true;
                }
            }
        } 
        dfs(s,path,0,dp);
        return ans;
    }
    //回溯
    void dfs(String s,List<String> path,int start,boolean[][] dp){
        if(start>len){
            ans.add(new ArrayList<>(path));
            return;
        }
        for(int i=start;i<=len;i++){
            if(dp[start][i]){
                path.add(s.substring(start,i+1));
                dfs(s,path,i+1,dp);
                path.remove(path.size()-1);
            }
        }
    }
    
}

282. 给表达式添加运算符

class Solution {
    List<String> ans;
    int len;
    int t;
    String model;
    public List<String> addOperators(String num, int target) {
        t=target;
        ans=new ArrayList<>();
        len=num.length();
        model=num;
        dfs(0,0,0,"");
        return ans;
    }
    void dfs(int n,long prev,long cur,String path){
        if(n==len){
            if(cur==t)
                ans.add(path);
            return;    
        }
        //回溯
        for(int i=n;i<len;i++){
            //该题重点,前导0处理
            if(i!=n && model.charAt(n)=='0')
                break;
            long next=Long.parseLong(model.substring(n,i+1));
            if(n==0){
                dfs(i+1,next,next,""+next);
            }else{
                dfs(i+1,next,cur+next,path+"+"+next);
                dfs(i+1,-next,cur-next,path+"-"+next);
                dfs(i+1,prev*next,cur-prev+prev*next,path+"*"+next);
            }

        }
    }
}