代码随想录DAY22 - 回溯算法 - 08/21

目录

理论回顾

什么是回溯法

回溯法的效率

回溯法解决的问题

如何理解回溯

组合

题干

思路和代码

递归法

递归优化:剪枝

组合总和Ⅲ

题干

思路和代码

递归法

递归优化

电话号码的字母组合

题干

思路和代码

递归法


理论回顾

什么是回溯法

回溯是一种类似枚举的搜索方法,回溯和递归相辅相成。

回溯法的效率

回溯法本质是穷举,也就是检索所有可能最后才找出结果,因此效率并不高。一般为了提高效率都会进行剪枝操作。

回溯法解决的问题

回溯法,一般可以解决如下几种问题:

  • 组合问题:N个数里面按一定规则找出k个数的集合

  • 切割问题:一个字符串按一定规则有几种切割方式

  • 子集问题:一个N个数的集合里有多少符合条件的子集

  • 排列问题:N个数按一定规则全排列,有几种排列方式

  • 棋盘问题:N皇后,解数独等等

如何理解回溯

回溯解决的问题可以抽象为树形结构,因为回溯法解决的都是在集合中递归查找子集,集合的大小就构成了树的宽度,递归的深度就构成了树的深度。递归就要有终止条件,所以必然是一棵高度有限的树(N叉树)。

组合

题干

题目:给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。你可以按 任何顺序 返回答案。

链接:. - 力扣(LeetCode)

思路和代码

递归法

将问题拆解为先选定组合中的一个数,先记录包含这个数且满足条件的组合,再递归从剩余的 n-1 个数中找组合。

  • 递归参数和返回值:递归参数是集合大小 n,组合大小 k,组合起始位置 startIndex;在递归过程中不断更新组合和结果集。

  • 递归结束的条件:当组合数组已经达到题目要求的组合大小,说明一个组合已经被找到,将该组合插入结果集后返回。

  • 递归顺序:先确定组合的起始元素 startIndex,再递归从剩余的 n - startIndex 个元素里继续填充组合。

class Solution {
public:
    vector<int> composition; // 记录组合
    vector<vector<int>> result; // 记录所有组合结果
    void backTracking(int n, int k, int startIndex){
        if (composition.size() == k){
            // 当组合大小已满足 k,说明已经找到一个组合,插入结果集
            result.push_back(composition);
            // 返回到上一层
            return;
        }
        for (int i = startIndex; i <= n; ++i) {
            composition.push_back(i); // 填充组合的一个位置,接下来要填充下一个位置
            backTracking(n,k,i+1); // 找起点为 j+1 的组合数
            composition.pop_back(); // 回溯的时候要记得弹出之前插入的数
        }
    }
    vector<vector<int>> combine(int n, int k) {
        backTracking(n,k,1); // 起始从 1 开始
        return result;
    }
};
递归优化:剪枝

当集合中剩余的元素已经没法凑够组合大小时,就不必再进入循环找组合中了。也就是说,只有当 组合中已有的元素个数 + 集合中剩余的元素个数 ≥ 组合大小 时,即当 composition.size() + (n-i+1) >= k 时才需要进入循环。

class Solution {
public:
    vector<int> composition; // 记录组合
    vector<vector<int>> result; // 记录所有组合结果
    void backTracking(int n, int k, int startIndex){
        if (composition.size() == k){
            // 当组合大小已满足 k,说明已经找到一个组合,插入结果集
            result.push_back(composition);
            // 返回到上一层
            return;
        }
        for (int i = startIndex; composition.size()+(n-i+1) >= k; ++i) { // for 循环条件中进行剪枝
                                // 修改了进入循环的条件!!
            composition.push_back(i); // 填充组合的一个位置,接下来要填充下一个位置
            backTracking(n,k,i+1); // 找起点为 j+1 的组合数
            composition.pop_back(); // 回溯的时候要记得弹出之前插入的数
        }
    }
    vector<vector<int>> combine(int n, int k) {
        backTracking(n,k,1); // 起始从 1 开始
        return result;
    }
};

组合总和Ⅲ

题干

题目:找出所有相加之和为 nk 个数的组合,且满足下列条件:

  • 只使用数字 1 到 9

  • 每个数字 最多使用一次

