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

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

前言

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


子集和问题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]

	}
}
相关推荐
狐572 小时前
2026-01-17-LeetCode刷题笔记-3047-求交集区域内的最大正方形面积
笔记·算法·leetcode
Yzzz-F2 小时前
P3509 [POI 2010] ZAB-Frog[单调队列+倍增快速幂思想]
算法
代码无bug抓狂人2 小时前
C语言之5位黑洞数
c语言·算法
CodeByV2 小时前
【算法题】BFS:FloodFill
算法
long3162 小时前
弗洛伊德·沃肖算法 Floyd Warshall Algorithm
java·后端·算法·spring·springboot·图论
有一个好名字2 小时前
力扣-咒语和药水的成功对数
java·算法·leetcode
Loo国昌2 小时前
【LangChain1.0】第一篇:基础认知
后端·python·算法·语言模型·prompt
H Corey2 小时前
Java--面向对象之继承与多态
java·开发语言·windows·学习·算法·intellij-idea
じ☆冷颜〃3 小时前
交换代数的解析延拓及在CS的应用
c语言·数据结构·笔记·线性代数·密码学