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

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

前言

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


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

	}
}
相关推荐
笨笨阿库娅20 小时前
从零开始的算法基础学习
学习·算法
不想睡觉_20 小时前
优先队列priority_queue
c++·算法
那个村的李富贵1 天前
CANN加速下的AIGC“即时翻译”:AI语音克隆与实时变声实战
人工智能·算法·aigc·cann
power 雀儿1 天前
Scaled Dot-Product Attention 分数计算 C++
算法
Yvonne爱编码1 天前
JAVA数据结构 DAY6-栈和队列
java·开发语言·数据结构·python
熬夜有啥好1 天前
数据结构——哈希表
数据结构·散列表
琹箐1 天前
最大堆和最小堆 实现思路
java·开发语言·算法
renhongxia11 天前
如何基于知识图谱进行故障原因、事故原因推理,需要用到哪些算法
人工智能·深度学习·算法·机器学习·自然语言处理·transformer·知识图谱
坚持就完事了1 天前
数据结构之树(Java实现)
java·算法
算法备案代理1 天前
大模型备案与算法备案,企业该如何选择?
人工智能·算法·大模型·算法备案