【算法刷题 | 回溯思想 01】4.11(回溯算法理论、组合、组合总和 ||| )

文章目录

回溯

1.回溯算法理论基础

1.1什么是回溯法?

  1. 回溯法也可以叫做回溯搜索法,它是一种搜索的方式。
  2. 回溯是递归的副产品,只要有递归就会有回溯。

1.2回溯法的效率

  1. 虽然回溯法很难,很不好理解,但是回溯法并不是什么高效的算法
  2. 因为回溯的本质是穷举,穷举所有可能,然后选出我们想要的答案,如果想让回溯法高效一些,可以加一些剪枝的操作,但也改不了回溯法就是穷举的本质。

1.3回溯法解决的问题

  • 组合问题:N个数里面按一定规则找出k个数的集合
  • 排列问题:N个数按一定规则全排列,有几种排列方式
  • 切割问题:一个字符串按一定规则有几种切割方式
  • 子集问题:一个N个数的集合里有多少符合条件的子集
  • 棋盘问题:N皇后,解数独等等

补充:组合是不强调元素顺序的,排列是强调元素顺序

例如:{1, 2} 和 {2, 1} 在组合上,就是一个集合,因为不强调顺序,而要是排列的话,{1, 2} 和 {2, 1} 就是两个集合了。

1.4如何理解回溯法?

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

1.5回溯法模板

  • 回溯三部曲:

    • 回溯函数模板返回值以及参数(回溯算法中函数返回值一般为void)

    • 回溯函数终止条件:(搜到叶子节点了,也就找到了满足条件的一条答案,把这个答案存放起来,并结束本层递归。)

      if (终止条件) {
          存放结果;
          return;
      }
      
    • 回溯搜索的遍历过程(回溯法一般是在集合中递归搜索,集合的大小构成了树的宽度,递归的深度构成的树的深度)

      for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
          处理节点;
          backtracking(路径,选择列表); // 递归
          回溯,撤销处理结果
      }
      
      • for循环就是遍历集合区间,可以理解一个节点有多少个孩子,这个for循环就执行多少次。
      • 大家可以从图中看出for循环可以理解是横向遍历,backtracking(递归)就是纵向遍历,这样就把这棵树全遍历完了,一般来说,搜索叶子节点就是找的其中一个结果了。
  • 回溯算法模板框架:

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

2.组合

2.1问题

给定两个整数 nk,返回范围 [1, n] 中所有可能的 k 个数的组合。

你可以按 任何顺序 返回答案。

  • 示例一:

    输入:n = 4, k = 2
    输出:
    [
    [2,4],
    [3,4],
    [2,3],
    [1,2],
    [1,3],
    [1,4],
    ]

  • 示例二:

    输入:n = 1, k = 1
    输出:[[1]]

2.2解法一:暴力解法(循环次数不确定)

  • 使用for循环,例如示例中k为2,很容易想到 用两个for循环,这样就可以输出 和示例中一样的结果。

    c 复制代码
    for (int i = 1; i <= n; i++) {
        for (int j = i + 1; j <= n; j++) {
            cout << i << " " << j << endl;
        }
    }
  • 如果n为100,k为50呢,那就50层for循环,循环写不出来

2.3解法二:回溯

2.3.1回溯思路

  • 分解成一层一层的树形结构:
  • 使用res存放全部合适的结果;
  • 使用paths存放当前的路径;
(1)回溯函数模板返回值以及参数
  • 回溯函数返回值一般为void;
  • 参数:
    • 题目要求 求范围[1,n]中所有可能的k个数的组合;
    • 参数有 n、k
    • 还要有一个startIndex,代表每次循环从数组第几个数开始遍历
java 复制代码
private void backtracking(int n,int k,int startIndex)
(2)回溯函数终止条件
  • 当当前路径 path 的个数达到k时,添加到res中
java 复制代码
if(paths.size()==k){
    res.add(paths);
	return;	
}
(3)回溯搜索的遍历过程
  • 从startIndex遍历该层的元素
java 复制代码
for(int i=startIndex;i<=n;i++){
	paths.add(i);
    //递归
    backtracking(n,k,i+1);
    //回溯
    paths.remove(paths.size()-1);
}

2.3.2代码实现

java 复制代码
	List<List<Integer>> res=new ArrayList<>();
    List<Integer> paths=new ArrayList<>();
    
    public List<List<Integer>> combine(int n, int k) {
        
        backtracking(n,k,1);
        return res;
    }

    private void backtracking(int n,int k,int startIndex){
        if(paths.size()==k){
            res.add(new ArrayList<>(paths));
	        return;	
        }

        for(int i=startIndex;i<=n;i++){
	        paths.add(i);
            //递归
            backtracking(n,k,i+1);
            //回溯
            paths.remove(paths.size()-1);
        }
    }

