代码随想录第二十四天 | Leecode 93. 复原IP地址 、78. 子集、 90.子集II

Leecode 93. 复原IP地址

题目描述

有效 IP 地址 正好由四个整数(每个整数位于 0255 之间组成,且不能含有前导 0),整数之间用 '.' 分隔。

例如:"0.1.2.201""192.168.1.1" 是 有效 IP 地址,但是 "0.011.255.245""192.168.1.312""192.168@1.1" 是 无效 IP 地址。
给定一个只包含数字的字符串 s ,用以表示一个 IP 地址,返回所有可能的有效 IP 地址,这些地址可以通过在 s 中插入 '.' 来形成。你 不能 重新排序或删除 s 中的任何数字。你可以按 任何 顺序返回答案。

  • 示例 1:

输入:s = "25525511135"
输出:["255.255.11.135","255.255.111.35"]

  • 示例 2:

输入:s = "0000"
输出:["0.0.0.0"]

  • 示例 3:

输入:s = "101023"
输出:["1.0.10.23","1.0.102.3","10.1.0.23","10.10.2.3","101.0.2.3"]

回溯法,解题思路与代码

感觉这道题对我来说确实有不小难度,思考了并尝试了很久都没有什么思路,最后是看完了卡哥的讲解才想出来该怎么做的。首先是需要明确本题目标,要在给定的字符串中插入字符'.',同时还需要将所有满足条件的字符串都记录在一个vector中输出。那么首先是对于字符串中插入与删除字符的操作:

  • str.insert(str.begin() + pos, 'char'),在字符串中插入字符使用insert()函数
  • str.erase(pos , length),在字符串中删除指定长度字符,使用erase()函数

随后进一步讨论回溯的思路:

  • 由于本题要求给定合法IP的条件比较繁杂,所以一个自然的想法是使用一个返回布尔类型的函数来判断当前字符串是否合法。而此时又需要明确该函数的输入,如果考虑输入的是整个字符串,那么可能会遇到当前字符串前一半加了'.',而后一半没加'.'的情况,也不能说每次只在已经将三个'.'都插入之后我们再去判断当前字符串是否合法,否则会导致消耗很多时间在构建不合法的IP字符串的过程中。为此我们需要一个能够实时判断当前分割字符串并插入一个'.'的操作是否合法的函数,从而能够在一旦发现本次分割不合法后迅速剪枝。那么我们理想的情况是输入字符串的一个区间,即本次分割后所产生的长度不大于3的数字字符是否小于255且不能以0开头。这里输入的参数采用:(const string& s, int left, int right)的方式,表示本次判断字符串s中的左闭右闭区间[left, right]是否合法。
  • 在能够判断本次分割是否合法之后,就可以按照回溯的框架来写代码了:
    • 如果当前已经分割完成(已经插入过3个点,故需要一个变量pointNum来记录已经插入过几个点),并且最后一次分割合法,那么将整个字符串存入结果中
    • (如果还未分割完成)则在for循环中用i遍历分割长度,此时需要一个变量start来记录开始分割的位置
      • 在循环中每一次判断不合法则直接返回,如果合法则继续插入、递归、回溯

那么根据上面的思路不难写出下面代码:

class Solution {
public:
    vector<string> result;

    void backTracking(string& s, int start, int pointNum){
        if(pointNum == 3 && isValid(s, start, s.size()-1)){
            result.push_back(s);
            return;
        }

        for(int j = start; j < s.size(); j++){
            if(j - start > 2) break; // 剪枝,如果长度大于3,则直接跳出循环
            if(!isValid(s, start, j)) return;
            s.insert(s.begin()+j +1, '.'); // 插入一个.
            backTracking(s, j+2, pointNum+1); // 递归
            s.erase(j+1, 1); // 回溯,删掉插入的.
        }
    }

    bool isValid(const string& s, int left, int right){
        if(left > right) return false; // 输入的left要小于等于right
        if(right - left > 2) return false; // 检查长度不能超过3
        if(s[left] == '0' && right != left) return false; // 检查不能有前导0
        if(right - left == 2){ // 检查数值要小于等于255
            if(s[left] > '2') return false;
            if(s[left] == '2' && s[left+1] > '5') return false;
            if(s[left] == '2' && s[left+1] == '5' && s[left+2] > '5') return false;
        } 
        return true; 
    }

    vector<string> restoreIpAddresses(string s) {
        backTracking(s, 0 , 0);
        return result;
    }
};

Leecode 78. 子集

题目描述

给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。

解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。

  • 示例 1:

输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]

  • 示例 2:

输入:nums = [0]
输出:[[],[0]]

迭代法求解

虽然这部分学习的都是回溯,但是本题我最先想出来的解法却是使用迭代法来求解。

思考如何构造一个集合中的子集且不重复,首先一开始有一个初始的空集,存放到一个用于存放子集的结果族中(族,表示是由集合组成的集合),随后开始逐个选取集合中的元素(记作元素i)。每次取到的元素i都是当前结果族中所有集合中都没有的全新元素,那么此时如果要考虑包含这个新的元素的子集,就将目前结果族中的所有集合都复制一份的同时往里面添加当前这个新元素,再把复制得到的这些集合全部存入子集族中。只需要使用同样的方法,遍历完集合中的所有元素,我们就完成了不重复地构造子集。

