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

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

前言

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


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

	}
}
相关推荐
草履虫建模13 小时前
力扣算法 1768. 交替合并字符串
java·开发语言·算法·leetcode·职场和发展·idea·基础
naruto_lnq15 小时前
分布式系统安全通信
开发语言·c++·算法
Jasmine_llq16 小时前
《P3157 [CQOI2011] 动态逆序对》
算法·cdq 分治·动态问题静态化+双向偏序统计·树状数组(高效统计元素大小关系·排序算法(预处理偏序和时间戳)·前缀和(合并单个贡献为总逆序对·动态问题静态化
爱吃rabbit的mq16 小时前
第09章:随机森林:集成学习的威力
算法·随机森林·集成学习
(❁´◡`❁)Jimmy(❁´◡`❁)17 小时前
Exgcd 学习笔记
笔记·学习·算法
YYuCChi17 小时前
代码随想录算法训练营第三十七天 | 52.携带研究材料(卡码网)、518.零钱兑换||、377.组合总和IV、57.爬楼梯(卡码网)
算法·动态规划
不能隔夜的咖喱18 小时前
牛客网刷题(2)
java·开发语言·算法
VT.馒头18 小时前
【力扣】2721. 并行执行异步函数
前端·javascript·算法·leetcode·typescript
进击的小头18 小时前
实战案例:51单片机低功耗场景下的简易滤波实现
c语言·单片机·算法·51单片机
咖丨喱19 小时前
IP校验和算法解析与实现
网络·tcp/ip·算法