DAY11

回溯原理以及伪代码

// 递归出口
if (//终⽌条件) {
 //存放结果;
 return;
}
for (//选择:本层集合中元素(树中节点孩⼦的数量就是集合的⼤⼩)) {
 //处理节点;
 backtracking(//路径,选择列表); // 递归
 //回溯,撤销处理结果
}
void backtracking(//参数) 
{
    if (// 终止条件) 
    {
        // 存放结果;
        return;
    }
    for (// 选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) 
    {
        // 处理节点;
        backtracking(// 路径,选择列表); // 递归
        // 回溯,撤销处理结果
    }
}

套用模板

组合

题目一

某人准备跑20圈来锻炼自己的身体,他准备分多次(>1)跑完,每次都跑正整数圈,然后休息下再继续跑。 为了有效地提高自己的体能,他决定每次跑的圈数都必须比上次跑的多 设第一次圈数不能小于0,那么请问他可以有多少种跑完这 20 圈的方案? 输出方案总数,以及每种方案的排序。(比如1,19/ 1,2,17 都是有效方案)
#include <iostream>
#include <vector>
using namespace std;
#define Num 20
// 跑20圈
// 每次都跑整数圈
// 后一次比前一次多
// 第一次不能小于0
// 1,19 | 1,2,17
​
void dfs(int times, int num, vector<int> &temp, vector<vector<int>> &result)
{
    // 递归出口
    if(times == 0)
    {
        result.push_back(temp);
        return; 
    }
    // 横向递归,从1到19开始穷举
    for(int i = num + 1; i <= times; ++i)
    {
        temp.push_back(i);
        // 自增
        dfs(times-i,i,temp,result);
        // 回溯
        temp.pop_back();
    }
}
int main()
{
    vector<int> temp;
    vector<vector<int>> result;
    dfs(Num, 0, temp, result);
    cout << result.size() << "\n";
    for(auto & i : result)
    {
        for(auto & j : i)
        {
            cout << j << "  ";
        }
        cout << "\n";
    }
}

题目二

给定两个整数 n 和 k,返回 1 ... n 中所有可能的 k 个数的组合。

先横向遍历然后对剩余的结果进行穷举,每次递归前首先判断递归出口,满足该次纵向遍历就结束了,否则继续穷举。横向遍历确定每一种情况。

class Solution {
public:
    void dfs(int n, int currNum, vector<int>&path,vector<vector<int>>&result,int k)
    {
        // 递归出口
        if(path.size() == k)
        {
            result.push_back(path);
            return;
        }
        // 横向递归
        for(int i = currNum + 1; i <= n;++i)
        {
            // 处理
            path.push_back(i);
            // 纵向递归
            dfs(n, i,path,result,k);
            // 回溯
            path.pop_back();
        }
    }
    vector<vector<int>> combine(int n, int k) {
        vector<int> path;
        vector<vector<int>> result;
        dfs(n,0,path,result,k);
        for(auto &i: result)
        {
            for(auto & j : i)
            {
                cout << j << "  ";
            }
            cout << "\n";
        }
        return result;
    }
};

题目三

找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。
说明:
所有数字都是正整数。
解集不能包含重复的组合。
class Solution {
public:
    void dfs(int n, int currNum, vector<int>&path,vector<vector<int>>&result,int k)
    {
        // 递归出口
        if(path.size() == k && n == 0)
        {
            result.push_back(path);
            return;
        }
        // 横向递归
        for(int i = currNum + 1; i <= 9;++i)
        {
            // 处理
            path.push_back(i);
            // 纵向递归
            dfs(n-i, i,path,result,k);
            // 回溯
            path.pop_back();
        }
    }
    vector<vector<int>> combinationSum3(int k, int n) {
        vector<int> path;
        vector<vector<int>> result;
        dfs(n, 0, path, result, k);
        for(auto &i: result)
        {
            for(auto & j : i)
            {
                cout << j << "  ";
            }
            cout << "\n";
        }
        return result;
    }
};

从上面三题来看,这一种类型的题目的重心是确定递归出口,以及每一次横向递归的for循环的参数的设置

由横向递归来区分每一种情况,确保组合的每个数都不会相同

题目4

// 给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
// 给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

class Solution {
private:
    unordered_map<int, vector<char>> keypad;
public:
    void InitKeyPad() {
        keypad[2] = {'a', 'b', 'c'};
        keypad[3] = {'d', 'e', 'f'};
        keypad[4] = {'g', 'h', 'i'};
        keypad[5] = {'j', 'k', 'l'};
        keypad[6] = {'m', 'n', 'o'};
        keypad[7] = {'p', 'q', 'r', 's'};
        keypad[8] = {'t', 'u', 'v'};
        keypad[9] = {'w', 'x', 'y', 'z'};
    }
    void dfs(string& str, vector<string>& result, string digits, int index) {
        // 递归出口
        if (str.size() == digits.size()) {
            result.push_back(str);
            return;
        }
        // 横向递归确保选取每个vector数组的不同字符
        // 纵向递归确保选取不同的vector数组
        int num = digits[index] - '0';
        cout << "num = " << num << "\n";
        vector<char> vec = keypad[num];
        cout << vec.size() << "\n";
        for (int j = 0; j < vec.size(); ++j) {
            // 处理逻辑
            str.push_back(vec[j]);
            // 纵向递归
            dfs(str, result, digits,index+1);
            // 退回上一步
            str.pop_back();
        }
    }
    vector<string> letterCombinations(string digits) {
        InitKeyPad();
        vector<string> result;
        string str;
        int index = 0;
        cout << digits.size() << "\n";
        if (digits.size() == 0) {
            return result;
        }
        dfs(str, result, digits, index);
        for (auto& i : result) {
            cout << i << "  ";
        }
        return result;
    }
};

这一题与上述的三题是不一样的,特殊点在于有两个递归出口

其次是横向递归,之前的题目,横向递归是为了确保选取不同的字符或者数字,纵向递归是在剔除了第一次选取的元素的数组上进行操作。

而这一题横向递归依旧是在剔除了前一个元素的数组上进行操作,但是该次for循环的操作是在不同的数组上进行操作的,使用index来确保了前一次递归和后一次递归所操作的数组是不一致的

使用递归for循环是确保穷举出所有情况。

题目5

//给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
//candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。 
//对于给定的输入,保证和为 target 的不同组合数少于 150 个。
//示例 1:
//输入:candidates = [2,3,6,7], target = 7
//输出:[[2,2,3],[7]]
//解释:
//2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。
//7 也是一个候选, 7 = 7 。
//仅有这两种组合。
class Solution {
private:
    vector<int>path;
    vector<vector<int>> result;
public:
    void dfs(vector<int> &candidates, int target, int startNum)
    {
        // 递归出口
        if(target == 0)
        {
            result.push_back(path);
            return;
        }
        if(target < 0)
        {
            return;
        }
        // 可以重复选取参数
        // 穷举
        for(int i = startNum; i < candidates.size(); ++i)
        {
            // 处理逻辑
            path.push_back(candidates[i]);
            // 分割数组的操作是由dfs的参数startNum决定的
            dfs(candidates,target - candidates[i],i);
            path.pop_back();
        }
    }
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        path.clear();
        result.clear();
        dfs(candidates,target,0);
        for(auto &i : result)
        {
            for(auto &j : i)
            {
                cout << j << "  ";
            }
            cout << "\n";
        }
        return result;
    }
};

这一题与1-3题的明显区别就是回溯的时候使用的数组不再进行切割,而是否进行重复选取的核心决定于纵向递归的参数startNum,横向操作只是为了穷举出所有情况。

通过上述五题梳理一下思路,遇到这种组合题,首先定义递归出口,无论成功还是失败都会回溯到上一步继续执行剩余的操作,区别是成功的情况会添加到result中,失败是直接返回

横向递归是为了定义从数组的哪一个位置开始穷举,而纵向递归是为了穷举当前位置开始的出所有的情况。无论成功还是失败都会进行回溯,回溯后,横向递归重新定义起始位置,这要根据回溯的情况来判断。

// 输入: k = 3, n = 7
// 输出: [[1,2,4]]
// 解释:
// 1 + 2 + 4 = 7

首先正常情况是1,2,3,......,9,一共递归了9层,在第9层时for循环不符合结束,回溯到第8层,此时i = 8,让i取9结果就变成了1,2,3,4,5,6,7,9;在第8层for循环退出,以此往复,一直回溯到第3层发现分支4是成功的,走到递归出口,然后继续执行这一层的for循环,直到1,2,9为止退出,最终回溯到第一层,再接着从2开始,2,3,4,5,6,7,8,9重复上述的过程。

为什么path.pop_back()会导致回溯呢?

首先递归用一个形象化的故事来解释

假如你坐在电影院的最后一排看电影,小美坐在第一排,你没有带手机却想告诉小美,晚上一起去吃火锅。

你需要拍一拍前面一个人的肩膀说,你能帮我带个话吗?我想请第一排的小美吃火锅。于是前面的人拍了拍后面一个人的肩膀重复这句话,直到第二排的人拍了小美的肩膀传递了这句话,并等待小美的回复。此时所有人都在等待这个小美的回复才能继续进行后续的回复。当小美回复说可以,第二排的人回头告诉第三排,以此往复,你终于收到了小美的答复。

第8层的递归(第8层的for循环)只有获取到了第9层的结果才能继续往下执行(执行for循环,获取下一个参数),而path.pop_back()就是为了保持第8层的穷举结果的

整体的逻辑就是先穷举后判断,打破穷举的唯一途径就是该层的for循环被打破,导致函数执行结束。

横向递归是为了确保每次穷举的数组的情况,而纵向递归是穷举的深度,即递归到第几层。

所以横向递归最重要的是判断条件,它决定了每次穷举数组的宽度,纵向递归根据是否选取可重复参数来设置横向递归,更倾向于递归层度之间的联系来设置横向递归

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值