代码随想录第二十七天 | 回溯:没个数限制的组合求和(leetcode 39),对组合两个维度讨论去重的组合求和(leetcode 40),分割问题(加动规判断回文改进)(leetcode 131)

1、没个数限制的组合求和

1.1 leetcode 39:组合求和

第一遍代码
刚开始没思路,直到看到这张图
组合求和
看图写代码:

class Solution {
public:
    vector<vector<int>> res;
    void backTracking(vector<int> candidates, vector<int>& path, int target, int start) {
        //target记录距离目标剩下的值,注意对下层递归去重
        if(target == 0) {
            res.push_back(path);
            return;
        }
        if(target < 0) return;
        for(int i = start; i < candidates.size(); i++) {
            path.push_back(candidates[i]);
            target -= candidates[i];
            backTracking(candidates, path, target, i);
            target += candidates[i];
            path.pop_back();
        }
        return;
    }

    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        vector<int> path;
        backTracking(candidates, path, target, 0);
        return res;
    }
};

与leetcode 77,216相比,本题没有数量要求,可以无限重复,但是有总和的限制,所以间接的也是有个数的限制(递归终止条件
组合求和示意
注意图中叶子节点的返回条件,因为本题没有组合数量要求,仅仅是总和的限制,所以递归没有层数的限制,只要选取的元素总和超过target就返回

在leetcode 77,216中都可以知道要递归K层,因为要取k个元素的组合

回溯三部曲

1、递归函数参数
这里依然是定义两个全局变量二维数组result存放结果集数组path存放符合条件的结果(这两个变量可以作为函数参数传入)
首先是题目中给出的参数,集合candidates, 和目标值target
此外我还定义了int型的sum变量统计单一结果path里的总和,其实这个sum也可以不用,用target做相应的减法就可以了(第一遍代码),最后如何target==0就说明找到符合的结果了,但为了代码逻辑清晰,代码随想录依然用了sum

本题还需要startIndex来控制for循环的起始位置,对于组合问题,什么时候需要startIndex呢?
我举过例子,如果是一个集合来求组合的话,就需要startIndex,例如:leetcode 77,216,因为要在那个集合中明确下一层递归从哪个元素开始
如果是多个集合取组合各个集合之间相互不影响,那么就不用startIndex,例如:leetcode 17
注意以上我只是说求组合的情况,如果是排列问题,又是另一套分析的套路,后面我在讲解排列的时候会重点介绍

代码如下:

vector<vector<int>> result;
vector<int> path;
void backtracking(vector<int>& candidates, int target, int sum, int startIndex)

2、递归终止条件
看上面的树形图,从叶子节点可以清晰看到,终止只有两种情况,sum大于target和sum等于target
sum等于target的时候,需要收集结果,代码如下:

if (sum > target) {
    return;
}
if (sum == target) {
    result.push_back(path);
    return;
}

3、单层搜索的逻辑
单层for循环依然是从startIndex开始,搜索candidates单一集合
注意本题和leetcode 77、leetcode 216的一个区别是:本题元素为可重复选取的,区别其实就是下层递归的起始位置从下一个元素变为本元素

for (int i = startIndex; i < candidates.size(); i++) {
    sum += candidates[i];
    path.push_back(candidates[i]);
    backtracking(candidates, target, sum, i); // 关键点:不用i+1了,表示可以重复读取当前的数
    sum -= candidates[i];   // 回溯
    path.pop_back();        // 回溯
}

C++完整代码:

/ 版本一
class Solution {
private:
    vector<vector<int>> result;
    vector<int> path;
    void backtracking(vector<int>& candidates, int target, int sum, int startIndex) {
        if (sum > target) {
            return;
        }
        if (sum == target) {
            result.push_back(path);
            return;
        }

        for (int i = startIndex; i < candidates.size(); i++) {
            sum += candidates[i];
            path.push_back(candidates[i]);
            backtracking(candidates, target, sum, i); // 不用i+1了,表示可以重复读取当前的数
            sum -= candidates[i];
            path.pop_back();
        }
    }
public:
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        result.clear();
        path.clear();
        backtracking(candidates, target, 0, 0);
        return result;
    }
};

