【前端算法系列】回溯算法

46.全排列

就是返回其所有可能的组合种类
在这里插入图片描述

  • 第1种方法 递归:遍历每一层还有哪些子节点没有被访问过,没有访问过的就push到当前组合里
    终结条件:遍历的层级数等于传入数字个数时,返回结果
const permute = function(nums) {
  const len = nums.length
  const curr = []  // 用来记录当前的排列内容
  const res = [] // 记录所有的排列顺序
  const visited = {}  // 用来避免重复使用同一个数字  {1: 1, 2: 1, 3: 1}
  // 定义dfs函数,入参从0计数
  function dfs(nth) {
      // 终止条件,nth是第几层级,到了第3层级就返回结果并return
      if(nth === len) {
        res.push(curr.slice()) 
        return 
      }
      // 检查手里剩下的数字有哪些
      for(let i=0;i<len;i++) {
          // 如果当前nums[i]没有被访问过,就push到curr里,并标识为1
          if(!visited[nums[i]]) {
              visited[nums[i]] = 1 // 访问过后打标识
              curr.push(nums[i]) 
              // 下转到下列
              dfs(nth+1) 

              // 初始化num[i]和已访问过的标识 ===> 回溯的特点:初始化
              curr.pop() 
              visited[nums[i]] = 0 
          }
      }
  }
  // 从0开始dfs
  dfs(0)
  return res
}
  • 第2种递归方法
/* 遍历nums,参数作为空数组,判断此数组是否已经存在数值,有就return,没有就放到数组里并下探
直到数组的长度等于nums长度,说明下探到底了,把数组push到res并return出来
*/
/** 时间复杂度:O(n!) n的阶乘 n!=1x2x3x...x(n-1)xn 只要遇到嵌套的for循环,就是乘积
 *  空间复杂度:递归,内部形成调用堆栈,线性增长趋势,O(n) n是递归的层数
 */
var permute = function(nums) {
    const res = []
    const backtrack = (path) =>{ // 进来的时候path是空数组[],然后遍历nums,没有在此数组中就concat进来
        // 终止条件
        if(path.length === nums.length){
            res.push(path)
            return 
        }
        nums.forEach(n=>{
            if(path.includes(n)) return 
                // 继续下探
            backtrack(path.concat(n)) // concat不会改变到path,相当于拷贝一份,push会改变数组
        })
    }
    backtrack([])
    return res
}

78. 子集

跟46题的区别在于,每个数字可能出现,可能不出现
所以数组长度不必为3,可能是空数组,可能是一个或两个

终止条件:组合里数字个数最大值为3个数,操作3终止

var subsets = function(nums) {
    const res=[]
    const list=[]
    const dfs=(index)=>{
        // 每次进来都更新
        res.push(list.slice())

        // 终止条件
        for(let i=index;i<nums.length;i++){
            list.push(nums[i])
            // 下探
            dfs(i+1)
            list.pop()
        }
    }
    dfs(0)
    return res
}
// 把有变化的,会更新的都作为参数
// 只能使用第n个数字后面的数字,所以start要当参数传进去
var subsets = function(nums) {
    const res=[]
    const backTrack=(path, len, start)=>{
        // 终止条件
        if(path.length === len){
            res.push(path.slice())
            return
        }
		// i等于start的作用是防止数组中出现重复的数字,如[1, 1]是重复的
        for(let i=start;i<nums.length;i++){
        	// 不断下探,更新start
            backTrack(path.concat(nums[i]), len, i+1)
        }
    }

    // 遍历,每个数组可能是0、1、2、3个元素
    for(let i=0;i<=nums.length;i++){ 
        backTrack([], i, 0) // i表示一个数组里有多少个元素
    }

    return res
}
var subsets = function(nums) {
    let res = []
    let list = []
    if(nums == null) return res

    function dfs(i){
        if(i == nums.length){
            res.push(list.concat())
            return
        }

        dfs(i+1) // 不选择
        list.push(nums[i])
        dfs(i+1) // 选择

        // reverse state 把最后加的数从里面删除,因为不是本层变量,每次递归都会改变,所以要重置 
        list.pop() 
    }

    dfs(0)
    return res
}

77. 组合

分析题目:终止条件是当数组长度等于k时,返回结果

