[日记]LeetCode算法·十——回溯②

文章介绍了使用回溯算法解决LeetCode中的几个问题,包括分割回文串、复原IP地址、子集、子集II、递增子序列和全排列。通过动态规划预处理和剪枝策略优化了算法效率,展示了回溯在解决组合问题中的有效性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1 分割回文串

LeetCode:分割回文串

分割回文串完全可以想象成一个组合问题,即在哪些位置下刀切开字符串

理解这一问题的关键在于,需要明白回溯的意义,我们应该进行有意义的探索,因此需要先对子字符串进行回文判断,再进行深一步的搜索与剪枝。

其中类似于KMP问题,这里也可以对字符串进行事先的预处理,利用动态规划的思想:[i,j]为回文串等价于[i+1,j-1]为回文串+s[i]==s[j]。因此实现设立最初的起点(i最大,j最小),进行动态规划。

class Solution {
public:
    vector<vector<string>> result;
    vector<string> path;
    vector<vector<bool>> isPalindrome;
    void computePalindrome(string& s)
    {
        //重新初始化
        isPalindrome.resize(s.size(), vector<bool>(s.size(), false));
        //[i,j]是回文等价于[i+1,j-1]是回文&&s[i]==s[j]
        //因此需要从最大的i和最小j开始,进行动态规划,逐步推进
        for(int i=s.size()-1;i>=0;i--)
        {
            for(int j=i;j<s.size();j++)
            {
                //1个字符的情况下
                if(i==j)
                    isPalindrome[i][j]=true;
                //2个字符的情况下
                else if(j-i==1)
                    isPalindrome[i][j]=(s[i]==s[j]);
                //字符更多的情况可以拆解为[i+1,j-1]进行分析,因为i从大到小,j从小到大,所以是s[i+1][j-1]必然已经被算过了
                else
                    isPalindrome[i][j] = ( s[i]==s[j] && isPalindrome[i+1][j-1]);
            }
        }
    }

    void backTrack(string& s,int startIndex)
    {
        if(startIndex>=s.size())
        {
            result.push_back(path);
            return;
        }
        for(int i=startIndex;i<s.size();i++)
        {
            //[start,i]是回文子串
            if(isPalindrome[startIndex][i])
            {
                string str=s.substr(startIndex,i-startIndex+1);
                path.push_back(str);
                //回溯
                backTrack(s,i+1);
                path.pop_back();
            }
        }
    }

    vector<vector<string>> partition(string s) {
        computePalindrome(s);
        backTrack(s,0);
        return result;
    }
};

2 复原IP地址

LeetCode:复原IP地址

与上述的字符串分割一样的思路,但因为对于分割的次数有所限制,所以应该在邻近分割上限时,将最后一刀直接切到末尾

其余就是一些琐碎的合法检测。

class Solution {
public:
    vector<string> result;
    vector<string> path;

    bool isValid(string& s,int start,int end)
    {
        //4位及以上排除
        if(end-start>=3)return false;
        //1位都是允许的
        if(end==start)return true;
        //接下来考虑2/3位
        //首位为0排除
        if(s[start]=='0')return false;
        //此时2位都是允许的
        if(end-start==1)return true;
        //考虑3位
        //300以上排除
        if(s[start]>'2')return false;
        //100-199ok
        if(s[start]=='1')return true;
        //只剩200+的情况
        //260以上排除
        if(s[start+1]>'5')return false;
        //256-259排除
        if(s[start+1]=='5' && s[start+2]>='6')return false;
        //剩下全是200-255之间
        return true;
    }

    void backTrack(string& s,int startIndex,int num_slice)
    {
        if(num_slice==4)
        {
            string validIP=path[0];
            for(int i=1;i<=3;i++)
            {
                validIP+="."+path[i];
            }
            result.push_back(validIP);
            return;
        }
        for(int i=startIndex;i<s.size();++i)
        {
            //已经分了3段就必须直接到末尾
            if(num_slice==3)
                i=s.size()-1;

            //[startIndex,i]符合条件
            if(isValid(s,startIndex,i))
            {
                path.push_back(s.substr(startIndex,i-startIndex+1));
                backTrack(s,i+1,num_slice+1);
                path.pop_back();
            }
        }
    }

    vector<string> restoreIpAddresses(string s) {
        result.clear();
        path.clear();
        backTrack(s,0,0);
        return result;
    }
};

3 子集

LeetCode:子集

子集问题区别于组合与排序最大的不同是:子集问题收集的是所有节点上的结果,而排列与组合问题收集的是叶子上的结果

这就意味着,我们应该在每一次合法的回溯的最开始,直接记录下当前的路径结果,以防止漏过未选节点时的结果。

与此同时,还有另外一种思路,即根据选与不选的2^N种选择进行递归,避开循环。

思路1:记录当前路径结果
class Solution {
public:
    vector<vector<int>> result;
    vector<int> path;

    void backTrack(vector<int>& nums,int startIndex)
    {
        result.push_back(path);
        for(int i=startIndex;i<nums.size();i++)
        {
            path.push_back(nums[i]);
            backTrack(nums,i+1);
            path.pop_back();
        }
    }
    vector<vector<int>> subsets(vector<int>& nums) {
        backTrack(nums,0);
        return result;
    }
};
思路2:根据选与不选递归
class Solution {
public:
    vector<vector<int>> result;
    vector<int> subset;