1.2 leetcode 39:剪枝优化

已经知道下一层的sum会大于target,就没有必要进入下一层递归
那么可以在for循环的搜索范围上做做文章了

对总集合排序之后,如果下一层的sum(就是本层的 sum + candidates[i])已经大于target,就可以结束本轮for循环的遍历,注意因为对for改了,所以在开始之前需要对输入的数组进行排序
对组合求和的剪枝优化
for循环剪枝代码如下:

for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++)

总体代码:

class Solution {
private:
    vector<vector<int>> result;
    vector<int> path;
    void backtracking(vector<int>& candidates, int target, int sum, int startIndex) {
        if (sum == target) {
            result.push_back(path);
            return;
        }

        // 如果 sum + candidates[i] > target 就终止遍历
        for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++) {
            sum += candidates[i];
            path.push_back(candidates[i]);
            backtracking(candidates, target, sum, i);
            sum -= candidates[i];
            path.pop_back();

        }
    }
public:
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        result.clear();
        path.clear();
        sort(candidates.begin(), candidates.end()); // 需要排序
        backtracking(candidates, target, 0, 0);
        return result;
    }
};

时间复杂度: O(n * 2n),注意这只是复杂度的上界,因为剪枝的存在,真实的时间复杂度远小于此
空间复杂度: O(target)

2、对组合两个维度讨论去重的组合求和

2.1 leetcode 40:组合总和II

第一遍代码报错,没有能删除重复相同的组合一个组合内可以有数值相同的不同数字

class Solution {
public:
    vector<vector<int>> res;
    vector<int> path;
    void backTracking(vector<int>& candidates, int start, int target) {
        if(target  == 0) {
            res.push_back(path);
            return;
        }
        for(int i = start; i < candidates.size() && candidates[i] <= target; i++) {
            target -= candidates[i];
            path.push_back(candidates[i]);
            backTracking(candidates, i+1, target);
            target += candidates[i];
            path.pop_back();
        }
        return;
    }

    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
        sort(candidates.begin(), candidates.end());
        backTracking(candidates, 0, target);
        return res;
    }
};

这道题目和leetcode 39如下区别
1、本题candidates中的每个数字在每个组合中只能使用一次
2、本题数组candidates的元素是有重复的,而leetcode 39无重复元素的数组candidates
最后本题和leetcode 39要求一样,解集不能包含重复的组合

本题的难点在于区别2中:集合(数组candidates)有重复元素,但还不能有重复的组合
一些同学可能想了:我把所有组合求出来,再用set或者map去重,这么做很容易超时!
所以要在搜索的过程中就去掉重复组合

都知道组合问题可以抽象为树形结构,那么 “使用过” 在这个树形结构上是有两个维度的,一个维度是同一树枝上使用过,一个维度是同一树层上使用过。没有理解这两个层面上的“使用过” 是造成大家没有彻底理解去重的根本原因

回看一下题目,元素在同一个组合内是可以重复的,怎么重复都没事,但两个组合不能相同
所以我们要去重的是同一树层上的“使用过”同一树枝上的都是一个组合里的元素不用去重

举一个例子,candidates = [1, 1, 2], target = 3,(方便起见candidates已经排序了)
强调一下,树层去重的话,需要对数组排序
两个维度的去重
可以看到图中,每个节点相对于 leetode 39 多加了used数组,这个used数组下面会重点介绍(第一遍代码没能成功去重没想到的思路)

回溯三部曲
1、递归函数参数
与leetcode 39套路相同,此题还需要加一个bool型数组used,用来记录同一树枝上的元素是否使用过
这个集合去重的重任就是used来完成

