算法总结归纳(第六天)(回溯算法、递归类型)

目录

一、组合问题

1、组合

①、代码实现

②、剪枝优化

2、组合总和Ⅲ

①、代码实现

②、剪枝优化

3、组合总和Ⅰ

①、代码实现

②、剪枝优化 

4、组合总和Ⅱ

①、代码实现

②、剪枝优化

5、电话号码的字母组合

 小结

二、分割问题

①、分割回文串

②、复原ip地址

小结

三、子集问题

1、子集Ⅰ

2、子集Ⅱ

①、used数组去重

②、set去重

小结

四、排列问题

1、全排列Ⅰ

2、全排列Ⅱ

小结

五、棋盘问题

1、N皇后

2、解数独

小结

六、其他问题

1、递增子序列

总结


一、组合问题

1、组合

题目链接:组合

①、代码实现

回溯本身就是递归的一种,只不过是在递归基础上加上了递归前和递归后的代码,而组合就是1 - n的范围内排列组合。

for循环是为了将每种情况遍历到,然后回溯的处理是为了将每一种情况容纳进去。

vector> res;
vector path;

void backtracking(int n, int k, int i)
{
    if(path.size() == k){
        res.push_back(path);
        return;
    }
    for(; i<=n; i++){
        path.push_back(i);
        backtracking(n, k, i + 1);
        path.pop_back();
    }
}

    vector> combine(int n, int k) {
        backtracking(n, k, 1);
        return res;
    }

②、剪枝优化

其实就是再for循环的判断条件中加了一个,n - (k - path.size()) + 1。

假设n = 4, k = 4,这样,第一层从2开始得遍历都没有意义了,第二层从3开始得遍历都没意义了,第三层从4开始没有意义。

 + 1 是因为包括起点位置,左闭所以加一。

for(; i<=n - (k - path.size()) + 1; i++){
        path.push_back(i);
        backtracking(n, k, i + 1);
        path.pop_back();
    }

2、组合总和Ⅲ

题目链接:组合总和Ⅲ

①、代码实现

这个就是在上面一道题的基础上加了sum用来记录路径上的和,同时因为题目中说了只要1 - 9的数字,因此for循环的限定条件为i <= 9。

vector> res;
vector path;

void backtracking(int k, int n, int i, int sum)
{
    if(path.size() == k){
        if(sum == n)res.push_back(path);
        return;
    }
    for(; i <= 9; i++){
        path.push_back(i);
        sum += i;
        backtracking(k, n, i + 1, sum);
        sum -= i;
        path.pop_back();
    }
}

    vector> combinationSum3(int k, int n) {
        backtracking(k, n, 1, 0);
        return res;
    }

②、剪枝优化

其实很简单,在backtracking()函数最上面加个这就可以,可以避免没必要的选取。

 if(sum > n) return;

3、组合总和Ⅰ

题目链接:组合总和Ⅰ

①、代码实现

本题与组合总和Ⅲ的区别就是区间的数可重复使用,同时给的数不是从1开始递增的,所以我们进行代码实现的时候,应该传入i,这样可保证前面的一定是相对小的,后面的是大的,最终加和是target而且不会重复。

