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

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

前言

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


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

	}
}
相关推荐
不知名的老吴3 分钟前
高阶函数的应用与函数对象概念
算法
Mr_pyx8 分钟前
【LeetCode Hot 100】 - 缺失的第一个正数完全题解
数据结构·算法
wydxry14 分钟前
深入解析自适应光学中的哈特曼波前传感技术:原理、算法与智能化前沿
大数据·人工智能·算法
xieliyu.18 分钟前
Java顺序表实现扑克牌Fisher-Yates 洗牌算法
java·数据结构·算法·javase
guygg8825 分钟前
极化码(Polar Codes)的MATLAB实现
开发语言·数据结构·matlab
yuannl1037 分钟前
数据结构----树
数据结构
ICscholar1 小时前
推荐系统常用指标NDCG含义及公式
人工智能·深度学习·算法
闲人xyz1 小时前
01|把一次用户请求做成可持续执行的回合:主循环才是 Agent 的骨架
算法·面试
超级码力6661 小时前
【Latex魔术注解+导言区】Latex魔术注解+导言区分类介绍
算法·数学建模
闲人xyz1 小时前
02|Tool Runtime 不是工具箱,而是行动层:从 FileRead / FileEdit 看到 Agent 工程
算法