代码随想录算法训练营第二十五天 | 491.递增子序列,46.全排列,47.全排列 II,332.重新安排行程,51.N皇后,37.解数独,回溯总结

第二十五天继续打卡,今天的题做了两天,主要想把后面3个hard问题看懂,递增子序列的难度也挺大的,排列问题需要用到used数组。


491.非递减子序列

题目链接

做题过程

  • 有几个点没想清楚没做出来:
    • 数组里只有一个数该怎样收获结果,答案是数组size大于1就可以收获了
    • 数组如何去重,答案是用哈希表标记同一树层遍历过的数,不能用之前的方法因为不能打乱数组的顺序

知识点

  • unordered_set<int> uset; 是记录本层元素是否重复使用,新的一层uset都会重新定义(清空),所以要知道uset只负责本层
  • 时间复杂度: O(n * 2^n)
  • 空间复杂度: O(n)
  • 数值范围[-100,100],所以完全可以用数组来做哈希

回溯法

class Solution {
public:
    vector<vector<int>>result;
    vector<int>path;
    void backtracking(vector<int>& nums, int startIndex) {
        if (path.size() > 1) {
            result.push_back(path);
        }
        unordered_set<int>uset;
        for (int i = startIndex; i < nums.size(); i++) {
            if ((!path.empty() && nums[i] < path.back()) || uset.find(nums[i]) != uset.end()) {
                continue;
            }
            uset.insert(nums[i]);
            path.push_back(nums[i]);
            backtracking(nums, i + 1);
            path.pop_back();
        }
    }
    vector<vector<int>> findSubsequences(vector<int>& nums) {
        backtracking(nums, 0);
        return result;
    }
};

46.全排列

题目链接

解题过程

  • 使用数组来记录哪一个数组元素已经用过了,每次从下标0开始遍历,用过的数就跳过

知识点

  • 时间复杂度: O(n!)
  • 空间复杂度: O(n)

回溯法

class Solution {
public:
    vector<vector<int>>result;
    vector<int>path;
    void backtracking(vector<int>& nums, vector<bool>& used) {
        if (path.size() == nums.size()) {
            result.push_back(path);
        }
        for (int i = 0; i < nums.size(); i++) {
            if (used[i] == true) continue;
            path.push_back(nums[i]);
            used[i] = true;
            backtracking(nums, used);
            used[i] = false;
            path.pop_back();
        }
    }
    vector<vector<int>> permute(vector<int>& nums) {
        vector<bool>used(nums.size(), false);
        backtracking(nums, used);
        return result;
    }
};

47.全排列Ⅱ

题目链接

做题过程

  • 用used数组记录使用过的数,本题当前的数使用过(排列去重) 或者 之前的数未使用且现在的数等于之前的数(树层去重) 就跳过

知识点

  • 去重一定要对元素进行排序,这样我们才方便通过相邻的节点来判断是否重复使用了
  • 时间复杂度: O(n! * n)
  • 空间复杂度: O(n)

回溯法

class Solution {
public:
    vector<vector<int>>result;
    vector<int>path;
    void backtracking(vector<int>& nums, vector<bool>& used) {
        if (path.size() == nums.size()) {
            result.push_back(path);
            return;
        }
        for (int i = 0; i < nums.size(); i++) {
            if ((i != 0 && nums[i] == nums[i - 1] && used[i - 1] == false) || used[i] == true) continue;
            path.push_back(nums[i]);
            used[i] = true;
            backtracking(nums, used);
            used[i] = false;
            path.pop_back();
        }
    }
    vector<vector<int>> permuteUnique(vector<int>& nums) {
        vector<bool> used(nums.size(), false);
        sort(nums.begin(), nums.end());
        backtracking(nums, used);
        return result;
    }
};

332.重新安排行程

题目链接

做题过程

  • 题目看懂了,知道用dfs思想做,但看到有排序就不知道怎么做了

