LeetCode | C++ 回溯算法——332.重新安排行程、51. N皇后、37. 解数独、总结

52 篇文章 0 订阅
文章介绍了如何使用回溯算法解决重新安排行程、N皇后、解数独等经典问题,强调了剪枝策略的重要性,并分析了不同问题的时间和空间复杂度。在组合问题中,重点在于起点的范围控制和元素总和的剪枝;在子集和排列问题中,关注递归过程中的状态管理。
摘要由CSDN通过智能技术生成

332.重新安排行程

在记录机场与机场之间的映射关系时,使用的是 unordered_map<string, map<string, int>> targets, unordered_map<出发机场, map<到达机场, 航班次数>> targets

原因为:

  • 出发机场 和 到达机场可能会重复,例如【1,2】【2,1】,所以对于使用过的票,需要及时删除,不然程序就会进入死循环
  • 由于要返回最小的行程组合,所以到达机场 的容器 需要是 有序的,可以用multiset 和 map, 但是对于multiset 而言,删除元素之后,迭代器就会失效,所以使用 map 里面 的int 来存储 航班次数,以此来标记 到达机场是否使用过,相当于通过标记来达到 删除的目的,实际上并没有删。

回溯函数的返回值 类型bool 是因为 只需要找到一个行程就行(题目:如果存在多种有效的行程,请按字典排序返回最小的行程组合),而不是需要找到所有的

参数里面 定义了 票的数量 int ticketNum,把所有票用完 即终止,vector<string> result 存储 航班,当航班的数量 等于 票的数量 + 1 时, 票即用完

class Solution {
public:
    unordered_map<string, map<string, int>> targets;
    bool backtracking(int ticketNum, vector<string>& result) {
        if (result.size() == ticketNum + 1) {
            return true;
        }
        // 访问 result 存储的最后一个航班 对应的 到达机场
        // pair里要有const,因为map中的key是不可修改的,所以是pair<const string, int>
        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) {
        targets.clear();
        // 变量初始化
        for (const vector<string>& vec : tickets) {
            targets[vec[0]][vec[1]]++; // 记录映射关系
        }
        vector<string> result;
        result.push_back("JFK");
        backtracking(tickets.size(), result);
        return result;
    }
};

51. N皇后

N皇后问题可以抽象为 1 棵树, 二维矩阵的高 即为 树高, 二维矩阵的宽 即为 树宽,用皇后的约束条件 来 回溯搜索 这棵树,只要搜索到了 树的叶子结点,即说明找到了所以皇后的合理位置。

定义了row 代表当前遍历的层数, 当遍历到最后一层 即 结束

关键点在于 如何去 约束

如果同列 , 斜对角 存在皇后就不合法,

在单层搜索中,每一层递归,只会选for 循环 (同行)里的一个元素,所以不用去验证同行是否合法

