专栏首页一Li小麦Js算法与数据结构拾萃(6):回溯

Js算法与数据结构拾萃(6):回溯

Js算法与数据结构拾萃(6):回溯

导言

说起回溯法,笔者就想起曾经有过这么一件事:

笔者之前公司招了个初级前端 小F,马上就让他上项目,接着遇到这么一个问题

后端返回层级结构:

问:如何根据id找到需要的数据,并输出它的层次路径?

然后他写了一个星期没写出来。于是混完一个月之后,交接不办,直接跑路了。

至今同事圈还把他作为笑谈。


回溯法(backtracking)是暴力搜索法中的一种。

对于某些计算问题而言,回溯法是一种可以找出所有(或一部分)解的一般性算法,尤其适用于约束满足问题(在解决约束满足问题时,我们逐步构造更多的候选解,并且在确定某一部分候选解不可能补全成正确解之后放弃继续搜索这个部分候选解本身及其可以拓展出的子候选解,转而测试其他的部分候选解)。

回溯法采用试错的思想,它尝试分步的去解决一个问题。在分步解决问题的过程中,当它通过尝试发现现有的分步答案不能得到有效的正确的解答的时候,它将取消上一步甚至是上几步的计算,再通过其它的可能的分步解答再次尝试寻找问题的答案。回溯法通常用递归来实现,在反复重复上述的步骤后可能出现两种情况:

•找到一个可能存在的正确的答案•在尝试了所有可能的分步方法后宣告该问题没有答案

树形结构遍历

回到引言的案例,初级前端 小F 面临的是这样 一个结构:

这棵树不妨称之为决策树,在每个节点你都要做决策。我们根据决策的逻辑,在这个树上游走。

因此查找的思路是:

1.定义一个空数组(栈)存放层级路径(path)2.一个while循环:如果 当前节点无目标节点,path出栈,遍历下一个,3.查找一个节点时,在path中push这个节点,判断当前节点的name是否为想要的id,•是则返回该节点和path为最终结果,•不是则查找它的children=>如果没有children,•如果没有children判定为当前节点无目标节点,回到第二步逻辑


八皇后问题

在经典的教科书中,八皇后问题[1]展示了回溯法的用例。

国际象棋中,皇后 横、直、斜都可以走,步数不受限制,但是,不能越子行棋。该棋也是棋力最强的棋子。

1848年,国际象棋手马克斯·贝瑟尔提出一个问题,如何能够在8×8的国际象棋棋盘上放置八个皇后,使得任何一个皇后都无法直接吃掉其他的皇后?

数学大佬高斯穷尽一生都没有数清楚八皇后问题到底有几种可能的放置方法,直到计算机的出现。

该题仍然可以用回溯法来解:决策树的每一层row表示棋盘上的每一行;每个节点可以做出的选择是,在该行的任意一列(col)放置一个皇后。

1.入参获取一个二维数组作为棋盘board,row为当前行,定义返回值res2.当row遍历完了之后,作为决策的终止条件。返回res。3.遍历这个棋盘当前行的每列(col),判断点位是否合法:•不合法:跳过此循环•合法:•落子。board[row][col]=true•把当前棋盘的局势和row+1作为入参,进行下一行决策•撤销选择 board[row][col]=false

至于合不合“法”,可以写一个独立的方法来判断:上边,右上方,左上方是否有皇后。——下方都不需要判断。因为没落子。

解题思路

好了,现在可以小结一下了。回溯算法可以有这么一个套路:

result = []
function backtrack(路径, 选择列表){
    if 满足结束条件:
        result.add(路径)
        return result

    for 选择 in 选择列表:
        做选择
        backtrack(路径, 选择列表)
        撤销选择
}

当然很多时候需要具体情况具体分析。

在最坏的情况下,回溯法的算法时间复杂度略大于O( N! ) ,空间复杂度为O( N! ),因为穷举整棵决策树是无法避免的。这也是回溯算法的一个特点,不像动态规划存在重叠子问题可以优化,回溯算法复杂度一般都很高


案例1:Permutations(全排列)

对应leetcode 46题,难度中等 https://leetcode.com/problems/permutations/

Given a collection of distinct integers, return all possible permutations.

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

Example:

