C++学习笔记-回溯算法(2)

文章详细介绍了回溯算法在解决9.分割回文串、10.复原IP地址、11.子集问题、13.子集II、14.递增子序列、15.全排列及16.全排列II等编程问题中的应用。通过递归和剪枝策略,解决组合、分割、子集和排列问题,同时讨论了去重的方法,如使用unordered_set和数组标记已使用元素。
摘要由CSDN通过智能技术生成

资料来源:代码随想录

9.分割回文串 131

本题有如下几个难点:

  • 切割问题其实类似组合问题(也可以画树形结构)
  • 如何模拟那些切割线(startIndex)
  • 切割问题中递归如何终止(startIndex到size了)
  • 在递归循环中如何截取子串
  • 如何判断回文(双指针法)

切割问题和组合问题的思想是类似的,组合问题中选取哪个元素,切割问题中就是从哪个元素开始切割。例如aab,组合问题中选第一个a,切割问题中就是从第一个a后面切开,选第二个a就是从第二个a后面切开。

因为是在一个集合中切割,相当于从一个集合中选取元素进行组合,所以也要用到startIndex来避免切到重复的元素。在组合问题中,startIndex代表从这个元素开始往后进行选取,在切割问题中就是从这个元素开始切割。

切割出来的子串应该如何表示呢?[startIndex,i]。这是因为每一层的startIndex是一样的,是上一层确定了的切割线,但从左往右i是一直在递增的,是这一层的切割线,所以从startIndex到i就是切出来的子串。

class Solution {
private:
    vector<vector<string>> result;  //结果集
    vector<string> path;  //最终切割好后的结果也在叶子节点收集,所以一个结果还是相当于一条路径

    void backtracking(const string& s, int startIndex)
    {
        //终止条件:当startIndex>s的大小时,说明这一条路径已经切到最后了
        if(startIndex>=s.size())
        {
            result.push_back(path);  //这里没有判断是否是回文子串就直接把结果存起来,是因为是否是回文子串会在单层递归逻辑中判断
            return;
        }
        
        //单层递归
        for(int i=startIndex; i<s.size(); i++)
        {
            //判断切割出来的[startIndex,i]是否是回文子串
            if(isPalindrome(s,startIndex,i)) //是回文子串
            {
                string str=s.substr(startIndex,i-startIndex+1);  //把[startIndex,i]从s中取出来
                path.push_back(str);
            }
            else  //不是,则直接跳过
            {
                continue;
            }

            backtracking(s,i+1);  //下一层不能和这一层重复切割
            path.pop_back();  //回溯
        }
    }

    //判断是否是回文子串:双指针法,一个从前向后,一个从后向前,如果两个指针指向的元素始终相同,说明是回文子串
    //这个函数一定是写在递归函数外面的
    bool isPalindrome(const string& s, int start, int end)
        {
            for(int i=start,j=end; i<=j; i++,j--)
            {
                if(s[i]!=s[j])
                {
                    return false;  //前后不相等,则不是
                }
            }
            return true;
        }
        
public:
    vector<vector<string>> partition(string s) {
        result.clear();
        path.clear();
        backtracking(s,0);
        return result;
    }
};

10.复原IP地址 93

往字符串中间插入点,也是相当于在分割字符串,是在处理输入的源字符串,最后把所有符合要求的加入字符串类型数组。

本题不是组成新的结果,而是修改传入的字符串!

分割过程:

 解释num=num*10+(s[i]-'0'):

class Solution {
private:
    vector<string> result;  //把所有可能结果放进一个字符串类型数组

    void backtracking(string& s, int startIndex, int pointNum)  //需要避免上下层之间重复切割;需要记录插入.的数量
    //注意,这里不能是const string&s这样引用,下面s.insert等迭代器需要更改传入的字符串,所以应该用复制的方式传入
    {
        //终止条件:最多分成四段,所以最多插入三个.就应该结束了
        if(pointNum==3)
        {
            if(isValid(s,startIndex,s.size()-1))  //判断最后一段是否合法,是的话把处理好的字符串s存进结果
            {
                result.push_back(s);
            }
            return;  //终止迭代
        }

        //单层递归
        for(int i=startIndex; i<s.size(); i++)  //这是开始截取子串了,步骤:截取子串-判断子串是否合法-插入点.
        {
            if(isValid(s,startIndex,i))  //[startIndex,i]就是截取的子串,满足要求再进行处理
            {
                s.insert(s.begin()+i+1,'.');  //在截取的子串后面插入.
                pointNum++;
                backtracking(s,i+2,pointNum);  //因为还插入了一个点,所以下次分割(下一层递归)从i+2的地方开始,即下一层递归的startIndex是本层的i+2
                pointNum--;  //回溯
                s.erase(s.begin()+i+1);  //回溯,把插入的.删掉
            }
            else  //不满足要求,后面的都可以不用看了,所以直接结束本层循环
            {
                break;
            }
        }
    }

