首页 > 编程语言 >代码随想录DAY24 - 回溯算法 - 08/23

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

时间:2024-08-25 16:23:58浏览次数:10  
标签:nums DAY24 23 随想录 back startIndex vector composition size

目录

分割回文串

题干

思路和代码

递归法

复原 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 循环确定了组合中的什么?

  • 递归改变了什么?

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

标签:nums,DAY24,23,随想录,back,startIndex,vector,composition,size
From: https://blog.csdn.net/chan_lay/article/details/141527306

相关文章

  • 代码随想录DAY25 - 回溯算法 - 08/24
    目录非递减子序列题干思路和代码递归法递归优化全排列题干思路和代码递归法全排列Ⅱ题干思路和代码方法一:用集合set去重方法二:先排序,再用数组去重非递减子序列题干题目:给你一个整数数组nums,找出并返回所有该数组中不同的递增子序列,递增子序列中至少有......
  • CSP-J 2023 初赛试题解析(第三部分:完善程序(1-2))
    第一题补全后完整代码:#include<iostream>#include<vector>usingnamespacestd;intfind_missing(vector<int>&nums){intleft=0,right=nums.size()-1;while(left<right){intmid=left+(right-left)/2;if(nums[mi......
  • 历年CSP-J初赛真题解析 | 2015年CSP-J初赛阅读程序(23-26)
    学习C++从娃娃抓起!记录下CSP-J备考学习过程中的题目,记录每一个瞬间。附上汇总贴:历年CSP-J初赛真题解析|汇总_热爱编程的通信人的博客-CSDN博客#include<iostream>usingnamespacestd;intmain(){inta,b,c;a=1;b=2;c=3;if(a>b){......
  • 8.23 PTA实验5-8 使用函数求圆台体积
    实验5-8使用函数求圆台体积本题要求实现函数求圆台体积,定义并调用函数volume_tc(r_lower,r_upper,h)计算下底半径为r_lower、上底半径为r_upper、高度为h的圆台的体积,函数类型是double。函数接口定义:doublevolume_tc(doubler_lower,doubler_upper,doubleh);其中r_......
  • C++ //练习 19.23 为你的Token类添加移动构造函数和移动赋值运算符。
    C++Primer(第5版)练习19.23练习19.23为你的Token类添加移动构造函数和移动赋值运算符。环境:LinuxUbuntu(云服务器)工具:vim 代码块classToken{ public: Token():tok(INT),ival(0){} Token(constToken&t):tok(t.tok){copyUnion(t);} Token&operator=(......
  • 2023-2024年最值得选的Java毕业设计选题大全:500个热门选题推荐✅
    一、前言......
  • 数据库系统 第23节 并发控制
    并发控制是数据库管理系统中的一个重要概念,它确保在多个用户或事务同时访问和修改数据时,数据的完整性和一致性得到维护。下面是对您提到的几种并发控制技术的详细解释和例子:锁(Locks)锁是最基本的并发控制机制之一。它通过在数据上放置锁来防止多个事务同时修改同一数据......
  • CSP-2023游寄
    反正考炸了。反正第一次参加。反正还有一年。心态好点了还是来写一篇游记吧。Day-7:波波:选几个人停课啊,没选到的也没关系,就是来试试水,折腾一下你们的文化课。波波(念名单):【】【】(同级机房大佬),【】【】【】【】(九年级大佬们),【】。(我自己)。意料之外,所以停了七天文化课,备战csp。......
  • 代码随想录第15天,110.平衡二叉树,257. 二叉树的所有路径, 404.左叶子之和, 222.完全二叉
    110.平衡二叉树//平衡二叉树,理解稍微有点难度#include<iostream>#include<algorithm>//Forstd::absandstd::maxfunctionsstructTreeNode{intval;TreeNode*left;TreeNode*right;TreeNode(intx):val(x),left(nullptr),right(nullptr......
  • 代码随想录第16天:513.找树左下角的值、112. 路径总和、106.从中序与后序遍历序列构造
    513.找树左下角的值,层序遍历//找树左下角的值,用层序遍历很容易实现#include<iostream>#include<queue>structTreeNode{intval;TreeNode*left;TreeNode*right;TreeNode(intx):val(x),left(nullptr),right(nullptr){}};//找到最底层......