void backtracking(vector& candidates, int target, int sum, int index)
{
    if(sum > target) return;
    if(sum == target) {
        res.push_back(path);
        return;
    }
    for(int i = index; i> combinationSum(vector& candidates, int target) {
        backtracking(candidates, target, 0, 0);
        return res;    
    }

②、剪枝优化 

我们在for循环上进行处理,这样就可以减少进入循环的次数,从而实现优化。

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

4、组合总和Ⅱ

题目链接:组合总和Ⅱ

①、代码实现

如果我们直接提交这个代码,部分数据会超时,所以,我们应该用上上道题目的剪枝优化。

void backtracking(vector& candidates, int target, int sum, int i, vector& used)
{
    if(sum == target){
        res.push_back(path);
        return;
    }
    for(; i < candidates.size(); i++){
        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);
        used[i] = false;
        path.pop_back();
        sum -= candidates[i];
    }
}

    vector> combinationSum2(vector& candidates, int target) {
        vector used(candidates.size(), false);
        sort(candidates.begin(), candidates.end());
        backtracking(candidates, target, 0, 0, used);
        return res;
    }

②、剪枝优化

将for循环里面改成这样就可以了。

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

5、电话号码的字母组合

题目链接:电话号码组合

const string letterMap[10] = {
    "", // 0
    "", // 1
    "abc", // 2
    "def", // 3
    "ghi", // 4
    "jkl", // 5
    "mno", // 6
    "pqrs", // 7
    "tuv", // 8
    "wxyz", // 9
};
vector res;
string s;

void backtracking(string& digits, int index)
{
    if(index == digits.size()){
        res.push_back(s);
        return;
    }
    int digit = digits[index] - '0';
    string letter = letterMap[digit];
    for(int i = 0; i < letter.size(); i++){
        s.push_back(letter[i]);
        backtracking(digits, index + 1);
        s.pop_back();
    }
}

    vector letterCombinations(string digits) {
        if(digits.size()==0) return res;
        backtracking(digits,0);
        return res;
    }

 小结

组合问题中,我们通过组合,知道了组合问题的模板和剪枝思路,

然后组合总和Ⅲ中,我们知道了(数组中无重复数字,数组中不可重复使用)sum在回溯过程中求和的写法

之后组合总和中, 我们解决了一个数组中数字可重复使用(数组中无重复数字)的加和问题,

然后组合总和Ⅱ中, 我们解决了一个数组中数字只能使用一次(数组中有重复数字)的问题,引入了used的bool数组来判断。

最后的电话号码则是一个和上面类似,同时可以练习练习string的用法。

二、分割问题

题目链接分割回文串

①、分割回文串

我们将判断是不是回文串的函数先分离出来,然后再根据回文的坐标判断。

所谓分割,就是根据循环的 i 的变化和传入的初始值的变化来控制。

vector> res;
vector path;

bool is_reverse(const string& s, int i, int j)
{
    while(i < j){
        if(s[i] != s[j]) return false;
        i ++; j--;
    }
    return true;
}

void backtracking(string& s, int index)
{
    if(index >= s.size()){
        res.push_back(path);
        return;
    }
    for(int i = index; i < s.size(); i++){
        if(is_reverse(s,index, i)){
            string str = s.substr(index, i - index + 1);
            path.push_back(str);
            backtracking(s, i + 1);
            path.pop_back();
        }
    }
}

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

②、复原ip地址

题目链接:复原ip地址

bool isValid(const string& s, int start, int end) {
        if (start > end) {
            return false;
        }
        if (s[start] == '0' && start != end) { // 0开头的数字不合法
                return false;
        }
        int num = 0;
        for (int i = start; i <= end; i++) {
            if (s[i] > '9' || s[i] < '0') { // 遇到非数字字符不合法
                return false;
            }
            num = num * 10 + (s[i] - '0');
            if (num > 255) { // 如果大于255了不合法
                return false;
            }
        }
        return true;
    }


void backtracking( string& s, int index, int pointnum)
{
    if(pointnum == 3){//第四个字符串是否合法
        if(isValid(s, index, s.size() - 1)) res.push_back(s);
        return;
    }
    for(int i = index; i restoreIpAddresses(string s) {
        backtracking(s, 0, 0);
        return res;
    }

小结

分割类型的问题,其思路基本都是将每一小部分判断的函数分离出来,然后将判断加入到主函数中,进行分割保存。

三、子集问题

1、子集Ⅰ

题目链接:子集Ⅰ

本题和组合总和有点相似,不同是我们集合要取得是每一层的集合,而组合总和只要符合条件的集合。所以我们path放入res的步骤改到了递归代码的下面。

vector> res;
vector path;

void backtracking(vector& nums, int index)
{
    if(index >= nums.size()) return;
    for(int i = index; i < nums.size(); i++){
        path.push_back(nums[i]);
        backtracking(nums, i + 1);
         res.push_back(path);
        path.pop_back();
    }
}

    vector> subsets(vector& nums) {
        backtracking(nums, 0);
        res.push_back(path);
        return res;
    }

上面的代码,我们的存储集合是由大到小的存储。

下面的代码,我们的存储集合是由小到大的存储。 

void backtracking(vector& nums, int index)
{
    res.push_back(path);
    if(index >= nums.size()) return;
    for(int i = index; i < nums.size(); i++){
        path.push_back(nums[i]);
        backtracking(nums, i + 1);
        path.pop_back();
    }
}

2、子集Ⅱ

题目链接:子集Ⅱ

本题去重逻辑和组合总和Ⅱ有很大相似。

①、used数组去重

vector> res;
vector path;

void backtracking(vector& nums, int index, vector& used)
{
    res.push_back(path);
    if(index >= nums.size()) return;
    for(int i = index; i < nums.size(); i++){
        if(i > 0 && used[i - 1] == false && nums[i - 1] == nums[i]) continue;
        path.push_back(nums[i]);
        used[i] = true;
        backtracking(nums, i + 1, used);
        used[i] = false;
        path.pop_back();
    }
}

    vector> subsetsWithDup(vector& nums) {
        vector used(nums.size(), false);
        sort(nums.begin(), nums.end());
        backtracking(nums, 0, used);
        return res;
    }

②、set去重

set去重比起used数组来说简单许多,这也取决于set本身的特性,善于利用stl容器可以使得代码简洁。

void backtracking(vector& nums, int index)
{
    res.push_back(path);
    if(index >= nums.size()) return;
    unordered_set uset;
    for(int i = index; i < nums.size(); i++){
        if(uset.find(nums[i]) != uset.end()){
            continue;
        }
        path.push_back(nums[i]);
        uset.insert(nums[i]);
        backtracking(nums, i + 1);
        path.pop_back();
    }
}

小结

子集问题其实和组合的代码很相似,区别就是,组合一般最后才放结果,而子集问题在每一次遍历都会放入结果,

子集Ⅰ主要就是一个数组五重复数字(不需要去重) 的数组求解。

子集Ⅱ是一个数组有重复数字(需要去重)的数组的子集求解。

四、排列问题

1、全排列Ⅰ

题目链接:全排列Ⅰ

排列的特点就是可以有重复的集合,但是每个集合中的数的顺序必须不一样。

本题特点就是当前层用过的数字在接下来的层就不可以用了,

也就是数组中的数不可重复使用(需要去重)。两个集合元素可以相同,但是顺序可以不同。

vector> res;
vector path;

void backtracking(vector& nums, int index, vector& used)
{
    if(path.size() == nums.size()){
        res.push_back(path);
        return;
    }
    for(int i = 0; i < nums.size(); i++){
        if(used[i] == true) continue;
        used[i] = true;
        path.push_back(nums[i]);
        backtracking(nums, i + 1, used);
        used[i] = false;
        path.pop_back();
    }
}

    vector> permute(vector& nums) {
        vector used(nums.size(), false);
        backtracking(nums, 0, used);
        return res;
    }

2、全排列Ⅱ

题目链接:全排列Ⅱ

本题目就是数组中有重复数字,同时重复数字可重复使用的题目。

使用used树层去重的逻辑是使用used[i - 1] == false。

使用used树枝去重的逻辑是使用used[ i ] == true。

vector> res;
vector path;

void backtracking(vector& nums, vector& used){
    if(path.size() == nums.size()){
        res.push_back(path);
        return;
    }
    for(int i = 0; i 0 && nums[i] == nums[i - 1] && used[i - 1] == false){
            continue;
        }
        if(used[i] == false){
            used[i] = true;
            path.push_back(nums[i]);
            used[i] = true;
            backtracking(nums, used);
            used[i] = false;
            path.pop_back();
        }
    }
}

    vector> permuteUnique(vector& nums) {
        vector used(nums.size(), false);
        sort(nums.begin(), nums.end());
        backtracking(nums, used);
        return res;
    }

小结

排列问题中for循环都是从 0开始的,同时used[]中的去重和组合中有相同的逻辑。

排列、组合、集合这三个部分代码有很多相似处,但是细节有很大的差别。

五、棋盘问题

1、N皇后

题目链接:N皇后

我们重点判断的函数单拿出来,别的部分和普通回溯的代码一摸一样。

判断重复的部分我们只探讨左上、右上、正上方是否重复,下方不需要判断,因为如果不合适,回溯部分会自动剔除不合适的情况。

(本题属于一维的递归)。

vector> res;

bool isvalid(int row, int col, vector& path, int n){
    for(int i = row - 1; i>=0; i--){//直线范围判重
        if(path[i][col] == 'Q') return false;
    }
    //45度角判重
    for(int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--){
        if(path[i][j] == 'Q') return false;
    }
    //135度角
    for(int i = row - 1, j = col + 1; i >= 0 && j < n; j++, i--){
        if(path[i][j] == 'Q') return false;
    }
    return true;
}

void backtracking(int n,vector& path, int row){
    if(row == n){
        res.push_back(path);
        return;
    }
    for(int col = 0; col < n; col++){
        if(isvalid(row, col, path, n)){
            path[row][col] = 'Q';
            backtracking(n, path, row + 1);
            path[row][col] = '.';
        }
    }
}


    vector> solveNQueens(int n) {
        vector path(n, string(n, '.'));
        backtracking(n, path, 0);
        return res;
    }

2、解数独

题目链接:解数独

这里最重要的就是需要同时判断行和列,N皇后是只需要判断每行,因此,解数独需要多加一层for循环,后面的步骤就和上面一样了,处理判断条件的函数isValid()。

之后,我们通过backtracking()的返回为真还是假来判断是否成立。最后的结果存在了board中。

(本题为二维的递归)

bool backtracking(vector>& board) {
    for (int i = 0; i < board.size(); i++) {        // 遍历行
        for (int j = 0; j < board[0].size(); j++) { // 遍历列
            if (board[i][j] == '.') {
                for (char k = '1'; k <= '9'; k++) {     // (i, j) 这个位置放k是否合适
                    if (isValid(i, j, k, board)) {
                        board[i][j] = k;                // 放置k
                        if (backtracking(board)) return true; // 如果找到合适一组立刻返回
                        board[i][j] = '.';              // 回溯,撤销k
                    }
                }
                return false;  // 9个数都试完了,都不行,那么就返回false
            }
        }
    }
    return true; // 遍历完没有返回false,说明找到了合适棋盘位置了
}
bool isValid(int row, int col, char val, vector>& board) {
    for (int i = 0; i < 9; i++) { // 判断行里是否重复
        if (board[row][i] == val) {
            return false;
        }
    }
    for (int j = 0; j < 9; j++) { // 判断列里是否重复
        if (board[j][col] == val) {
            return false;
        }
    }
    int startRow = (row / 3) * 3;
    int startCol = (col / 3) * 3;
    for (int i = startRow; i < startRow + 3; i++) { // 判断9方格里是否重复
        for (int j = startCol; j < startCol + 3; j++) {
            if (board[i][j] == val ) {
                return false;
            }
        }
    }
    return true;
}
public:
    void solveSudoku(vector>& board) {
        backtracking(board);
    }

小结

这类问题,我们涉及到一维还是二维的问题,同时,我们处理的主要逻辑还是回溯的思维,将判断成立条件的函数独立出来,最后实现解题,

六、其他问题

1、递增子序列

该搜索问题特点是单层不可重复使用相同元素,同时path.size() > 1 的部分都可以被收入答案。

单层使用set去重。

vector> res;
vector path;

void backtracking(vector& nums, int index)
{
    if(path.size() > 1){
        res.push_back(path);
    }
    unordered_set uset;
    for(int i = index; i < nums.size(); i++){
        if(!path.empty() && nums[i] < path.back() || uset.find(nums[i]) != uset.end()){
            continue;
        }
        uset.insert(nums[i]);
        path.push_back(nums[i]);
        backtracking(nums, i + 1);
        path.pop_back();
    }
}

    vector> findSubsequences(vector& nums) {
        backtracking(nums, 0);
        return res;
    }

总结

回溯整体来说,涉及一个递归前和递归后的处理,还有常用的模板,这些题目的模板多刷几次,思路理通顺了,见到这类问题可以很好的解决。回溯中的代码相似性还是很多的。

你可能感兴趣的:(算法,c++)