首页 > 其他分享 >回溯-组合

回溯-组合

时间:2024-04-11 20:45:53浏览次数:34  
标签:index target 组合 int candidates result 回溯 path

回溯-组合

回溯法的本质是穷举,其主要是为了解决n个for循环的问题,在for循环中套着递归从而不断实现for循环。回溯法解决的问题都可以抽象为树形结构,一般是在给定集合是取出符合一定规则的数据。集合的大小构成树的宽度,也即第一层的for循环遍历的范围,递归的深度构成树的深度,也就暴力for循环实现的层数。

可以理解为回溯实现不同层级的for循环,通过递归函数进入下一层,同时控制每层的遍历范围。一旦找到合适的子结果或者当前结果到达最底层,终止当前遍历并返回上一层继续for 循环遍历。回溯实际上是暴力搜索,减少其复杂度的方式主要靠剪枝优化。

回溯法的模板为:

void backtracking(参数){
    if(终止条件){
        存放结果;
        return;//返回上一层for循环
    }
    for(选择:本层集合中的元素(树中节点孩子的数量))//也即当前层for循环的遍历范围{
    	处理节点;
        backtracking(路径,选择列表);//实现递归
    	回溯,撤销处理结果
	}
}

组合问题是回溯算法中常见的问题,大体包括在一个或者多个集合中选择元素,元素选择可否重复选择等问题,对字符串实现分割使其满足条件也是组合问题。

Part1 单集合和多集合

leetcode77

这个是在单个集合里寻找满足条件的组合,而且是不可重复选择的

该问题可抽象为如下树结构:

其代码如下:

Class Solution(){
private:
    vector<vector<int>> result;//保存结果
    vector<int> path;//单个路径的叶子节点
    void backtracking(int n,int k,int index){
        if(path.size()==k){
			result.push_back(path);//保存符合条件的叶子节点
            return;
        }
        for(int i = index;i<=n;i++){//设置index是为了控制每层for循环的起始位置
            path.push_back(i);//保存当前的节点
            backtracking(n,k,i+1);//进入下一层for循环,同时因为是不重复取元素,故下一层的for循环从第i+1开始
            path.pop_back();//当前叶子节点已被保存,回退到上一层的for循环遍历,故需要撤销元素,使之返回到上一层,例如当n=4,k=2时,[1, 2]这个叶子节点被保存,此时删除2,是为了保存下一个叶子节点即[1,3]
        }
    }
public:
    vector<vector<int>> combine(int n,int k){
        backtracking(n,k,1);
        return result;
    }
};

上述代码存在剪枝优化的余地,考虑我们需要的是k个数的组合,因此在第i层for循环遍历的时候,此时路径path已经保存了i-1个元素,还需要k-i+1个元素,所以第i层for循环遍历的终止位置应该是n-(k-i+1)+1=n-(k-i),即n-(k-(path.size()+1))=n-(k-path.size())+1,因此for循环可以改为

for(int i = index;i <= n-(k-path.size()))+1;i++)

由于共有\(C^k_n\)种组合,每种组合需要\(O(k)\)的时间复杂度,因此时间复杂度为\(O(C^k_n\times K)\).空间复杂度取决于递归深度,即系统栈所用空间为\(O(n)\).
leetcode17

与上述单个集合中求组合的问题不同的是,这个问题是在多个集合中求组合。具体的不同是在for循环中遍历起始位置的不同。

class Solution{
private:
    const string letterMap[10]={
        " ",
        " ",
        "abc",
        "def",
        "ghi",
        "jkl",
        "mno",
        "pqrs",
        "tuv",
        "wxyz",
    };
public:
    vector<string> result;
    string s;
    void backtracking(const string& digits,int index){
        if(index == digits.size()){//index即按键字符串索引,用以进入不同的按键子集
            result.push_back(s);
            return;
        }
        int digit = digits[index]-'0';// 将当前按键数字变为int型
        string letter = letterMap[digit];//取按键对应的字符串
        for(int i = 0;i<letter.size();i++){//由于是不同集合的组合,故每层for循环只需要遍历完当前子集的整个范围即可。
            s.push_back(letter[i]);
            backtracking(digits,index+1);//进入下一个按键的子集
            s.pop_back();
        }
    }
    vector<string> letterCombinations(string digits){
        if(digits.size()==0){
			return result;
        }else{
            backtracking(digits,0);
            return result;
        }
    }
};

该算法的时间复杂度是\(O(3^m\times 4^n)\),其中m为输入对应3个字母的数字个数,n为输入对应4个字母的数字个数,空间复杂度则是\(O(m+n)\),空间复杂度主要取决于哈希表以及递归调用层数。