vector<vector<int>> result; // 存放组合集合
vector<int> path;           // 符合条件的组合
void backtracking(vector<int>& candidates, int target, int sum, int startIndex, vector<bool>& used) {

2、递归终止条件
与leetcode 39相同,终止条件为 sum > targetsum == target
代码如下:

if (sum > target) { // 这个条件其实可以省略
    return;
}
if (sum == target) {
    result.push_back(path);
    return;
}

sum > target 这个条件其实可以省略,因为在递归单层遍历的时候,会有剪枝的操作

3、单层搜索的逻辑
这里与leetcode 39最大的不同就是要去重了
利用判断与 前一个 节点是否相同前一个 结点是否使用过,再加上对下一层的回溯逻辑保证只去重同一层递归(for)而不影响下一层递归使用值重复的元素

前面我们提到:要去重的是“同一树层上的使用过”,如何判断同一树层上元素(相同的元素)是否使用过了呢

自己的思路(也通过了),看注释即可

void backTracking(vector<int>& candidates, int start, int target, vector<bool>& used) {
        if(target  == 0) {
            res.push_back(path);
            return;
        }
        for(int i = start; i < candidates.size() && candidates[i] <= target; i++) {
            if(i > 0) {
                if(candidates[i] == candidates[i - 1] && used[i - 1] == true) {//4
                    used[i] = true;//1
                    continue;
                }
            }
            used[i] = false;//2
            /*
            注意:下一次之前一定把used[i]恢复成false
            不然以下例子通不过:
            testcase:[3,1,3,5,1,1] 8
            Expected Answer:[[1,1,1,5],[1,1,3,3],[3,5]]
            Answer:[[1,1,1,5],[3,5]]
            为什么[1,1,3,3]会丢了,因为上一个遍历for到底的时候把3 used[3]标记成true了,
            但是对于这一个for遍历来说3还没用过,所以在每次执行for循环的逻辑开头一定要used[i] = false

            具体used[i]怎么控制让同层的遍历去重,在一根树枝上的遍历不去重呢
            有点像操作系统的信号量机制,1,2,3,4语句完成了控制
            其中,4,1,3负责让同层的遍历去重,主要体现在对for循环的控制
            2负责一根树枝上的遍历不去重不被之前的去重影响
            */
            target -= candidates[i];
            path.push_back(candidates[i]);
            backTracking(candidates, i+1, target, used);
            used[i] = true;//3
            target += candidates[i];
            path.pop_back();
        }
        return;
    }

具体used[i]怎么控制让同层的遍历去重,在一根树枝上的遍历不去重

有点像操作系统的信号量机制(回溯)通过1,2,3,4语句完成了控制
其中,4,1,3负责让同层的遍历去重主要体现在对for循环的控制
2负责一根树枝上的遍历不去重不被之前的去重影响
注意:下一次之前一定把used[i]恢复成false
不然以下例子通不过:

testcase:[3,1,3,5,1,1] 8
Expected Answer:[[1,1,1,5],[1,1,3,3],[3,5]]
Answer:[[1,1,1,5],[3,5]]

为什么[1,1,3,3]会丢了,因为上一个遍历for到底的时候把3 used[3]标记成true了,
但是对于这一个for遍历来说3还没用过,所以在每次执行for循环的逻辑开头一定要used[i] = false

代码随想录思路:(个人理解就是true/false互换了一下used[i - 1] == false表示使用过是因为这样自己代码里的1处代码就可以省了,因为used的初值就是false,最后再加上一个剪枝
如果candidates[i] == candidates[i - 1] 并且 used[i - 1] == false,就说明:前一个树枝,使用了candidates[i - 1],也就是说同一树层使用过candidates[i - 1]
此时for循环里就应该做continue的操作

这块比较抽象,如图:
对同一树层上的去重
在图中将used的变化用橘黄色标注上,可以看出在candidates[i] == candidates[i - 1]相同的情况下:
used[i - 1] == true,说明同一树 “枝” candidates[i - 1]使用过
used[i - 1] == false,说明
同一树 “层” candidates[i - 1]使用过
可能有的录友想,为什么 used[i - 1] == false 就是同一树层呢,因为
同一树层
used[i - 1] == false 才能表示,当前取的 candidates[i] 是从 candidates[i - 1] 回溯而来
used[i - 1] == true,说明是进入下一层递归去下一个数,所以是树枝上,如图所示:
用used数组去重
这块去重的逻辑很抽象,代码随想录单层搜索的逻辑代码如下:

for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++) {
    // used[i - 1] == true,说明同一树枝candidates[i - 1]使用过
    // used[i - 1] == false,说明同一树层candidates[i - 1]使用过
    // 要对同一树层使用过的元素进行跳过
    if (i > 0 && candidates[i] == candidates[i - 1] && used[i - 1] == false) {
        continue;
    }
    sum += candidates[i];
    path.push_back(candidates[i]);
    used[i] = true;
    backtracking(candidates, target, sum, i + 1, used); // 和39.组合总和的区别1:这里是i+1,每个数字在每个组合中只能使用一次
    used[i] = false;
    sum -= candidates[i];
    path.pop_back();
}

注意sum + candidates[i] <= target剪枝操作,同leetcode 39
代码随想录整体C++代码如下:

class Solution {
private:
    vector<vector<int>> result;
    vector<int> path;
    void backtracking(vector<int>& candidates, int target, int sum, int startIndex, vector<bool>& used) {
        if (sum == target) {
            result.push_back(path);
            return;
        }
        for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++) {
            // used[i - 1] == true,说明同一树枝candidates[i - 1]使用过
            // used[i - 1] == false,说明同一树层candidates[i - 1]使用过
            // 要对同一树层使用过的元素进行跳过
            if (i > 0 && candidates[i] == candidates[i - 1] && used[i - 1] == false) {
                continue;
            }
            sum += candidates[i];
            path.push_back(candidates[i]);
            used[i] = true;
            backtracking(candidates, target, sum, i + 1, used); // 和39.组合总和的区别1,这里是i+1,每个数字在每个组合中只能使用一次
            used[i] = false;
            sum -= candidates[i];
            path.pop_back();
        }
    }

public:
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
        vector<bool> used(candidates.size(), false);
        path.clear();
        result.clear();
        // 首先把给candidates排序,让其相同的元素都挨在一起。
        sort(candidates.begin(), candidates.end());
        backtracking(candidates, target, 0, 0, used);
        return result;
    }
};

