Day30【回溯算法】332.重新安排行程、51.N皇后、37.解数独

今天的题都很抽象,主要理解理解思路  

332.重新安排行程

力扣题目链接/文章讲解

本题过于抽象,关键是捋清树形结构

大致看一看:本题还是在一路上选择飞往哪里,是在收集路径,因此选用记录边的回溯代码模板

然后本题只需要找到一个结果就能停止回溯搜索了。在Day18112.路径总和中,我们介绍过:能够通过回溯函数的返回值标记“搜索到结果了”,从而使整体回溯过程在找到一个结果后就能停止继续搜索,不断向上返回

本题的一大难点:需要用一种方法知道在当前节点有哪些路径可选择? 需要根据机票信息,记录下从出发机场能到达的所有机场,以及从出发机场到各机场的航班次数

用数据结构unordered_map<出发机场, map<到达机场, 航班次数> >来存储上述信息

// unordered_map<出发机场, map<到达机场, 航班次数>> targets
unordered_map<string, map<string, int>> targets;

根据机票对该数据结构进行初始化 

for (const vector<string>& vec : tickets) {
    targets[vec[0]][vec[1]]++; // 记录映射关系
}

有了这些准备工作,就可以开始写代码了 

class Solution {
private:
// unordered_map<出发机场, map<到达机场, 航班次数>> targets
unordered_map<string, map<string, int>> targets;
vector<string> path;    // 用于记录一路上到达的城市
int ticketNum;
bool backtracking() {
    if (path.size() == ticketNum + 1) { // 终止条件,告诉上一层“找到目标结果了”
        return true;
    }
    const string & now = path.back(); // 当前所在机场为path的最后一个记录
    for (pair<const string, int>& target : targets[now]) {
        if (target.second > 0 ) { // 说明还有前往target.first的机票(还有剩余航班),这里我们看出了,targets就是在控制到达某个节点的可选择路径
            path.push_back(target.first);   // 记录路径
            target.second--;    // 标记使用了该机票
            bool flag = backtracking(); // 前往这条路径,并返回“在树上按这条路走下去是否找到目标结果了”
            if (flag) return true;  // 如果已经找到了,就直接告诉再上层找到目标结果了
            path.pop_back();    // 回溯,撤销记录
            target.second++;    // 撤销该机票的使用标记
        }
    }
    return false;   // 如果能够执行到这里,说明上述for循环中没有retuan true,进一步表明走到树的这个节点后,没有一条可行方案能够找到结果,需要向上层返回false
}
public:
    vector<string> findItinerary(vector<vector<string>>& tickets) {
        for (const vector<string>& vec : tickets) {
            targets[vec[0]][vec[1]]++; // 记录映射关系,将所有机票信息整合进我们的数据结构
        }
        path.push_back("JFK"); // 起始机场
        ticketNum = tickets.size();
        backtracking();
        return path;    // 这里的path就是result
    }
};

51.N皇后

力扣题目链接/文章讲解/视频讲解 

本题也很抽象。看看树形结构吧 

本题需要通过“放置位置的合法性”确定在某个节点时,有哪些可选择路径 

本题回溯函数传入参数rowIndex来标记“当前走到了哪一行的节点” 

chessboard记录的是合法的棋盘,因为我们是在做记录之前判断合法性的,就能够保证chessboard一直是合法的 

class Solution {
private:
    vector<string> chessboard;
    vector<vector<string> > result;
public:
    vector<vector<string>> solveNQueens(int n) {
        chessboard = vector<string>(n, string(n, '.'));
        backtracking(0);
        return result;
    }
    void backtracking(int rowIndex) {
        if (rowIndex > chessboard.size() - 1) { // 终止条件,rowIndex超过了最大行索引,则记录结果并返回
            result.push_back(chessboard);
            return;
        }
        for (int colIndex = 0; colIndex < chessboard.size(); ++colIndex) {    // 开始选择路径
            if (isValid(rowIndex, colIndex)) {  // 如果在chessboard[rowIndex][colIndex]放置是合法的
                chessboard[rowIndex][colIndex] = 'Q';   // 放置
                backtracking(rowIndex + 1); //  前往下一行进行放置
                chessboard[rowIndex][colIndex] = '.';   // 撤销放置
            } else
                continue;
        }
        return;
    }
    bool isValid(int rowIndex, int colIndex) {  // 检查在chessboard[rowIndex][colIndex]放置是否合法
        // 检查列是否有皇后
        for (int i = 0; i < rowIndex; i++) {   
            if (chessboard[i][colIndex] == 'Q')
                return false;
        }
        // 检查行是否有皇后(不需要检查行,因为在backtracking的逻辑中我们能够保证每行只有一个皇后)
        // 检查45度角是否有皇后
        for (int i = rowIndex - 1, j = colIndex - 1; i >= 0 && j >= 0; --i, --j) {
            if (chessboard[i][j] == 'Q')
                return false;
        }
        // 检查135度角是否有皇后
        for (int i = rowIndex - 1, j = colIndex + 1; i >= 0 && j < chessboard.size(); --i, ++j) {
            if (chessboard[i][j] == 'Q')
                return false;
        }
        return true;
    }
};

37.解数独 

力扣题目链接/文章讲解/视频讲解 

树形结构如下 

本题仅需要搜索到一个结果即可,故回溯函数可以有返回值标记是否在树枝上找到结果 

本题大致思路:一个for循环遍历棋盘的行,一个for循环遍历棋盘的列,一行一列确定下来之后,递归遍历这个位置放9个数字的可能性 

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] != '.') continue;
            for (char k = '1'; k <= '9'; k++) {     // (i, j) 这个位置放k是否合适
                if (isValid(i, j, k, board)) {
                    board[i][j] = k;                // 放置k
                    if (backtracking(board)) return true; // 如果找到合适一组立刻返回
                    board[i][j] = '.';              // 回溯,撤销k
                }
            }
            return false;                           // 9个数都试完了,都不行,那么就返回false
        }
    }
    return true; // 遍历完没有返回false,说明找到了合适棋盘位置了
}

整体代码

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 (char k = '1'; k <= '9'; k++) {     // (i, j) 这个位置放k是否合适
                    if (isValid(i, j, k, board)) {
                        board[i][j] = k;                // 放置k
                        if (backtracking(board)) return true; // 如果找到合适一组立刻返回
                        board[i][j] = '.';              // 回溯,撤销k
                    }
                }
                return false;  // 9个数都试完了,都不行,那么就返回false
            }
        }
    }
    return true; // 遍历完没有返回false,说明找到了合适棋盘位置了
}
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++) { // 判断9方格里是否重复
        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);
    }
};

回顾总结 

回溯章节结束。我们在回溯章节的题目中主要的关注点:

怎么抽象出树形结构?

怎么确定在当前节点有哪些路径可去? 

这两个是重点,我们的去重等操作目的也是为了排除一些不可去的边从而找到“找到哪些路径可去”  

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

林沐华

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值