题目:51.N皇后
本题的难点在于本题是一个二维的数组,很陌生,应该怎么搜索呢?怎么搜棋盘呢?
在重申一遍回溯的优势,用递归取代多层for循环的嵌套
思路:
1.对于参数的设定:每个棋盘是一个二维数组,有N个棋盘即有N种解,所以定义result是一个三位数组
2.消除遍历二维矩阵的陌生感。
3.在实际问题中,用抽象符号来表示实际情况。本题用“Q”代表放了皇后,“,”代表没有放皇后。
代码能力还是很弱,代码细节上写不对,一些语法还是没有理解,第二轮的时候一定要注意。写出了以下错误代码。
class Solution {
public:
vector<vector<string>> result;
bool isValid(vector<string> chessboard, int col, int row, int n){ //判断该位置是否有效
for(int i = 0; i < n; i++){ //判断本行是否有效
if(chessboard[row][i] == "Q") return false;
}
for(int i = 0; i < n; i++){
if(chessboard[i][col] == "Q") return false;
}
for(int i = 0, j = 0; i < n && j < n; i++, j++){
if(chessboard[i][j] == "Q") return false;
}
return true;
}
void backtracking(vector<string>& chessboard, int n, int row){
if(row == n){
result.push_back(chessboard);
}
for(int i = 0; i < n; i++){
if(isValid(chessboard, i, row, n) == false) continue;
//如果判断该位置不能再放皇后,则跳过
//传入i, row分别作为列数和行数
chessboard[i][row] = "Q";
backtracking(chessboard, n, row + 1);
chessboard[i][row] = ",";
}
}
vector<vector<string>> solveNQueens(int n) {
vector<string> chessboard; //如何进行初始化?
backtracking(chessboard, n, 0);
return result;
}
};
难点在于判断isValid,代码如下
bool isValid(vector<string>& chessboard, int col, int row, int n) {
// 检查列
for (int i = 0; i < row; i++) { // 这是一个剪枝
if (chessboard[i][col] == 'Q') {
return false;
}
}
// 检查 45度角是否有皇后
for (int i = row - 1, j = col - 1; i >=0 && j >= 0; i--, j--) {
if (chessboard[i][j] == 'Q') {
return false;
}
}
// 检查 135度角是否有皇后
for(int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) {
if (chessboard[i][j] == 'Q') {
return false;
}
}
return true;
}
理解:
1.backtracking函数中要向新的一行里填入元素,截至到目前,该行以上的每一行已经有位置填入了皇后,该行以下还没有(因为从递归深度上看还没到)。所以只用检查列上符不符合要求,也就是说控制列数不变,检查在其他行中该列位置有没有放过皇后。不需要对行进行检查,本行肯定还没有放过元素,因为一旦放了,必然会向下递归了,不会再在同一行停留。
2.在检查列时进行的剪枝:根据1.所说,改行以下未填入元素,所以该行以下不用检查,只用检查以上的行中本列是否加入了皇后。故for循环的终止条件设置为i < row即可。
3.检查45和135度角时用相同的原理进行剪枝。
树形结构如下(简化版):
完整代码如下:
class Solution {
public:
vector<vector<string>> result;
bool isValid(vector<string>& chessboard, int col, int row, int n) {
// 检查列
for(int i = 0; i < row; i++){ // 这是一个剪枝
if(chessboard[i][col] == 'Q') return false;
}
// 检查 45度角是否有皇后
for(int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--){
if(chessboard[i][j] == 'Q') return false;
}
// 检查 135度角是否有皇后
for(int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++){
if(chessboard[i][j] == 'Q') return false;
}
return true;
}
void backtracking(vector<string>& chessboard, int n, int row){ //n表示for循环遍历的宽度,row表示向下递归的深度
if(row == n){
result.push_back(chessboard);
return;
}
for(int col = 0; col < n; col++){
if(isValid(chessboard, col, row, n) == false) continue;
//如果判断该位置不能再放皇后,则跳过
//传入i, row分别作为列数和行数
chessboard[row][col] = 'Q';
backtracking(chessboard, n, row + 1);
chessboard[row][col] = '.';
}
}
vector<vector<string>> solveNQueens(int n) {
// vector<string> chessboard; //如何进行初始化?
std::vector<std::string> chessboard(n, std::string(n, '.'));
backtracking(chessboard, n, 0);
return result;
}
};
有几个需要加深的问题:
1.对result初始化时这是在干啥。
2.string类型和char类型在赋值等操作时有什么不同和需要注意的地方。
题目:37.解数独(还需要多学几遍)
与n皇后的区别:n皇后明确说了每一行只能有一个皇后,所以你一行放成功了,就直接下一行,就直接进入下层的递归,而解数独,由于要每一行都要填,所以两次for去遍历,如果一个放成功了,就放右边的一个,也就是进入下层递归,直至最后一次递归发现填完了,也就是该返回最后的那一个true了,如果遇到了false,则不进入下次递归了,证明这次递归填的数有问题,还原后,继续填char的下一个,套娃到结果。本题比多皇后多了一个维度
尝试解答(及其不成熟):
终止条件:
设置一个行数:row,if(row == 9),说明剩最后一行选择成功了,深度达到了9行,整个表就填完了。
本位置是否合法:
每一行设置一个unordered_set<char> uset,来查询本层是否使用过了,那列上怎么判断呢?
正确思路:
树形结构
二维递归——两个for循环。
递归三步:
1.参数和返回值:返回值为bool类
搜索整棵树用void,因为要收集到所有结果。只求一种解的情况用bool,得到一种true的解立即返回,意思就是说只用搜索到一枝正确的树枝。
2.终止条件:填满了最后一行,for循环会终止,所以即使不设置终止条件也会停止,跳出。
3.单层递归逻辑:
代码如下:
class Solution {
private:
bool backtracking(vector<vector<char>>& board){
for(int i = 0; i < board.size(); i++){ //遍历行
for(int j = 0; j < board[0].size(); j++){ //遍历列
if(board[i][j] == '.'){
for(int k = '1'; k <= '9'; k++){
if(isValid(i, j, k, board)){ //(i,j)这个位置放k是否合适
board[i][j] = k; //放置k
if(backtracking(board)) return true; //如果这一步backtracking返回了true,说明该步填入的k是合适的,继续向上返回
board[i][j] = '.'; //如果这一步backtracking返回了false,说明该步骤填了k,在后序的递归中会导致无解,所以本层应该尝试其他k值。这里进行回溯
}
}
return false; //如果程序会执行到这一步,说明for循环找不到一个合适的k填进去,使数独有解,意味着在本层之前的数填的不合适。所以应该返回false告诉上面一层,上面一层需要换一个数。
}
}
}
return true;
}
bool isValid(int row, int col, char val, vector<vector<char>>& board) {
for(int i = 0; i < 9; i++) { //检查行里是否有元素
if(board[row][i] == val) {
return false;
}
}
for (int j = 0; j < 9; j++){ //检查列里是否有元素
if(board[j][col] == val) {
return false;
}
}
int startRow = (row / 3) * 3;
int startCol = (col / 3) * 3;
for(int i = startRow; i < startRow + 3; i++){ //不理解这些数值
for(int j = startCol; j < startCol + 3; j++){ //自己模拟一下
if(board[i][j] == val){
return false;
}
}
}
return true;
}
public:
void solveSudoku(vector<vector<char>>& board) {
backtracking(board);
}
};
难点:
1.最开始的true和false在哪里返回?
返回false:如果for循环找不到一个合适的k填进去,使数独有解,说明这里填132456789都不合适,意味着在本层之前的数填的不合适。所以应该返回false告诉上面一层,上面一层需要换一个数。
返回true:直到棋盘填满,if判断不会在为true,也就是说不会执行if里面的语句,直接返回true,然后一层一层向上返回true.
2.判断是否isValid