时间复杂度: O(n * 2n)
空间复杂度: O(n)

除了used数组外,另一种去重方式
这里直接用startIndex来去重,不用used数组了,但不管哪种思路,之前一定要 排序

要对同一树层使用过的元素进行跳过,注意一定要是同一树层的,所以不能是i > 0,而是i > startIndex

if (i > startIndex && candidates[i] == candidates[i - 1]) {
	continue;
}
class Solution {
private:
    vector<vector<int>> result;
    vector<int> path;
    void backtracking(vector<int>& candidates, int target, int sum, int startIndex) {
        if (sum == target) {
            result.push_back(path);
            return;
        }
        for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++) {
            // 要对同一树层使用过的元素进行跳过,注意一定要是同一树层的,所以不能是i > 0,而是i > startIndex
            if (i > startIndex && candidates[i] == candidates[i - 1]) {
                continue;
            }
            sum += candidates[i];
            path.push_back(candidates[i]);
            backtracking(candidates, target, sum, i + 1); 
			// 和leetcode 39,这里是i+1,每个数字在每个组合中只能使用一次
            sum -= candidates[i];
            path.pop_back();
        }
    }

public:
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
        path.clear();
        result.clear();
        // 首先把给candidates排序,让其相同的元素都挨在一起。
        sort(candidates.begin(), candidates.end());
        backtracking(candidates, target, 0, 0);
        return result;
    }
};

3、分割问题

3.1 leetcode 131:分割回文串

第一遍代码输出为空
思路与代码随想录一致,见后文

class Solution {
public:
    bool isHuiwen(string s) {
        for(int i = 0; i < s.size()/2; i++) {
            if(s[i] != s[s.size() - 1 - i]) {
                return false;
            }
        }
        return true;
    }

