1

回溯

是什么

穷举(列举所有情况),时间和空间复杂度很高,但是已经是最好的办法了

  1. 一种算法思想
  2. 一种渐进式寻找并构建问题解决方式的策略
  3. 会先从一个可能的动作开始path.push()解决问题,如果不行,就回溯并选择另一个动作path.pop(),直到将问题解决

    模板

    path不能重复选择nums里的元素

         function fn(nums) {
             const res = []
             const dfs = (start, path) => {
                 if (path.length === nums.length) { //终结条件
                     res.push([...path])
                     return
                 }
                 //如果不能重复选择nums里的元素,则+1
                 for (let i = start; i < nums.length; i++) {
                     path.push(nums[i])
                     dfs(i + 1, path) 
                     path.pop()
                 }
             }
             dfs(0, [])
             return res;
         }

    path能重复选择nums里的元素

         function fn(nums) {
             const res = []
             const dfs = (start, path) => {
                 if (path.length === nums.length) { //终结条件
                     res.push([...path])
                     return
                 }
                 //如果不能重复选择nums里的元素,则直接传i
                 for (let i = start; i < nums.length; i++) {
                     path.push(nums[i])
                     dfs(i, path) 
                     path.pop()
                 }
             }
             dfs(0, [])
             return res;
         }

    案例: 鬼吹灯寻路

leetcode

46 全排列

思路

所有情况(出路死路)

image.png

过滤后的所有情况
image.png

image.png

image.png

代码

写法一

    function premute(nums) {
      // 1. 设置结果集
      const res = []
      // 2. 回溯
      const backtrack = (path) => {
        // 2.1 设置回溯终止条件
        if(path.length === nums.length){
          // 2.1.1 推入结果集
          res.push(path)
          // 2.1.2 终止递归
          return;
        }
        // 2.2 遍历数组
        nums.forEach(n => {
          let pathIncludesN = path.includes(n)
          // 2.2.1 必须是不存在 数组 中的元素
          if(pathIncludesN){
            return
          }
          // 2.2.2 本地递归条件
          let pathConcatN = path.concat(n)
          // 2.2.3 进一步递归
          backtrack(pathConcatN)
        })
      }
      backtrack([])
      // 3. 返回结果
      return res;
    }
    var nums = [1, 2]
    // var nums = [1, 2,3]
    var result = premute(nums)
    console.log('result', result)

写法二(推荐)

    function permute(nums){
      const result = []
      const dfs = function(path){
        if(path.length === nums.length){
          result.push([...path])
          return;
        }
        for(let i =0;i<nums.length;i++){
          if(!path.includes(nums[i])){
            path.push(nums[i])
            dfs(path)
            path.pop(nums[i])
          }
        }
      }
      dfs([])
      return result;
    }

复杂度分析

  • 时间复杂度:O(n∗n!),其中 n 为序列的长度
  • 空间复杂度:O(n),其中 n 为序列的长度。除答案数组以外,递归函数在递归过程中需要为每一层递归函数分配栈空间,所以这里需要额外的空间且该空间取决于递归的深度,这里可知递归调用深度为O(n)。

参考

b站
leetcode

39 组合总和

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

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

说明:

所有数字(包括 target)都是正整数。

解集不能包含重复的组合。

输入:candidates = [2,3,6,7], target = 7, 
所求解集为: [ [7], [2,2,3] ]
        function combinationSum(candidates, target) {
            const res = []
            const dfs = (start, path, sum) => {
                if (sum >= target) {
                    if (sum == target) {
                        res.push([...path])
                    }
                    return
                }
                for (let i = start; i < candidates.length; i++) {
                    path.push(candidates[i])
                    dfs(i, path, sum + candidates[i])
                    path.pop()
                }
            }
            dfs(0, [], 0)
            return res;
        }

40组合总和2

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

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

注意:解集不能包含重复的组合。

