Skip to content

回溯算法

回溯算法是一种通过探索所有可能情况来找到问题的解的方法。它的基本思想是“试错”:当探索到某一步时,发现不能得到有效的解,就回溯一步进行另一种选择。

回溯算法的基本思想

回溯算法的核心是深度优先搜索(DFS)+剪枝。它的基本步骤是:

  1. 选择:做出当前的选择
  2. 约束:判断当前选择是否符合约束
  3. 目标:判断是否达到目标
  4. 回溯:如果不符合约束或者不是目标,则回溯并尝试下一个选择

经典回溯问题

1. 全排列

问题描述:给定一个不含重复数字的数组,返回其所有可能的全排列。

javascript
function permute(nums) {
    const result = [];
    const used = new Array(nums.length).fill(false);
    
    function backtrack(path) {
        // 达到目标,找到一个排列
        if (path.length === nums.length) {
            result.push([...path]);
            return;
        }
        
        for (let i = 0; i < nums.length; i++) {
            // 如果当前数字已经使用过,跳过
            if (used[i]) continue;
            
            // 做选择
            path.push(nums[i]);
            used[i] = true;
            
            // 继续探索
            backtrack(path);
            
            // 回溯,撤销选择
            path.pop();
            used[i] = false;
        }
    }
    
    backtrack([]);
    return result;
}

面试要点

  1. 如何处理数组中有重复元素的情况?
  2. 如何优化空间复杂度?

2. 组合

问题描述:给定一个无重复元素的数组 nums 和一个目标数 k,返回所有可能的 k 个数的组合。

javascript
function combine(n, k) {
    const result = [];
    
    function backtrack(start, path) {
        // 达到目标,找到一个组合
        if (path.length === k) {
            result.push([...path]);
            return;
        }
        
        // 注意这里的剩余元素个数的判断,是一个重要的剪枝条件
        for (let i = start; i <= n - (k - path.length) + 1; i++) {
            // 做选择
            path.push(i);
            
            // 继续探索
            backtrack(i + 1, path);
            
            // 回溯,撤销选择
            path.pop();
        }
    }
    
    backtrack(1, []);
    return result;
}

3. 子集

问题描述:给定一个无重复元素的整数数组 nums,返回该数组所有可能的子集。

javascript
function subsets(nums) {
    const result = [];
    
    function backtrack(start, path) {
        // 每个路径都是一个子集
        result.push([...path]);
        
        for (let i = start; i < nums.length; i++) {
            // 做选择
            path.push(nums[i]);
            
            // 继续探索
            backtrack(i + 1, path);
            
            // 回溯,撤销选择
            path.pop();
        }
    }
    
    backtrack(0, []);
    return result;
}

4. N皇后问题

问题描述:在 n × n 的棋盘上放置 n 个皇后,使得它们不能互相攻击。

javascript
function solveNQueens(n) {
    const result = [];
    const board = Array(n).fill().map(() => Array(n).fill('.'));
    
    function isValid(row, col) {
        // 检查同一列
        for (let i = 0; i < row; i++) {
            if (board[i][col] === 'Q') return false;
        }
        
        // 检查左上导对角
        for (let i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {
            if (board[i][j] === 'Q') return false;
        }
        
        // 检查右上导对角
        for (let i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) {
            if (board[i][j] === 'Q') return false;
        }
        
        return true;
    }
    
    function backtrack(row) {
        if (row === n) {
            result.push(board.map(row => row.join('')));
            return;
        }
        
        for (let col = 0; col < n; col++) {
            if (!isValid(row, col)) continue;
            
            // 做选择
            board[row][col] = 'Q';
            
            // 继续探索
            backtrack(row + 1);
            
            // 回溯,撤销选择
            board[row][col] = '.';
        }
    }
    
    backtrack(0);
    return result;
}

实际应用场景

  1. 路径规划

    • 课程安排
    • 旅行路线规划
    • 机器人路径规划
  2. 游戏开发

    • 数独求解
    • 迷宫生成
    • 棋类游戏 AI
  3. 资源分配

    • 任务调度
    • 资源分配
    • 工作流程调度

面试要点

1. 回溯算法的框架

javascript
function backtrack(path, 选择列表) {
    if (终止条件) {
        result.push(path);
        return;
    }
    
    for (选择 in 选择列表) {
        if (不符合条件) continue;
        
        // 做选择
        path.push(选择);
        
        // 继续探索
        backtrack(path, 选择列表);
        
        // 回溯
        path.pop();
    }
}

2. 常见面试问题

  1. 回溯与动态规划的区别

    • 回溯是穷举所有可能的解
    • 动态规划是寻找最优解
    • 回溯通常用于找到所有解
  2. 如何进行剪枝

    • 基于问题约束条件
    • 基于已知的不可能情况
    • 基于数学公式推导
  3. 如何提高效率

    • 合理的剪枝
    • 避免重复计算
    • 使用适当的数据结构

总结

回溯算法是一种重要的算法设计方法,面试中需要注意:

  1. 问题分析

    • 明确问题的约束条件
    • 确定回溯的终止条件
    • 识别可能的剪枝点
  2. 解题框架

    • 选择合适的数据结构
    • 实现回溯的基本框架
    • 正确处理终止条件
  3. 代码实现

    • 保持代码结构清晰
    • 注意变量的作用域
    • 正确处理回溯操作
  4. 测试用例

    • 考虑边界情况
    • 测试不同规模的输入
    • 验证结果的正确性

基于 MIT 许可发布