具体可以写出代码如下所示:

class Solution {
public:
    vector<vector<int>> subsets(vector<int>& nums) {
        vector<vector<int>> result;
        vector<int> curVec;
        result.push_back(curVec); // 存入空集
        for(int i = 0; i < nums.size(); i++){ // 对于nums中的每一个新的、还没考虑过包含这个元素的子集的元素
            int size = result.size(); // 记录目前已有的子集的个数
            for(int j = 0; j < size; j++){ // 在目前已经存放的所有子集中都加上这个元素再进行一次存放
                vector<int> newVec = result[j]; // 遍历已有的所有子集,都复制一份
                newVec.push_back(nums[i]); // 将复制的子集中新增一个元素
                result.push_back(newVec); // 将得到的新数组存入结果当中
            }
        }
        return result;
    }
};

注意到上面算法中,每一次都将原本的子集族中的元素复制一份并全部添加新元素,就相当于乘以一次2,最终对于\(X\)个元素的集合,就会有\(2^X\)个子集。而这也与数学集合论中幂集的个数相同。

回溯法

本题的回溯法也非常简单,和之前几道组合的题目非常类似,区别在于本题的子集是需要取到树中的每一个节点,而组合是只取树的叶节点。既然是要取到所有节点,那么只需将原本组合中存放节点时的条件判断去掉即可。故我们可以得到如下代码:

class Solution {
public:
    vector<vector<int>> result;
    vector<int> curVec;

    void backTracking(vector<int>&nums, int start){
        result.push_back(curVec); // 每一个节点都进行存放,不需要条件判断
        for(int i = start; i < nums.size(); i++){
            curVec.push_back(nums[i]); // 将当前节点的数存入自己
            backTracking(nums, i+1); // 递归
            curVec.pop_back(); // 回溯
        }
    }

    vector<vector<int>> subsets(vector<int>& nums) {
        backTracking(nums, 0);
        return result;
    }
};

Leecode 90. 子集 II

题目描述

给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的 子集(幂集)。

解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。

  • 示例 1:

输入:nums = [1,2,2]
输出:[[],[1],[1,2],[1,2,2],[2],[2,2]]

  • 示例 2:

输入:nums = [0]
输出:[[],[0]]

使用集合set容器进行回溯

本题与上题的不同在于一开始给定的集合中有重复元素,意味着子集中也可以出现重复元素。但是最终存放到vector<vector>中的子集却不能重复。为此可以考虑先使用一个sec<vector> 来存放所有子集,再将其转换为vector,其余操作均和上一题一致。那么本题可以写作:

class Solution {
public:
    set<vector<int>> result;
    vector<int> curVec;

    void backTracking(vector<int>& nums, int start){
        result.insert(curVec);
        for(int i = start; i < nums.size(); i++){
            curVec.push_back(nums[i]);
            backTracking(nums, i+1);
            curVec.pop_back();
        }
    }

    vector<vector<int>> subsetsWithDup(vector<int>& nums) {
        sort(nums.begin(), nums.end());
        backTracking(nums, 0);
        vector<vector<int>> re(result.begin(), result.end());
        return re;
    }
};

这样的做法和上一题完全一致。只是先用set后再将其转换为vector,从而进行去重。接下来再尝试使用start相关的判断来去重。

使用start判断去重

本题要去重的是值相等时不同组合的情况。即例如下面输入测试案例:

输入:[1, 2, 2];
如果不进行去重判断,则会依次存入:[] -> [1] -> [1, 2] -> [1, 2, 2] -> [1, 2] -> ...注意此时就存入了两个[1,2]

上面例子中出现了两个的[1,2],其中第一个[1,2]中的2来自于原始集合中的第一个2,第二个[1,2]中的2来自于原始集合中的第二个2。为了避免这样的重复结果存入,可以在for循环中加入下面判断进行去重:

            if(i != start && nums[i] == nums[i-1] ) continue; // 当有重复元素出现时,只有当其为第一次出现时才进行保留,否则直接跳过

上面注意到上面判断中,在判断是否重复之前加了一条i != start的判断,原因是为了保证该元素第一次出现时的子集结果能够存入。对应上面列举出的例子,也就是为了确保[1,2,2]子集能够被存入,而不是看到重复元素就直接跳过。

在将上面if判断语句加入for循环后,可以得到代码如下:

class Solution {
public:
    vector<vector<int>> result;
    vector<int> curVec;

    void backTracking(vector<int>& nums, int start){
        result.push_back(curVec);
        for(int i = start; i < nums.size(); i++){
            if(i != start && nums[i] == nums[i-1] ) continue;
            curVec.push_back(nums[i]);
            backTracking(nums, i+1);
            curVec.pop_back();
        }
    }

    vector<vector<int>> subsetsWithDup(vector<int>& nums) {
        sort(nums.begin(), nums.end());
        backTracking(nums, 0);
        return result;
    }
};

这样使用上面代码即可完成去重操作。

今日总结

这第二十四天的打卡实际上晚了两天,原本该在4.18完成的但是拖到了4.20。最近马上有两门考试,所以可能Leecode打卡会不太及时,但是也会尽量检查全部跟上刷完的,

当前力扣已刷88题,再接再励!

posted on 2025-04-19 18:26  JQ_Luke  阅读(331)  评论(0)    收藏  举报