关于子集和问题的几种解法

关于子集和问题的几种解法

前言

这几个子集和问题还蛮典型的,是使用回溯法的经典例题,有好几种做法,在此总结记录一下。


子集和问题1

leecode 39

回溯法

经典的组合问题,通过递归的方式来搜索所有的可能。 由于每个元素可以选择多次,所以每层递归需要遍历所有的元素。当然,由于不能有重复的组合,这里需要先从小到大排序,如果当前元素选择完毕,后续就从下一个元素开始。

完成的代码如下:

go 复制代码
func combinationSum(candidates []int, target int) [][]int {
	ans := make([][]int, 0)
	sort.Ints(candidates)
	dfsCombinationSum(candidates, []int{}, target, 0, &ans)
	return ans
}

func dfsCombinationSum(candidates []int, curCollection []int, target int, currentIndex int, ans *[][]int) {
	if target == 0 {
		curAns := slices.Clone(curCollection)
		*ans = append(*ans, curAns)
		return
	}

	if target < 0 {
		return
	}

	// each call should  traverse from currentIndex to the end of candidates
	for startIndex := currentIndex; startIndex < len(candidates); startIndex++ {
		canNum := candidates[startIndex]
		if canNum > target {
			return
		}

		curCollection = append(curCollection, canNum)
		dfsCombinationSum(candidates, curCollection, target-canNum, startIndex, ans)
		// restore curCollection
		curCollection = curCollection[:len(curCollection)-1]
	}
}

子集和问题2

问题描述

leecode 40

分治法

分治法的核心是分解子问题,然后合并子问题。这题子集和的组合问题,每个数字最多能使用一次,最终的结果是要么选中,要么不选中,可以考虑按照这个维度进行分解。

方法1
go 复制代码
func combinationSum2(candidates []int, target int) [][]int {
	sort.Ints(candidates)
	ans := dfsCombinationSumV2(candidates, target, 0)
	return ans
}

func dfsCombinationSumV1(candidates []int, target int, index int) [][]int {
	//march target
	if target == 0 {
		return make([][]int, 0)
	}

	// out of bound
	if index >= len(candidates) {
		return nil
	}

	if target < 0 {
		return nil
	}

	if candidates[index] > target {
		return nil
	}

	// not choose current candidate, target not change
	notUseCurValAnsList := dfsCombinationSumV1(candidates, target, index+1)

	// choose current candidate, nextTarget = target-candidates[index]
	useCurValAnsList := dfsCombinationSumV1(candidates, target-candidates[index], index+1)

	if notUseCurValAnsList == nil && useCurValAnsList == nil {
		return nil
	}

	// collection sub problem result
	curAnsList := make([][]int, 0)
	if notUseCurValAnsList != nil {
		curAnsList = notUseCurValAnsList
	}

	if useCurValAnsList != nil {
		if len(useCurValAnsList) == 0 {
			ans := []int{candidates[index]}
			curAnsList = uniqSetToAns(curAnsList, ans)
		} else {
			for _, ans := range useCurValAnsList {
				ans = append(ans, candidates[index])
				curAnsList = uniqSetToAns(curAnsList, ans)
			}
		}
	}

	return curAnsList
}

func uniqSetToAns(ansList [][]int, curAns []int) [][]int {
	for _, oneAns := range ansList {
		if slices.Equal(oneAns, curAns) {
			return ansList
		}
	}

	ansList = append(ansList, curAns)
	return ansList
}

这种做法是正确的,但是还不够好,对于重复的数字case会超时。

方法2

可以针对上述的情况进行优化,candidate 排序之后,相同的数字集中在一起,比如说有{1,2,2,2,3,4,5,6}, 对于中间的{2,2,2} 来说,可以看成一个整体,这个整体要么最终选择0个,1个,2个,3个。对应组合的值就是0,2,4,6; 看成一个整体之后,对于这个整体****就可以把原来幂次的执行变成多项式的执行。

go 复制代码
func dfsCombinationSumV2(candidates []int, target int, index int) [][]int {
	//march target
	if target == 0 {
		return make([][]int, 0)
	}

	// out of bound
	if index >= len(candidates) {
		return nil
	}

	if target < 0 {
		return nil
	}

	if candidates[index] > target {
		return nil
	}

	var curAnsList [][]int
	nextEqualIndex := findNextEqualIndex(candidates, index)
	diff := nextEqualIndex - index + 1
	currentCandidateList := make([]int, 0, diff)

	for count := 0; count <= diff; count++ {
		total := candidates[index] * count
		// only count > 0, need to add to useCandidateList
		if count > 0 {
			currentCandidateList = append(currentCandidateList, candidates[index])
		}

		useCurValAnsList := dfsCombinationSumV2(candidates, target-total, nextEqualIndex+1)

		if useCurValAnsList != nil {
			cloneUseCandidateList := slices.Clone(currentCandidateList)
			if len(useCurValAnsList) == 0 {
				curAnsList = append(curAnsList, cloneUseCandidateList)
			} else {
				for _, ans := range useCurValAnsList {
					ans = append(ans, cloneUseCandidateList...)
					curAnsList = append(curAnsList, ans)
				}
			}
		}
	}

	return curAnsList
}

// find equal range
func findNextEqualIndex(candidate []int, index int) int {
	source := candidate[index]
	for index < len(candidate) {
		if candidate[index] != source {
			return index - 1
		}

		index++
	}
	return index - 1
}

