代码随想录算法训练营第25天 | 力扣491.非递减子序列、力扣46.全排列、力扣.全排列II、力扣332.重新安排行程、力扣51.N皇后、力扣37.解数独

目录

LeetCode491.非递减子序列

LeetCode46.全排列

LeetCode47.全排列II

LeetCode332.重新安排行程

LeetCode51.N皇后

LeetCode37.解数独


LeetCode491.非递减子序列

给你一个整数数组 nums ,找出并返回所有该数组中不同的递增子序列,递增子序列中 至少有两个元素 。你可以按 任意顺序 返回答案。

数组中可能含有重复元素,如出现两个整数相等,也可以视作递增序列的一种特殊情况。

思路:这里需要注意,需要去重但是去重逻辑与之前的题目有差别

之前去重的时候需要排序,但是这里不能排序,因为一排序,全部元素都有序了。

这里使用的是set集合,递归的每一层都会有一个set记录本层的使用过的元素,避免重复,同时也要小心一点,如何处理后面一个元素小于前面元素的情况。

    vector<vector<int>> result;//记录最终结果
    vector<int> path;//记录单次结果
    void backtracking(vector<int>& nums, int startIndex){
        if(path.size() >= 2) result.push_back(path);//当path的小至少为2时才能加入result中
        unordered_set<int> st;//每层一个set集合避免重复元素的再次使用
        for(int i = startIndex; i < nums.size(); i ++){
            //这里是对同层元素的去重,同时跳过不满足递增的元素
            if((!path.empty() && nums[i] < path.back()) || st.find(nums[i]) != st.end()) continue;
            st.insert(nums[i]);//每次插入使用过的元素           
            path.push_back(nums[i]);
            backtracking(nums, i + 1);
            path.pop_back();
        }
    }
    vector<vector<int>> findSubsequences(vector<int>& nums) {
        result.clear();
        path.clear();
        backtracking(nums, 0);
        return result;
    }

 但是其实使用set会造成空间和时间的一个消耗,因为插入和更新时会涉及到一些操作,并且这些操作每层都有,所以这里我们可以使用全局数组来代替set,这样能够减轻时间和空间的消耗。

    vector<vector<int>> result;//记录最终结果
    vector<int> path;//记录单次结果
    void backtracking(vector<int>& nums, int startIndex){
        if(path.size() >= 2) result.push_back(path);//当path的小至少为2时才能加入result中
        int record[201] = {0};//每层一个record数组记录元素使用情况
        for(int i = startIndex; i < nums.size(); i ++){
            //这里是对同层元素的去重,同时跳过不满足递增的元素
            if((!path.empty() && nums[i] < path.back()) || record[nums[i] + 100] == 1) continue;
            record[nums[i] + 100] = 1; //每次将使用过的元素置为1           
            path.push_back(nums[i]);
            backtracking(nums, i + 1);
            path.pop_back();
        }
    }
    vector<vector<int>> findSubsequences(vector<int>& nums) {
        result.clear();
        path.clear();
        backtracking(nums, 0);
        return result;
    }

时间复杂度:O(n*2^n)

空间复杂度:O(n)

LeetCode46.全排列

给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。 

思路:排列和组合的最大区别就是排列的元素可以重复,只要顺序不一样,就是一种排列可能。

于是这道求全排列的题目中的for循环就需要每次从0开始,并且需要记录元素使用情况。

下面是使用的vector数组的find函数查找是否加入过元素,其实不太常见,所以不是很推荐。

    vector<vector<int>> result;//记录最终结果
    vector<int> path;//记录单次结果
    void backtracking(vector<int>& nums){
        if(path.size() == nums.size()){//path的大小等于nums的大小时,说明path包含了一个全排列解
            result.push_back(path);
            return;
        }
        for(int i = 0; i < nums.size(); i ++){//注意这里的下标是从0开始的
            //当path不为空且path中出现了已经遍历过的元素后,直接跳过
            if(!path.empty() && find(path.begin(), path.end(), nums[i]) != path.end()) continue;
            path.push_back(nums[i]);
            backtracking(nums);
            path.pop_back();

        }
    }
    vector<vector<int>> permute(vector<int>& nums) {
        result.clear();
        path.clear();
        backtracking(nums);
        return result;
    }

