题: leetcode 51
隐式图, 状态/关系, DFS与回溯法
图的DFS的搜索过程形成搜索树, 在七月君的视频及pdf里有针对图的DFS/BFS的详细过程的讲解. 注意这个讲解是针对图(邻接表实现)的结构进行的; DFS/BFS的时间复杂度都是O(n + m); (n - 节点数量, m - 边数量); 但现实解题中图的结点和边并不是一开始就明确给出的, 需要把现实问题转换成一个"隐式图"。
隐式图, 需要根据问题去划分"状态"与"关系"。状态是指从初始到目标, 问题经历的中间过程; 状态一般是隐式图的结点; 关系是指状态之间进行一步操作转到下一个状态, 一般是隐式图的边。 以N皇后问题为例, 状态可以是棋盘上摆了一个子的状态, 再摆上另一个子,使得棋盘从状态1到状态2; 所以实际上, N皇后问题是遍历隐式图, 找出符合约束条件(皇后不能互相攻击 - 在同一列, 同一行和斜线上)的结点(状态)集合; 并在出现符合条件的状态结点时(摆满N个皇后,且不互相攻击),记录该结点。
所以像N皇后问题, 图的结点集合并不是一开始就唾手可得的,我们也不知道沿着哪些边集的路径去遍历能构造出期望的结果, 我们并没有必要像一个给定顶点集和边集的图的实例一样去实例化邻接表或邻接矩阵; 七月君里给了一个DFS的代码框架:
void dfs(int i, int v[]) {
if (visited[i])
return;
visited[i] = true;
for (j 是邻接的边的vertex) { //N皇后问题并没有现成的构造好的邻接表结构
dfs(j, v);
}
}
所以我们需要回溯法(探索与回溯法)的框架。从某一个状态(结点), 执行某个动作(往某个坐标放个棋子), 以DFS的方式探索下一个状态, 如果进行到某一步, 不符合约束条件或不是最优解, 则回退一步(退回上一步的部分解构造的数据结构以及副作用), 当上一步还有的可选的状态空间时, 重新选择;
在N皇后问题里, 选择 - 放置棋子导致状态迁移, 状态参数是row, col, 大致的回溯框架是:
void backtrack(int row) {
for (col 0 -> n) {
if (notUnderAttack(row, col)) {
placeQueen(row, col);
// 找到一个解(状态结点集合符合约束)
if (row + 1 == n)
addSolution();
else
backtrack(row + 1);
// 回溯, 取消掉当前一步row, col相关的数据或副作用; 待尝试下一个col;
removeQueen(row, col);
}
}
回溯法的框架和dfs框架的执行过程是相似的,其递归过程都实质上构成了多叉树, 并且可以用备忘录缓存或剪枝,去除不需要的搜索路径; 回溯法需要有明确的回溯动作,DFS的抽象含义不涉及回溯这个问题。
在解题的时候, 我们需要考虑的是, 状态的参数是什么? e.g. 子集问题, 状态参数只有数组index, N皇后问题状态参数是row, col; 状态参数还和"选择"的动作有关 - 每次开始选择之前/回溯之后, 下一步可选状态集合往往是某个状态参数;
// N皇后问题算法
//
- 从row 0开始 backtrack(0);
- loop col 0 -> n
+ 如果满足约束 notUnderAttack(row, col)
* 放置皇后;
* 如果找到了一个解(放置完皇后, row + 1 == n, 说明在深度递归到这一步某个row,在某个col上,可以放置了n个皇后), 输出;
* 如果没有, 则尝试下一个row, backtrack(row + 1);
* 回溯掉当前row,col对应的数据结构(移除row, col的皇后,清除其他数据结构), 尝试下一个col;
时间复杂度
DFS的时间是O(n + m), n是隐式图的结点数, m是隐式图的边数; N皇后问题的状态结点有多少个呢? N*N
的棋盘, N个棋子, N行N列,每行有N种摆法,有N行, 状态(vertex结点)个数是N^N; n = N^N; 所以N皇后问题的时间复杂度非常大, 是O(N^N)的; 据说目前能计算到的最大的N是13;
约束条件 notUnderAttack
这个约束条件的设计和操作确实是一个难点, 如果不是看了leetcode题解, 我很难设计的出;
int queue[n]; // 标记放置queue的位置; e.g. queue[row] = col; queue[0] = 2; 0,2
int rows[n]; // 标记目前放置在哪一列上; e.g. rows[2] = 1; 2nd col 不能放;
hills[n-1]; // 标记 "/" 主对角线main_axis, 一共有n - 1个位置;
// 主对角线 row + col = 常数, e.g. 0 + 0 = 0, 1 + 0 = 0 + 1 = 1,
dales[n-1]; // 标记 "\" 次对角线, row - col = 常数, e.g. 0 - 3 = -3, 0 - 2 = -2, 1 - 3 = -2;
调用placeQueen(row, col), 更新相应以上结构;
注意,
- 原题解的hills[4 * n]的范围过大了,无需给出4 * n - 1的范围;
- 另外, removeQueen, placeQueen中的hill数组中的索引是 row - col + n - 1, 这样被映射到 0 - n-1; 不正确的话leetcode会有sanitizer测出访问的地址越界报出AddressSanitizer: heap-buffer-overflow错误;
代码
递归实现
#include <iostream>
#include <vector>
using namespace std;
class Solution {
public:
vector<vector<string>> solveNQueens(int n) {
this->n = n;
rows = new int[n];
queen = new int[n];
hills = new int[2 * n - 1]; // e.g. n = 4, "/" hills有7个位置;
dales = new int[2 * n - 1];
for (int i = 0; i < n; ++i) {
rows[i] = 0; queen[i] = 0;
}
for (int j = 0; j < 2 * n - 1; ++j) {
hills[j] = dales[j] = 0;
}
backtrack(0);
return output;
}
void backtrack(int row) {
for (int col = 0; col < n; col++) {
if (notUnderAttack(row, col)) {
placeQueue(row, col);
if (row + 1 == n) {
addSolution();
} else
backtrack(row + 1);
removeQueue(row, col);
}
}
}
void placeQueue(int row, int col) {
queen[row] = col;
rows[col] = 1;
hills[row - col + n - 1] = 1;
dales[row + col] = 1;
}
void removeQueue(int row, int col) {
queen[row] = 0;
rows[col] = 0;
hills[row - col + n - 1] = 0;
dales[row + col] = 0;
}
bool notUnderAttack(int row, int col) {
int result = rows[col] + hills[row - col + n - 1] + dales[row + col];
return result == 0 ? true : false;
}
void addSolution() {
vector<string> row;
for (int i = 0; i < n; ++i) {
string s = "";
for (int j = 0; j < n; ++j) {
if (queen[i] == j) {
s += "Q";
} else
s += ".";
}
row.push_back(s);
}
output.push_back(row);
}
private:
int* rows;
int* queen;
int* hills;
int* dales;
int n;
vector<vector<string>> output;
};
非递归实现
- 栈的结构struct Position,需要有3个字段, row, col, need_remove_queen;
- 类Solution中辅助的数据结构需要增加一个cols数组; 即
rows[row] = 1
标记本行被占用,cols[col] = 1
标记本列被占用; - 增加重载placeQueen()函数
下面只贴出最重要的与递归实现不同的代码部分
struct Position {
int row;
int col;
bool need_remove_queen;
Position() { Position(-1, -1); }
Position(int r, int c, bool b_remove = false):
row(r), col(c), need_remove_queen(b_remove) {}
};
void Solution::placeQueen(struct Position& x, int row, int col) {
x.need_remove_queen = true;
placeQueen(row, col);
}
void Solution::robot(int n) {
vector<Position> stack;
stack.push_back({-1, -1});
stack.push_back({0, 0});
while (stack.size()) {
Position x;
while ((x = stack.back(), x.col < n && x.col >= 0)) {
if (notUnderAttack(x.row, x.col)) {
Position &z = stack.back();
placeQueen(z, z.row, z.col);
if (x.row + 1 == n) {
addSolution();
} else {
stack.push_back({ ++x.row, 0 });
continue;
}
} else {
stack.pop_back();
if (x.need_remove_queen)
removeQueen(x.row, x.col);
stack.push_back({x.row, ++x.col});
}
}
stack.pop_back();
if (stack.size()) {
auto y = stack.back();
stack.pop_back();
if (y.need_remove_queen)
removeQueen(y.row, y.col);
if (y.col < n - 1 && y.col >= 0) {
stack.push_back({y.row, ++y.col});
}
}
}
}
vector<vector<string>> Solution::solveNQueens(int n) {
this->n = n;
rows = new int[n];
cols = new int[n];
queen = new int[n];
hills = new int[2 * n - 1]; // e.g. n = 4, "/" hillsÓÐ7¸öλÖÃ;
dales = new int[2 * n - 1];
for (int i = 0; i < n; ++i) {
rows[i] = cols[i] = queen[i] = 0;
}
for (int j = 0; j < 2 * n - 1; ++j) {
hills[j] = dales[j] = 0;
}
// backtrack(0);
robot(n);
return output;
}
递归版本和非递归版本的代码都在leetcode通过