代码随想录算法训练营day25

代码随想录算法训练营

---day23

文章目录

  • 代码随想录算法训练营
  • 前言
  • [一、491. 非递减子序列](#一、491. 非递减子序列)
  • [二、46. 全排列](#二、46. 全排列)
  • [三、47.全排列 II](#三、47.全排列 II)
  • 四、332.重新安排行程
  • [五、51. N皇后(先占个坑,还没做)](#五、51. N皇后(先占个坑,还没做))
  • [六、37. 解数独(先占个坑,还没做)](#六、37. 解数独(先占个坑,还没做))
  • 总结

前言

今天是算法营的第25天,希望自己能够坚持下来!

今日任务:

● 491. 非递减子序列

● 46. 全排列

● 47. 全排列 II

● 332. 重新安排行程


一、491. 非递减子序列

题目链接
文章讲解
视频讲解

这道题因为要求的是递增子序列,而且数组中有重复元素,需要去重,但是如果用之前的used数组的方法,是需要对数组排列的,那么就会改变了原来的顺序且每个子序列都是递增了,结果就会多了本来没有的递增子序列出来。

所以这道题改用set来去重。

思路:

  1. 递归函数的参数:本题求子序列,很明显一个元素不能重复使用,所以需要startIndex
  2. 终止条件:本题收集结果要求递增子序列,所以大小至少为2,path.size() > 1收集结果。
  3. 单层递归的逻辑:需要递增,所以当前元素小于前一个元素或者set集合里已经用过了就跳过。
    (unordered_set uset; 是记录本层元素是否重复使用,新的递归uset都会重新定义(清空),所以要知道uset只负责本层!)
cpp 复制代码
class Solution {
public:
    vector<int> path;
    vector<vector<int>> result;

    void backtracking(vector<int>& nums, int startIndex) {
        if (path.size() > 1) result.push_back(path);

        unordered_set<int> uset;  // 使用set对本层元素进行去重
        for (int i = startIndex; i < nums.size(); i++) {
            if ((!path.empty() && nums[i] < path.back()) || uset.find(nums[i]) != uset.end()) continue;
            path.push_back(nums[i]);
            uset.insert(nums[i]); // 记录这个元素在本层用过了,本层后面不能再用了
            backtracking(nums, i + 1);
            path.pop_back();
        }
    }

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

优化

unordered_set需要做哈希映射,相对比较耗时。而题目中,数值范围[-100,100],范围比较小,所以可以使用数组来代替set。用数组来做哈希,效率就高了很多。

代码如下:

cpp 复制代码
class Solution {
public:
    vector<int> path;
    vector<vector<int>> result;

    void backtracking(vector<int>& nums, int startIndex) {
        if (path.size() > 1) result.push_back(path);

        int used[201] = {0};  // 这里使用数组来进行去重操作,题目说数值范围[-100, 100]
        for (int i = startIndex; i < nums.size(); i++) {
            if ((!path.empty() && nums[i] < path.back()) || used[nums[i] + 100] == 1) continue;
            path.push_back(nums[i]);
            used[nums[i] + 100] = 1; // 记录这个元素在本层用过了,本层后面不能再用了
            backtracking(nums, i + 1);
            path.pop_back();
        }
    }

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

二、46. 全排列

题目链接
文章讲解
视频讲解

排列跟组合的区别:

组合是没有顺序的所以[1,2]和[2,1]是同一个组合,但是对于排列来说,排列是有顺序的,[1,2]和[2,1]两个不同的排列。所以for循环都需要从0开始,就不需要startIndex了。

思路:

  1. 递归函数参数:对于排列来说,一个结果里面没有多个同样的元素。我们要用一个used数组来进行树枝去重。
  2. 终止条件:当收集元素的数组path的大小达到和nums数组一样大的时候,说明找到了一个全排列。
  3. 单层处理逻辑:每次都要从头开始搜索,用used数组记录此时path里都有哪些元素使用了,一个排列里一个元素只能使用一次。

代码如下:

cpp 复制代码
class Solution {
public:
    vector<int> path;
    vector<vector<int>> result;

    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 (used[i] == true) continue; // path里已经收录的元素,直接跳过
            path.push_back(nums[i]);
            used[i] = true;
            backtracking(nums, used);
            path.pop_back();
            used[i] = false;
        }
    }

    vector<vector<int>> permute(vector<int>& nums) {
        path.clear();
        result.clear();
        vector<bool> used (nums.size(), false);
        backtracking(nums, used);
        return result;
    }
};

三、47.全排列 II

题目链接
文章讲解
视频讲解

这道题多了重复元素的条件,所以需要树层去重。跟组合问题和子集问题一样,也是用used数组来完成。

思路:

跟46. 全排列一样,只是多了树层去重的判断。

cpp 复制代码
class Solution {
public:
    vector<int> path;
    vector<vector<int>> result;

    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) continue; //树层去重,不重复取前一个分支已经用过的元素
            if (used[i] == true) continue;

            path.push_back(nums[i]);
            used[i] = true;
            backtracking(nums, used);
            path.pop_back();
            used[i] = false;
        }
    }

    vector<vector<int>> permuteUnique(vector<int>& nums) {
        path.clear();
        result.clear();
        vector<bool> used (nums.size(), 0);
        sort(nums.begin(), nums.end()); //使用used来去重需要排序
        backtracking(nums, used);
        return result;
    }
};

四、332.重新安排行程

题目链接
文章讲解

思路:

首先需要找到一个合适的方式去存放机票,因为机票起点和终点是会重复的,所以需要将起点,终点,次数建立联系。

使用unordered_map<string,map<string,int>> targets来存放,

相当于unordered_map<起点,map<终点,终点次数>>,

那么终点次数 = targets[起点][终点]。

因为起点一定是"JFK",所以可以用for循环遍历以"JFK"为起点的map<终点,终点次数>,递归去存储路线并寻找以当前终点为起点,下一个终点。

用终点次数--来表示已经机票已经用过了。回溯的时候终点次数++。

cpp 复制代码
class Solution {
public:
    unordered_map<string,map<string,int>> targets;

    bool backtracking(int ticketNum, vector<string>& result) {
        if (result.size() == ticketNum + 1) return true; //最终地点的个数是航班数+1

        string last = result.back();
        for (auto& target:targets[last]) { //这里需要引用,不然只是修改到了target.second的拷贝值
            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();

        // 构建出发机场到到达机场的映射关系,并记录航班次数
        //使用&避免对ticket进行拷贝,并且用const表示只遍历,不修改值
        for (const vector<string>& ticket:tickets) {  
            targets[ticket[0]][ticket[1]]++; //targets[起点][终点] = 次数
        }

        vector<string> result;
        result.push_back("JFK"); //起始机场是"JFK"
        if (!backtracking(tickets.size(), result)) result.clear(); // 如果没有找到有效行程,则清空结果

        return result;
    }
};

五、51. N皇后(先占个坑,还没做)

题目链接
文章讲解
视频讲解

cpp 复制代码
class Solution {
private:
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 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();
        std::vector<std::string> chessboard(n, std::string(n, '.'));
        backtracking(n, 0, chessboard);
        return result;
    }
};

六、37. 解数独(先占个坑,还没做)

题目链接
文章讲解
视频讲解

cpp 复制代码
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] == '.') {
                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.需要用used数组去重的时候,要看清楚题目有没有顺序要求,有要求的情况下不能用used数组,只能用set。

2.在数据范围不大的情况下,可以用数组来代替set,效率会更高。

2.排列问题不需要sertIndex,但是需要用used来进行树枝去重。

后面三道题,做了一道已经汗流浃背了,之后二刷再做吧。

明天继续加油!

相关推荐
涛ing2 分钟前
10. C语言 函数详解
linux·c语言·开发语言·c++·vscode·ubuntu·vim
float_六七4 分钟前
C/C++头文件uitility
c语言·c++
执着的小火车16 分钟前
【2024华为OD-E卷-100分-boss的收入】(题目+思路+Java&C++&Python解析)
数据结构·算法·华为od·华为·排序算法
終不似少年遊*18 分钟前
机器学习模型评估指标
人工智能·算法·机器学习·回归·模型评价
剁椒排骨24 分钟前
冒泡排序(C语言)
c语言·算法·排序算法·算法与结构
苹果41 分钟前
C++二十三种设计模式之解释器模式
c++·设计模式·解释器模式
code04号1 小时前
图论:三维搜索
算法·图论
code04号1 小时前
图论:Tarjan算法的使用,找连通分量、割点、桥
算法·图论
ExRoc1 小时前
蓝桥杯真题 - 魔法阵 - 题解
c++·算法·蓝桥杯
Flash.kkl2 小时前
C++红黑树封装map和set
开发语言·c++