回溯算法总结 -组合篇

回溯算法主要分为五个部分
组合问题
分割问题
子集问题
排列问题
棋盘问题

  1. 组合问题
    在这里插入图片描述
    最简单的组合问题如上,组合问题可以抽象成一颗多叉树,结点中是当前可以取的数字的集合,当一条路径中所包含的数字数量到达k,则代表找到了一个输出。
    代码如下:
class Solution {
public:
    vector<vector<int>> result;
    vector<int> path;
    void backtracking(int n , int k , int startindex ) //每层的开始index
    {
        // 什么时候知道到达最后一层,该取出结果了呢,就是在path的长度到达k的时候
        if(path.size() == k)
        {
            result.push_back(path);
            return;
        }


        // 这个循环语句可以剪枝操作
        // for(int i = startindex ; i< n;i++)
        for(int i = startindex;i<=n-k+path.size()+1;i++)
        {
            path.push_back(i);
            // 相当于在树中下沉了一层
            backtracking(n,k,i+1);
            //递归返回后回到当前层,需要把当前层本次for循环压入的元素pop出
            path.pop_back();
        }
    }
    vector<vector<int>> combine(int n, int k) {
        //组合问题不考虑顺序,要去重
        result.clear();
        path.clear();
        backtracking(n,k,1);
        return result;

    }
};

for 循环中有剪枝操作, 因为在n个数中取k个数 , 那么应该保证 当前结点剩余的数字数量 >= 还需要的数字数量
即 k - path.size() <= n - i 。
startindex 在元素不能重复的时候需要用到。

在这里插入图片描述
这里的组合多出了 相加之和为n 的 k个数 ,那么找到一个组合的条件是path中有k个数的时候 && 这些数的sum是 target

递归函数的参数中 增加一个sum值,用来记录当前path中的所有数字的和,由于只需要向全局变量中的path和result增加元素,所以递归函数并不需要返回值
void backtracking (int k , int n , int startindex ,int sum)

class Solution {
public:
    vector<vector<int>> result;
    vector<int>path;
    void backtracking(int k ,int n ,int startindex ,int sum) //相加之和为n的k个数的组合
    {   
        //退出条件 size = k && sum = n
        if(path.size() == k )
        {
            if(sum == n)
            {
                result.push_back(path);
            }
            return;
        }

        for(int i = startindex ; i<=10 -k +path.size();i++)
        {
            path.push_back(i);
            sum += i;
            backtracking(k,n,i+1,sum);
            sum -= i;
            path.pop_back();
        }
    }
    vector<vector<int>> combinationSum3(int k, int n) {
        result.clear();
        path.clear();
        backtracking(k,n,1,0);
        return result;
    }
};

电话号码的组合
在这里插入图片描述
这里对于不同的输入,有不同的组合,这里由于使用的不是同一个序列,所以不需要startindex来防止重复取。
首先用一个二维数组来存放各个数位之间的对应关系

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

树的同一层都在同一个数组元素中取字符,递归往下一层的时候才会转到另一个字符串,所以这里需要一个参数来记录当前所在层次,层次可以用来取字符串。

所以递归函数定义如下:
void backtracking( string digits , int level) // level代表目前操作的是digits的第几个位置,也代表目前树递归到了第几层。

class Solution {
public:
    // 数字到字母的映射
    // 1 * # 等异常情况
    // std::map<int, std::string> map {
    //     {0, ""}, {1, ""},
    //     {2, "abc"}, {3, "def"},
    //     {4, "ghi"}, {5, "jkl"},
    //     {6, "mno"}, {7, "pqrs"},
    //     {8, "tuv"}, {9, "wxyz"}
    // };
    const string nums[10]{
    "", // 0
    "", // 1
    "abc", // 2
    "def", // 3
    "ghi", // 4
    "jkl", // 5
    "mno", // 6
    "pqrs", // 7
    "tuv", // 8
    "wxyz", // 9
    };
    vector<string> result;
    string path;
    void backtracking(string &digits ,int level )
    {
        //退出条件
        if(path.size()==digits.size())
        {
            result.push_back(path);
            return;
        }
        int index = digits[level] - '0'; // 取出digits中的数字
        string s = nums[index]; // 找到这个数字代表的字符串
        // 这里不需要startindex是因为操作的是不同的字符串
        for(int i = 0 ; i< s.size();i++ )
        {
            path.push_back(s[i]);

            backtracking(digits,level+1);
            path.pop_back();
        }
    }
    vector<string> letterCombinations(string digits) {
        if(digits.size() == 0)
        return result;
        backtracking(digits,0); 
        return result;
        
    }
};

在这里插入图片描述
这里的组合总和和上面的不同在于这里的数字可以无限重复取。
一个集合来求组合就会需要startindex
这里依旧需要一个startindex 来保证同层的元素不会取到一样的数字
( for循环代表同一层 , 在for循环中的递归是往下深入的) , 但是在当前的下一层中,依旧可以取到上层取过的数字。

class Solution {
public:
    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();
        }

    }

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

在这里插入图片描述

这题与上题的不同在于,元素之间是有重复的,对于一个组合来说,在同一树层上不能取一样的元素,因为这会导致有多个相同的子树,导致最终的结果集中会有多个相同的集合。
startindex只能保证startindex之前的数字不会再被取到,但是当存在重复的数字时,只使用startindex无法保证后面相同的数字不被取到,所以需要另外的标记来进行去重。
首先先对整个序列进行排序。

在当前树层的for循环中,需要对使用过的相同元素进行去重,考虑使用一个used数组来标识当前元素是否被选中过。
假设存在两个相邻的相同元素, 那么在同一树层中 它们表现为nums[i] = nums[i-1] ,在同一树层中,nums[i-1]会比nums[i]更先被遍历到,所以 当nums[i-1] 在本层进入递归的时候, used[i-1]为true , 而当它从递归返回到本层的时候,由于回溯, 它会被设为false ,所以,若 (nums[i-1] == nums[i] && used[i-1] == false)的时候,代表nums[i-1]在本层已经被选中过了,序列后面与它相同的元素都不应该再被选中。

class Solution {
public:
    vector<vector<int>> result;
    vector<int> path;
    
    void backtracking(vector<int>& candidates ,int target,int sum , int startindex ,vector<bool>&used)
    {
        if(sum > target) return;
        if(sum == target){
            result.push_back(path);
            return;
        }
        // 不同层次间可以选一样的数字, 同层之间不能选一样的
        for(int i = startindex; i < candidates.size() ;i++)
            //去重逻辑
            //used 数组中,若candidates[i] = candidates[i-1],那么对cadidates[i] 来说 ,used[i-1] = true ,代表当前树枝上,前一个相同值使用过,那么树枝上相同值是可以再使用的
            //             used[i-1] = false,代表当前树层上,前一个相同值使用过,那么同一树层上不能再使用,防止重复的set出现
        {   if(i > 0 && candidates[i] == candidates[i-1] && !used[i-1])
            {
                continue;
            }
            sum += candidates[i];
            used[i] = true; 
            path.push_back(candidates[i]);
            backtracking(candidates,target,sum,i+1 ,used); 
            sum -= candidates[i];
            used[i] = false;
            path.pop_back();
        }

    }
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
        vector<bool> used(candidates.size(), false);
        result.clear();
        path.clear();
        sort(candidates.begin(),candidates.end());
        backtracking(candidates,target,0,0,used);
        return result;
    }
};

那么到现在, 组合问题就已经结束了。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值