LeetCode刷题总结 --- 排列组合框架

12 篇文章 0 订阅
8 篇文章 1 订阅

求解排列组合问题

导言

  1. 以下代码都存放于 我的GitHub仓库 ,如果小伙伴觉得有用,请给我颗星星哈。
  2. 以下代码都是提交过的,正确性可以保证。

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 组合问题 — 没有重复元素、可重复选取

39. 组合总和

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 组合问题 — 有重复元素、不可重复选取

40. 组合总和 Ⅱ

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
}

90. 子集 Ⅱ

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 组合问题 — 没有重复元素、不可重复选取

78. 子集

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
}

216. 组合总和 Ⅲ

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 排列问题 — 没有重复元素、不可重复选取

46. 全排列

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 排列问题 — 有重复元素、不可重复选取

47.全排列 Ⅱ

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. 注意点

  • 上面的框架可以求出具体的排列组合,于是我们也可以得出这些排列组合的长度、种数,但是实际上还有更好的方法求出排列组合的种数,比如采用动态规划。
  • 以上框架在实际问题中可能需要经过一些转换、改变,所以需要灵活运用。

6. 练习题

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值