2.3.3剪枝操作

  • 回溯法虽然是暴力搜索,但也有时候可以有点剪枝优化一下的

  • 遍历代码如下:

    java 复制代码
    for(int i=startIndex;i<=n;i++){
        
    	paths.add(i);
        //递归
        backtracking(n,k,i+1);
        //回溯
        paths.remove(paths.size()-1);
    }
  • 这个遍历的范围是可以剪枝优化的,怎么优化呢?

    • 来举一个例子,n = 4,k = 4 的话,那么第一层for循环的时候,从元素2开始的遍历都没有意义了。 在第二层for循环,从元素3开始的遍历都没有意义了。

    • 如果for循环选择的起始位置之后的元素个数 已经不足 我们需要的元素个数了,那么就没有必要搜索了

  • 优化过程如下:

    • 已经选择的元素个数:path.size();
    • 还需要的元素个数为: k - path.size();
    • 在集合n中至多要从该起始位置 : n - (k - path.size()) + 1,开始遍历
  • 为什么有个+1呢,因为包括起始位置,我们要是一个左闭的集合。

  • 举个例子,n = 4,k = 3, 目前已经选取的元素为0(path.size为0),n - (k - 0) + 1 即 4 - ( 3 - 0) + 1 = 2。

  • 从2开始搜索都是合理的,可以是组合[2, 3, 4]。

java 复制代码
for(int i=startIndex;i<= n - (k - paths.size()) + 1;i++){
	paths.add(i);
    //递归
    backtracking(n,k,i+1);
    //回溯
    paths.remove(paths.size()-1);
}

3.组合总和 |||

3.1问题

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

  • 只使用数字1到9
  • 每个数字 最多使用一次

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

  • 示例一:

    输入: k = 3, n = 7
    输出: [[1,2,4]]
    解释:
    1 + 2 + 4 = 7
    没有其他符合的组合了。

3.2解法:回溯

3.2.1回溯思路

(1)函数返回值以及参数
  • 无返回值
  • 参数:
    • k:总共需要k个数
    • n:总和为n
    • startIndex:从startIndex开始遍历
    • count:当前路径总和
java 复制代码
private void backtracking(int k,int n,int startIndex,int count)
(2)终止条件
  • 判断路径个数是否为k,路径总和是否为n,满足则添加到res中并返回
java 复制代码
if( paths.size()==k ){
	if(count==n){
    	res.add(new ArrayList<>(paths));   
    }
	return;	
}
(3)遍历过程
  • 从startIndex开始递归(paths加上该节点、count总和加上)
  • 递归后回溯(paths移除最后一个节点、count总和减去该节点值)
java 复制代码
for(int i=startIndex;i<=9;i++){
    
	paths.add(i);
    //递归
    backtracking(k,n,i+1,count+=i);
    
    //回溯
    paths.remove(paths.size()-1);
    count-=i;
}

3.2.2代码实现

java 复制代码
	List<List<Integer>> res=new ArrayList<>();
    List<Integer> paths=new ArrayList<>();

    public List<List<Integer>> combinationSum3(int k, int n) {
        backtracking(k,n,1,0);
        return res;
    }

    private void backtracking(int k,int n,int startIndex,int count){
        if( paths.size()==k ){
            if(count==n){
                res.add(new ArrayList<>(paths));   
            }
	        return;	
        }

        for(int i=startIndex;i<=9;i++){
	        paths.add(i);
            //递归
            backtracking(k,n,i+1,count+=i);
            //回溯
            paths.remove(paths.size()-1);
            count-=i;
        }
    }

3.2.3剪枝操作

java 复制代码
		for(int i=startIndex;i<= 9-(k-paths.size())+1 ;i++){
	        paths.add(i);
            //递归
            backtracking(k,n,i+1,count+=i);
            //回溯
            paths.remove(paths.size()-1);
            count-=i;
        }
相关推荐
南石.1 分钟前
JVM 基础、GC 算法与 JProfiler 监控工具详解
jvm·算法
哎呀呀嗯呀呀17 分钟前
class 031 位运算的骚操作
java·算法·位运算
2401_8581205320 分钟前
古典舞在线交流平台:SpringBoot设计与实现详解
java·spring boot·后端
大白飞飞28 分钟前
IDEA创建、导入、删除maven项目
java·maven·intellij-idea
赐你岁月如歌32 分钟前
如何使用ssm实现基于web的网站的设计与实现+vue
java·后端·ssm
时清云33 分钟前
【算法】合并两个有序链表
前端·算法·面试
凯子坚持 c1 小时前
C语言复习概要(四)
c语言·开发语言
轩辰~1 小时前
磁盘存储链式结构——B树与B+树
数据结构·b树·算法
何陈陈1 小时前
【Linux】线程池
linux·服务器·开发语言·c++
清风玉骨1 小时前
Qt-QHBoxLayout布局类控件(42)
开发语言·qt