力扣:“如何解之”

768 阅读4分钟

回溯

又到了我们的力扣代码时间,这次的题目又需要靠我们的老朋友来解决————回溯。

先来回忆一下,什么是回溯

回溯法是一个既带有系统性又带有跳跃性的搜索算法。它在包含问题的所有解的解空间树中,按照深度优先的策略,从根结点出发搜索解空间树。搜索到某个结点时,总是要先判断该结点是否肯定包含问题的解,如果不包含,则跳过以该结点为根的子树的系统搜索,逐层向祖先结点回溯;否则就进入该子树,继续按照深度优先的策略进行搜索。回溯法在求问题的所有解释要回溯到根,并且根节点的所有子树都要被搜索遍之后才会结束,而在寻求任一个解时,通常搜索到问题的一个解就可以结束。

总的来说,类似于一颗树的结构

模板

1、路径:也就是已经做出的选择。

2、选择列表:也就是你当前可以做的选择。

3、结束条件:也就是到达决策树底层,无法再做选择的条件。

回溯算法的框架:

const result = []
function backtrack(路径, 选择列表) {
    if 满足结束条件:
        result.push(路径)
        return
    
    for 选择 of 选择列表:
        做出选择
        backtrack(路径, 选择列表)
        撤销选择 
}

回顾完以后,我们直接看题

例题

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

示例 1:

输入:s = "aab"
输出:[["a","a","b"],["aa","b"]]

示例 2:

输入:s = "a"
输出:[["a"]]

思路分析

我们先来审题,将s分割成子字符串后,我们要使每个子串都是回文串,并且单个字符都是回文串,那么每个字符串至少都有一种分割方法使其每个子串回文,也就是单个字母为单个子串,就可以做到。

我们就用示例s="aab"来分析一下

首先,分割出a来,判断是回文串后,接下来判断剩下的ab,将ab看做一个整体,如果ab是回文串,那么我们就返回结果,因为下一步已经超过下标索引了;如果不是回文串,那么我们就返回上一步骤的ab,将ab分开,来分别判断a b是否为回文串;如果a为回文串,那么就判断剩余的b是否为回文串,否则,这条路就走不通了,因为已经不可再次分割了。当然很明显,第一次分割到最后的时候,一定是一个单个字母为子串的结果。

那么第二次分割则是将aa看作一个整体,判断是否为回文串,如果不是,那么后续就不用判断了,因为该字符串已经不是回文串了,不符合条件,假如aa不是回文的话,我是说假如,那么这个时候我们就直接判断aab是否为回文串了。那么这里aa是一个回文,只需要判断b是否为回文串了,如果是就可以直接返回结果了,不是回文串,下一步也超过了下标索引,

后续步骤也是这样,我们来看看图解

图解

未命名文件(11).png

具体代码

class Solution {
private:
    vector<vector<string>> result;
    vector<string> path; // 放已经回文的子串
    void backtracking (const string& s, int startIndex) {
        // 如果起始位置已经大于s的大小,说明已经找到了一组分割方案了
        if (startIndex >= s.size()) {
            result.push_back(path);
            return;
        }
        for (int i = startIndex; i < s.size(); i++) {//横向截取
            if (isPalindrome(s, startIndex, i)) {   // 是回文子串
                // 获取[startIndex,i]在s中的子串
                string str = s.substr(startIndex, i - startIndex + 1);
                path.push_back(str);
            } else {                                // 不是回文,终结本次循环,跳过
                continue;
            }
            backtracking(s, i + 1); // 寻找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;
    }
};

代码分析

我们利用我们的回溯模板来分析

const result = []
function backtrack(路径, 选择列表) {
    if 满足结束条件:
        result.push(路径)
        return
    
    for 选择 of 选择列表:
        做出选择
        backtrack(路径, 选择列表)
        撤销选择 
}

路径对应s,选择列表则是我们的下标索引

结束条件:即下标索引超过字符串最大长度的时候

做选择:如果是回文子串,就记录下来,否则,寻找下一个下标索引,对应我们的横向截取

backtrack 也就是深度遍历,对应我们的纵向截取

后面一步对应我们的回溯过程

isPalindrome就是判断是否回文,我们传入字符串以及首字符及最后一个字符的下标,利用双指针法来判断。startend两边同时收缩,比较对应的字母是否相同,只要有一个不同,就返回false,否则,返回true。

总结

模板很值得我们去记住,对付一些回溯题,我们的模板还是有一定效果的,但是不一定所有的回溯都适合这个模板,可能某些地方还需要改动,我们还是需要随机应变的。

我是小白,我们一起学习算法,谢谢!