知识点

  • 本题回溯需要有返回值,如果没有返回值的话会取到然排序更大更靠后的有效的行程。如果有返回值,第一次遇到正确答案就会直接返回不会进行后面的回溯过程。
  • 存放映射关系定义为unordered_map<string, map<string, int>> targets
  • 含义为unordered_map<string, map<string, int>> targets:unordered_map<出发机场, map<到达机场, 航班次数>> targets

回溯法

class Solution {
public:
    vector<string>result;
    unordered_map<string, map<string, int>>targets;
    bool backtracking(vector<vector<string>>& tickets) {
        if (result.size() == tickets.size() + 1) return true;
        for (pair<const string, int>& target : targets[result.back()]) {
            if (target.second > 0) {
                result.push_back(target.first);
                target.second--;
                if (backtracking(tickets)) return true;
                target.second++;
                result.pop_back();
            }
        }
        return false;
    }
    vector<string> findItinerary(vector<vector<string>>& tickets) {
        for (vector<string>&ticket : tickets) {
            targets[ticket[0]][ticket[1]]++;
        }
        result.push_back("JFK");
        backtracking(tickets);
        return result;
    }
};

51.N皇后

题目链接

做题过程

  • 想用两层循环嵌套回溯法遍历棋盘,但其实只用一层棋盘即可,棋盘的宽度就是for循环的长度,递归的深度就是棋盘的高度,这样就可以套进回溯法的模板里了

知识点

  • 由于我们是从上到下添加皇后,所以判断皇后是否有效只需要判断该棋子上方、左上方、右上方是否有皇后

回溯法

class Solution {
public:
    vector<vector<string>>result;
    void backtracking(int n, int row, vector<string>&board) {
        if (row == n) {
            result.push_back(board);
            return;
        }
        for (int col = 0; col < n; col++) {
            if (!isValid(row, col, n, board)) continue;
            board[row][col] = 'Q';
            backtracking(n, row + 1, board);
            board[row][col] = '.';
        }
    }
    bool isValid(int row, int col, int n, vector<string>&board) {
        for (int i = 0; i < row; i++) { // 检查棋子上方
            if (board[i][col] == 'Q') return false;
        }
        for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) { // 检查棋子左上角
            if (board[i][j] == 'Q') return false;
        }
        for (int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) { // 检查棋子右上角
            if (board[i][j] == 'Q') return false;
        }
        return true;
    }
    vector<vector<string>> solveNQueens(int n) {
        vector<string>board(n, string(n, '.'));
        backtracking(n, 0, board);
        return result;
    }
};

37.解数独

题目链接

做题过程

  • 想到用二维递归,但没想到i j的递归范围和回溯for用什么来循环

知识点

  • 递归函数的返回值需要是bool类型,因为解数独找到一个符合的条件(就在树的叶子节点上)立刻就返回,相当于找从根节点到叶子节点一条唯一路径,所以需要使用bool返回值。

回溯法

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;
                for (char c = '1'; c <= '9'; c++) {
                    if (isValid(i, j, c, board)) {
                        board[i][j] = c;
                        if (backtracking(board)) return true;
                        board[i][j] = '.';
                    }
                }
                return false;
            }
        }
        return true;
    }
    bool isValid(int row, int col, char c, vector<vector<char>>& board) {
        for (int i = 0; i < 9; i++) {
            if (board[i][col] == c) return false;
        }
        for (int j = 0; j < 9; j++) {
            if (board[row][j] == c) return false;
        }
        int rowBase = row / 3 * 3;
        int colBase = col / 3 * 3;
        for (int i = rowBase; i < rowBase + 3; i++) {
            for (int j = colBase; j < colBase + 3; j++) {
                if (board[i][j] == c) return false;
            }
        }
        return true;
    }
    void solveSudoku(vector<vector<char>>& board) {
        backtracking(board);
    }
};

回溯总结

回溯法理论基础

回溯是递归的副产品,只要有递归就会有回溯,所以回溯法也经常和二叉树遍历,深度优先搜索混在一起,因为这两种方式都是用了递归。

回溯法就是暴力搜索,并不是什么高效的算法,最多再剪枝一下。

