回溯算法:一个模板解决排列组合问题

news2024/11/25 1:08:53

回溯算法

在初遇排列组合题目时,总让人摸不着头脑,但是当我做了很多题目后,发现几乎能用同一个模板做完所有这种类型的题目,大大提高了解题效率。回溯是递归的副产品,只要有递归就会有回溯。

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

有的同学会问,既然回溯法并不是高效的算法,为什么还要用它?因为没得选,有一些算法能暴力搜出来就不错了,撑死了再剪枝一下,也没有更高效的解法,还要啥自行车。

题目分类

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

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

在leetcode上的题目我也列出来了。

在这里插入图片描述

本文讲解其中的:

39.组合总和
40.组合总和 II

46.全排列
47.全排列 II

78.子集
90.子集 II

模版代码(77.组合)

本文所有题目都可以用以下模板代码解决:

//二维数组result
private  List<List<Integer>> result = new ArrayList<>();

//一维数组path路径
private  List<Integer> path = new ArrayList<>();


private  void dsf(int n, int k, int startIndex) {
    if (path.size() == k) {
        result.add(new ArrayList<>(path));
        return;
    }
    for (int i = startIndex; i <= n; i++) {
        path.add(i);
        dsf(n, k, i + 1);
        path.remove(path.size() - 1);
    }
}

中文伪代码

void backtracking(参数) {
    if (终止条件) {
        存放结果;
        return;
    }

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

模板代码以77.组合进行讲解

题目描述

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

示例 1:

输入: n = 4, k = 2

输出:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]

示例 2:

输入: n = 1, k = 1

输出: [[1]]

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

以n = 4, k = 2为例,如下图所示,从上往下看,在第一层的时候,可以选择1,2,3,4,假设第一个选定为1(将选定的元素存入path中,即path=[1]),那么第二层的元素只能选择2,3,4(要保证不相等,不能重复),当第二层的元素选择2,即path=[1,2]时,第二个元素选定后,此时path的长度等于k的长度,一个排列结果就计算出来了,加入到结果result中去,接着回溯,按照同样的逻辑运行下去,最后所有组合结果。

当第一层元素为2的时候,他的下面第二层只能选3,4,因为这里是组合,在元素1的时候已经有了[1,2],在2的时候如果再选1,即[2,1]其实是同一个结果。

在这里插入图片描述

39.组合总和

题目描述

给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。

candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。

对于给定的输入,保证和为 target 的不同组合数少于 150 个。

示例 1:

输入: candidates = [2,3,6,7], target = 7
输出: [[2,2,3],[7]]
解释:
2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。
7 也是一个候选, 7 = 7 。
仅有这两种组合。

示例 2:

输入: candidates = [2,3,5], target = 8
输出: [[2,2,2,2],[2,3,3],[3,5]]

示例 3:

输入: candidates = [2], target = 1
输出: []

下面是画的树形结构
在这里插入图片描述

第一层毫无疑问有三种方式,2,3,5,当为2的时候第二层有三种选择,还是2,3,5,因为在题目当中是允许同一个元素重复使用的,当为3的时候,只能选择3和5,为什么不能选择2呢,因为这一题是组合,即[2,3]和[3,2]是一样的,如果这里能选2,和前面的2选3势必会造成重复。也就是当前元素的下一层,只能选当前元素以及后面的元素。

代码只需要在模版代码上稍加修改即可:

    // 二维数组result
    private List<List<Integer>> result = new ArrayList<>();

    // 一维数组path路径
    private List<Integer> path = new ArrayList<>();

    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        dsf(candidates, target, 0);
        return result;
    }

    private void dsf(int[] candidates, int target, int startIndex) {
        Integer sum = path.stream().mapToInt(Integer::intValue).sum();
        if (sum > target) {
            return;
        }
        if (sum == target) {
            result.add(new ArrayList<>(path));
            return;
        }
        for (int i = startIndex; i < candidates.length; i++) {
            path.add(candidates[i]);
            dsf(candidates, target, i);
            path.remove(path.size() - 1);
        }
    }

40.组合总和 II

题目描述

给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。

candidates 中的每个数字在每个组合中只能使用 一次

注意: 解集不能包含重复的组合。

示例 1:

输入: candidates = [10,1,2,7,6,1,5], target = 8,
输出:
[
[1,1,6],
[1,2,5],
[1,7],
[2,6]
]

示例 2:

输入: candidates = [2,5,2,1,2], target = 5,
输出:
[
[1,2,2],
[5]
]

本题和39题有两个区别:

1、本题candidates 中的每个数字在每个组合中只能使用一次,39题是可以多次使用。

2、本题数组candidates的元素是有重复的,39题是无重复元素的数组candidates。

区别1只能使用一次,问题不大,区别2candidates里有重复的元素,这个区别很大,难点在于去重。去重是什么意思,示例 1中的[1,2,5]有两个含义,因为candidates里有两个1,所以计算出来的结果会有两个[1,2,5],即第二个1和第六个1,去重就是去的这里的重。有的同学想,我把所有组合求出来,再用set或者map去重,这么做很容易超时!所以要在搜索的过程中就去掉重复组合。 这也是这道题的难点。

下面继续画树形结构:

这里以集合[1,1,2],target=3做讲解,注意在做题目之前要先将candidates数组排序,不然这道题也是不好做的。

在这里插入图片描述

这道题的关键就在于i > 0 && candidates[i] == candidates[i - 1] && used[i - 1] == 0 这句话,一些解释说明,为什么这么写,我也写在上面的树形图上了,大家再想下思考下理解下。

代码如下:

    // 二维数组result
    private List<List<Integer>> result = new ArrayList<>();

    // 一维数组path路径
    private List<Integer> path = new ArrayList<>();

    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        // 排序
        Arrays.sort(candidates);
        int[] used = new int[candidates.length];
        dsf(candidates, target, 0, used);
        return result;
    }

    private void dsf(int[] candidates, int target, int startIndex, int[] used) {
        Integer sum = path.stream().mapToInt(Integer::intValue).sum();
        if (sum > target) {
            return;
        }
        if (sum == target) {
            result.add(new ArrayList<>(path));
            return;
        }
        for (int i = startIndex; i < candidates.length; i++) {
            if (i > 0 && candidates[i] == candidates[i - 1] && used[i - 1] == 0) {
                continue;
            }
            path.add(candidates[i]);
            used[i] = 1;
            dsf(candidates, target, i + 1, used);
            path.remove(path.size() - 1);
            used[i] = 0;
        }
    }

46.全排列

题目描述

给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。

示例 1:

输入: nums = [1,2,3]
输出: [[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

示例 2:

输入: nums = [0,1]
输出: [[0,1],[1,0]]

示例 3:

输入: nums = [1]
输出: [[1]]

本题是全排列,和组合不一样,即[1,2]和[2,1]算两种答案,差异点就在for循环数组的时候,组合是从startIndex开始,而这里是从0开始。下面是树形结构:

在这里插入图片描述

下面是代码:

    // 二维数组result
    private List<List<Integer>> result = new ArrayList<>();

    // 一维数组path路径
    private List<Integer> path = new ArrayList<>();

    public List<List<Integer>> permute(int[] nums) {
        int[] used = new int[nums.length];
        dsf(nums, used);
        return result;
    }

    private void dsf(int[] nums, int[] used) {
        if (path.size() == nums.length) {
            result.add(new ArrayList<>(path));
            return;
        }
        for (int i = 0; i < nums.length; i++) {
            if (used[i] == 1) {
                continue;
            }
            path.add(nums[i]);
            used[i] = 1;
            dsf(nums, used);
            path.remove(path.size() - 1);
            used[i] = 0;
        }
    }

做过上面三道题以后,这道题真的是很容易,在模版代码上稍微改改即可。

47.全排列 II

题目描述

给定一个可包含重复数字的序列 nums按任意顺序 返回所有不重复的全排列。

示例 1:

输入: nums = [1,1,2]
输出:
[[1,1,2],
 [1,2,1],
 [2,1,1]]

示例 2:

输入: nums = [1,2,3]
输出: [[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

这道题和40.组合总和 II是一样的,核心就是那两行代码,所以我讲题目是有循序渐进的过程,40题做过了,这道题也是很快就能通过的。

i > 0 && nums[i] == nums[i - 1] && used[i - 1] == 0
树形结构不画完整了,就是和大家讲下为啥要写这行代码,就是当第一个1选出来,剩下的集合是[1,2],最终结果是[1,1,2]和[1,2,1],第二个1选出来,剩下的集合也是[1,2],那么最终结果也是[1,1,2]和[1,2,1],那么这个结果就是重复的,所以分支2要剪掉,所以当nums[i] == nums[i - 1] 的时候要continue掉,为啥不是break,因为后面的2即分支3还得执行。

而used[i - 1] == 0是用来控制树层去重,used[i - 1] == 1是树枝去重。
在这里插入图片描述

代码如下:

    // 二维数组result
    private List<List<Integer>> result = new ArrayList<>();

    // 一维数组path路径
    private List<Integer> path = new ArrayList<>();

    public List<List<Integer>> permuteUnique(int[] nums) {
        int[] used = new int[nums.length];
        // 排序
        Arrays.sort(nums);
        dsf(nums, used);
        return result;
    }

    private void dsf(int[] nums, int[] used) {
        if (path.size() == nums.length) {
            result.add(new ArrayList<>(path));
            return;
        }
        for (int i = 0; i < nums.length; i++) {
            // 树层去重
            if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == 0) {
                continue;
            }
            if (used[i] == 1) {
                continue;
            }
            path.add(nums[i]);
            used[i] = 1;
            dsf(nums, used);
            path.remove(path.size() - 1);
            used[i] = 0;
        }
    }

78. 子集

题目描述

给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。

解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。

示例 1:

输入: nums = [1,2,3]
输出: [[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]

示例 2:

输入: nums = [0]
输出: [[],[0]]

这道题在模版代码77的基础上改改即可,只是收集结果的时机阶段不同,77题是在叶子节点,本题是在每个节点就收集,然后排列方式是组合,即[2,3]和[3,2]是一样的,所以在下一层递归的时候要用startIndex控制开始的位置,只能是当前元素的后面元素。

树形结构:
在这里插入图片描述

代码:

    // 二维数组result
    private List<List<Integer>> result = new ArrayList<>();

    // 一维数组path路径
    private List<Integer> path = new ArrayList<>();

    public List<List<Integer>> subsets(int[] nums) {
        dsf(nums, 0);
        return result;
    }

    private void dsf(int[] nums, int startIndex) {
        result.add(new ArrayList<>(path));
        if (startIndex >= nums.length) {
            return;
        }
        for (int i = startIndex; i < nums.length; i++) {
            path.add(nums[i]);
            dsf(nums, i + 1);
            path.remove(path.size() - 1);
        }
    }

90.子集 II

给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。

解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。

示例 1:

输入: nums = [1,2,2]
输出: [[],[1],[1,2],[1,2,2],[2],[2,2]]

示例 2:

输入: nums = [0]
输出: [[],[0]]

有了前面组合,去重的基础,本题可以说是秒了,直接贴代码

    // 二维数组result
    private List<List<Integer>> result = new ArrayList<>();

    // 一维数组path路径
    private List<Integer> path = new ArrayList<>();

    public List<List<Integer>> subsetsWithDup(int[] nums) {
        // 排序
        Arrays.sort(nums);
        int[] used = new int[nums.length];
        dsf(nums, 0, used);
        return result;
    }

    private void dsf(int[] nums, int startIndex, int[] used) {
        result.add(new ArrayList<>(path));
        if (startIndex >= nums.length) {
            return;
        }
        for (int i = startIndex; i < nums.length; i++) {
            if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == 0) {
                continue;
            }
            path.add(nums[i]);
            used[i] = 1;
            dsf(nums, i + 1, used);
            path.remove(path.size() - 1);
            used[i] = 0;
        }
    }

后记

本文在做回溯算法的时候一定要画树形结构,这样有助于思考解题,不然光想是很抽象的。作者在几年前也刷过题,这次在写文章的时候专门整理又刷了一遍,第二次做会比第一次做更加清晰明了。刷题做题的难点在于坚持,大家共勉。

在这里插入图片描述

本文树形结构:https://www.processon.com/view/link/6702576cda38ea3e04a5ae42

本文代码:https://github.com/xuhaoj/DataStructure

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2195393.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

77寸OLED透明触摸屏有哪些应用场景

说到77寸OLED透明触摸屏&#xff0c;那可真是市场营销中的一大亮点&#xff0c;应用场景多到数不清&#xff01;我这就给你细数几个热门的&#xff1a; 商业展示&#xff1a;这可是77寸OLED透明触摸屏的拿手好戏&#xff01;在高端零售店铺里&#xff0c;它可以作为陈列窗口&am…

yolov测试各项指标的流程

# yolov测试各项指标的流程: 载入模型, 其中包括类别数等; 按照 batch_size 逐张图片进行预测 得到预测标签: predn 和 实际标签 labelsn, 其中 末尾的 n 表示经过了原图适配的 bbox坐标. predn: {tensor: (3,6)},表示预测到了3个标签, 表示[x1, y1, x2, y2, confidence, clas…

IO重定向

文章目录 IO重定向概念3个标准文件描述符“最低可用文件描述符”原则 默认的连接&#xff1a;tty使用close then open将stdin定向到文件使用open..close..dup..close将stdin定向到文件使用open..dup2..close将stdin重定向到文件课上实验 IO重定向 大多数的程序不接收输出文件名…

Deformable Transformer论文笔记

原文链接 [2010.04159] Deformable DETR: Deformable Transformers for End-to-End Object Detection (arxiv.org)https://arxiv.org/abs/2010.04159 原文笔记 What 作者结合了可变形卷积的稀疏空间采样和 Transformer 的关系建模能力的优点。提出了Deformable Detr Defor…

算法笔记(十三)——BFS 解决最短路问题

文章目录 迷宫中离入口最近的出口最小基因变化单词接龙为高尔夫比赛砍树 BFS 解决最短路问题 BFS(广度优先搜索) 是解决最短路径问题的一种常见算法。在这种情况下&#xff0c;我们通常使用BFS来查找从一个起始点到目标点的最短路径。 迷宫中离入口最近的出口 题目&#xff1a;…

can 总线入门———can简介硬件电路

文章目录 0. 前言1. CAN简介2. 主流通讯协议对比3. CAN 硬件电路4. CAN 电平标准5. CAN 收发器 0. 前言 博客内容来自B站上CAN总线入门教程视频讲解&#xff0c;博客中的插图和内容均为视频中的内容。视频链接 CAN总线入门教程 1. CAN简介 先来看看一它名字的意思&#xff0c…

Redis 缓存策略详解:提升性能的四种常见模式

在现代分布式系统中&#xff0c;缓存是提升性能和减轻数据库负载的关键组件。Redis 作为一种高性能的内存数据库&#xff0c;被广泛应用于缓存层。本文将深入探讨几种常用的 Redis 缓存策略&#xff0c;包括旁路缓存模式&#xff08;Cache-Aside Pattern&#xff09;、读穿透模…

强化学习入门到不想放弃-4

上回的地址&#xff1a;强化学习入门到不想放弃-3 (qq.com) 上上回地址&#xff1a;强化学习入门到不想放弃-2 (qq.com) 上上上回地址&#xff1a;强化学习入门到不想放弃-1 (qq.com) 好久没更新了&#xff0c;也是不知道写啥啊&#xff0c;&#xff08;有些文章刚写了就被有些…

鸽笼原理与递归 - 离散数学系列(四)

目录 1. 鸽笼原理 鸽笼原理的定义 鸽笼原理的示例 鸽笼原理的应用 2. 递归的定义与应用 什么是递归&#xff1f; 递归的示例 递归与迭代的对比 3. 实际应用 鸽笼原理的实际应用 递归的实际应用 4. 例题与练习 例题1&#xff1a;鸽笼原理应用 例题2&#xff1a;递归…

三、Python基础语法(注释、三种波浪线、变量)

一、注释 注释是对代码进行解释说明的文字&#xff0c;不会被解释器执行&#xff0c;可以更方便阅读代码和了解代码的作用。 1.单行注释 使用#开头的文字就是注释&#xff0c;可以使用快捷键Ctrl / 2.多行注释 多行注释就是注释的内容&#xff0c;可以换行书写&#xff0c…

集智书童 | 用于时态动作检测的预测反馈 DETR !

本文来源公众号“集智书童”&#xff0c;仅用于学术分享&#xff0c;侵权删&#xff0c;干货满满。 原文链接&#xff1a;用于时态动作检测的预测反馈 DETR ! 视频中的时间动作检测&#xff08;TAD&#xff09;是现实世界中的一个基本且具有挑战性的任务。得益于 Transformer …

提升 CI/CD 稳定性:Jenkins 开机自检与推送通知

简介&#xff1a;Jenkins 是一个广泛使用的开源自动化服务器&#xff0c;常用于持续集成和持续交付。在某些情况下&#xff0c;服务器重启可能导致 Jenkins 构建任务中断或失败。为了解决这个问题&#xff0c;可以使用一个自检服务&#xff0c;定期检查系统的启动时间&#xff…

3559 pcie配置流程

目录 EP配置 uboot配置 uboot代码修改 内核代码修改 带宽配置 带宽查看 硬件管脚配置 EP配置 uboot配置 1)make CROSS_COMPILE=aarch64-himix100-linux- hi3559av100_emmc_defconfig 2) make menuconfig CROSS_COMPILE=aarch64-himix100-linux- 修改配置: 3) 合入…