输入: candidates = [10,1,2,7,6,1,5], target = 8, 
输出: [ [1,1,6], [1,2,5], [1,7], [2,6] ]
        function combinationSum2(candidates, target) {
            candidates.sort((a, b) => a - b)
            const res = []
            const dfs = (start, path, sum) => {
                if (sum >= target) {
                    if (sum == target) {
                        res.push([...path])
                    }

                }
                for (let i = start; i < candidates.length; i++) {
                    if (i > start && candidates[i - 1] == candidates[i]) {
                        continue
                    } //有candidates[i - 1] == candidates[i],证明是有2个数,假设start = 0; 则i >0,就只能是从下标1开始,就能凑够2个数
                    path.push(candidates[i])
                    dfs(i + 1, path, sum + candidates[i])
                    path.pop()
                }
            }
            dfs(0, [], 0)
            return res;
        }

216组合总和3

找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。

说明:

所有数字都是正整数。

解集不能包含重复的组合。 

输入: k = 3, n = 9 
输出: [[1,2,6], [1,3,5], [2,3,4]]
        function combinationSum3(k, n) {
            const res = []
            const dfs = (start, path, sum) => {
                if (path.length == k && sum === n) {
                    res.push([...path])
                }
                for (let i = start; i <= 9; i++) {
                    path.push(i)
                    dfs(i + 1, path, i + sum)
                    path.pop()
                }
            }
            dfs(1, [], 0)
            return res;
        }

46全排列

给定一个不重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。

输入:nums = [1,2,3] 输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
        function subsets(nums) {
            const res = []
            const dfs = (path) => {
                if (path.length === nums.length) {
                    res.push([...path])

                }
                for (let i = 0; i < nums.length; i++) {
                    if (path.includes(nums[i])) continue
                    path.push(nums[i])
                    dfs(path)
                    path.pop()
                }
            }
            dfs([])
            return res;
        }

47 全排列2

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

输入:nums = [1,1,2] 
输出: [[1,1,2], [1,2,1], [2,1,1]]
        function permuteUnique(nums) {
            const res = []
            nums.sort((a, b) => a - b)
            const used = new Array(nums.length)
            const dfs = (path) => {
                if (path.length === nums.length) {
                    res.push([...path])

                }
                for (let i = 0; i < nums.length; i++) {
                    if (used[i]) {
                        continue
                    }
                    if (i  > 0 && nums[i - 1] == nums[i] && !used[i - 1]) {
                        continue
                    }
                    path.push(nums[i])
                    used[i] = true;
                    dfs(path)
                    path.pop()
                    used[i] = false
                }
            }
            dfs([])
            return res;
        }

78 子集

给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。

解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。

输入:nums = [1,2,3] 
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
        function subsets(nums) {
            const res = []
            const dfs = (start, path) => {
                if (path.length <= nums.length) {
                    res.push([...path])
                }
                for (let i = start; i < nums.length; i++) {
                    path.push(nums[i])
                    dfs(i + 1, path)
                    path.pop()
                }
            }
            dfs(0, [])
            return res;
        }

90 子集2

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

解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。

输入:nums = [1,2,2] 
输出:[[],[1],[1,2],[1,2,2],[2],[2,2]]
        var subsetsWithDup = function (nums) {
            nums = nums.sort((a, b) => { return a - b });
            const res = [];
            const used = []
            const dfs = (start, path) => {
                if (path.length <= nums.length) {
                    res.push([...path])
                }
                for (let i = start; i < nums.length; i++) {
                    if (i > 0 && nums[i - 1] == nums[i] && !used[i - 1]) continue;
                    // if(used[i] || (i > 0 && nums[i-1] == nums[i] && !used[i-1])) continue;
                    used[i] = true;
                    path.push(nums[i]);
                    dfs(i + 1, path);
                    used[i] = false;
                    path.pop();
                }
            }
            dfs(0, []);
            return res;
        };