这里推荐的是使用一个全局used数组,进入之前判断一下元素是否用过,用过就跳过,没用过就进入循环。

    vector<vector<int>> result;//记录最终结果
    vector<int> path;//记录单次结果
    void backtracking(vector<int>& nums, vector<bool>& used){
        if(path.size() == nums.size()){//path的大小等于nums的大小时,说明path包含了一个全排列解
            result.push_back(path);
            return;
        }
        for(int i = 0; i < nums.size(); i ++){//注意这里的下标是从0开始的
            //used中记录了元素使用情况
            if(used[i] == true) continue;
            used[i] = true;
            path.push_back(nums[i]);
            backtracking(nums, used);
            path.pop_back();//开始回溯
            used[i] = false;
        }
    }
    vector<vector<int>> permute(vector<int>& nums) {
        result.clear();
        path.clear();
        vector<bool> used(nums.size(), false);//初始化used为nums大小,并且初始值为false
        backtracking(nums, used);
        return result;
    }

时间复杂度:O(n!)

空间复杂度:O(n)

LeetCode47.全排列II

给定一个可包含重复数字的序列 nums按任意顺序 返回所有不重复的全排列。 

思路:这道题相较于上面的题目就是多了重复的数字,其实跟之前的组合去重思路一样的,先排序,再使用used数组判断同层元素是否重复,重复就跳过,不重复就入循环。

    vector<vector<int>> result;//记录最终结果
    vector<int> path;//记录单次结果
    void backtracking(vector<int>& nums, vector<bool>& used){
        if(path.size() == nums.size()){//path的大小满足了一个全排列的大小
            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) continue;
            //避免一次全排列中重复使用相同元素
            if(used[i] == true) continue;
            used[i] = true;
            path.push_back(nums[i]);
            backtracking(nums, used);
            path.pop_back();
            used[i] = false;
        }
    }
    vector<vector<int>> permuteUnique(vector<int>& nums) {
        result.clear();
        path.clear();
        sort(nums.begin(), nums.end());//去重需要排序
        vector<bool> used(nums.size(), false);
        backtracking(nums, used);
        return result;
    }

时间复杂度:O(n! )

空间复杂度:O(n)

LeetCode332.重新安排行程

给你一份航线列表 tickets ,其中 tickets[i] = [fromi, toi] 表示飞机出发和降落的机场地点。请你对该行程进行重新规划排序。

所有这些机票都属于一个从 JFK(肯尼迪国际机场)出发的先生,所以该行程必须从 JFK 开始。如果存在多种有效的行程,请你按字典排序返回最小的行程组合。

  • 例如,行程 ["JFK", "LGA"]["JFK", "LGB"] 相比就更小,排序更靠前。

假定所有机票至少存在一种合理的行程。且所有的机票 必须都用一次 且 只能用一次。

思路:这道题如果只是求所所有的可能行程的话,是比较简单的,跟前面的题目差不多。

但是这里需要对最终路径挑选,因为一旦最终路径不止一条,就需要按字典顺序找小的那条路径,而如果先统计所有可能路径,再来挑选,大概率会超时。

所以怎么办呢?

需要在处理之前就使得待处理元素有序!

我们需要找起始站,一个起始站可能对应多个终点站,并且稍有不慎就可能会死循环,比如两个站相互往来。

所以可以选择一个unordered_map存放起始站和终点站,unordered_map是无序的,终点站需要有序,这样才能在递归中找到的第一条就是解。所以加上为了避免死循环这个要求,终点站可以采用一个map数组,map是自动有序的,map中存放的是终点站,以及起点到终点的次数,这样当为0的时候也就是说走完了,不会再进入,从而避免了死循环。

