代码随想录算法训练营第三十天 | 332. 重新安排行程、51. N 皇后、37. 解数独

332. 重新安排行程

主要思路

(1)参数和终止条件

// unordered_map<出发机场, map<到达机场, 航班次数>> targets
unordered_map<string, map<string, int>> targets;
bool backtracking(int ticketNum, vector<string>& result) {

定义了一个用于存储机场之间映射关系的数据结构 targets,以及一个递归函数 backtracking 用来实现深度优先遍历。

具体来说,targets 是一个 unordered_map,其中键为出发机场,值为一个 map该 map 的键为到达机场,值为这两个机场之间对应的航班次数。这样构造的的是每个出发机场到目的机场的航班次数的映射,首先从一个机场可以到不止一个机场,其次两个相同机场之间不止一个航班

而 backtracking 函数用于实现深度优先遍历。该函数接受两个参数,一个是已经选择的机票数目 ticketNum,另一个是当前已选择的机场序列 result。该函数会根据已经选择的机票和当前所在的机场,查找可以前往的下一个机场,然后进行递归。当所有机票都被使用时,函数返回 true,并返回结果序列;否则,函数返回 false。这个函数的目的是求出满足题目要求的最小行程组合,即所有机票都必须用且只能用一次,并且按照字典序排序返回最小的行程组合。

初始化

        for (const vector<string>& vec : tickets) { //遍历题目所给的二维tickers字符串
            cout << vec[0] << "  " << vec[1] << endl;   //vec[0]表示所取字符串中起点,vec[1]表示终点
            cout << targets[ vec[0] ] [ vec[1] ] << endl;   //targets[ vec[0] ] [ vec[1] ]表示targets中以[ vec[0] ]为起点,[ vec[1] ]为终点的值
            cout << "---------------------------------------" << endl;
            targets[ vec[0] ] [ vec[1] ]++; // 记录映射关系
        }

首先,使用一个范围 for 循环遍历输入的机票列表 tickets,每次循环获取一个机票 vec,类型为 vector<string>

然后,将 vec[0](即机票的出发机场)作为键,vec[1](即机票的目的地机场)作为值,将它们的映射关系存储到一个二维 vector targets 中。这里使用了 C++ 中的 unordered_map 来实现,其语法为 targets[ vec[0] ]   [ vec[1] ]++,表示将 vec[0] 和 vec[1] 映射关系的计数器加一,用来记录这两个机场之间有多少个航班。

最后,将起始机场 "JFK" 加入结果数组 result 中,作为遍历的起点。

注意函数返回值我用的是bool!因为我们只需要找到一个行程,就是在树形结构中唯一的一条通向叶子节点的路线,如图:

(2)终止条件:

回溯遍历的过程中,遇到的机场个数,如果达到了(航班数量+1),那么就找到了一个行程,把所有航班串在一起了。

(3)单层递归逻辑

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

【1】

for (pair<const string, int>& target : targets[result[result.size() - 1]]) {
    // ...
}

使用范围 for 循环遍历了一个 map 容器 targets 中的一个子 map,

targets[result[result.size() - 1]] 中,result 表示当前已经遍历过的机场序列,result.size() - 1 表示当前到达的机场,result[result.size() - 1] 即为当前到达机场的名称,targets[result[result.size() - 1]] 表示以当前到达机场为起点的所有映射关系,其中键表示到达机场,值表示对应的航班数量。

易错点:

(1)容易陷入死循环:
 

出发机场和到达机场也会重复的,很可能就一直在JFK与NRT里一直兜圈

代码实现:

class Solution {
private:
// unordered_map<出发机场, map<到达机场, 航班次数>> targets
unordered_map<string, map<string, int>> targets;
bool backtracking(int ticketNum, vector<string>& result) {
    if (result.size() == ticketNum + 1) {
        return true;
    }
    //遍历起点机场所对应的所有机场
    for (pair<const string, int>& target : targets[result[result.size() - 1]]) {
        //result的声明是vector<string>,用于存储飞行路线
        //result[result.size() - 1]表示的是以result中最后一个元素为起点,遍历该元素对应的所有可能路线
        //pair<const string, int>& target表示终点机场和机票数量的map
        if (target.second > 0 ) { // 表示机票还没有用过
            result.push_back(target.first);
            target.second--;
            if (backtracking(ticketNum, result)) return true;
            result.pop_back();
            target.second++;
        }
    }
    return false;
}
public:
    vector<string> findItinerary(vector<vector<string>>& tickets) {
        targets.clear();
        vector<string> result;
        result.push_back("JFK"); // 起始机场
        for (const vector<string>& vec : tickets) { //遍历题目所给的二维tickers字符串
            targets[ vec[0] ] [ vec[1] ]++; // 记录映射关系
        }
        backtracking(tickets.size(), result);
        return result;
    }
};

 

补充

(1)C++中类循环

语法形式

for (element : container) {
    // 迭代操作
}

type 是容器中元素的类型,var 是循环中变量的名称,container 是容器本身。该循环语句用于遍历容器中的所有元素,并对每个元素执行指定的语句。

其中 container 可以是任何可迭代对象,例如 std::vectorstd::mapstd::set、C数组等等,而 element 则是 container 中的元素。

循环过程中,每一次迭代都会把 container 中的元素赋值给 element,然后执行循环体中的语句。对于 std::vectorstd::array、C数组等顺序容器,迭代顺序是按照容器中元素的顺序;对于 std::mapstd::set 等关联容器,则是按照元素的 key 值升序遍历。

例如,下面的代码使用类循环遍历一个 std::vector<int> 容器:

std::vector<int> nums = {1, 2, 3, 4, 5};
for (int num : nums) {
    std::cout << num << " ";
}
// 输出:1 2 3 4 5

如果需要修改 container 中的元素,必须使用引用类型的 element,例如:

std::vector<int> nums = {1, 2, 3, 4, 5};
for (int& num : nums) { // 使用引用类型
    num *= 2; // 修改元素
}
for (int num : nums) {
    std::cout << num << " ";
}
// 输出:2 4 6 8 10

如果要遍历unordered_map<string, map<string, int>> targets;

        for (auto& [from, to_map] : targets) {  
            cout << "出发机场" << from << endl;
            for (auto& [to, count] : to_map) {
                cout << "终点机场" << to << '\t' << "机票数量" << count << endl;
            }
            cout << "----------------------------------" << endl;
        }

在这个代码中,外部的for循环遍历targets的键值对,其中from为出发机场的名字,to_map为以该出发机场为键的map<string, int>类型,其储存了到达机场和航班次数的键值对。内部的for循环遍历每个to_map中的键值对,其中to为到达机场的名字,count为该航线的航班次数。

上面中规中矩写法 

for (const std::pair<std::string, std::map<std::string, int>>& target1 : targets) {
    const std::string& departure = target1.first;
    const std::map<std::string, int>& arrivalMap = target1.second;
    for (const std::pair<std::string, int>& target2 : arrivalMap) {
        const std::string& arrival = target2.first;
        const int& flightCount = target2.second;
        // 处理每个起点、终点及对应的航班次数
    }
}

51. N 皇后

视频讲解

主要思路:

(1)皇后们的约束条件:

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

(2)二维矩阵中矩阵的高就是这棵树的高度,矩阵的宽就是树形结构中每一个节点的宽度。

(矩阵的高是指矩阵中行的数量,通常用字母m表示;矩阵的宽是指矩阵中列的数量,通常用字母n表示)

代码实现:

class Solution {
private:
vector<vector<string>> result;
// n 为输入的棋盘大小
// row 是当前递归到棋盘的第几行了
void backtracking(int n, int row, vector<string>& chessboard) {
    if (row == n) {
        // for(int i = 0; i < chessboard.size(); i++) {
        //     cout << chessboard[i] << endl;
        // }
        // cout << "--------------------------------" << endl;
        result.push_back(chessboard);
        return;
    }
    for (int col = 0; col < n; col++) {
        if (isValid(row, col, chessboard, n)) { // 验证合法就可以放
            chessboard[row][col] = 'Q'; // 放置皇后
            backtracking(n, row + 1, chessboard);
            chessboard[row][col] = '.'; // 回溯,撤销皇后
        }
    }
}
//判断皇后放的位置是否合法
bool isValid(int row, int col, vector<string>& chessboard, 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;
}
public:
    vector<vector<string>> solveNQueens(int n) {
        result.clear();
        vector<string> chessboard(n, string(n, '.'));
        backtracking(n, 0, chessboard);
        return result;
    }
};

补充

(1)vector<string> chessboard(n, string(n, '.'))

这行代码创建了一个大小为n x n的字符串矩阵,用于表示N皇后问题的棋盘。具体来说,它使用了vector<string>的构造函数,该构造函数接受两个参数,分别为vector的大小和初始值。

具体而言,该构造函数的第一个参数n表示vector中元素的个数,第二个参数string(n, '.')表示初始化每个元素为一个大小为n的字符串,其中每个字符都为'.',即棋盘上的初始状态为每个位置都没有皇后。

因此,这行代码的作用就是创建一个n x n的字符串矩阵,用于表示N皇后问题的棋盘,其中每个位置都没有皇后,即棋盘的初始状态。这个字符串矩阵被存储在一个vector<string>类型的变量chessboard中。

如果不是因为要读取每次棋盘大小n,chessboard其实可以像其他回溯题一样定为全局变量

力扣

视频讲解

易错点:

(1)终止条件:无,因为如果遍历完整个矩阵发现都已经填满,那自然会回溯返回true,如果有一个格子发现一个数字都不好填,那就会回溯返回false

(2)二维遍历整个矩阵,不像上面N皇后遍历行就行,因为上题遍历的一行里只会有一个皇后,本题则是要把矩阵填满

(3)每一个格子填上就可以return true,只有有一个填不上就return false,最后循环遍历矩阵结束都没有return false就return true

(4)判断一个位置应该放那个数字中最难就是九宫格里,关键是确定这是第几个九宫格,可以利用整除知识理解

代码实现:

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] == '.') {   //如果这个位置为空(题目中用"."表示),就开始遍历1~9数字
                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);
    }
};

算法训练营额外收获

(1)什么时候加 std

std表示命名空间,头文件有using namespace std;就不用加

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值