总结:何时需要index控制for循环遍历的起始位置?

一个集合求组合,需要index;

多个集合求组合,集合之间互相不影响,不需要index.

Part2 重复与去重

LeetCode39:组合总和

给定无重复元素的数组candidates和目标数 target,找出candidates中所有可以使数字和为target的组合。candidates中的数字可以被无限制选取。解集不可包含重复的组合

由于数字可被无限制选取,故树的深度是由target制约的。一旦元素总和超过target,则返回至上一层for循环。由于这个是单集合中选取组合,仍然需要index控制每层for循环的起始位置,至于集合中元素可以被无限制选取,则对于第i层循环,遍历到第i 个元素时,控制下一层的for循环起始位置为i,则可实现集合元素的无限制选取。

class Solution{
private:
    vector<vector<int>> result;
    vector<int> path;
    void backtracking(int sum,int target,const vector<int>& candidates,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(sum,target,candidates,i);//i表示可以重复读取当前的元素
            sum -= candidates[i];
            path.pop_back();
        }
    }
public:
    vector<vector<int>> combinationSum(vector<int>& candidates, int target){
		backtracking(0,target,candidates,0);
    	return result;
	}
}

这种求和问题,我们可以利用排序来优化剪枝,排序后一旦当前求和sum大于target,该层for循环后面的遍历全部是满足大于sum的,因此就可以退出该层。因此对总集合排序后,如果本层的sum+candidates[i]大于 target,则可以结束本层的for循环。

for(int i = index;i<candidates.size()&&sum+candidates[i] <= target;i++)
//candidates需要先排序再进行回溯

Leetcode 40 组合总和II

给定数组candidates (存在重复数字)和一个目标数target,找出candidates中所有可以使数字和为target的组合,candidates中的每个数字在每个组合中只能使用一次。解集中不能包含重复的组合。

难点在于集合即数组candidates中存在重复元素,但是解集中不能有重复的组合。树形结构去重需要考虑两个维度,即同一个树枝和同一个树层,由于每个元素只能使用一次,利用i+1则可以实现每个元素的不重复使用。由于解集中不能包含重复的组合,需要同一树层上所取元素进行去重,例如[1,1,2],取第一个1之后选择[1,2], 取第二个1之后同样再选择[1,2],这样就会造成解集中包含相同的组合。对树层去重,还需要先对集合排序,如图:

image-20240411164217628
class Solution{
private:
    vector<vector<int>> result;
    vector<int> path;
    void backtracking(vector<int>& candidates, int target, int sum, int index){
        if(sum == target){
			result.puah_back(path);
            return;
        }
        for(int i = index;i<candidates.size()&& sum+candidates[i] <= target;i++){//剪枝操作,对集合排序后在某层第i个值求和若出现sum>target,则该层第i个值之后的元素完全不用遍历。
            if(i>index&&candidates[i]==candidates[i-1]){//树层上去重,防止解集合中出现重复的组合
				continue;
            }
			sum += candidates[i];
            path.push_back(candidates[i]);
            backtracking(candidates,target,sum,i+1);//i+1是为了保证集合中的元素不重复取用
            sum -= candidates[i];
            path.pop_back();
        }
    }
public:
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target){
    sort(candidates.begin(),candidates.end());
    backtracking(candidtaes,target,0,0);
    return result;
	}
};

Leetcode216 组合总结III

找出所有相加之和为n的k个数之和,组合中只允许含有1-9的正整数,并且每种组合中不存在重复数字。该题和组合总和一样,只是将集合的的范围即横向遍历范围限制在 1-9.

同样由于是单集合,需利用index控制for循环的起始位置

class Solution{
private:
    vector<vector<int>> result;
    vector<int> path;
    void backtracking(int sum,int k,int n,int index){
        //剪枝优化
        //if(sum > n){
            //return;
        //}
		if(path.size() == k){
			if(sum == n){
				result.push_back(path);
                return;
            }
        }
        for(int i = index;i<=9;i++){
            sum += i;
            path.push_back(i);
            backtracking(sum,k,n,i+1);
            sum -= i;
            path.pop_back();
        }
    }
public:
    vector<vector<int>> combinationSum3(int k, int n){
        backtracking(0,k,n,1);
        return result;
    }
};

剪枝优化:由于集合可以视为已排序,第i层遍历时可根据path.size()的大小确定该层for循环遍历的终止位置,即将for循环更改为

for(int i = index;i<=9-(k-path.size())+1;i++)

当sum大于n,也没必要往后遍历,也可进行相应的剪枝操作。