    //判断子串是否符合地址的条件
    bool isValid(const string& s, int start, int end)  //这个不需要改字符串,所以可以用const传入
    {
        if(start>end) return false;
        if(s[start]=='0' && start!=end)  return false;  //以0开头,非法
        
        int num=0;
        for(int i=start; i<=end; i++)
        {
            if(s[i]>'9' || s[i]<'0') return false;  //不在0-9范围内,非法
            num=num*10+(s[i]-'0');   //s[i]-'0'是把字符转化为数字
            if(num>255) return false;  //子串超过255,说明数字长度超了,非法
        }

        return true;
    }

public:
    vector<string> restoreIpAddresses(string s) {
        result.clear();
        if(s.size()<4 || s.size()>12) return result;  //初步剪枝
        backtracking(s,0,0);
        return result;
    }
};

11.子集问题 78

本题和组合、切割问题不同的地方在于:组合和切割问题都是到了叶子节点才会收集一条路径,本题需要访问树形结构里的所有节点,所以每到一个节点,都要收集一次路径

class Solution {
private:
    vector<vector<int>> result;
    vector<int> path;

    void backtracking(vector<int>& nums, int startIndex)
    {
        result.push_back(path); 
        //把收集结果的语句放在这里,是因为每一次递归包含最后一个节点的path也要被收集进来,所以要在上一轮递归结束、到终止条件之前进行结果收集
        //如果在终止条件里或者之后再收集,包含叶子节点的路径就会被漏掉

        //终止条件:
        if(startIndex>=nums.size()) return; 

        //单层递归
        for(int i=startIndex; i<nums.size(); i++)
        {
            path.push_back(nums[i]);  
            backtracking(nums,i+1);
            path.pop_back();
        }
    }

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

13.子集II 90

子集问题可以不用写递归函数里的终止条件。因为单层递归的循环条件是i<size,而i是从startIndex开始的,但每一层递归传进去的startIndex都是i+1,所以随着递归层数的增加,startIndex是一直在增加的,迟早会不满足i<size的条件,递归就会结束,最终还是会返回的。

本题和上一题的区别是:本题中的集合有重复元素,但最终的结果集中不允许有重复的结果,所以需要在上一题的基础上加上去重的过程。

class Solution {
private:
    vector<vector<int>> result;
    vector<int> path;

    void backtracking(vector<int>& nums, int startIndex, vector<bool> used)
    {
        result.push_back(path); //每一个节点都是一个子集,所以每个节点都要被收集,收集结果的语句就不能放在终止条件里
        
        //终止条件
        if(startIndex>=nums.size())
        {
            return;
        }

        //单层递归
        for(int i=startIndex; i<nums.size(); i++)
        {
            if(i>0 && nums[i]==nums[i-1] && used[i-1]==false)  
            {
                continue;
            }   //跳过树层重复的元素

            path.push_back(nums[i]);
            used[i]=true;
            backtracking(nums,i+1,used);
            used[i]=false;  //回溯
            path.pop_back();
        }
    }

public:
    vector<vector<int>> subsetsWithDup(vector<int>& nums) {
        result.clear();
        path.clear();
        sort(nums.begin(),nums.end());  //去重之前要排序
        vector<bool> used(nums.size(),false);
        backtracking(nums,0,used);
        return result;
    }
};

14.递增子序列 491

题目中要求不能有重复的子集,所以需要去重。但从以往的去重过程来看,去重之前需要先对原集合进行排序。本题不可以进行排序,不然的话,集合里全都是递增子序列了。要在原集合的顺序上,直接挑选能构成递增子序列的元素

用set来去重。定义一个set来存放本层已经访问过的元素,每遇到一个元素,就和set里的元素对比一下看是否本层已经有过了。每层递归都会重新定义一个set,所以不需要回溯。

continue和break的区别:

class Solution {
private:
    vector<vector<int>> result;
    vector<int> path;