var combine = function(n, k) {
    const res=[]
    const list=[]
    const dfs=(start)=>{
        if(list.length === k){
            res.push(list.slice())
            return
        }
        // i等于start的作用是防止数组中出现重复的数字,如[1, 1]是重复的
        for(let i=start;i<=n;i++){
            list.push(i)
            dfs(i+1)
            list.pop()
        }
    } 
    dfs(1)
    return res
}

51. N 皇后 (剪枝)

每一个皇后都不能在其它皇后的攻击范围里(即不能在其它皇后的行、列、对角线上)

遍历每一行的每一列中,判断现在要填的皇后在不在其它皇后攻击范围里(这个范围要存储起来),在就下一个,不在就放上去然后继续递归

1)遍历列,记录放置的皇后不会被攻击的位置,以及记录列、撇、捺会攻击的位置
2)下探到下一层,继续遍历,直到行数等于棋盘长度,返回结果

// 列,撇,捺存储着皇后能攻击的范围,之后需要放置的皇后不能占用set里面的值
var solveNQueens = function(n) {
    if(n<1) return []
    const res=[]
    // 之前的皇后占的列,撇,捺的位置(皇后能攻击的范围),之后的皇后不能占用set里面的值
  	// row + col 是撇,row - col 是捺
    const col = new Set([])
    const pie = new Set([])
    const na = new Set([])
    const queens=[] // 存储每一行皇后的位置
    const dfs=(row)=>{ // row表示当前行
        // 终止条件
        if(row == n){
            let arr=[]
            for(let i in queens){
                arr.push('Q'.padStart(queens[i]+1, '.').padEnd(n, '.'))
            }
            res.push(arr)
            return 
        }

        for(let i=0;i<n;i++){ // i表示每一列
            // 判断剪枝:判断i是否在攻击范围,是就跳出
            if(col.has(i)||pie.has(row-i)||na.has(row+i)) continue 
                // 没在攻击位置,就存储皇后和列、撇、捺位置
                col.add(i)
                pie.add(row-i)
                na.add(row+i)
                queens.push(i)

                // 下探一层
                dfs(row+1)

                // 恢复当前层的状态
                queens.pop()
                col.delete(i)
                pie.delete(row-i)
                na.delete(row+i)
        }
    }
    dfs(0)
    return res
}

52. N皇后 II

跟51的区别:输出最终结果的方法数量

var totalNQueens = function(n) {
    if(n<1) return []
    const res=[]
    // 之前的皇后占的列,撇,捺的位置(皇后能攻击的范围),之后的皇后不能占用set里面的值
  	// row + col 是撇,row - col 是捺
    const col = new Set([])
    const pie = new Set([])
    const na = new Set([])
    const queens=[] // 存储每一行皇后的位置
    const dfs=(row)=>{ // row表示当前行
        // 终止条件
        if(row == n){
            let arr=[]
            for(let i in queens){
                arr.push('Q'.padStart(queens[i]+1, '.').padEnd(n, '.'))
            }
            res.push(arr)
            return 
        }

        for(let i=0;i<n;i++){
            // 判断i是否在攻击范围,是就跳出
            if(col.has(i)||pie.has(row-i)||na.has(row+i)) continue
                // 没在攻击位置,就存储皇后和列、撇、捺位置
                col.add(i)
                pie.add(row-i)
                na.add(row+i)
                queens.push(i)

                // 下探一层
                dfs(row+1)

                // 恢复当前层的状态
                queens.pop()
                col.delete(i)
                pie.delete(row-i)
                na.delete(row+i)
        }
    }
    dfs(0)
    return res.length
}

