前端必会数据结构与算法系列之回溯(十一)

784 阅读4分钟

1. 什么是回溯

回溯法采用试错的思想,它尝试分步的去解决一个问题。 在分步解决问题的过程中,当它通过尝试发现现有的分步答案不能得到有效的正确的解答的时候,它将取消上一步甚至是上几步的计算,再通过其它的可能的分步解答再次尝试寻找问题的答案。

回溯法通常用最简单的递归方法来实现,在反复重复上述的步骤后可能出现两种情况:

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

在最坏的情况下,回溯法会导致一次复杂度为指数时间的计算。

回溯是一种渐进式寻找并构建问题解决方式的策略。我们从一个可能的动作开始并试着用这个动作解决问题。如果不能解决,就回溯并选择另一个动作直到将问题解决。根据这种行为,回溯算法会尝试所有可能的动作(如果更快找到了解决办法就尝试较少的次数)来解决问题。

适合场景

  • 有很多路。
  • 这些路里,有死路,也有出路。
  • 通常需要递归来模拟所有的路。

有一些可用回溯解决的著名问题

  • 骑士巡逻问题
  • N 皇后问题
  • 迷宫老鼠问题
  • 数独解题器

DFS可以被认为是回溯

回溯框架

result = [];
function backtrack (path, list) {
    if (满足条件) {
        result.push(path);
        return
    }
    
    for () {
        // 做选择(前序遍历)
        backtrack (path, list)
        // 撤销选择(后续遍历)
    }
}

核⼼就是 for 循环⾥⾯的递归,在递归调⽤之前「做选择」,在递归调⽤之后「撤销选择」--labuladong的算法小抄

回溯的时间复杂度一般为O(N!)/O(2^N)

2. 常见场景

2.1 迷宫老鼠

假设我们有一个大小为 N × N 的矩阵,矩阵的每个位置是一个方块。每个位置(或块)可以是空闲的(值为 1)或是被阻挡的(值为 0),如下图所示,其中 S 是起点,D 是终点,找出从S到D的路径。

image.png

function ratInAMaze(maze) { 
    const solution = []; 
    // 将每个位置初始化为零
    for (let i = 0; i < maze.length; i++) {
        solution[i] = []; 
        for (let j = 0; j < maze[i].length; j++) { 
            solution[i][j] = 0; 
        } 
    } 
    if (findPath(maze, 0, 0, solution) === true) {// 如果有解,返回该矩阵
        return solution; 
    } 
    return 'NO PATH FOUND';
}

function findPath(maze, x, y, solution) { 
     const n = maze.length; 
     if (x === n - 1 && y === n - 1) { // 验证老鼠是否到达了终点
         solution[x][y] = 1; 
         return true; 
     } 
     if (isSafe(maze, x, y) === true) { // 验证老鼠能否安全移动至该位置
         solution[x][y] = 1; // 将这步加入路径
         if (findPath(maze, x + 1, y, solution)) { // 水平移动(向右)到下一个位置
             return true; 
         } 
         if (findPath(maze, x, y + 1, solution)) { // 垂直向下移动到下一个位置 
             return true; 
         } 
         solution[x][y] = 0; // 如果水平和垂直都不能移动,那么将这步从路径中移除并回溯
         return false; 
     } 
     return false;
}

function isSafe(maze, x, y) { 
    const n = maze.length; 
    if (x >= 0 && y >= 0 && x < n && y < n && maze[x][y] !== 0) { 
        return true;
    } 
    return false; 
}

2.2 数独解题器

用数字 1~9 填满一个 9 × 9 的矩阵,要求每行和每列都由这九个数字构成。矩阵还包含了小方块(3 × 3 矩阵),它们同样需要分别用这九个数字填满

image.png

 sudokuSolver(matrix) { 
     if (solveSudoku(matrix) === true) { 
         return matrix; 
     } 
     return '问题无解!'; 
}

const UNASSIGNED = 0; 
function solveSudoku(matrix) { 
    let row = 0; 
    let col = 0; 
    let checkBlankSpaces = false; 
    for (row = 0; row < matrix.length; row++) { // 验证谜题是否已被解决
        for (col = 0; col < matrix[row].length; col++) {
            if (matrix[row][col] === UNASSIGNED) { 
                checkBlankSpaces = true; // 有空白位置
                break; 
            } 
        } 
        if (checkBlankSpaces === true) { // 从两个循环中跳出
            break; 
        } 
    } 
    if (checkBlankSpaces === false) { 
        return true; // 如果没有空白的位置(值为 0 的位置),表示谜题已被完成
    } 
    for (let num = 1; num <= 9; num++) { // 试着用 1~9 填写这个位置,一次填一个
        if (isSafe(matrix, row, col, num)) { // 检查添加的数字是否符合规则
            matrix[row][col] = num; // 将数字填入
            if (solveSudoku(matrix)) { // 尝试填写下一个位置
                return true; 
            } 
            matrix[row][col] = UNASSIGNED; // 如果一个数字填在了不正确的位置,我们就再将这个位置标记为空
        } 
    } 
    return false; // 回溯再尝试一个其他数字
}

function isSafe(matrix, row, col, num) { 
    return ( 
        !usedInRow(matrix, row, num) && 
        !usedInCol(matrix, col, num) && 
        !usedInBox(matrix, row - (row % 3), col - (col % 3), num) 
    ); 
}

function usedInRow(matrix, row, num) {
    // 通过迭代矩阵中给定行 row 中的每个位置检查数字是否在行 row 中存在
    for (let col = 0; col < matrix.length; col++) { 
     if (matrix[row][col] === num) { 
         return true; 
     } 
    } 
    return false; 
} 
function usedInCol(matrix, col, num) { 
    // 迭代所有的列来验证数字是否在给定的列中存在
    for (let row = 0; row < matrix.length; row++) {
        if (matrix[row][col] === num) { 
            return true; 
        } 
    } 
    return false; 
} 
function usedInBox(matrix, boxStartRow, boxStartCol, num) { 
    // 检查通过迭代 3 × 3矩阵中的所有位置来检查数字是否在小矩阵中存在
    for (let row = 0; row < 3; row++) { 
        for (let col = 0; col < 3; col++) { 
            if (matrix[row + boxStartRow][col + boxStartCol] === num) {
                return true; 
            } 
        } 
    } 
    return false; 
}

3.leetcode常见考题

3.1 easy

3.2 medium

1.全排列

难度:中等

题解:全排列(回溯)

2. 全排列 II

难度:中等

题解:全排列 II(回溯)

3.子集

难度:中等

题解:子集(回溯)

4.电话号码的字母组合

难度:中等

题解:电话号码的数字组合(回溯)

5.组合

难度:中等

题解:组合(回溯+DFS)

6.括号生成

难度:中等

题解:括号生成(DFS/回溯)

7. 有效的数独

难度:中等

题解:

3.3 hard

1.N 皇后

难度:困难

题解:N皇后(回溯)

2. 解数独

难度:困难

题解: