代码随想录Day26-回溯:力扣第46m、47m、332h题

46m. 全排列

题目链接
代码随想录文章讲解链接

方法一:回溯搜索

用时:16m36s

思路

在回溯搜索的基础上设置一个哈希集合hashSet,用于去重,每次遍历的时候不能遍历重复的元素。哈希集合可以用unordered_set或者数组实现。

  • 时间复杂度: O ( n ⋅ n ! ) O(n \cdot n!) O(nn!),全排列数有 n ! n! n!种排列方式,将结果保存至res需要 O ( n ) O(n) O(n)时间复杂度。
  • 空间复杂度: O ( n ) O(n) O(n)
C++代码
class Solution {
private:
    vector<vector<int>> res;
    vector<int> path;
    unordered_set<int> hashSet;

    void backTracking(vector<int>& nums) {
        if (path.size() == nums.size()) {  // 如果全部数组全部遍历完了,则将结果保存并终止递归
            res.push_back(path);
            return;
        }
        for (int i = 0; i < nums.size(); ++i) {
            if (hashSet.find(nums[i]) == hashSet.end()) {  // 只有当遍历的元素是非重复元素是才加入path
                path.push_back(nums[i]);
                hashSet.insert(nums[i]);
                backTracking(nums);  // 递归
                path.pop_back();  // 回溯
                hashSet.erase(nums[i]);
            }
        }
    }

public:
    vector<vector<int>> permute(vector<int>& nums) {
        backTracking(nums);
        return res;
    }
};

看完讲解的思考

无。

代码实现遇到的问题

无。


47m. 全排列 II

题目链接
代码随想录文章讲解链接

方法一:回溯

用时:7m19s

思路

在上一题的基础上再使用一个哈希表used进行去重,与hashSet不同,used是在每层递归中创建的,用于for循环时不要遍历当前for循环已经遍历过的元素。

  • 时间复杂度: O ( n ⋅ n ! ) O(n \cdot n!) O(nn!)
  • 空间复杂度: O ( n 2 ) O(n^2) O(n2)。unordered_set在每次递归中总共需要的空间是 O ( ( n − 1 ) + ( n − 2 ) + . . . + 1 ) = O ( n 2 ) O((n-1)+(n-2)+...+1)=O(n^2) O((n1)+(n2)+...+1)=O(n2)
C++代码
class Solution {
private:
    vector<vector<int>> res;
    vector<int> path;
    bool hashSet[8] = {false};

    void backTracking(vector<int>& nums) {
        if (path.size() == nums.size()) {
            res.push_back(path);
            return;
        }
        unordered_set<int> used;
        for (int i = 0; i < nums.size(); ++i) {
            if (!hashSet[i] && used.find(nums[i]) == used.end()) {
                path.push_back(nums[i]);
                hashSet[i] = true;
                used.insert(nums[i]);
                backTracking(nums);
                path.pop_back();
                hashSet[i] = false;
            }
        }
    }
    
public:
    vector<vector<int>> permuteUnique(vector<int>& nums) {
        backTracking(nums);
        return res;
    }
};

方法二:排序+回溯

用时:8m27s

思路

方法一每层递归都要创建一个哈希集合,空间复杂度高。
可以通过先排序来去重。首先将数组排序,然后在for循环遍历过程中,如果当前元素等于上一个元素,并且上一个元素并不是已经选取在path中的元素,那么就跳过。

  • 时间复杂度: O ( n ⋅ n ! ) O(n \cdot n!) O(nn!)
  • 空间复杂度: O ( n ) O(n) O(n)
C++代码
class Solution {
private:
    vector<vector<int>> res;
    vector<int> path;
    bool hashSet[8] = {false};

    void backTracking(vector<int>& nums) {
        if (path.size() == nums.size()) {
            res.push_back(path);
            return;
        }
        for (int i = 0; i < nums.size(); ++i) {
            // 条件一 !hashSet[i] :当前位置的元素并不在path中,防止重复选取某个位置的元素
            // 条件二 i == 0 || nums[i] != nums[i - 1] || hashSet[i - 1] :
            //     当前位置是第一个,或者当前位置的元素不等于上一个位置的元素,或者上一个位置的元素是被选取的
            // 条件一是为了不要选取到重复位置的元素,因为每个位置的元素只能用一次
            // 条件二是为了不要有重复的全排列结果
            if (!hashSet[i] && (i == 0 || nums[i] != nums[i - 1] || hashSet[i - 1])) {
                path.push_back(nums[i]);
                hashSet[i] = true;
                backTracking(nums);
                path.pop_back();
                hashSet[i] = false;
            }
        }
    }

public:
    vector<vector<int>> permuteUnique(vector<int>& nums) {
        sort(nums.begin(), nums.end());
        backTracking(nums);
        return res;
    }
};

看完讲解的思考

回溯算法中如果要去重就要想到先排序!!!

代码实现遇到的问题

无。


332h. 重新安排行程

题目链接
代码随想录文章讲解链接

方法一:回溯

用时:1h23m32s

思路

首先遍历航班列表,记录每个出发机场都可以去到哪个机场,以及有几趟航班,这里选择的数据结构是unordered_map<string, map<string, int>>,表示<出发机场, <到达机场, 航班次数>>,unordered_map出发机场的键值用map数据结构的原因是,我们最终的答案需要的是字典排序最小的路径,而map会自动根据键进行排序,这样在遍历的过程我们就是先遍历字典序小的机场。