n皇后还可以用位运算解答


    /**
     * 解法二 巧用位运算 替代原来set方案 只需得到解个数
     * 利用int二进制位来表示相应的列撇捺 有没有被占据掉 
     * 一个int的二进制位至少有32位 现代的计算机一般是64位的 64以内的皇后问题都可以存储 处理
     * col pie na 分表表示先前皇后占据的位置 (假设我们只看后8位 即8皇后 前24位都是0 )
     * col  00000001
     * pia  00000010
     * na   00000100
     * col | pia | na => 000001111 表示所有被皇后攻击的所占据的格子
     * 取反~ 则表示 即11111000 没有被占的格子变为1了
     * x & ((1 << n) - 1) 将x最高位至第n位(含)清零:x & ((1 << n) - 1) 因为不需要前面那些位置
     * 最终得到 0*24(前24高位清零) + 11111000
     * 
     * row    col            pia               na              bits          p(最低位1)  bits(最低位清零) 
     * 0    00000000       00000000         00000000        11111111      00000001      11111110
     * 1    00000001       00000010         00000000        11111100      00000100      11111000
     * 2    00000101       00001100         00000010        11110000      00010000      11100000
     * 3    ... 依次执行 指导bits=0b000000 没有可用位置了 
     */
    let count = 0

    void (function DFS(row, col, pie, na) {
        if (row >= n) return count++

        // 得到当前所有的空位 即皇后可以放的地方
        let bits = (~(col | pie | na)) & ((1 << n) - 1)

        // 只要bits中含有1 等价于还有皇后可以放位置 DFS 直到全部全部搜索完0*32
        while(bits) {
            let p = bits & -bits // 得到最低位的1 表示当前可皇后可放入的位置
            bits &= bits - 1     // 清零最低位的1 表示在p位置上放入皇后
            DFS(row + 1, col | p, (pie | p) << 1, (na | p) >> 1 )
        }
    })(0, 0, 0, 0)

    return count
}

37. 解数独

1)每一个格子使用暴力解法,’.'表示要填的空格,试探当前空格是否合法,合法就把空位填上
2)剩下的空格继续递归,如果都没问题return true,如果中途失败就还原回‘.’ ,如果都不行就return false

/**
 * 数字1-9只能在每一行、列出现一次
 * 在3*3宫内只能出现一次
 */
var solveSudoku = function(board) {
    if(!board) return 
    const len = board.length
    // 判断剪枝
    const valid=(borad, row, col, target)=>{
        // 判断当前行、列有没有出现这个数字,如果有就不合法;再判断3*3的格子里有没有出现这个数字,有就不合法
        // 就是把数字从行走一遍、从列走一遍,看看有没有重复的数字
        for(let i=0;i<9;i++){
            if(board[i][col]!='.' && board[i][col] == target ) return false
            if(board[row][i]!='.' && board[row][i] == target ) return false

            let x=3*Math.floor(row/3) + Math.floor(i/3)
            let y=3*Math.floor(col/3) + Math.floor(i%3)
            if(board[x][y] !='.' && board[x][y] == target) return false

        }
        return true
    }
    const solve=(board)=>{
        for(let i=0;i<len;i++){
            for(let j=0; j<board[0].length;j++){
                if(board[i][j] == '.'){
                    for(let target=1;target<=9;target++){
                        // 递归遍历下去,通过整个返回true, 否则恢复为'.'
                        if(valid(board, i, j, target)){
                            board[i][j] = target.toString() // 如果通过就填上target
                            if(solve(board)){
                                return true
                            }
                            board[i][j]='.'
                        }
                    }
                    return false
                }
            }
        }
        return true
    }
    solve(board)
}


/*
Math.floor(i / 3) * 3 即可得到对应相加值:
0/1/2:+0
3/4/5:+3
6/7/8:+6
*/

在这里插入图片描述

36. 有效的数独

方法:

  1. 哈希set,不断拿前面的值跟后面的做比较
  2. 布尔,要么存在要么不存在
  3. 位运算:存在为1,不存在为0,于出来看是0还是1判断同一列是否已经存在相同数字 (大家都是1才是1,都是0才是0)
    按位或,只要有一个1就是1

如果只有0和1,可以考虑位运算压缩空间

var isValidSudoku = function(board) {
    if(!board) return 

    for (let i = 0; i < 9; i++) {
    // 遍历行、列
    let row = new Set(), col = new Set()
      // 遍历3*3小宫格
    let block = new Set()
    let x = (i / 3 >> 0) * 3, y = i % 3 * 3

    for (let j = 0; j < 9; j++) {
      if (board[i][j] !== '.') {
        if (row.has(board[i][j])) return false
        row.add(board[i][j])
      }
      if (board[j][i] !== '.') {
        if (col.has(board[j][i])) return false
        col.add(board[j][i])
      }

      if (board[x][y] !== '.') {
        if (block.has(board[x][y])) return false
        block.add(board[x][y])
      }
      y++
      if ((j + 1) % 3 === 0) { // 是否到一个小宫格右侧,是就换行,列也清空
        x += 1
        y -= 3
      }
    }
  }
  return true
}

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值