    vector<vector<string>> res;
    vector<string> path;
    void backTracking(string& s, int start) {
        if(isHuiwen(s) == false) return;
        if(isHuiwen(s) == true) {
            path.push_back(s);
        }
        if(s.size() == 0) {
            res.push_back(path);
            return;
        }
        for(int i = start; i < s.size(); i++) {
            string ss(s.begin() + start, s.begin() + i);
            backTracking(ss, i+1);
            if(isHuiwen(ss) == true) {
                path.pop_back();
            }
        }
    }

    vector<vector<string>> partition(string s) {
        backTracking(s, 0);
        return res;
    }
};

修改思路(通过)
1、因为最后for循环是循环到s.size(),所以s不能改变,但是还是要判断不同长度的子串是不是满足条件
2、通过for控制递归的执行
把这个不同长度的子串是否是回文的判断直接放在for函数里,这样就不需要再写一个终止位置的参数了。如果不需要递归下去了直接在for函数里面直接进入下一个循环(相当于continue),这样来控制递归的进行。那么递归结束条件也要改变,改成只使用start和s的判断
3、string ss(s.begin() + start, s.begin() + i);左闭右开初始化的,backTracking(s, i);因为前面初始化子串是左闭右开,所以实际上i已经后移了,不需要加一

修改后的第一遍代码

class Solution {
public:
    bool isHuiwen(string s) {
        for(int i = 0; i < s.size()/2; i++) {
            if(s[i] != s[s.size() - 1 - i]) {
                return false;
            }
        }
        return true;
    }

    vector<vector<string>> res;
    vector<string> path;
    void backTracking(string& s, int start) {
        if(s.size() <= start) {
            res.push_back(path);
            return;
        }
        /*
        1、因为最后是循环到s.size(),所以s不能改变,但是要判断不同长度的子串是不是满足条件
        2、把这个不同长度的子串的判断直接放在for函数里,这样就不需要再写一个终止位置的参数了
        如果不需要递归下去了直接在for函数里面continue,这样来控制递归的进行
        那么递归结束条件也要改变,改成只使用start和s的判断
        */
        for(int i = start+1; i <= s.size(); i++) {
            string ss(s.begin() + start, s.begin() + i);//左闭右开
            if(isHuiwen(ss) == true) {
                path.push_back(ss);
                backTracking(s, i);//因为前面初始化子串是左闭右开,所以实际上i已经后移了,不需要加一了
                path.pop_back();
            }
        }
    }

    vector<vector<string>> partition(string s) {
        backTracking(s, 0);
        return res;
    }
};

思路
本题这涉及到两个关键问题
1、切割问题,有不同的切割方式
2、判断回文

相信这里不同的切割方式可以搞懵很多同学了
这种题目,想用for循环暴力解法,可能都不那么容易写出来,所以要换一种暴力的方式,就是回溯

分析一下切割,其实切割问题类似组合问题
例如对于字符串abcdef:
组合问题:选取一个a之后,在bcdef中再去选取第二个,选取b之后在cdef中再选取第三个…
切割问题:切割一个a之后,在bcdef中再去切割第二段,切割b之后在cdef中再切割第三段…

