关于子集和问题的几种解法
前言
这几个子集和问题还蛮典型的,是使用回溯法的经典例题,有好几种做法,在此总结记录一下。
子集和问题1
回溯法
经典的组合问题,通过递归的方式来搜索所有的可能。 由于每个元素可以选择多次,所以每层递归需要遍历所有的元素。当然,由于不能有重复的组合,这里需要先从小到大排序,如果当前元素选择完毕,后续就从下一个元素开始。
完成的代码如下:
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
问题描述
分治法
分治法的核心是分解子问题,然后合并子问题。这题子集和的组合问题,每个数字最多能使用一次,最终的结果是要么选中,要么不选中,可以考虑按照这个维度进行分解。
方法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]
}
}