一种将RAG、KG、VS、TF结合增强领域LLM性能的框架

SMART-SLIC框架&#xff1a;旨在将RAG结合向量存储&#xff08;Vector Stores&#xff09;、知识图谱&#xff08;Knowledge Graphs&#xff09;和张量分解&#xff08;Tensor Factorization&#xff09;来增强特定领域的大型语言模型&#xff08;LLMs&#xff09;的性能。 SM…

codetop标签动态规划大全C++讲解(二)!!动态规划刷穿地心!!学吐了家人们o(╥﹏╥)o

一篇只有十题左右&#xff0c;写少一点好复习 1.目标和2.分割等和子集3.完全平方数4.比特位计数5.石子游戏6.预测赢家7.不同的二叉搜索树8.解码方法9.鸡蛋掉落10.正则表达式匹配11.通配符匹配12.交错字符串 1.目标和 给你一个非负整数数组 nums 和一个整数 target 。 向数组中…

01-python+selenium自动化测试-基础学习

前言 基于python3和selenium3做自动化测试&#xff0c;俗话说&#xff1a;工欲善其事必先利其器&#xff1b;没有金刚钻就不揽那瓷器活&#xff0c;磨刀不误砍柴工&#xff0c;因此你必须会搭建基本的开发环境&#xff0c;掌握python基本的语法和一个IDE来进行开发&#xff0c…