返回 所有可能的有效组合的列表 。该列表不能包含相同的组合两次,组合可以以任何顺序返回。

链接:. - 力扣(LeetCode)

思路和代码

从 1~9 九个数字中选 k 个数组合,若组合之和满足 n ,则插入结果集。

递归法

在上一题的基础上,找出每一个组合,如果组合之和为 n,才插入结果集,否则直接返回。

class Solution {
public:
    int sum = 0; // 记录组合数之和
    vector<int> composition; // 记录组合
    vector<vector<int>> result; // 记录所有组合的结果
    void backTracking(int k, int n, int startIndex){
        if (composition.size() == k){
            if (sum == n){ // 当组合之和 sum 等于目标和 n 时,才插入结果集!
                result.push_back(composition);
            }
            return;
        }
        for (int i = startIndex; i <= 9; ++i) {
            composition.push_back(i);
            sum += i;
            backTracking(k,n,i+1);
            composition.pop_back();
            sum -= i;
        }
    }
    vector<vector<int>> combinationSum3(int k, int n) {
        backTracking(k,n,1);
        return result;
    }
};
递归优化

原来的集合中包含 1~9 九个数字,且是升序的,而我们是从前往后、从小到大寻找组合,当组合之和 sum 已经大于目标和 n 时,说明后续的组合只会更大,不需要再进入循环中了。

class Solution {
public:
    int sum = 0; // 记录组合数之和
    vector<int> composition;
    vector<vector<int>> result;
    void backTracking(int k, int n, int startIndex){
        if (composition.size() == k){
            if (sum == n){
                result.push_back(composition);
            }
            return;
        }
        for (int i = startIndex; composition.size()+(9-i+1) >= k && sum+i <= n; ++i) { // 剪枝
            					// 同理,第一个剪枝操作是当剩余的元素已经凑不够组合大小时,停止循环
            					// 第二个剪枝操作是若 组合之和 已经大于 n 了,就没必要进入循环了
            composition.push_back(i);
            sum += i;
            backTracking(k,n,i+1);
            composition.pop_back();
            sum -= i;
        }
    }
    vector<vector<int>> combinationSum3(int k, int n) {
        backTracking(k,n,1);
        return result;
    }
};

电话号码的字母组合

题干

题目:给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。

给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

说明:

  • 0 <= digits.length <= 4

  • digits[i] 是范围 ['2', '9'] 的一个数字。也就是说不会输入除了 2 ~ 9 以外的字符

链接:. - 力扣(LeetCode)

思路和代码

首先要知道对应的数字都能映射哪些字母,那么就建立一个 map,键值对为 {数字,字符串(能映射的所有字母)}。这道题本质上也是找组合,只不过找的是不同字符的组合,且在找组合之前得先映射。

递归法

递归法的思路是将问题分解为:每次先确认传入的字符串中第一个数字对应的字符,再递归去确认字符串之后的数字对应的字符。

class Solution {
public:
    unordered_map<int,string> numsToChar{
        {2,"abc"},{3,"def"},{4,"ghi"},{5,"jkl"},
        {6,"mno"},{7,"pqrs"},{8,"tuv"},{9,"wxyz"}
    };
    string composition; // 存储组合
    vector<string> result; // 记录所有组合的结果
    void backTracking(string digits){
        if (digits.size() == 0){
            // 当传入的字符串为空,说明已经找到一个组合,插入结果集
            result.push_back(composition);
            return;
        }
        
        int num = digits[0]-'0'; // 每次都要确认第一个数字
        string s = numsToChar[num]; // 将数字映射为字符串
        for (char c : s) {
            composition.push_back(c); // 已经插入一个字符
            string newDigits;
            // 调整 digits,而不是改 startIndex
            if (digits.size() > 1) newDigits = digits.substr(1); // 要从剩余的字符串中寻找元素插入组合
            else newDigits = "";
            backTracking(newDigits); // 继续递归寻找元素
            composition.pop_back();
        }
    }
    vector<string> letterCombinations(string digits) {
        if (digits.empty()) return {};
        backTracking(digits);
        return result;
    }
};

  • 8
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值