所以切割问题,也可以抽象为一棵树形结构,如图:
切割问题抽象成树
递归用来纵向遍历for循环用来横向遍历切割线(就是图中的红线,定义一个变量记住这个数字切割到字符串的结尾位置,说明找到了一个切割方法
切割问题的回溯搜索的过程组合问题的回溯搜索的过程差不多的(到这里与第一遍代码的思路一致)

回溯三部曲
1、递归函数参数
全局变量数组path存放切割后回文的子串二维数组result存放结果集(这两个参数可以放到函数参数里
本题递归函数参数还需要startIndex,因为切割过的地方,不能重复切割,和组合问题也是保持一致

代码如下

vector<vector<string>> result;
vector<string> path; // 放已经回文的子串
void backtracking (const string& s, int startIndex) {

2、递归函数终止条件
切割问题递归终止条件
从树形结构的图中可以看出:切割线切到了字符串最后面,说明找到了一种切割方法,此时就是本层递归的终止条件
处理组合问题的时候,递归参数需要传入startIndex,表示下一轮递归遍历的起始位置,这个startIndex就是切割线

终止条件代码如下:

void backtracking (const string& s, int startIndex) {
    // 如果起始位置已经大于s的大小,说明已经找到了一组分割方案了
    if (startIndex >= s.size()) {
        result.push_back(path);
        return;
    }
}

3、单层搜索的逻辑
递归循环中如何截取子串呢?
for (int i = startIndex; i < s.size(); i++)循环中,我们 定义了起始位置startIndex,那么 [startIndex, i] 就是要截取的子串
首先判断这个子串是不是回文,如果是回文,就加入vector<string> path中,path用来记录切割过的回文子串

代码如下:

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();        // 回溯过程,弹出本次已经添加的子串
}

注意切割过的位置不能重复切割,所以,backtracking(s, i + 1); 传入下一层的起始位置i + 1

判断回文子串
可以使用双指针法(像第一遍代码的一个指针也行),一个指针从前向后一个指针从后向前,如果前后指针所指向的元素是相等的,就是回文字符串了

代码如下:

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;
 }

整体代码:
**获取[startIndex,i]在s中的子串
string str = s.substr(startIndex, i - startIndex + 1);substr() 求子集 参数为初始位置 和 长度

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;
    }
};

相同思路 自己实现

class Solution {
private:
    vector<vector<string>> res;
    bool isHuiwen(const string& vec) {
        for (int i = 0; i < vec.size() / 2; i++) {
            if (vec[i] != vec[vec.size() - 1 - i])
                return false;
        }
        return true;
    }
    void backTracking(const string& s, vector<string>& path, int startIndex) {
        if (startIndex == s.size()) {
            if (!path.empty())
                res.push_back(path);
            return;
        }
        for (int i = startIndex; i < s.size(); i++) {
            string tmp(s.begin() + startIndex, s.begin() + i + 1);
            if (isHuiwen(tmp)) {
                path.push_back(tmp);
                backTracking(s, path, i + 1);
                path.pop_back();
            }
        }
    }
public:
    vector<vector<string>> partition(string s) {
        vector<string> path;
        backTracking(s, path, 0);
        return res;
    }
};

3.2 leetcode 131:用动态规划优化判断回文串

上面的代码还存在一定的优化空间, 在于如何更高效的计算一个子字符串是否是回文字串
上述代码isPalindrome函数运用双指针的方法来判定对于一个字符串s, 给定起始下标和终止下标, 截取出的子字符串是否是回文字串。但是其中有一定的重复计算存在:
例如给定字符串"abcde", 在已知"bcd"不是回文字串时, 不再需要去双指针操作"abcde"可以直接判定它一定不是回文字串

具体来说, 给定一个字符串s, 长度为n, 它成为回文字串充分必要条件s[0] == s[n-1]s[1:n-1]是回文字串

大家如果熟悉动态规划这种算法的话, 我们可以高效地事先一次性计算出, 针对一个字符串s, 它的任何子串是否是回文字串, 然后在我们的回溯函数中直接查询即可动态规划省时间的原因), 省去了双指针移动判定这一步骤

具体参考代码如下:

class Solution {
private:
    vector<vector<string>> result;
    vector<string> path; // 放已经回文的子串
    vector<vector<bool>> isPalindrome; // 放事先计算好的是否回文子串的结果,动态规划步骤
    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[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(); // 回溯过程,弹出本次已经添加的子串
        }
    }
	//动态规划判断回文串
    void computePalindrome(const string& s) {
        // isPalindrome[i][j] 代表 s[i:j](双边包括)是否是回文字串 
        isPalindrome.resize(s.size(), vector<bool>(s.size(), false)); // 根据字符串s, 刷新布尔矩阵的大小
		// 动态规划
        for (int i = s.size() - 1; i >= 0; i--) { 
            // 需要倒序计算, 保证在i行时, i+1行已经计算好了
            for (int j = i; j < s.size(); j++) {
                if (j == i) {isPalindrome[i][j] = true;}
                else if (j - i == 1) {isPalindrome[i][j] = (s[i] == s[j]);}
                else {isPalindrome[i][j] = (s[i] == s[j] && isPalindrome[i+1][j-1]);}
            }
        }
    }