回溯法

回溯法的本质思路是从当前节点先向前搜索,搜到头之后折回来从当前节点(这个时候需要回溯之前的上下文状态,简单理解就是一些数据结构)再搜寻其他情况。 这种思想和递归一脉相承。

方法1

和上面分治法的思路有些相同,对于每一个节点都有选择和不选择之分,不同的是,下述做法每次将当前选择带入到下一层。 回溯体现在对于当前节点来说,每一次传递给下一层的都是 从当前节点进行选择的状态(不是把之前递归的结果状态带回来)。 下文中显示的恢复上下文状态主要体现在

go 复制代码
slices.Clone(nextCandidate)

完整代码如下:

go 复制代码
func combinationSum2(candidates []int, target int) [][]int {
	//remove unused num
	candidates = slices.DeleteFunc(candidates, func(num int) bool {
		return num > target
	})
	
	sort.Ints(candidates)

	ans := make([][]int, 0)
	dfsCombinationSumV3(candidates, []int{}, 0, target, &ans)
	return ans
}

func dfsCombinationSumV3(candidates []int, currentCandidate []int, currentIndex, target int, ans *[][]int) {
	// match the target, collect current ans
	if target == 0 {
		*ans = append(*ans, currentCandidate)
		return
	}

	if currentIndex >= len(candidates) {
		return
	}

	if target < 0 {
		return
	}

	nextEqualIndex := findNextEqualIndex(candidates, currentIndex)
	diff := nextEqualIndex - currentIndex + 1
	nextCandidate := slices.Clone(currentCandidate)
	for count := 0; count <= diff; count++ {
		totalMinus := candidates[currentIndex] * count

		if count == 0 {
			// not use current candidate
			dfsCombinationSumV3(candidates, nextCandidate, nextEqualIndex+1, target-totalMinus, ans)
			continue
		}

		nextCandidate = slices.Clone(nextCandidate)
		nextCandidate = append(nextCandidate, candidates[currentIndex])

		// use current candidate
		dfsCombinationSumV3(candidates, nextCandidate, nextEqualIndex+1, target-totalMinus, ans)
	}

	return

}

// find equal range
func findNextEqualIndex(candidate []int, index int) int {
	source := candidate[index]
	for index < len(candidate) {
		if candidate[index] != source {
			return index - 1
		}

		index++
	}
	return index - 1
}
方法2

前面是以元素为角度来进行选择,每种元素是选还是不选。还有一种思路,以位置为角度进行选择,每一个位置可以选择哪些元素。比如说有candidate=10,1,2,7,6,1,5, 那么第一个位置可以是{10,1,2,7,6,5},每一个节点是一个循环。

完整代码如下:

go 复制代码
func combinationSum2(candidates []int, target int) [][]int {
	candidates = slices.DeleteFunc(candidates, func(num int) bool {
		return num > target
	})
	sort.Ints(candidates)

	ans := make([][]int, 0)
	dfsCombinationSumV4(candidates, []int{}, 0, target, &ans)
	return ans
}

func dfsCombinationSumV4(candidates []int, currentCandidate []int, currentIndex, target int, ans *[][]int) {
	// match the target, collect current ans
	if target == 0 {
		*ans = append(*ans, slices.Clone(currentCandidate))
		return
	}

	if target < 0 {
		return
	}

	for startIndex := currentIndex; startIndex < len(candidates); startIndex++ {

		// skip same candidate
		if startIndex > currentIndex && candidates[startIndex] == candidates[startIndex-1] {
			continue
		}

		currentCandidate = append(currentCandidate, candidates[startIndex])

		nextTarget := target - candidates[startIndex]
		dfsCombinationSumV4(candidates, currentCandidate, startIndex+1, nextTarget, ans)
		// restore state
		currentCandidate = currentCandidate[:len(currentCandidate)-1]

	}
}
相关推荐
数模竞赛Paid answer6 小时前
2026年中青杯数学建模A题数学建模论文智能评估系统与多智能体优化方法求解全过程论文及程序
算法·数学建模·中青杯
银-豆豆6 小时前
数据结构与算法-动态规划、回溯与贪心
算法·动态规划
想吃火锅10058 小时前
【leetcode】200. 岛屿数量
算法·leetcode·职场和发展
hold?fish:palm8 小时前
24 回文链表
数据结构·c++·链表
Nil2089 小时前
leetcode 54螺旋矩阵
算法·leetcode·矩阵
依然鸣9 小时前
PTA团体程序设计天梯赛L1真题讲解L1-077-080
开发语言·c++·算法·深度优先·pat考试·图论
qeen8712 小时前
【数据结构】自平衡二叉搜索树各种旋转算法原理解析及AVL树的C++实现
数据结构·c++·算法
lucas_AI12 小时前
1.2B 小模型赢过 235B 大模型:NaviDC-OCR 把文档解析卷明白了
人工智能·深度学习·算法
冻柠檬飞冰走茶13 小时前
PTA基础编程题目集 7-35有理数均值(C++语言实现)
开发语言·数据结构·c++·算法·均值算法
民乐团扒谱机13 小时前
【微实验】倒谱算法(Cepstrum)深度解析:原理、数学推导与代码实现
人工智能·算法·语音识别