递归三部曲:

  • 返回值及参数:
    本题的答案路径是唯一的,所以我们不需要用额外的数组来记录路径,只用一个数组来记录当前的路径即可,由于答案唯一,当我们找到符合要求的路径时,我们就直接结束所有递归,返回res即可。因为需要判断不同的递归路径是否找到有效路径,递归函数的返回值我们设置为bool。当递归函数返回true时,表明当前递归路径已经找到了有效路径,不用继续循环和递归了;当递归函数返回false时,表明当前递归路径还没找到有效路径,需要继续循环和递归。
    当前路径res可以作为参数传递,也可以直接放在类的成员属性中。

  • 递归终止条件:当res的长度等于机票数ticketNum+1时,表明全部地点都已去过,终止递归。

  • 单层搜索逻辑:

    1. 当前所在的机场是res.back(),遍历当前机场能去到的机场:
      for (pair<const string, int>& target : targets[res.back()]) {
                  if (target.second > 0) {...}
      
      由于map会自动根据键进行排序,这样在遍历的过程我们就是先遍历字典序小的机场,所以找到的第一条有效路径也是字典序最小的有效路径。
    2. “飞往”下一个机场进行递归:
      res.push_back(target.first);  // 飞往下一个机场,则路径上添加新机场
      --target.second;  // 此条航线的航班次数减一
      if (backTracking()) return true;
      
      如果在当前递归路径上找到了有效路径,也就是递归函数返回了true,则直接return true。
    3. 如果没有找到有效路径,即递归函数返回了false,则回溯,遍历下一个机场。
    4. 遍历完当前机场的所有航线都没有找到有效路径,则返回false。
  • 时间复杂度: O ( n ) O(n) O(n)

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

C++代码
class Solution {
private:
    unordered_map<string, map<string, int>> targets;  // <出发机场, <到达机场, 航班次数>>
    int ticketNum;
    vector<string> res;
    
    bool backTracking() {
        // 由于答案是唯一的,所以找到符合要求的路径后,直接终止递归并返回当前路径,所以不需要用额外的数组来记录答案
        if (res.size() == ticketNum + 1) return true;
        
        // 由于map会根据key自动排序,所以遍历的过程中,是先遍历字典排序靠前的字符串,那么第一条满足要求的路径就一定是字典排序最小的有效路径
        for (pair<const string, int>& target : targets[res.back()]) {
            if (target.second > 0) {  // 当剩余航班次数大于0
                res.push_back(target.first);  // 飞往下一个地方
                --target.second;  // 此条航线的航班次数减一
                if (backTracking()) return true;  // 递归,如果当前递归路径找到了有效路径,则直接返回true,无需再继续搜索其他路径
                res.pop_back();  // 若没有找到有效路径,则回溯
                ++target.second;
            }
        }
        // 当前机场遍历完全部航线都没能找到有效路径,返回false,继续寻找其他路径
        return false;
    }

public:
    vector<string> findItinerary(vector<vector<string>>& tickets) {
        ticketNum = tickets.size();
        for (const vector<string>& ticket : tickets) ++targets[ticket[0]][ticket[1]];
        res.push_back("JFK");
        backTracking();
        return res;
    }
};

看完讲解的思考

本题回溯的逻辑不难,难的是想到用这样的数据结构来存储信息以及设计这整个代码逻辑。

代码实现遇到的问题

一开始自己写的代码其实已经实现功能了,但是一开始自己想的方法还在纠结于怎么比较当前找到的路径是不是字典序最小的路径,用各种字符串操作之类的,最后时间复杂度太高,力扣上当测试案例数据量较大时就超出时间限制了,没有想到用map自动排序。


最后的碎碎念

今天做了目前接触到的第二道hard,这道hard也没有想象中的难,但还是不能独自AC,而且也搞了好久,回溯专题准备结束噜。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
目描述: 给你两个版本号 version1 和 version2 ,请你比较它们。 版本号由一个或多个修订号组成,各修订号由一个 '.' 连接。每个修订号由多位数字组成,可能包含前导零。每个版本号至少包含一个字符。修订号从左到右编号,下标从0开始,最左边的修订号下标为0 ,下一个修订号下标为1,以此类推。例如,2.5.33 和 0.1 都是有效的版本号。 比较版本号时,请按从左到右的顺序依次比较它们的修订号。比较修订号时,只需比较忽略任何前导零后的整数值。也就是说,修订号1和修订号001相等。如果版本号没有指定某个下标处的修订号,则该修订号视为0。例如,版本1.0 小于版本1.1,因为它们下标为0的修订号相同,而下标为1的修订号分别为0和1,0 < 1。 返回规则如下: 如果 version1 > version2 返回 1, 如果 version1 < version2 返回 -1, 否则返回 0。 示例 1: 输入:version1 = "1.01", version2 = "1.001" 输出:0 解释:忽略前导零,"01" 和 "001" 都表示相同的整数 "1" 示例 2: 输入:version1 = "1.0", version2 = "1.0.0" 输出:0 解释:version1 没有指定下标为 2 的修订号,即视为 "0" 示例 3: 输入:version1 = "0.1", version2 = "1.1" 输出:-1 解释:version1 中下标为 0 的修订号是 0,version2 中下标为 0 的修订号是 1 。0 < 1,所以 version1 < version2 示例 4: 输入:version1 = "1.0.1", version2 = "1" 输出:1 示例 5: 输入:version1 = "7.5.2.4", version2 = "7.5.3" 输出:-1 提示: 1 <= version1.length, version2.length <= 500 version1 和 version2 仅包含数字和 '.' version1 和 version2 都是 有效版本号

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值