回溯算法能解决如下问题:

  • 组合问题:N个数里面按一定规则找出k个数的集合
  • 排列问题:N个数按一定规则全排列,有几种排列方式
  • 切割问题:一个字符串按一定规则有几种切割方式
  • 子集问题:一个N个数的集合里有多少符合条件的子集
  • 棋盘问题:N皇后,解数独等等

遍历过程抽象为树形结构方便理解

回溯法的模板:

void backtracking(参数) {
    if (终止条件) {
        存放结果;
        return;
    }

    for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
        处理节点;
        backtracking(路径,选择列表); // 递归
        回溯,撤销处理结果
    }
}

回溯法经典问题

  • 组合问题
    • 77.组合
      • 剪枝精髓是:for循环在寻找起点的时候要有一个范围,如果这个起点到集合终止之间的元素已经不够题目要求的k个元素了,就没有必要搜索了
    • 216.组合总和III
      • 已选元素总和如果已经大于n(题中要求的和)了,那么往后遍历就没有意义了,直接剪掉
      • 在for循环加上 i <= 9 - (k - path.size()) + 1 的限制
    • 39. 组合总和
      • 如果是一个集合来求组合的话,就需要startIndex
      • 如果是多个集合取组合,各个集合之间相互不影响,那么就不用startIndex
    • 40.组合总和II
      • **“树层去重”**很重要
    • 17.电话号码的字母组合
      • 本题每一个数字代表的是不同集合,也就是求不同集合之间的组合

  • 切割问题
    • 131.分割回文串
      • 几个难点:
        • 切割问题其实类似组合问题
        • 如何模拟那些切割线
        • 切割问题中递归如何终止
        • 在递归循环中如何截取子串
        • 如何判断回文

  • 子集问题
    • 78.子集
      • 在树形结构中子集问题是要收集所有节点的结果,而组合问题是收集叶子节点的结果
    • 90.子集II
      • 和40.组合总和II一样的套路
    • 491.递增子序列
      • 用set针对同一父节点本层去重

  • 排列问题
    • 46.全排列
      • 每层都是从0开始搜索而不是startIndex
      • 需要used数组记录path里都放了哪些元素了
    • 47.全排列 II
      • 使用(used[i - 1] == false),即树层去重

  • 重新安排行程(图论额外拓展)
  • 棋盘问题
    • 51. N皇后
      • 了棋盘的宽度就是for循环的长度,递归的深度就是棋盘的高度,这样就可以套进回溯法的模板里了
    • 37. 解数独
      • 本题中棋盘的每一个位置都要放一个数字,并检查数字是否合法,解数独的树形结构要比N皇后更宽更深

  • 性能分析

    • 子集问题分析:

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

      N皇后问题分析:

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

      解数独问题分析:

      • 时间复杂度:O(9m) , m是’.'的数目。
      • 空间复杂度:O(n2),递归的深度是n2
代码随想录算法训练营是一个优质的学习和讨论平台,提供了丰富的算法训练内容和讨论交流机会。在训练营中,学员们可以通过观看视频讲来学习算法知识,并根据讲内容进行刷题练习。此外,训练营还提供了刷题建议,例如先看视频、了自己所使用的编程语言、使用日志等方法来提高刷题效果和语言掌握程度。 训练营中的讨论内容非常丰富,涵盖了各种算法知识点和题方法。例如,在第14训练营中,讲了二叉树的理论基础、递归遍历、迭代遍历和统一遍历的内容。此外,在讨论中还分享了相关的博客文章和配图,帮助学员更好地理和掌握二叉树的遍历方法。 训练营还提供了每日的讨论知识点,例如在第15的讨论中,介绍了层序遍历的方法和使用队列来模拟一层一层遍历的效果。在第16的讨论中,重点讨论了如何进行调试(debug)的方法,认为掌握调试技巧可以帮助学员更好地决问题和写出正确的算法代码。 总之,代码随想录算法训练营是一个提供优质学习和讨论环境的平台,可以帮助学员系统地学习算法知识,并提供了丰富的讨论内容和刷题建议来提高算法编程能力。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [代码随想录算法训练营每日精华](https://blog.csdn.net/weixin_38556197/article/details/128462133)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值