public:
    vector<vector<string>> partition(string s) {
        result.clear();
        path.clear();
        computePalindrome(s);
        backtracking(s, 0);
        return result;
    }
};

按照思路修改第一次代码注意小的错误,见注释

class Solution {
public:
    vector<vector<string>> res;
    vector<string> path;
    vector<vector<bool>> storHuiwen;//左闭右闭
    //动态规划
    void isHuiwen(string s) {
        storHuiwen.resize(s.size(), vector<bool>(s.size(), false));
        for(int i = s.size() - 1; i >= 0; i--) {//i--别整错了
            for(int j = i; j <= s.size() - 1; j++) {
                if(i == j) storHuiwen[i][j] = true;
                else if(j - i == 1) {//j在后i在前
                    if(s[i] == s[j]) {
                        storHuiwen[i][j] = true;
                    }
                }
                else storHuiwen[i][j] = (s[i] == s[j]) && storHuiwen[i + 1][j - 1];//i+1别整错了
            }
        }
    }

    void backTracking(string& s, int start) {
        if(s.size() <= start) {
            res.push_back(path);
            return;
        }
        for(int i = start+1; i <= s.size(); i++) {
            string ss(s.begin() + start, s.begin() + i);
            if(storHuiwen[start][i - 1] == true) {
                path.push_back(ss);
                backTracking(s, i);
                path.pop_back();
            }
        }
    }

    vector<vector<string>> partition(string s) {
        isHuiwen(s);
        backTracking(s, 0);
        return res;
    }
};

3.3 leetcode 131:总结

回溯就是暴力遍历

切割问题其实是一种组合问题

动规就是后面的求解可以用前面的结论,暴力的加强版

4、双向迭代器操作(以set为例,和随机访问迭代器的区别)

set 容器不支持通过下标(如 set[index])的方式进行访问。set 是基于平衡二叉树(通常是红黑树)实现的,它是一个存储唯一元素的有序集合,其中的元素按照特定的顺序进行自动排序

对于 set<int> 的迭代器操作,s.begin() + i 的形式不起作用,因为 set 使用的迭代器是双向迭代器,而不是随机访问迭代器。这两种迭代器的支持的操作有显著的区别:
1)随机访问迭代器:支持在迭代器上进行加减操作,可以直接跳过多个元素,类似于指针的行为。这种迭代器类型通常用于如 std::vector 和 std::deque
2)双向迭代器:只能一个一个元素地前进(++iterator)或后退(–iterator),不能进行跳跃式的多元素移动。set 和 map 等基于树的容器使用的就是这种迭代器

因此,要在 set 中访问第 i 个位置的元素,不能直接使用 s.begin() + i 这种语法,因为这要求随机访问迭代器支持。相反,你必须从 s.begin() 开始,逐步向前移动迭代器,直到移动 i 次。可以使用 std::advance 函数来完成这个任务,这是一个在任何类型的迭代器上都可用的函数,可以自动根据迭代器的类型选择最优的方式来移动迭代器:

#include <iostream>
#include <set>
#include <iterator>

int main() {
    std::set<int> s = {1, 2, 3, 4, 5};
    int index = 3;  // 想要访问的下标位置(0起始)

    auto it = s.begin();
    std::advance(it, index);  // 移动迭代器

    if (it != s.end()) {
        std::cout << "Element at index " << index << " is: " << *it << std::endl;
    } else {
        std::cout << "Index out of bounds." << std::endl;
    }

    return 0;
}

这种方式效率较低(O(n) 时间复杂度),因为每次调用 std::advance 时,都需要从当前位置开始,逐个元素地遍历。如果你需要经常以随机访问的方式来处理元素,set 可能不是最佳的容器选择。在这种情况下,考虑使用 std::vector 或 std::deque 可能更加合适

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值