数据结构与算法学习day21-回溯法-组合、电话号码的字母组合、组合总和、组合总和II

一、组合

1.题目

. - 力扣(LeetCode)

2思路

把组合问题抽象成树形结构(N叉树)

每次从集合中选取元素,可选择的范围随着选择的进行而收缩,调整可选择的范围

图中可以发现n相当于树的宽度,k相当于树的深度

回溯法的搜索过程就是一个树型结构的遍历过程,在如下图中,可以看出for循环用来横向遍历,递归的过程是纵向遍历。

那么如何在这个树上遍历,然后收集到我们要的结果集呢?

图中每次搜索到了叶子节点,我们就找到了一个结果

相当于只需要把达到叶子节点的结果收集起来,就可以求得 n个数中k个数的组合集合。

按照递归法写法去写回溯法即可。

1.递归函数的返回值以及参数

函数里一定有两个参数,既然是集合n里面取k个数,那么n和k是两个int型的参数。

然后还需要一个参数,为int型变量startIndex,这个参数用来记录本层递归的集合从哪里开始遍历,防止取到重复值。

2.回溯函数终止条件

path这个数组的大小如果达到k,说明我们找到了一个子集大小为k的组合了,在图中path存的就是根节点到叶子节点的路径。

3.单层搜索的过程

for循环每次从startIndex开始遍历,然后用path保存取到的节点i。

可以看出backtracking(递归函数)通过不断调用自己一直往深处遍历,总会遇到叶子节点,遇到了叶子节点就要返回。backtracking的下面部分就是回溯的操作了,撤销本次处理的结果。

总体代码:

class Solution {
private:
    vector<vector<int>> result; // 存放符合条件结果的集合
    vector<int> path; // 用来存放符合条件结果
    void backtracking(int n, int k, int startIndex) {
        if (path.size() == k) {
            result.push_back(path);
            return;
        }
        for (int i = startIndex; i <= n; i++) {
            path.push_back(i); // 处理节点
            backtracking(n, k, i + 1); // 递归
            path.pop_back(); // 回溯,撤销处理的节点
        }
    }
public:
    vector<vector<int>> combine(int n, int k) {
        backtracking(n, k, 1);
        return result;
    }
};

二、电话号码的字母组合

1.题目

17. 电话号码的字母组合 - 力扣(LeetCode)

2.思路

2.1 数字和字母如何映射

定义一个二维数组letterMap来映射。

同时需要把字符转换成数字

2.2 回溯法来解决n个for循环的问题

回溯三部曲:

1.传入参数

参数指定是有题目中给的string digits,然后还要有一个参数就是int型的index。这个index是记录遍历第几个数字了,就是用来遍历digits的(题目中给出数字字符串),同时index也表示树的深度。

2.终止条件

当遍历到字符串最后一个字符时,把子集加入到结果中。

3.单层遍历逻辑

把字符转换为数字,同时映射出字符串。用for循环进行遍历目前字符串。需要注意的是,每次遍历字符串从0开始,因为这道题是多个字符串的组合题。上面那题属于单个数组的组合题。

总体代码:

class Solution {
private:
    const string letterMap[10] = {
        "", // 0
        "", // 1
        "abc", // 2
        "def", // 3
        "ghi", // 4
        "jkl", // 5
        "mno", // 6
        "pqrs", // 7
        "tuv", // 8
        "wxyz", // 9
    };
    
public:
    vector<string> result;
    string s;
    //index为处理的位数
    void backtracking(const string& digits,int index){
        if(index == digits.size()){
            result.push_back(s);
            return;
        }

        //转换成数字
        int  digit = digits[index] - '0';
        string temp = letterMap[digit];
        for(int i = 0;i < temp.size();i++){
            s.push_back(temp[i]);
            backtracking(digits,index+1);
            s.pop_back();
        }
    }

    vector<string> letterCombinations(string digits) {
        if(digits.size() == 0) return result;

        backtracking(digits,0);
        return result;
    }
};

 三、组合总和

1.题目

39. 组合总和 - 力扣(LeetCode)

2.思路

本题没有数量要求,可以无限重复,但是有总和的限制,所以间接的也是有个数的限制。为了不重复,只能往后取数字。

2.1 传入参数

除了原来给的两个参数candidate和target,另外加了sum和index。sum用来计算取的值总和大小,index用来标记for循环的开始位置。

本题还需要Index来控制for循环的起始位置,对于组合问题,什么时候需要Index呢?

我举过例子,如果是一个集合来求组合的话,就需要startIndex,例如:77.组合 (opens new window)216.组合总和III (opens new window)

如果是多个集合取组合,各个集合之间相互不影响,那么就不用startIndex,例如:17.电话号码的字母组合

2.2 终止条件

如果sum大于target,则回溯。

如果sum等于target,则加入result。

2.3 单层循环逻辑

因为是一个集合,所以依然是从index开始遍历。sum加上当前值,并把当前值加入到path子集,进行下一步的元素寻找。注意从i开始寻找,因为题目可以选取重复元素。大于或者等于则返回,进行回溯,同时sum要把当前值减掉。

 

总体代码: 

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

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

    }

    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        backtracking(candidates,target,0,0);
        return result;
    }
};

四、组合总和II

1.题目(去重)

40. 组合总和 II - 力扣(LeetCode)

2.思路

这道题目和39.组合总和 (opens 如下区别:

  1. 本题candidates 中的每个数字在每个组合中只能使用一次。
  2. 本题数组candidates的元素是有重复的,而39.组合总和 (opens new window)是无重复元素的数组candidates

所以这道题需要去重。

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

那么问题来了,我们是要同一树层上使用过,还是同一树枝上使用过呢?

回看一下题目,元素在同一个组合内是可以重复的,怎么重复都没事,但两个组合不能相同。

所以我们要去重的是同一树层上的“使用过”,同一树枝上的都是一个组合里的元素,不用去重

为了理解去重我们来举一个例子,candidates = [1, 1, 2], target = 3,(方便起见candidates已经排序了)

强调一下,树层去重的话,需要对数组排序!

2.1 传入参数

与组合总和相比,多了一个布尔类型的used数组,用来记录元素是否在同一树枝上的元素是否使用过。

2.2 终止条件

当sum==target时,result进行收集。大于target时,则直接返回。

2.3 单层循环逻辑

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

如果candidates[i] == candidates[i - 1] 并且 used[i - 1] == false,就说明:前一个树枝,使用了candidates[i - 1],也就是说同一树层使用过candidates[i - 1]。(因为进行了回溯,会把的该元素标记成false)

此时for循环里就应该做continue的操作。

如果candidates[i] == candidates[i - 1] 并且 used[i - 1]  == true,说明同一个树枝上用了相同元素。(因为进行了递归,上一个元素依然为true)

 

需要强调的是,原集合一定要排序!

 

总体代码: 

class Solution {
private:
    vector<vector<int>> result;
    vector<int> path;
    void backingtracking(vector<int>& candidates, int target,int sum,int startIndex, vector<bool>& used){
        //终止条件
        if(sum == target){
            result.push_back(path);
            return;
        }
        if(sum > target){
            return;
        }

        //单层循环逻辑
        for(int i = startIndex;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;
            //递归
            backingtracking(candidates,target,sum,i+1,used);
            //回溯
            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();
        //排序
        sort(candidates.begin(),candidates.end());
        backingtracking(candidates,target,0,0,used);
        return result;
    }
};

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值