代码随想录DAY24 - 回溯算法 - 08/23

目录

分割回文串

题干

思路和代码

递归法

复原 IP 地址

题干

思路和代码

子集(非重复)

题干

思路和代码

方法一:修改子集大小

方法二:收集树的每个节点

子集Ⅱ(含重复)

题干

思路和代码

方法一:先去重,记录每个元素重复的次数

方法二:先排序,再树层去重

使用 unordered_set 对树层去重

使用 bool 型数组对树层去重

回溯算法难点


分割回文串

题干

题目:给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是回文串。返回 s 所有可能的分割方案。

说明:s 仅由小写英文字母组成。

链接:. - 力扣(LeetCode)

思路和代码

‌‌回文串‌是指一个字符串,其从前往后读和从后往前读是完全相同的。也就是说字符串顺序和逆序都是相同的。那么首先要有一个函数判断字符串是否为回文串。

递归法

递归法的思路是先确定组合的第一个回文串,之后再递归从剩余的字符串中分割回文串。

Q:分隔符是什么?分隔符如何每次都重新遍历?

分隔符其实就是 startIndex,让分隔符从前往后遍历,每次往后移动一个字符,再判断是否是回文串。

  • 递归参数和返回值:参数是字符串 s,需要分割的起始位置 startIndex;无返回值。

  • 递归结束条件:当遍历到整个字符串的末尾时,说明找到了一个组合,此时递归结束返回。

  • 递归顺序:先确定组合的第一个回文串,之后再递归从剩余的字符串中分割回文串,将分割好的回文串插入组合中,若组合已经填满(即字符串遍历完),则回溯到上一层重新分割字符串。

class Solution {
public:
    // 判断是否为回文串,两个指针分别从头和从末尾遍历
    bool isPalindrome(string &s, int start, int end){
        while (start <= end){
            if (s[start] == s[end]){
                start++;
                end--;
            } else{
                return false;
            }
        }
        return true;
    }
    vector<string> composition;
    vector<vector<string>> result;
    // 分割字符串,也是回溯算法的实现
    void splitString(string &s, int startIndex){
        if (startIndex >= s.size()){
            // 分割到最后一个字符,说明字符串已经遍历完毕
            result.push_back(composition);
            return;
        }
        // 用 i 遍历子串的末尾
        // startIndex固定子串的起始位置
        for (int i = startIndex; i < s.size(); ++i) {
            // 如果当前子串是回文串,加入组合中
            if (isPalindrome(s,startIndex,i)){
                string sub = s.substr(startIndex,i-startIndex+1);
                composition.push_back(sub);
            } else{
                // 如果不是回文串,让末尾指针继续往后遍历
                continue;
            }
            // 找出一个当前回文串后,要在后续的字符串中继续分割子串
            splitString(s,i+1); // 后续字符串的起点为当前子串末尾的下一个元素
            composition.pop_back();
        }
    }
    vector<vector<string>> partition(string s) {
        composition.clear();
        result.clear();
        splitString(s,0);
        return result;
    }
};

复原 IP 地址

题干

题目:有效 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 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 中的任何数字。你可以按 任何 顺序返回答案。

链接:. - 力扣(LeetCode)

思路和代码

这道题和分割回文串有共同点,分割回文串需要找满足回文串的子串,而复原 IP 地址需要在字符串中满足IP地址格式的子串。不同点在于这道题不仅要做分割,还需要在分割的子串之间插入小数点 "."。

我们可以用一个整型数组 “ vector<int> ip ” 收集满足 IP 地址的四个整数,等需要插入结果集时,再把整数转化为字符串,并在每个整数之间插入小数点 “.”。

  • 递归参数和返回值:参数是传入的字符串和分割的起始位置;无返回值。

  • 递归结束的条件:

    • 已经收集好四个整数但是字符串却还没遍历完,说明字符串过长,直接返回。

    • 当 startIndex 已经遍历到字符串末尾,且四个整数已收集完毕,则插入结果集,并返回。

  • 递归顺序:先获取当前分割好的子串并转化为整数,判断其是否满足 IP 地址要求,如果不是则直接返回;如果满足,则递归分割剩余的字符串。

Q:为什么在这道题中不满足IP地址要求就直接 return,而不是 continue?

因为题目要求 IP地址 必须是字符串中连续的子串,子串不能是间断的元素,所以直接 return。

