通过排列组合问题理解回溯算法
排列
Question
给定一个不含重复数字的数组nums
,返回其所有可能的元素不重复的全排列。
以数组[1, 2, 3]
为例,其总共可能含有的全排列数为
首先选择
1
作为排列的第一位,则第二位可以选择的元素有2
和3
选择
2
作为排列的第二位,则第三位可以选择的元素仅有3
选择
3
作为排列的第三位,则该排列为[1, 2, 3]
第三步完成后,由于排列已经形成,此时需要考虑其他可能存在的排列
重新考虑此时排列的第三位,但
3
已被选过,且无其他选择,回退到上一位重新考虑此时排列的第二位,由于
2
已被选过,因此可以选择3
作为新排列的第二位最后选择
2
作为新排列的第三位,则该排列为[1,3,2]
继续考虑其他可能存在的排列
重新考虑此时排列的第三位,但
2
已被选过,且无其他选择,回退到上一位重新考虑此时排列的第二位,由于
2
与3
均已被选过,且无其他选择,回退到上一位重新考虑此时排列的第一位,由于
1
已被选过,可以选择的元素还有2
与3
选择
2
作为新排列的第一位…
依此规律,我们可以生成所有可能存在的6个排列。将这些排列生成过程以树的形式表示后不难发现,其规律类似于之前提到的DFS算法,因此该问题可以借助DFS算法实现。另外,所谓的“重新考虑”实际上是将排列复原至前一个状态上,尔后重新选择新元素,这种类似“时光倒流”的倒退算法即为著名的回溯算法。借助回溯算法,可以解决存在多个步骤且每个步骤有多种实现方法的问题(例如本文的排列与组合问题)。
首先确定用于生成排列的容器、变量与函数
1 | //用于生成排列的主要容器 |
尔后定义DFS算法的具体内容
1 | void dfs(vector<vector<int>>& ans, vector<int>& path, vector<int>& nums, vector<int>& visited, int n){ |
其整体运行过程与前文分析部分所展示的过程一致,完整代码为
1 | class Solution{ |
组合
Question
给定两个整数n
和k
,返回范围 k
个数的组合。
以数组[1,2,3]
为例,此时若k
值为2,则其总共可能含有的组合数为
选择
1
作为组合的第一位,下一个可选元素有2
和3
选择
2
作为组合的第二位,则该组合为[1, 2]
第二步完成后,由于组合已经形成,此时需要考虑其他可能存在的组合
回退至上一位
重新选择
3
作为组合的第二位,则该组合为[1, 3]
继续考虑其他可能存在的组合
回退至上一位,发现都已被选过,继续回退
重新选择
2
作为组合的第一位…
依此规律,我们可以生成所有可能存在的6种数组结果,其中包含了3种组合。由此可见,虽然其大致过程与处理排列问题时的流程一致,但在生成过程中难免会产生重复的组合。该生成过程在树结构下可表示为
因此,我们需要去除生成结果中的重复组合,即去除上图中冗余组合所属的“枝条”,该过程一般被称为剪枝操作。通过剪枝操作,可以保证所生成的组合均不重复。经过剪枝操作后,生成过程被简化为
首先确定用于生成组合的容器、变量与函数
1 | //用于生成组合的主要容器 |
尔后定义DFS算法的具体内容
1 | void dfs(int n, int k, int start){ |
在该算法中,剪枝操作实际上通过for
循环实现,它可以保证后续位的元素一定大于相邻的前一个元素。从组合的生成结果[[1, 2], [1, 3], [2, 3]]
不难看出,该结果符合该特征。因此完整代码为
1 | class Solution{ |
总结
从上述排列与组合的问题中可以发现,这些问题所使用的回溯算法结构类似,大致结构为
1 | void dfs(所需参数){ |
该结构即经典的回溯算法结构。一般情况下,由于回溯算法与DFS算法及栈结构关系密切,鉴于各个问题的特殊性,该结构中可能会添加visited
数组以验证选择情况,或进行剪枝操作等,但该主框架通用于绝大部分可以通过回溯算法处理的问题。
- Title: 通过排列组合问题理解回溯算法
- Author: Zielorem
- Created at : 2023-01-18 00:58:45
- Updated at : 2023-07-14 01:06:51
- Link: https://zielorem.github.io/2023/01/18/permutations-and-combinations/
- License: This work is licensed under CC BY-NC-SA 4.0.