class Solution {
public:
    // vector<string> path;
    vector<vector<string>> result;
    bool isValid(int row, int col, vector<string>& chessboard, int n) {
        // 不能同列
        for (int i = row - 1, j = col; i >= 0; i--) {
            if (chessboard[i][j] == '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 - 1; i--, j++) {
            if (chessboard[i][j] == 'Q') {
                return false;
            }
        }
        return true;
    }
    void backtracking (int n, int row, vector<string>& chessboard) {
        if (row == n) {
            result.push_back(chessboard);
            return;
        }
        for (int i = 0; i < n; i++) {
            if (isValid(row, i, chessboard, n)) {
                chessboard[row][i] = 'Q';
                backtracking(n, row + 1, chessboard);
                chessboard[row][i] = '.';       
            }
        }
        return;
    }
    vector<vector<string>> solveNQueens(int n) {
        result.clear();
        vector<string> chessboard(n, string(n, '.'));   
        backtracking(n, 0, chessboard);
        return result;
    }
};

37. 解数独

因为题目保证数独只有一个解,所以返回值类型 为 bool, 找到符合条件的就返回

注意返回值的位置, return false 也是有讲究的, 当在一个位置 试了9个数都不行的时候,说明这个棋盘找不到解决数独问题的解

如果把整个棋盘遍历完 都没有返回false, 即说明找到了合适的棋盘位置, return true

在单层递归逻辑里面,如果找到合适的一组就立刻返回。

该题主要是 使用了 二维递归 (卡哥词汇)

class Solution {
public:
    bool backtracking (vector<vector<char>>& board) {
        for (int i = 0; i < 9; i++) {
            for (int j = 0; j < 9; j++) {
                if (board[i][j] != '.') continue;
                else {
                    for (char k = '1'; k <= '9'; k++) {
                        if (isValid(i, j, k, board)) {
                            board[i][j] = k;
                            if (backtracking(board)) return true;
                            board[i][j] = '.';
                        }
                    }
                }
                return false;
            }
        }
        return true;
    }
    // i : 行  j : 列   k  (i, j) 位置的值
    bool isValid(int row, int col, char val, vector<vector<char>>& board) {
        // 同行只能出现一次
        for (int j = 0; j < 9; j++) {
            if (board[row][j] == val) {
                return false;
            }
        }

        // 同列只能出现一次
        for (int i = 0; i < 9; i++) {
            if (board[i][col] == val) {
                return false;
            }
        }

        // 3 × 3 宫内 只能出现一次
        for (int i = row / 3 * 3; i < row / 3 * 3 + 3; i++) {
            for (int j = col / 3 * 3; j < col / 3 * 3 + 3; j++) {
                if (board[i][j] == val) {
                    return false;
                }
            }
        }
        return true;
        
    }
    void solveSudoku(vector<vector<char>>& board) {
        backtracking(board);
    }
};

总结

优化回溯算法只有剪枝一种方法

(1)组合问题

对于求组合问题而言,剪枝的精髓是:for循环在寻找起点的时候要有一个范围,如果这个起点到集合终止之间的元素已经不够题目要求的k个元素了,就没有必要搜索了

在for循环上做剪枝操作是回溯法剪枝的常见套路!

对于求组合总和而言,可以再加上一个剪枝:已选元素总和如果已经大于n(题中要求的和)了,那么往后遍历就没有意义了,直接剪掉

对于组合问题而言,什么时候需要startIndex呢,

如果是一个集合来求组合的话,就需要startIndex;如果是多个集合取组合,各个集合之间相互不影响,那么就不用startIndex

对于集合中包含重复元素,去求组合总和,但要求解集不能包含重复的组合:需要树枝去重 和 树层去重, bool used数组

(2)切割问题

利用startIndex 模拟切割线, [startIndex, i] 子串的截取

(3)子集问题

在树形结构中子集问题是要收集所有节点的结果,而组合问题是收集叶子节点的结果

所以基本不需要添加终止条件, 如果要加的话,一定加在 result.push_back(path)的后面

对于递增子序列 :同一父节点下的同层上使用过的元素就不能再使用了

(4)排列问题

排列是有序的

  • 每层都是从0开始搜索而不是startIndex
  • 需要used数组记录path里都放了哪些元素了

(5)性能分析

以下在计算空间复杂度的时候都把系统栈(不是数据结构里的栈)所占空间算进去。

子集问题分析:

  • 时间复杂度:O(2n),因为每一个元素的状态无外乎取与不取,所以时间复杂度为O(2n)
  • 空间复杂度:O(n),递归深度为n,所以系统栈所用空间为O(n),每一层递归所用的空间都是常数级别,注意代码里的result和path都是全局变量,就算是放在参数里,传的也是引用,并不会新申请内存空间,最终空间复杂度为O(n)

排列问题分析:

  • 时间复杂度:O(n!),这个可以从排列的树形图中很明显发现,每一层节点为n,第二层每一个分支都延伸了n-1个分支,再往下又是n-2个分支,所以一直到叶子节点一共就是 n * n-1 * n-2 * … 1 = n!。
  • 空间复杂度:O(n),和子集问题同理。

组合问题分析

  • 时间复杂度:O(2^n),组合问题其实就是一种子集的问题,所以组合问题最坏的情况,也不会超过子集问题的时间复杂度。
  • 空间复杂度:O(n),和子集问题同理。

N皇后问题分析:

  • 时间复杂度:O(n!) ,其实如果看树形图的话,直觉上是O(n^n),但皇后之间不能见面所以在搜索的过程中是有剪枝的,最差也就是O(n!),n!表示n * (n-1) * … * 1。
  • 空间复杂度:O(n),和子集问题同理。

解数独问题分析:

  • 时间复杂度:O(9^m) , m是’.'的数目。
  • 空间复杂度:O(n2),递归的深度是n2

一般说道回溯算法的复杂度,都说是指数级别的时间复杂度,这也算是一个概括吧!

参考

代码随想录

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值