短剧系统源码短剧平台开发(H5+抖小+微小)部署介绍流程

有想法加入国内短剧赛道的请停下脚步&#xff0c;耐心看完此篇文章&#xff0c;相信一定会对您有所帮助的&#xff0c;下面将排序划分每一个步骤&#xff0c;短剧源码、申请资料、服务器选择、部署上架到正常运行等几个方面&#xff0c;整理了一些资料&#xff0c;来为大家举例…

中广核CGN25届校招网申SHL测评题库、面试流程、招聘对象,内附人才测评认知能力真题

​中国广核集团校园招聘在线测评攻略&#x1f680; &#x1f393; 校园招聘对象 2024届、2025届海内外全日制应届毕业生&#xff0c;大专、本科、硕士、博士&#xff0c;广核集团等你来&#xff01; &#x1f4c8; 招聘流程 投递简历 简历筛选 在线测评&#xff08;重点来啦…

C++ 算法学习——1.6 前缀和与二维前缀和算法

前缀和算法&#xff08;Prefix Sum Algorithm&#xff09;&#xff1a; 概念&#xff1a;前缀和算法通过在遍历数组时计算前缀和&#xff08;从数组的第一个元素开始累加到当前元素的和&#xff09;&#xff0c;可以在O(1)时间内得到任意区间的子数组和&#xff0c;而不需要重复…

告别音乐小白!字节跳动AI音乐创作工具,让你一键变作曲家!

还在羡慕别人能创作动听的音乐&#xff1f;五音不全的你&#xff0c;也梦想着谱写属于自己的乐章&#xff1f;现在&#xff0c;机会来了&#xff01;字节跳动推出了一款AI音乐创作工具——抖音推出的海绵音乐&#xff0c;它能让你轻松一键创作音乐&#xff0c;即使是“音乐小白…