文章目录
求解排列组合问题
导言
- 以下代码都存放于 我的GitHub仓库 ,如果小伙伴觉得有用,请给我颗星星哈。
- 以下代码都是提交过的,正确性可以保证。
1. 框架
// 这个框架能够求出数组中所有的具体排列组合
// 比如求:
// 在可重复选取的数组 [1,2,3] 中组成 4 的组合。
// 那么这个框架可以得出的结果集是: [[1,1,1,1],[1,1,2],[2,2],[1,3]],
// 且最终这个结果存放在 resultSet 中。
var resultSet [][]int // 结果集
// 返回结果集的函数
func resultSetReturner() [][]int {
/*
1. 进行一些预处理
a. 如果数组中有重复元素,且该问题是组合问题的话,那么这里需要排序,
为什么要排序? 请看下面的 3. 对框架的一些解答
*/
/* 2. 调用回溯函数 */
/* 5. 返回结果集 */
}
// 回溯函数
func backtracer() {
/* 3. 判断是否需要加入结果集以及进行剪枝 */
isVisit := make(map[int]bool) // 有重复元素才需要这个结构,没有重复元素的话,这个结构可以直接删除。
for i : =0; i < len(nums); i++{
// 判断在该层,这个数字是否已经使用过了
if isVisit[nums[i]] == true {
// 使用过时
continue
}
isVisit[nums[i]] = true
/*
4.继续调用回溯函数,这里会有以下几种情况。
a. 如果题目要求: 求组合数,不能重复选取的话, 那么下一层处理的应该是 nums[i+1:]。
b. 如果题目要求: 求组合数,能重复选取的话, 那么下一层处理的应该是 nums[i:]。
c. 如果题目要求: 求排列数,能重复选取的话,那么下一层处理的应该是 nums[:],即还是 nums。
d. 如果题目要求: 求排列数,不能重复选取的话,那就把nums[i]与nums[0]交换后,处理nums[1:],处理好后再交换回来。
*/
}
}
2. 力扣排列组合问题分析
3. 实例
3.1 组合问题 — 没有重复元素、可重复选取
var combinationSequence [][]int // 结果集
// 返回结果集的函数
func combinationSum(candidates []int, target int) [][]int {
/* 1. 进行一些预处理 */
combinationSequence = make([][]int, 0)
/* 2. 调用回溯函数 */
combinationSumExec(candidates, target, make([]int, 0, 100))
/* 5. 返回结果集 */
return combinationSequence
}
// 回溯函数
func combinationSumExec(candidates []int, target int, sequence []int) {
/* 3. 判断是否需要加入结果集以及进行剪枝 */
// 剪枝
if target < 0 {
return
}
if target == 0 {
combinationSequence = append(combinationSequence, newSlice(sequence))
return
}
for i := 0; i < len(candidates); i++ {
/* 4. 继续调用回溯函数 */
// 因为题目要求的是组合数且能重复选取,所以下一层处理的是 candidates[i:]
combinationSumExec(candidates[i:], target-candidates[i], append(sequence, candidates[i]))
}
}
// 深拷贝
func newSlice(oldSlice []int) []int {
slice := make([]int, len(oldSlice))
copy(slice, oldSlice)
return slice
}
3.2 组合问题 — 有重复元素、不可重复选取
var combinationSequence [][]int // 结果集
// 返回结果集的函数
func combinationSum2(candidates []int, target int) [][]int {
/* 1. 进行一些预处理 */
combinationSequence = make([][]int, 0)
sort.Ints(candidates) // 有重复元素的组合问题就要排序。
/* 2. 调用回溯函数 */
combinationSumExec(candidates, target, make([]int, 0, 5))
/* 5. 返回结果集 */
return combinationSequence
}
// 回溯函数
func combinationSumExec(candidates []int, target int, sequence []int) {
/* 3. 判断是否需要加入结果集以及进行剪枝 */
if target < 0 {
return
}
if target == 0 {
combinationSequence = append(combinationSequence, newSlice(sequence))
return
}
isVisited := make(map[int]bool) // 题目有重复元素,所以需要这个结构
for i := 0; i < len(candidates) && target >= candidates[i]; i++ {
if isVisited[candidates[i]] == true {
continue
}
isVisited[candidates[i]] = true
/* 4. 继续调用回溯函数 */
// 因为题目要求的是组合数且不能重复选取,所以下一层处理的是 candidates[i+1:]
combinationSumExec(candidates[i+1:], target-candidates[i], append(sequence, candidates[i]))
}
}
// 深拷贝
func newSlice(oldSlice []int) []int {
slice := make([]int, len(oldSlice))
copy(slice, oldSlice)
return slice
}
var subsetSequence [][]int // 结果集
// 返回结果集的函数
func subsetsWithDup(nums []int) [][]int {
/* 1. 预处理 */
subsetSequence = make([][]int, 0)
sort.Ints(nums) // 有重复元素的组合问题就要排序。为什么要排序呢?后面会说
/* 2. 调用回溯函数 */
subsetsExec(nums, []int{})
/* 5. 返回结果集 */
return subsetSequence
}
// 回溯函数
func subsetsExec(nums []int, sequence []int) {
/* 3. 判断是否需要加入结果集以及进行剪枝 */
subsetSequence = append(subsetSequence, newSlice(sequence))
isVisit := make(map[int]bool) // 记录数字是否使用过,防止出现重复的结果
for i := 0; i < len(nums); i++ {
if isVisit[nums[i]] {
continue
}
isVisit[nums[i]] = true
/* 4. 继续调用回溯函数 */
// 因为题目要求的是组合数且不能重复选取,所以下一层处理的是 nums[i+1:]
subsetsExec(nums[i+1:], append(sequence, nums[i]))
}
}
// 深拷贝
func newSlice(slice []int) []int {
s := make([]int, len(slice))
copy(s, slice)
return s
}
3.3 组合问题 — 没有重复元素、不可重复选取
var subsetSequence [][]int // 结果集
// 返回结果集的函数
func subsets(nums []int) [][]int {
/* 1. 预处理 */
subsetSequence = make([][]int, 0)
/* 2. 调用回溯函数 */
subsetsExec(nums, []int{})
/* 5. 返回结果集 */
return subsetSequence
}
func subsetsExec(nums []int, sequence []int) {
/* 3. 判断是否需要加入结果集以及进行剪枝 */
subsetSequence = append(subsetSequence, newSlice(sequence))
for i := 0; i < len(nums); i++ {
/* 4. 继续调用回溯函数 */
// 因为题目要求的是组合数且不能重复选取,所以下一层处理的是 nums[i+1:]
subsetsExec(nums[i+1:], append(sequence, nums[i]))
}
}
func newSlice(slice []int) []int {
s := make([]int, len(slice))
copy(s, slice)
return s
}
var combinationSequence [][]int // 结果集
func combinationSum3(k int, n int) [][]int {
/* 1. 进行一些预处理 */
candidates := make([]int, 9)
combinationSequence = make([][]int, 0)
for i := 1; i <= 9; i++ {
candidates[i-1] = i
}
/* 2. 调用回溯函数 */
combinationSumExec(candidates, n, k, make([]int, 0, 10))
/* 5. 返回结果集 */
return combinationSequence
}
func combinationSumExec(candidates []int, n int, k int, sequence []int) {
/* 3. 判断是否需要加入结果集以及进行剪枝 */
if n == 0 && k == 0 {
combinationSequence = append(combinationSequence, newSlice(sequence))
return
}
if n == 0 || k == 0 {
return
}
for i := 0; i < len(candidates); i++ {
/* 4. 继续调用回溯函数 */
// 因为题目要求的是组合数且不能重复选取,所以下一层处理的是 candidates[i+1:]
combinationSumExec(candidates[i+1:], n-candidates[i], k-1, append(sequence, candidates[i]))
}
}
// 深拷贝
func newSlice(oldSlice []int) []int {
slice := make([]int, len(oldSlice))
copy(slice, oldSlice)
return slice
}
3.4 排列问题 — 没有重复元素、不可重复选取
var permuteSequence [][]int // 结果集
// 返回结果集的函数
func permute(nums []int) [][]int {
/* 1. 进行一些预处理 */
permuteSequence = make([][]int, 0)
/* 2. 调用回溯函数 */
permuteUniqueExec(nums, []int{})
/* 5. 返回结果集 */
return permuteSequence
}
// 回溯函数
func permuteUniqueExec(nums []int, sequence []int) {
/* 3. 判断是否需要加入结果集以及进行剪枝 */
if len(nums) == 0 {
permuteSequence = append(permuteSequence, newSlice(sequence))
return
}
for i := 0; i < len(nums); i++ {
/* 4. 继续调用回溯函数,这里会有以下几种情况。*/
// 因为题目要求的是排列数,且不可重复选取,所以处理如下。
nums[0], nums[i] = nums[i], nums[0]
permuteUniqueExec(nums[1:], append(sequence, nums[0]))
nums[0], nums[i] = nums[i], nums[0]
}
}
// 深拷贝
func newSlice(oldSlice []int) []int {
slice := make([]int, len(oldSlice))
copy(slice, oldSlice)
return slice
}
3.5 排列问题 — 有重复元素、不可重复选取
var permuteSequence [][]int // 结果集
// 返回结果集的函数
func permuteUnique(nums []int) [][]int {
/* 1. 进行一些预处理 */
permuteSequence = make([][]int, 0)
/* 2. 调用回溯函数 */
permuteUniqueExec(nums, []int{})
/* 5. 返回结果集 */
return permuteSequence
}
// 回溯函数
func permuteUniqueExec(nums []int, sequence []int) {
/* 3. 判断是否需要加入结果集以及进行剪枝 */
if len(nums) == 0 {
permuteSequence = append(permuteSequence, newSlice(sequence))
return
}
isVisit := make(map[int]bool) // 有重复元素才需要这个结构,没有重复元素的话,这个结构可以直接删除。
for i := 0; i < len(nums); i++ {
if isVisit[nums[i]] == true {
// 使用过时
continue
}
isVisit[nums[i]] = true
/* 4. 继续调用回溯函数,这里会有以下几种情况。*/
// 因为题目要求的是排列数,且不可重复选取,所以处理如下。
nums[0], nums[i] = nums[i], nums[0]
permuteUniqueExec(nums[1:], append(sequence, nums[0]))
nums[0], nums[i] = nums[i], nums[0]
}
}
// 深拷贝
func newSlice(oldSlice []int) []int {
slice := make([]int, len(oldSlice))
copy(slice, oldSlice)
return slice
}
4. 对框架的一些解答
4.1 为什么有重复元素的组合问题预处理时需要进行排序呢?
假如有一个不可重复选取的数组 [1, 4, 1]
, 我们要求出组成总和为 5
的所有组合。 我们调用上面的框架,但是不对他进行排序。
那么我们得出的结果是 : [[1, 4], [4, 1]]
,显然,这 2
个组合是一样的,那是什么导致了这个问题呢?
先说说这两个元组的形成原因:
- 在第一个
1
后,它发现自己和后面的4
相加就能组成5
,于是出现[1, 4]
这个组合。 - 而在
4
后面,它发现自己和后面的1
相加就能组成5
,于是出现[4, 1]
这个组合。
那我们如何避免这种重复组合的情况呢?
最简单的方法就是使用排序,让数组有序化,这样就不会出现 a
与后面的 b
组合后,b
又与后面另外一个 a
进行组合。
于是,[1, 4, 1]
这个数组经过排序后,变为了 [1, 1, 4]
,4
的后面没有 1
,这样就不会导致重复组合的情况了。
而你此时可能会问: 为什么此时不会出现两个[1, 4]
的情况呢?即返回结果是[[1, 4], [1, 4]]
。
这个问题我们已经用框架中的isVisited
这个结构解决了,当第一个 1
被加入 sequence
后,我们把 1
标记为已访问。
之后在该层又遇到 1
, 此时由于我们已经标记了 1
,即此时 isVisited[1] == true
,于是接下来会执行 continue
操作,
跳过了这个1
的后续操作,所以不会出现两个[1, 4]
的情况
5. 注意点
- 上面的框架可以求出具体的排列组合,于是我们也可以得出这些排列组合的长度、种数,但是实际上还有更好的方法求出排列组合的种数,比如采用动态规划。
- 以上框架在实际问题中可能需要经过一些转换、改变,所以需要灵活运用。