    void backTrack(vector<int>& nums,int startIndex)
    {
        if(startIndex>=nums.size())
        {
            result.push_back(subset);
            return;
        }
        //不选
        backTrack(nums,startIndex+1);
        //选
        subset.push_back(nums[startIndex]);
        backTrack(nums,startIndex+1);
        subset.pop_back();
    }
    vector<vector<int>> subsets(vector<int>& nums) {
        backTrack(nums,0);
        return result;
    }
};

4 子集II

LeetCode:子集II

因为有重复元素,直接利用排序+剪枝的思路即可,依然是在树层上进行去重。

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

    void backTrack(vector<int>& nums,int startIndex)
    {
        result.push_back(path);
        for(int i=startIndex;i<nums.size();i++)
        {
            //剪枝
            if(i>startIndex && nums[i-1]==nums[i])
                continue;
            path.push_back(nums[i]);
            backTrack(nums,i+1);
            path.pop_back();
        }
    }

    vector<vector<int>> subsetsWithDup(vector<int>& nums) {
        //排序,方便剪枝
        sort(nums.begin(),nums.end());
        backTrack(nums,0);
        return result;
    }
};

5 递增子序列

LeetCode:递增子序列

因为本体不宜对原数组进行排序修改,因此直接利用used数组对树层进行记录,判断该元素是在某一位置上已经被使用过。

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

    bool isUsed(vector<int>& used,int k)
    {
        for(int i=0;i<used.size();i++)
        {
            if(used[i]==k)return true;
        }
        return false;
    }

    void backTrack(vector<int>& nums,int startIndex)
    {
        if(path.size()>=2)
            result.push_back(path);
        if(startIndex>=nums.size())
            return;
        
        //使用used数组记录每一层选过的数字
        vector<int> used;
        for(int i=startIndex;i<nums.size();++i)
        {
            if(isUsed(used,nums[i]))
                continue;
            if(path.size()==0 || nums[i]>=path[path.size()-1])
            {
                path.push_back(nums[i]);
                backTrack(nums,i+1);
                path.pop_back();
                used.push_back(nums[i]);
            }
        }
    }

    vector<vector<int>> findSubsequences(vector<int>& nums) {
        backTrack(nums,0);
        return result;
    }
};

6 全排列

LeetCode:全排列

排列问题相比于组合问题,在于不同顺序是一致的,这代表着每一轮都应该从最初的元素进行判断与选择,并使用usedIndex记录不同层的使用情况。

组合问题因为顺序无所谓,反而可以利用数组起始坐标的改变,从而控制已出现的元素不必再出现,即靠前的元素没有后出现的道理(无序性)

理论上使用bool应该会更高效,但无所谓了。

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

    bool isUsed(vector<int>& usedIndex,int index)
    {
        for(int i=0;i<usedIndex.size();i++)
        {
            if(index==usedIndex[i])return true;
        }
        return false;
    }

    void backTrack(vector<int>& nums,vector<int>& usedIndex)
    {
        if(path.size()==nums.size())
        {
            result.push_back(path);
            return;
        }
        for(int i=0;i<nums.size();i++)
        {
            if(isUsed(usedIndex,i))
                continue;
            path.push_back(nums[i]);
            usedIndex.push_back(i);
            backTrack(nums,usedIndex);
            path.pop_back();
            usedIndex.pop_back();
        }
    }
    vector<vector<int>> permute(vector<int>& nums) {
        vector<int> usedIndex;
        backTrack(nums,usedIndex);
        return result;
    }
};

7 全排列II

LeetCode:全排列II

因为出现了重复元素的数组,因此针对排列问题的不同层使用usedIndex进行记录而针对重复元素利用排序+剪枝进行去重

这一类问题大体上都可以判断为:排列问题不重复(不同层间的记录)+相同元素的去重(同树层间的跳过)

class Solution {
public:
    vector<vector<int>> result;
    vector<int> path;
    void backTrack(vector<int>& nums,vector<bool>& indexUsed)
    {
        if(path.size()==nums.size())
        {
            result.push_back(path);
            return;
        }
        
        for(int i=0;i<nums.size();i++)
        {
            if(indexUsed[i]==true)
                continue;
            if(i>0 && nums[i]==nums[i-1] && indexUsed[i-1]==false)
                continue;

            path.push_back(nums[i]);
            indexUsed[i]=true;
            backTrack(nums,indexUsed);
            path.pop_back();
            indexUsed[i]=false;
        }
    }

    vector<vector<int>> permuteUnique(vector<int>& nums) {
        vector<bool> indexUsed(nums.size(),false);
        sort(nums.begin(),nums.end());
        backTrack(nums,indexUsed);
        return result;
    }
};

8 总结

感觉回溯问题似乎因为树的原因,没那么难,理解起来还是很快的,不过可能因为都是easy和mid题的缘故吧。

反省一下自己,昨天那个GAN的博客完全没有任何意义,以后绝对不会浪费时间写意义不明的Markdown了,要抓紧时间学习新的知识。

——2023.2.24

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值