class Solution {
public:
    vector<int> ip; // 记录ip地址的四个整数
    string composition;
    vector<string> result;
    void splitString(string &s, int startIndex){
        if (ip.size() == 4 && startIndex != s.size()){
            // 已经收集好四个整数但是字符串却还没遍历完,不满足条件,直接返回
            return;
        }
        if (startIndex >= s.size()){ // 已经遍历完整个字符串,需要将4个整数插入字符串组合中。
            composition.clear(); // 每次插入Ip地址前清空组合
            if (ip.size() == 4){ // 4个整数都收集完才插入结果,否则直接返回
                for (int i = 0; i < ip.size(); ++i) {
                    composition += to_string(ip[i]);
                    composition.push_back('.');
                }
                composition.pop_back(); // 去掉最后一个小数点
                result.push_back(composition);
            }
            return;
        }

        for (int i = startIndex; i < s.size(); ++i) {
            string sub = s.substr(startIndex,i-startIndex+1); // 获取当前分割的子串
            int num = stoi(sub); // 将子串转为整数
            // ... 判断是否满足 IP 地址需求
            if (num >= 0 && num <= 255){ // 整数必须在 0~255范围内
                ip.push_back(num); // 满足要求插入 ip 数组中
            } else{
                return; // 不满足 IP 地址条件的直接返回
            }
            // ...继续分割后续的字符串
            splitString(s,i+1);
            ip.pop_back(); // 回溯,弹出一个整数
            
            if (num == 0) return; // 如果数字为 0,0不可以作为前导,所以不需要再往后循环遍历,直接返回
        }
    }
    vector<string> restoreIpAddresses(string s) {
        if (s.length() < 4 || s.length() > 12){
            // 剪枝,IP地址的长度最小是 4 个整数,最大是 12 个整数
            return {};
        }
        splitString(s,0);
        return result;
    }
};

子集(非重复)

题干

题目:给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。注意,子集包括了空集。

链接:. - 力扣(LeetCode)

思路和代码

方法一:修改子集大小

设集合大小为 k,这道题就是在集合中找出 以 0 ~ k 为子集大小 的组合。

递归方法定义为找大小为 size 的组合,在找子集方法中循环递归不同 size 大小。

class Solution {
public:
    vector<int> composition;
    vector<vector<int>> result;
    void findCompositions(vector<int> &nums, int startIndex, int size){
        if (composition.size() == size){ // 当组合满足子集大小时,插入结果
            result.push_back(composition);
            return;
        }
        for (int i = startIndex; i < nums.size(); ++i) {
            composition.push_back(nums[i]);
            findCompositions(nums,i+1,size); // size 没有取地址,所以不需要回溯
            composition.pop_back();
        }
    }
    vector<vector<int>> subsets(vector<int>& nums) {
        result.push_back({}); // 插入空集
        for (int i = 1; i <= nums.size(); ++i) { // 循环设置不同的子集大小
            findCompositions(nums,0,i);
        }
        return result;
    }
};
方法二:收集树的每个节点

在之前找组合的过程中,我们都是收集树的叶子节点,因为题目很明确地告知组合的要求。而在找子集的过程中,组合的大小没有限制,如果将寻找子集的过程抽象为一棵树,则寻找子集就是寻找树中的每一个结点。

class Solution {
public:
    vector<int> composition;
    vector<vector<int>> result;
    void backTrack(vector<int> &nums, int startIndex){
        // 初始时,composition为空集,所以第一次进来的时候就已经插入了空子集
        result.push_back(composition); // 每层递归的组合都要保存
        if (startIndex >= nums.size()){
            return; // 结束条件其实不加也可以
        }
        for (int i = startIndex; i < nums.size(); ++i) {
            composition.push_back(nums[i]);
            backTrack(nums,i+1);
            composition.pop_back();
        }
    }
    vector<vector<int>> subsets(vector<int>& nums) {
        backTrack(nums,0); // 此时不用特意插入空集
        return result;
    }
};

子集Ⅱ(含重复)

题干

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

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

链接:. - 力扣(LeetCode)

思路和代码

方法一:先去重,记录每个元素重复的次数

用 unordered_map 记录数组中的每个元素以及元素的重复使用次数,用新数组 newNums 存储去重后的数组。