Input: ['吃',‘饭’,‘没’]
Output:
[
  ['吃',‘饭’,‘没’],
  ['吃',‘没’,‘饭’],
  ['饭',‘没’,‘吃’],
  ['饭',‘吃’,‘没’]
  ['没',‘吃’,‘饭’],
  ['没',‘饭’,‘吃’],
]

解析

这里的所谓所谓全排列,就是计算Ann,也就是n!是回溯算法的基本问题。

我们知道 n 个不重复的数,全排列共有 n! 个。以前是高中数学内容,但现在小学生都知道怎么列举了。

这张图就是这道题的决策树。现在要教会计算机如何像小学生一样思考。

解决问题的流程(backtack)应该是:

1.定义空数组tmp作为约束条件,list作为返回值。2.遍历这个树,•如果满足约束条件tmp,•push到tmp中•执行temp下的查找•tmp出栈(回溯)•不满足则,跳过此循环递归终止条件:tmp长度和nums一致,此时已经可遍历。

题解

function backtrack(list, temp, nums) {
  // 终止条件
  if (temp.length == nums.length) {
    return list.push([...temp])
  }

  for (let i = 0; i < nums.length; i++) {
    if (temp.includes(nums[i])) continue
    temp.push(nums[i])
    backtrack(list, temp, nums)
    temp.pop()
  }
}

var permute = function(nums) {
  let list = []
  backtrack(list, [], nums)
  return list
}

案例2:N皇后问题

对应leetcode 第51题;难度:困难 https://leetcode.com/problems/n-queens/

给你一个 N×N 的棋盘,让你放置 N 个皇后,使得它们不能互相攻击。

题解

每一种解法包含一个明确的N皇后问题的棋子放置方案,该方案中 'Q''.' 分别代表了皇后和空位。

根据很自然地想到,定义一个二维数组去操作这些数据。但是返回值是一维度数组,转为非引用对象操作起来异常高昂。所以考虑用递归遍历扫描每一行,然后用 存放盘面。比如[2,4,1]表示:第0行第2列,第1行第4列,第2行第1列,放了皇后。

接下来就是盘面判断,当每一行遍历的时候,我们发现

•行不能一样•列不能一样•行+列 不能一样•行-列不能一样

var solveNQueens = function(n) {
  let ret = []
    // 从第0行开始遍历
  find(0)

  // tmp是盘面形势,它的索引是行数据
  // 值是列数据:也就是摆放的棋子
  // 比如说[2,4,1]=>表示棋盘第2行第1列,棋盘第4行第2列,棋盘第1行第3列,放了棋子
  function find (row, tmp = []) {
    // 终止条件
    if (row == n) {
      // n-1已经是最后一行,tmp就是所有路径的结果
      ret.push(
        tmp.map(c => {
          // c是摆放的棋子
          let arr = new Array(n).fill('.')
          arr[c] = 'Q'
          return arr.join('')
        })
      )
    }

    // 1. 如何查找?遍历每列
    for (let col = 0; col < n; col++) {
      let canSet = tmp.some((colIndex, rowIndex) => {
        // colIndex和rowIndex是之前摆放的棋子的行列索引
        // col和row是当前所在位置的索引

        /**
         * 判断条件:
         * 1.行不能一样
         * 2.列不能一样
         * 3.行+列 不能一样
         * 4.行-列不能一样
         */
        return (
          colIndex == col ||
          rowIndex - colIndex == row - col ||
          rowIndex + colIndex == row + col
        )
      })
      if (canSet) {
        continue
      }

      // 如果能放,直接下一行
      find(row + 1, [...tmp, col])
    }
  }

  return ret
}

事实证明,用图来处理效率相当优秀:

案例3:word search(单词搜索)

对应leetcode 79题,难度中等 https://leetcode.com/problems/word-search/

Given a 2D board and a word, find if the word exists in the grid.

The word can be constructed from letters of sequentially adjacent cell, where "adjacent" cells are those horizontally or vertically neighboring. The same letter cell may not be used more than once.

给定一个二维网格和一个单词,找出该单词是否存在于网格中。

单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。

Example:

board =
[
  ['A','B','C','E'],
  ['S','F','C','S'],
  ['A','D','E','E']
]

Given word = "ABCCED", return true.
Given word = "SEE", return true.
Given word = "ABCB", return false.

题解

中间过程可以简化为索引值(cur)

