算法学习记录~2023.5.15~回溯Day4~332.重新安排行程 & 51. N皇后 & 37. 解数独

前言

第一次一天的所有题目都是hard…
好在终于是过完一遍回溯了


332.重新安排行程

题目链接

力扣题目链接

思路1:不使用map和set

关键问题以及对应解法:

  1. 去除环路 --> 使用used数组记录机票的使用情况,使用过就不能再使用
  2. 字典序返回 --> 预先按照每张票的终点进行机票排序,这样回溯算法找到的第一个一定是字典排序最小的

具体的实现结合下面代码的注释进行理解

代码

class Solution {
public:
    vector<string> path;
    vector<vector<string>> result;
    bool used[301] = {false};   //题目有说机票数量最多为300
    bool find = false;              //用于找到一条合规路径就停止继续加入到result

    static bool cmp (const vector<string>& s1, const vector<string>& s2){         //比较函数,用于排字典序
        return s1[1] < s2[1];
    }

    void backtracking (vector<vector<string>>& tickets, string outset){
        if (find == true)           //只要找到一个就返回即可,result只保存第一条路径
            return ;

        if (path.size() == tickets.size() + 1){     //全都用完则就是目标结果
            find = true;
            result.push_back(path);
            return;
        }

        for (int i = 0; i < tickets.size(); i++){
            if (tickets[i][0] == outset && used[i] == false){   //遍历到的机票的出发地符合起始点且没记用过则可以进行处理
                used[i] = true;
                path.push_back(tickets[i][1]);          //机票的目的地即为下一趟路程的起始点
                backtracking(tickets, tickets[i][1]);
                used[i] = false;
                path.pop_back();
            }
        }

    }

    vector<string> findItinerary(vector<vector<string>>& tickets) {
        sort(tickets.begin(), tickets.end(), cmp);  //先按照目的地字典排序
        path.push_back("JFK");      //第一个起点
        backtracking(tickets, "JFK");
        return result[0];
    }
};

思路2:使用map

	这个解法的思路以及中间好几条代码并没有完全理解,后续还需要再看

记录映射关系:
一个机场映射多个机场,机场之间要靠字母序排列,一个机场映射多个机场。
可以使用"航班次数"这个字段的数字做相应的增减,来标记到达机场是否使用过了。

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

代码

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

    bool backtracking (int ticketNum, vector<string>& result){       //使用bool因为找到一条符合的路径就立即返回
        if (result.size() == ticketNum + 1)
            return true;

        for(pair<const string, int>& target : targets[result[result.size()-1]]){    //**不太懂
            if (target.second > 0){
                result.push_back(target.first);
                target.second--;
                if (backtracking(ticketNum, result))
                    return true;
                result.pop_back();
                target.second++;
            }
        }
        return false;
    }

    vector<string> findItinerary(vector<vector<string>>& tickets) {
        vector<string> result;
        for (const vector<string>& vec : tickets){          //**不太懂
            targets[vec[0]][vec[1]]++;  //记录映射关系
            //targets[vec[0]]访问了map<string,int>(目的地map),然后[vec[1]]访问了map<string,int>的value(航程中目的地经过次数)
        }
        result.push_back("JFK");
        backtracking(tickets.size(), result);
        return result;
    }
};

总结

本题如果用map和set的解法目前自己来看还是太吃力了,无论是思路还是基础语法都非常模糊,花了很久其实还并没有搞懂。
再刷时候可以考虑js的其他思路和解法


51. N皇后

题目链接

力扣题目链接

思路

首先是约束条件:

  1. 不能同行
  2. 不能同列
  3. 不能同斜线

搜索皇后的位置,可以抽象为一棵树,二维矩阵中矩阵的高就是这棵树的高度,矩阵的宽就是树形结构中每一个节点的宽度。
用皇后们的约束条件,来回溯搜索这棵树,只要搜索到了树的叶子节点,说明就找到了皇后们的合理位置了。
在这里插入图片描述
参数需要总体的n,当前遍历到第几层的row,以及棋盘chessboard。

终止条件就是搜索到了叶子节点,也就是搜到了最后一层。

单层逻辑中,就是对于每一层都从左到右依次检查该位置合法性,如果满足就继续向下一层递归,否则就继续向本层的右或进入下一层。

验证合法性分别检查列,45度角和135度角即可,只向当前层的上层进行判断。对于本层,因为是一个个循环,所以不需要检查本行。

代码

class Solution {
public:
    vector<vector<string>> result;         //记录结果
    //n为输入棋盘大小
    //row为当前第几行
    void backtracking (int n, int row, vector<string>& chessboard){
        if (row == n){
            result.push_back(chessboard);
            return;
        }
        for (int column = 0; column < n; column++){     //搜索第row行,column为每一列
            if (isValid(row, column, chessboard, n)){   //合法则可以放置皇后
                chessboard[row][column] = 'Q';
                backtracking(n, row + 1, chessboard);   //继续向下递归
                chessboard[row][column] = '.';          //回溯
            }
        }
    }

    bool isValid (int row, int column, vector<string>& chessboard, int n){
        //检查列
        for (int i = 0; i < row; i++){
            if (chessboard[i][column] == 'Q'){
                return false;
            }
        }
        //检查45度
        for (int i = row - 1, j = column + 1; i >= 0 && j < n; i--, j++){
            if (chessboard[i][j] == 'Q'){
                return false;
            }
        }
        //检查135度
        for (int i = row - 1, j = column - 1; i >= 0 && j >= 0; i--, j--)
            if (chessboard[i][j] == 'Q'){
                return false;
            }
        //否则合规
        return true;
    }

    vector<vector<string>> solveNQueens(int n) {
        result.clear();
        //初始化棋盘
        vector<string> chessboard(n, string(n,'.'));        //**为什么要这么写?
        backtracking(n, 0, chessboard);
        return result;
    }
};

总结


37. 解数独

题目链接

力扣题目链接

思路

本题为二维递归。

对于返回类型:
使用bool,因为找到唯一路径就直接返回值。

对于终止条件:
本题不需要,因为需要遍历整个树形结构寻找可能的叶子节点,找到立即返回。由于递归的下一层的棋盘一定比上一层棋盘多一个数,如果棋盘填满了自然就会终止。

对于单层循环搜素逻辑:
在树形图中可以看出我们需要的是一个二维的递归(也就是两个for循环嵌套着递归)
一个for循环遍历棋盘的行,一个for循环遍历棋盘的列,一行一列确定下来之后,递归遍历这个位置放9个数字的可能性!
在这里插入图片描述
另外就是判断棋盘是否合法:

  • 同行是否重复
  • 同列是否重复
  • 9宫格里是否重复

其中第3点的解决思路是找出来行和列的开始位置,以此为基础的3个数之内就是范围。

        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;
            }
        }

代码

class Solution {
public:
    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++){      //对该位置依次试9个数
                        if (isValid(i, j, k, board)){
                            board[i][j] = k;
                            if (backtracking(board))    //如果找到一组合适立刻返回
                                return true;
                            board[i][j] = '.';
                        }
                    }
                    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++){
            for (int j = startCol; j < startCol + 3; j++){
                if (board[i][j] == val)
                    return false;
            }
        }
        //前面检查都通过则合规
        return true;
    }

    void solveSudoku(vector<vector<char>>& board) {
        backtracking(board);
    }
};

总结

本题是第一次接触到二维递归,但其实仍然没有逃脱回溯三步曲的范畴。
思考清楚每一步,想清楚怎样递归获取所有值,以及是否有特殊判定情况,即可解决。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

山药泥拌饭

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

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

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

打赏作者

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

抵扣说明:

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

余额充值