代码随想录算法训练营第25天|回溯算法Part04

记录题目:

  • 重新安排行程
  • N 皇后
  • 解数独

给定一个机票的字符串二维数组 tickets
其中 tickets[i] = [from, to] 表示从 fromto 的航班信息。
"JFK" 机场出发,返回重新安排后的行程,
行程按字典序排列且恰好使用所有机票一次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
var findItinerary = function (tickets) {
const result = ['JFK'], map = {};
// 构建邻接表
for (const [from, to] of tickets) {
if (!map[from]) {
map[from] = [];
}
map[from].push(to);
}
// 将目的地按字典序排序
for (const city in map) {
map[city].sort();
}
function backtracking() {
// 如果结果已包含所有航班(n 个票,对应 n + 1 个机场)
if (result.length === tickets.length + 1) {
return true;
}
const lastCity = result[result.length - 1];
const destinations = map[lastCity];
if (!destinations || destinations.length === 0) {
return false;
}
for (let i = 0; i < destinations.length; i++) {
const nextCity = destinations[i];
// 移除当前航线(表示已使用)
destinations.splice(i, 1);
result.push(nextCity);
if (backtracking()) return true;
// 回溯:撤销操作
result.pop();
destinations.splice(i, 0, nextCity);
}
return false;
}
backtracking();
return result;
};

如果当前路径成功走完(用了所有票),就 return true,一路向上传递终止; 如果当前路径走死了,就 return false,上一层回退换一条路走。

方法:slice(start, end)

属性 描述
参数形式 截取索引 [start, end) 之间的元素(不含 end
主要作用 复制子数组
返回值 新数组
是否修改原数组

方法:splice(start, deleteCount, ...itemsToAdd)

属性 描述
参数形式 从索引 start 开始,删除 deleteCount 个元素,
并插入 itemsToAdd(可选)
主要作用 删除或插入元素
返回值 被删除的元素组成的新数组
是否修改原数组

将 n 个皇后放置在 n×n 的棋盘上,使皇后彼此之间不能攻击(任意两个皇后不能处于同一行、同一列或同一对角线上)。
返回所有可能的解决方案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const backtrack = (row) => {
if (row === n) {
// 已放置完 n 个皇后,构造解
res.push(path.map(col => '.'.repeat(col) + 'Q' + '.'.repeat(n - col - 1)));
return;
}

for (let col = 0; col < n; col++) {
if (cols.has(col) || diag1.has(row - col) || diag2.has(row + col)) continue;

// 放置皇后
path.push(col);
cols.add(col);
diag1.add(row - col);
diag2.add(row + col);
// ...
//...
//...

编写一个程序,通过填充空格来解决给定的数独问题。
数独的解法需满足:

  • 数字 1-9 在每一行只能出现一次。
  • 数字 1-9 在每一列只能出现一次。
  • 数字 1-9 在每一个 3×3 宫格内只能出现一次。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
var solveSudoku = function (board) {
const rows = new Array(9).fill(0);
const cols = new Array(9).fill(0);
const blocks = Array.from({ length: 3 }, () => new Array(3).fill(0));
const blanks = [];
for (let i = 0; i < 9; i++) {
for (let j = 0; j < 9; j++) {
const num = Number(board[i][j]);
if (num) {
const mask = 1 << (num - 1);
rows[i] |= mask;
cols[j] |= mask;
blocks[~~(i / 3)][~~(j / 3)] |= mask;
} else {
blanks.push([i, j]);
}
}
}
const dfs = (k) => {
if (k === blanks.length) return true;
const [i, j] = blanks[k];
const bi = ~~(i / 3), bj = ~~(j / 3);
const used = rows[i] | cols[j] | blocks[bi][bj];
for (let x = 1; x <= 9; x++) {
const mask = 1 << (x - 1);
if (used & mask) continue;
board[i][j] = `${x}`;
rows[i] |= mask;
cols[j] |= mask;
blocks[bi][bj] |= mask;
if (dfs(k + 1)) return true;
board[i][j] = ".";
rows[i] ^= mask;
cols[j] ^= mask;
blocks[bi][bj] ^= mask;
}
return false;
};
dfs(0);
};

总结:回溯算法

回溯是递归的副产品,只要有递归就会有回溯,所以回溯法也经常和二叉树遍历,深度优先搜索混在一起,因为这两种方式都是用了递归。

优化回溯算法只有剪枝一种方法。

回溯经典题型复杂度分析

子集问题分析:

  • 时间复杂度O(2ⁿ) 每个元素有“取”与“不取”两种状态,组合总数为 2ⁿ

  • 空间复杂度O(n) 递归深度为 n,栈空间为 O(n),每层额外开销为常数级。

排列问题分析:

  • 时间复杂度O(n!) 每一层有 n 个分支,下一层为 n-1,依此类推,共 n! 种排列。

  • 空间复杂度O(n) 递归深度为 n,系统栈使用 O(n) 空间。

组合问题分析:

  • 时间复杂度O(2ⁿ) 属于子集问题的变形,最坏情况下为 2ⁿ

  • 空间复杂度O(n) 递归深度为 n,其他开销为常数级。

N 皇后问题分析:

  • 时间复杂度O(n!) 表面上看是 O(nⁿ),但因剪枝优化,最坏情况下为 n!

  • 空间复杂度O(n) 递归深度为 n,仅需记录当前皇后摆放状态。

解数独问题分析:

  • 时间复杂度O(9^m) m 是空格(’.’)的数量,每个位置最多尝试 9 种数字。

  • 空间复杂度O(n²) 最多递归 层(即 81),每次调用只处理一个空格。

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×