    void backtracking(vector<int>& nums, int startIndex)
    {
        //收集结果:要求子序列中至少有两个元素
        if(path.size()>1)
        {
            result.push_back(path);
            //return;  不可以在这收集结果之后就return,因为要取树上所有的点
        }

        unordered_set<int> uset; //每层递归都会重新定义一个set来存放本层已经访问过的元素
        for(int i=startIndex; i<nums.size(); i++)
        {
            //新加入的元素如果小于path中最后一个元素,说明不是递增序列,跳过这个,注意要先判断path是否为空!先判断是否为空!
            //如果set中已经有和新加入元素相等的元素,说明重复了,跳过这个
            if((!path.empty() && nums[i]<path.back()) || (uset.find(nums[i])!=uset.end()))
            {
                continue;  
            }
            uset.insert(nums[i]);  //把当前元素放入set
            path.push_back(nums[i]);
            backtracking(nums,i+1);
            path.pop_back();
        }
    }

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

15.全排列 46

和组合、分割、子集问题不同的是,排列问题中序列是有序的,[1,2]和[2,1]是两个不同的序列,而在其它问题中二者是同一个序列。在排列问题中,元素1在[1,2]中已经使用过了,但是在[2,1]中还要在使用一次1,所以处理排列问题就不用使用startIndex了。

但因为同一个排列中不能把相同的元素用两遍,即一个树枝上不能吧一个元素取两遍,所以还需要一个used数组来标记在同一树枝上已经使用过哪些元素

class Solution {
private:
    vector<vector<int>> result;
    vector<int> path;

    void backtracking(vector<int>& nums, vector<bool> used)
    {
        //终止条件:到叶子节点即构成全排列,可收集结果
        if(path.size()==nums.size())
        {
            result.push_back(path);
            return; //不需要收集树中的所有节点,所以收集完一个结果后可以return结束递归
        }

        for(int i=0; i<nums.size(); i++)
        {
            if(used[i]==true) continue;  //全排列中不能出现重复的元素,所以如果某元素已经在path中收集过了,则跳过

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

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

16.全排列II 47

跟上一题的区别是:上一题给出的数组nums中是没有重复元素的,而本题中有重复元素,并且需要返回的是不重复的排列结果。那么就涉及到了去重的问题。

class Solution {
private:
    vector<vector<int>> result;
    vector<int> path;

    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) || (i>0 && nums[i]==nums[i-1] && used[i-1]==false))  //用过的元素和重复的元素都要跳过
            {
                continue;
            }
            path.push_back(nums[i]);
            used[i]=true;
            backtracking(nums,used);
            used[i]=false;
            path.pop_back();
        }
    }

public:
    vector<vector<int>> permuteUnique(vector<int>& nums) {
        result.clear();
        path.clear();
        vector<bool> used(nums.size(),false);
        sort(nums.begin(),nums.end());
        backtracking(nums,used);
        return result;
    }
};

一个小小的总结:

到现在为止,已经学习了回溯算法的组合、分割、子集、排列问题。组合、分割、子集问题中,组合都是无序的,[1,2]和[2,1]是相同的结果,所以前面取过1之后,后面就不能再取1了,那么就需要startIndex(不全是这样,如果是从不同的集合中取元素、相互之间不影响的话,也不需要)。但排列问题中[1,2]和[2,1]是不同的结果,所以前面取过1之后,后面可以再取,就不需要startIndex了。但是因为要避免同一树枝上把相同的元素取两遍,所以需要used数组来标明哪些元素已经用过了。

去重有两种方法:nums[i]==nums[i-1]&&used[i-1]==false是一种方法,我觉得这种方法比较好用,这种方法需要在去重之前进行排序,所以不适用于本身有大小要求的题目。比如递增子序列,如果在去重之前对原数组进行排序,那么所有结果都会是递增子序列,就不符合要求了,所以不能用这种去重方法,改用set进行去重。用set去重:定义一个unordered_set,存放本层已经访问过的元素,要取某个元素之前现在set里找一下,如果能找到相同的元素,说明会重复,就要跳过当前元素。

后面还有三道hard题目没有做,有空再来看看吧!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值