请遵循以下的思路:

•怎么找(双循环)

var exist = function(board, word) {
  if (board.length == 0) {
    return false
  }
  if (word.length == 0) {
    return true
  }

  const row = board.length
  const col = board[0].length

  // 1. 怎么找
  for (let i = 0; i < row; i++) {
    for (let j = 0; j < col; j++) {
      const ret = find(i, j, 0, row, col, board, word)
      if (ret) {
        return true
      }
    }
  }
  return false
}

•何时终止(find)

// 判断终止条件
function find(i, j, cur, row, col, board, word) {
  // 越界
  if (i >= row || i < 0) return false
  if (j >= col || j < 0) return false

  // 找到当前字母
  const letter = board[i][j]
  // 字母不匹配
  if (letter !== word[cur]) return false
  // 找到最后一个,而且相等
  if (cur == word.length - 1) return true


  // 进行下一步判断(上下左右)
}

•如何储存中间过程,执行下一步?

// 判断终止条件
function find(i, j, cur, row, col, board, word) {
  // 。。。

  // 不允许使用已经使用的字母:当前路径标记为null
  // 回溯时,再标记回来
  board[i][j] = null

  // 上下左右
  const ret =
    find(i + 1, j, cur + 1, row, col, board, word) ||
    find(i - 1, j, cur + 1, row, col, board, word) ||
    find(i, j + 1, cur + 1, row, col, board, word) ||
    find(i, j - 1, cur + 1, row, col, board, word)

  // 把刚标记为null的撤销。
  board[i][j] = letter
  return ret
}

References

[1] 八皇后问题: https://w.upupming.site/wiki/八皇后问题

本文分享自微信公众号 - 一Li小麦(gh_c88159ec1309),作者:一li小麦

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-02-18

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Js算法与数据结构拾萃(6.5):回溯法解决数独问题

    回顾N皇后问题的解决方案,并没有采用二维数组。但实际上思路依然和所谓“回溯法通用解决模板”是一致的。

    一粒小麦
  • JavaScript设计模式之代理模式

    当你想买商业保险的时候,却不得不亲自去了解不同公司的方案和限制。在自己百忙之中分神和不同的销售博弈。有了职业的保险经纪人,你只要向他/她讲述你的需求,经纪人就用...

    一粒小麦
  • JavaScript设计模式之组合模式

    一个公司,可能分为很多个事业部,然后事业部又分为不同的部门。每个部门可能又分为不同的方向,每个方向又由不同的项目组组成。在程序设计中,也有一些和“事物是由相似的...

    一粒小麦
  • Js算法与数据结构拾萃(6.5):回溯法解决数独问题

    回顾N皇后问题的解决方案,并没有采用二维数组。但实际上思路依然和所谓“回溯法通用解决模板”是一致的。

    一粒小麦
  • 【重磅新闻】区块链链上治理协议Oath宣布纽约前地区检察官&amp;资深律师Jenny Vatrenko担任美国业务负责人

    Jenny Vatrenko, 资深律师,前纽约布鲁克林地区副检察官,擅长处理高风险商业案件。现作为联合创始人加入Oath,负责美国Oath项目的整体业务。

    区块链领域
  • 技术 | Hybrid载体的变化(三)

    前面两篇文章从客户端的两个角度来说了说变化,今天我们从前端的角度来看一看这些变化,对于我们的工作会有什么样的改变,记得在2013年下半年时在携程做Hybrid ...

    icepy
  • Go函数知识

    1.函数格式 func funcName(input1 type1, input2 type2) (output1 type1, output2 type2) ...

    苦咖啡
  • Go语言学习(五)| 控制结构

    Go 对于值之间的比较有非常严格的限制,只有两个类型相同的值才可以进行比较,如果值的类型是接口,它们也必须都实现了相同的接口

    Mervyn
  • 【LeetCode】只出现一次的数字 第二种解法

    给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。

    韩旭051
  • 从SAP最佳业务实践看企业管理(184)-FI-156总账

    FI156总账 总帐会计主要用于提供外部会计核算和科目的综合情况。在与公司所有其他操作范围相集成的软件系统中,记录所有的业务交易(初始过帐和内部会计核算中的结算...

    SAP最佳业务实践

扫码关注云+社区

领取腾讯云代金券