回溯算法
回溯算法是一种通过探索所有可能情况来找到问题的解的方法。它的基本思想是“试错”:当探索到某一步时,发现不能得到有效的解,就回溯一步进行另一种选择。
回溯算法的基本思想
回溯算法的核心是深度优先搜索(DFS)+剪枝。它的基本步骤是:
- 选择:做出当前的选择
- 约束:判断当前选择是否符合约束
- 目标:判断是否达到目标
- 回溯:如果不符合约束或者不是目标,则回溯并尝试下一个选择
经典回溯问题
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;
}
面试要点:
- 如何处理数组中有重复元素的情况?
- 如何优化空间复杂度?
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;
}
实际应用场景
路径规划:
- 课程安排
- 旅行路线规划
- 机器人路径规划
游戏开发:
- 数独求解
- 迷宫生成
- 棋类游戏 AI
资源分配:
- 任务调度
- 资源分配
- 工作流程调度
面试要点
1. 回溯算法的框架
javascript
function backtrack(path, 选择列表) {
if (终止条件) {
result.push(path);
return;
}
for (选择 in 选择列表) {
if (不符合条件) continue;
// 做选择
path.push(选择);
// 继续探索
backtrack(path, 选择列表);
// 回溯
path.pop();
}
}
2. 常见面试问题
回溯与动态规划的区别:
- 回溯是穷举所有可能的解
- 动态规划是寻找最优解
- 回溯通常用于找到所有解
如何进行剪枝:
- 基于问题约束条件
- 基于已知的不可能情况
- 基于数学公式推导
如何提高效率:
- 合理的剪枝
- 避免重复计算
- 使用适当的数据结构
总结
回溯算法是一种重要的算法设计方法,面试中需要注意:
问题分析:
- 明确问题的约束条件
- 确定回溯的终止条件
- 识别可能的剪枝点
解题框架:
- 选择合适的数据结构
- 实现回溯的基本框架
- 正确处理终止条件
代码实现:
- 保持代码结构清晰
- 注意变量的作用域
- 正确处理回溯操作
测试用例:
- 考虑边界情况
- 测试不同规模的输入
- 验证结果的正确性