77组合

给定两个整数 n 和 k,返回 1 ... n 中所有可能的 k 个数的组合。

输入: n = 4, k = 2 
输出: [ [2,4], [3,4], [2,3], [1,2], [1,3], [1,4], ]
        function combine(n, k) {
            const res = []
            const dfs = (start, path) => {
                if (path.length == k) {
                    res.push([...path])
                }
                for (let i = start; i <= n; i++) {
                    path.push(i)
                    dfs(i + 1, path)
                    path.pop()
                }
            }
            dfs(1, [])
            return res;
        }

93 有效ip

给定一个只包含数字的字符串,用以表示一个 IP 地址,返回所有可能从 s 获得的 有效 IP 地址 。你可以按任何顺序返回答案。

有效 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 '.' 分隔。

例如:"0.1.2.201" 和 "192.168.1.1" 是 有效 IP 地址,但是 "0.011.255.245"、"192.168.1.312" 和 "192.168@1.1" 是 无效 IP 地址。

输入:s = "25525511135" 
输出:["255.255.11.135","255.255.111.35"]
   const restoreIpAddresses = (s) => {
            const res = [];
            // 复原从start开始的子串
            const dfs = (subRes, start) => {
                if (subRes.length == 4 && start == s.length) { // 片段满4段,且耗尽所有字符
                    res.push(subRes.join('.'));                  // 拼成字符串,加入解集
                    return;                     // 返不返回都行,指针已经到头了,严谨的说还是返回
                }
                if (subRes.length == 4 && start < s.length) {  // 满4段,字符未耗尽,不用往下选了
                    return;
                }
                for (let len = 1; len <= 3; len++) {           // 枚举出选择,三种切割长度
                    if (start + len - 1 >= s.length) return;     // 加上要切的长度就越界,不能切这个长度
                    if (len != 1 && s[start] == '0') return;     // 不能切出'0x'、'0xx'

                    const str = s.substring(start, start + len); // 当前选择切出的片段
                    if (len == 3 && +str > 255) return;          // 不能超过255

                    subRes.push(str);                            // 作出选择,将片段加入subRes
                    dfs(subRes, start + len);                    // 基于当前选择,继续选择,注意更新指针
                    subRes.pop(); // 上面一句的递归分支结束,撤销最后的选择,进入下一轮迭代,考察下一个切割长度
                }
            };

            dfs([], 0);       // dfs入口
            return res;
        };

491 递增子序列

给定一个整型数组, 你的任务是找到所有该数组的递增子序列,递增子序列的长度至少是 2 。

输入:[4, 2, 7, 7]
输出:[[4,7],[4,7,7],[2,7],[2,7,7],[7,7]]
        const findSubsequences = (nums) => {
            const res = [];
            const len = nums.length;
            const set = new Set();

            const dfs = (start, path) => {
                if (path.length >= 2) {
                    const str = path.toString(); // path数组 转成字符串
                    if (!set.has(str)) {         // set中没有存有当前path
                        res.push(path.slice());    // 推入一份path的拷贝
                        set.add(str);              // 存入set,记录一下
                    }
                }
                for (let i = start; i < len; i++) {      // 枚举出当前所有的选项,从start到末尾
                    const prev = path[path.length - 1];    // 上一个选择,即path数组的末尾元素
                    const cur = nums[i];                   // 当前选择
                    if (path.length == 0 || prev <= cur) { // 
                        path.push(cur);                      // 选择当前的数如果path为空,或满足递增关系,则可选择字
                        dfs(i + 1, path);                    // 继续往下递归,注意传的是i+1
                        path.pop();                          // 撤销选择当前数字,选择别的数字
                    }
                }
            };
            dfs(0, []); //递归的入口,从下标0到末尾的数组中选择合适的数加入path,组成解集。初始path是空数组
            return res;
        };

渣渣辉
1.3k 声望147 粉丝