所以前期准备的时候就需要将票的信息统计到unordered_map中,这样方便后续操作。

    vector<string> result;//存放最终结果
    unordered_map<string, map<string, int>> targets;//记录各航班的起始点以及终点和到达终点的次数
    bool backtracking(int ticketNum, vector<string>& result){
        if(result.size() == ticketNum + 1){//票有ticktNum张,那么当result的大小比ticketNum多1时则可以返回了
            return true;//所有航班均已添加完成,因为本身就是有序的了,所以找到一条就可以返回了
        }
        for(pair<const string, int>& target: targets[result[result.size() - 1]]){
            //取result的最后一个元素,在targets中找到它的下一个终点
            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) {
        result.clear();
        for(auto item : tickets){
            targets[item[0]][item[1]] ++;//将tickets的情况记录在targets中
        }
        result.push_back("JFK");//加入起始站点
        backtracking(tickets.size(), result);
        return result;
    }

LeetCode51.N皇后

按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。

n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。

给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。

每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 'Q''.' 分别代表了皇后和空位。

思路:N皇后是比较经典的有关回溯的问题,同时也是一个棋盘问题。

关键就在于如何判断其放皇后的位置的合法性!

如果实在没头绪,建议先看代码,看着看着就理解了,会发现原来这么简单。

    vector<vector<string>> result;//记录最终结果
    void backtracking(int n, int row, vector<string>& chessboard){
        if(row == n){//当path的大小和n相等时,说明已经将N个皇后放好位置了
            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;
            }
        }
        //检查斜线,左上角
        for(int i = row - 1, j = col - 1; i >= 0 && j >= 0; i --, j --){
            if(chessboard[i][j] == 'Q'){
                return false;
            }
        }
        //检查斜线,右上角
        for(int i = row - 1, j = col + 1; i >= 0 && j < n; i --, j ++){
            if(chessboard[i][j] == 'Q'){
                return false;
            }
        }
        return true;
        //这里没有检查列,避免同行是因为本身在进入验证的时候,取的是某一行的一个位置来验证,尝试将Q放入,本身这一行也最多只有这一个元素,不会出现同行情况,所以没必要检查
    }
    vector<vector<string>> solveNQueens(int n) {
        result.clear();
        vector<string> chessboard(n, string(n, '.'));//初始化棋盘都是'.'
        backtracking(n, 0, chessboard);
        return result;
    }

LeetCode37.解数独

编写一个程序,通过填充空格来解决数独问题。

数独的解法需 遵循如下规则

  1. 数字 1-9 在每一行只能出现一次。
  2. 数字 1-9 在每一列只能出现一次。
  3. 数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。(请参考示例图)

数独部分空格内已填入了数字,空白格用 '.' 表示。

思路:这也是一道棋盘问题,比上面的N皇后还要难一点,N皇后是每次循环遍历一层,递归遍历一列,这样一层一列就能找到位置是否合法。

但是这里的数读问题是需要遍历每个位置,然后需要判断在该位置填上1~9的元素是否合法,也是涉及到合理性的判断。

当然,这里不需要另外写返回结束语句,因为当将每个空都填充完整,也就是找到了解后,会直接返回true;当然,如果说不存在解,也不会有无限递归的情况,因为如果1~9放在某个位置都不合适,会直接返回false,跳出一层递归,回溯,进行下面的操作。

    bool backtracking(vector<vector<char>>& board){
        //这里不需要写结束返回语句,当填充完成后能够自动返回true
        for(int row = 0; row < 9 ; row ++){
            for(int col = 0; col < 9; col ++){
                if(board[row][col] == '.'){
                    for(char k = '1'; k <= '9'; k ++){//检查将1-9放入棋盘,是否合理
                        if(isValid(row, col, k, board)){
                            board[row][col] = k;
                            if(backtracking(board)) return true;
                            board[row][col] = '.';
                        }
                    }
                    return false;//如果说9个元素都试过一遍都没办法加入,那就直接返回false
                }
            }
        }
        return true;
    }
    bool isValid(int row, int col, char k, vector<vector<char>>& board){
        //检查列,确保行不会重复
        for(int i = 0; i < 9; i ++){
            if(board[row][i] == k){
                return false;
            }
        }
        //检查行,确保列不会重复
        for(int j = 0; j < 9; j ++){
            if(board[j][col] == k){
                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] == k){
                    return false;
                }
            }
        }
        return true;
    }
    void solveSudoku(vector<vector<char>>& board) {
        backtracking(board);
    }

 整体来说有关棋盘的问题还是比较复杂的,建议直接看代码,看着看着可能就豁然开朗了!

感谢你的阅读,希望我的文章能够给你帮助,如果有帮助,麻烦点赞加收藏,或者点点关注,非常感谢。

如果有什么问题欢迎评论区讨论!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值