class Solution {
public:
    unordered_map<int,int> numsCount; // 记录不同数字的重复次数
    vector<int> composition;
    vector<vector<int>> result;
    void backTrack(vector<int> &nums, int startIndex){
        if (startIndex >= nums.size()){
            return;
        }
        for (int i = startIndex; i < nums.size(); ++i) {
            composition.push_back(nums[i]);
            result.push_back(composition);
            numsCount[nums[i]]--; // 使用次数减少
            if (numsCount[nums[i]] > 0){
                backTrack(nums,i); // 如果使用次数还有,递归传入自身 i
            } else{
                backTrack(nums,i+1); // 如果使用次数已经为 0,要传入下一个元素 i+1 
            }
            composition.pop_back();
            numsCount[nums[i]]++;
        }
    }
    vector<vector<int>> subsetsWithDup(vector<int>& nums) {
        vector<int> newNums; // 记录去重后的数组
        for (int i : nums) {
            numsCount[i]++; // 记录不同元素重复的次数
            if (numsCount[i] == 1){
                newNums.push_back(i);
            }
        }
        backTrack(newNums,0);
        result.push_back({}); // 插入空集
        return result;
    }
};
方法二:先排序,再树层去重

方法二的思路都是对树层去重(即在同一层 for 循环中去重),实现方法有两种,分别使用集合 set 或使用数组 isUsed 来辨别重复的元素是在树层中还是在树枝中,如果是在树层中,则去重。要注意的是,使用方法前都需要对数组进行排序,让相同的元素排在一起,才方便后续的去重。

Q:为什么递归不用去重?而要对 同一层 for 循环去重?

因为递归实际上改变的是元素在子集中填充的位置,元素值虽相同但位置不同,代表的也是不同的子集。

在同一层 for 循环中遍历的都是子集中的同一个位置,元素值当然要不同!

使用 unordered_set 对树层去重

我们需要对同一层 for 中出现过的重复元素进行去重,那么用一个集合记录在 for 循环中遍历过的元素,如果后续循环又出现了集合记录过的元素,则说明重复,跳过。

class Solution {
public:
    vector<int> composition;
    vector<vector<int>> result;
    void backTrack(vector<int> &nums, int startIndex){
        result.push_back(composition);
        if (startIndex >= nums.size()){
            return; // 递归条件可以不写
        }
        unordered_set<int> uSet; // 记录同一层 for 循环中遍历过的元素
        for (int i = startIndex; i < nums.size(); ++i) {
            if (uSet.find(nums[i]) != uSet.end()) continue; // 之后可能还有别的元素,所以是 continue
            composition.push_back(nums[i]);
            uSet.insert(nums[i]);
            backTrack(nums,i+1);
            composition.pop_back();
        }
    }
    vector<vector<int>> subsetsWithDup(vector<int>& nums) {
        sort(nums.begin(),nums.end()); // 先对数组进行排序,让重复元素相邻排在一起
        backTrack(nums,0);
        return result;
    }
};
使用 bool 型数组对树层去重

用 bool 型数组 isUsed[ ] 记录每个数组中的元素是否有被使用过。如果当前元素和前一个元素相同,且前一个元素没有被使用,说明当前元素和前一个元素不在递归中,而是在同一层 for 循环中,即在树层而非树枝,需要去重,跳过。

class Solution {
public:
    bool isUsed[11]; // 记录数组每个元素的使用状态,默认为 false, 1 <= nums.length <= 10
    vector<int> composition;
    vector<vector<int>> result;
    void backTrack(vector<int> &nums, int startIndex){
        result.push_back(composition);
        if (startIndex >= nums.size()) return;
        for (int i = startIndex; i < nums.size(); ++i) {
            if (i > 0 && nums[i] == nums[i-1] && !isUsed[i-1]){
                // 如果当前元素和前一个元素相同,且前一个元素没有被使用
                // 说明当前元素和前一个元素不在递归中,而是在 for 循环中,即在树层而非树枝,需要去重,跳过
                continue;
            }
            composition.push_back(nums[i]);
            isUsed[i] = true; // 修改当前元素的使用状态
            backTrack(nums,i+1);
            composition.pop_back();
            isUsed[i] = false;
        }
    }
    vector<vector<int>> subsetsWithDup(vector<int>& nums) {
        sort(nums.begin(),nums.end()); // 先对数组进行排序,让相同元素排在一起
        backTrack(nums,0);
        return result;
    }
};

回溯算法难点

  • 要知道 for 循环遍历的是什么?for 循环确定了组合中的什么?

  • 递归改变了什么?

  • 去重是对树枝去重还是对树层去重?去重前是否需要排序?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值