标签:index,target,组合,int,candidates,result,回溯,path
From: https://www.cnblogs.com/perngfey-note/p/18129983

相关文章

  • 组合数学
    逆元若\(i\cdotx=1\),则\(i^{-1}=x\)。递推求乘法逆元\[令模数为p,正整数i,a=\lfloor\frac{p}{i}\rfloor,b=p\operatorname{mod}i,且b\ne0。\\a\cdoti+b=p\\\Downarrow\\a\cdoti+b\equiv0(\operatorname{mod}p)\\\frac{i}{b}+\frac{1}{a}\equiv0(\o......
  • 回溯之全排列
    前言回溯算法是一种通过逐步构建解决方案来解决问题的方法。全排列问题是其中一个经典的应用之一。它的基本思想是尝试所有可能的排列方式,并逐步构建解决方案,如果发现当前尝试的排列不符合条件,则回溯到上一步,尝试其他可能的选择。回溯算法本质就是DFS,深搜。全排列就是可......
  • TypeScript 与组合式 API
    看吧:https://cn.vuejs.org/guide/typescript/composition-api.html为组件的props标注类型<scriptsetuplang="ts">constprops=defineProps({foo:{type:String,required:true},bar:Number})props.foo//stringprops.bar//number|undefine......
  • 组合数学程序包 by My_Desire
    BeginPackage["My`"]RTRow::usage="ReadTrianglebyRow"TpQ::usage="全正性判断"LSTP::usage="三角全正性判断"RiordanArray::usage="RiordanArray[d_Function,h_Function,n_]"ExpRiordanArray::usage="ex......
  • 组合创新,原创模型!多类型需求响应负荷标准化建模+共享储能(附matlab代码实现)
    专题推荐:论文推荐,代码分享,视角(点击即可跳转)所有链接建议使用电脑端打开,手机端打开较慢【代码推荐购买指南】电力系统运行优化与规划、时间序列预测、回归分类预测matlab代码公众号历史推文合集23.3.21(电力系统前沿视角/预测和优化方向matlab代码/电力系统优秀论文推程......
  • 组合数学
    生成函数使用母函数的方法求谢列数列的通项\(a_n.\)\((1)a_0=2,a_1=5,a_{n+2}=3a_{n+1}-2a_n(n=0,1,2,\cdots);\)解:设\(f(x)=a_0+a_1x+a_2x^2+a_3x^3+\cdots.\)则:\(\qquad-3f(x)=-3a_0x-3a_1x^2-3a_2x^3-\cdots.\)\(\quad\quad\qquad\qquad2f(x)=+2a_0x^2+2a_1x^3+2a_2x......
  • 二十八 211. 计算系数 (组合计数|逆元)
    211.计算系数(组合计数|逆元)数论之快速幂、扩欧算法、同余与逆元组合计数importjava.util.*;publicclassMain{privatestaticfinalintmod=10007;privatestaticint[][]C=newint[1010][1010];publicstaticvoidmain(String[]args)......
  • 混辗式混砂机 变速箱 旋耕灭茬机 污水处理 150T液压机 污水处理厂 饺子机 农村生活污
    UHT管式杀菌机说明书混辗式混砂机机械结构设计 论文CAD图纸开题报告毕业设计之专用机床图纸毕业设计推钢机液压图纸毕业设计3吨叉车3进3退变速箱(毕业设计)1G-160型旋耕灭茬机中央传动装置设计(共17张CAD加说明书与侧边传动装置配套污水处理课程设计图集150T液压机设计【10......
  • 学习笔记445—白盒测试用例设计方法(语句覆盖、判定覆盖、条件覆盖、判定/条件覆盖、组
    白盒测试用例设计方法(语句覆盖、判定覆盖、条件覆盖、判定/条件覆盖、组合覆盖、路径覆盖、基本路径覆盖语句覆盖:每条语句至少执行一次。判定覆盖:每个判定的所有可能结果至少出现一次。(又称“分支覆盖”)条件覆盖:每个条件的所有可能结果至少执行一次。判定/条件覆盖:一个判定中的每......
  • 组合数学——Min-Max容斥
    Min-Max容斥,即$$\max(S)=\sum_{T\inS,T\neq\emptyset}(-1)^{|T|-1}\min(T)$$接下来证明上面那个式子是对的。定义\(S\)中共有\(N\)个元素,由大到小分别为\(s_1,s_2,\dots,s_N\),\(T_i\)为所有\(S\)大小为\(i\)的子集。所有元素都大于\(s_i\)且大小为\(j\)的子集......