上篇文章讲了一下回溯算法的基本定义、代码模版和两道全排列相关的算法题,这次来看几道其他类型的回溯算法题。
子集 I
给定一组不含重复元素的整数数组nums,返回该数组所有可能的子集(幂集)。说明:解集不能包含重复的子集。
题目来源:leetcode.78
输入: nums = [1,2,3]
输出:
[
[3],
[1],
[2],
[1,2,3],
[1,3],
[2,3],
[1,2],
[]
]
依然是直接套模版然后三部曲,不过要思考子集和全排列实现的不同点,下面直接上代码。
function subset(arr) {
const result = []
function loop(path, count) {
result.push([...path]) // 把当前子集结果存起来 因为下面的循环里面递归后i初始值不是0了 所以这里不用设置边界条件
for (let i=count; i<arr.length; i++){ // 设置i = count
path.push(arr[i]) // 选择当前值
loop(path, i+1) // 进行递归,因为题解是子集,所以设置递归的i从下一位开始 选取arr中下一个值
path.pop() // 回溯
}
}
loop([],0)
return result
}
subset([1,2,3])
结合图解来看一下吧
和全排列不同的是,全排列的边界是结果和给定的数组长度相等就是一个全排列解,这道题是每一步的path的值就是子集题解,就是寻找树的所有节点。
这道题没有设置边界条件是因为每次递归都会设置i+1为下一次递归循环的初始值,其实就是把每一步的子集和给定的数组arr中剩下的元素做组合,所以递归的时候要设置i+1为下一次递归循环的初始值,当i>=arr.length的时候就是循环终止的时候,说明arr中没有剩余元素了,这就相当于设置了边界条件,大体思想和全排列一样,但是其中的不同点需要大家多思考一下。
下面来看一下子集的第二道题。
子集 II
给定一个可能包含重复元素的整数数组nums,返回该数组所有可能的子集(幂集)。说明:解集不能包含重复的子集。
题目来源:leetcode.90
输入: [1,2,2]
输出:
[
[2],
[1],
[1,2,2],
[2,2],
[1,2],
[]
]
熟悉的感觉又回来了,和全排列的两道题一样,这个也是上边子集题目的变种,大家想一下全排列有重复元素和没重复元素的做法有什么不同?
有重复元素的话我们就要在上边的基础上要增加剪枝判断了,即横向循环的时候,相邻的两个元素值一样且当前i>count的时候我们就不能再选择了,明确了这个思想之后,这道题解起来就很容易了,上代码:
function subset(arr) {
const result = []
arr.sort((a,b) => a-b) // 排一下序 方便后面剪枝函数的判断
function loop(path, used, count) {
result.push([...path])
for (let i=count; i<arr.length; i++){ // 设置i = count
// 横向循环 相邻两个元素相等时 因为每次递归循环设置的初始值i = count 所以 i>count时 就不能再选择了
if(i > 0 && arr[i] === arr[i-1] && !used[i-1]) continue
path.push(arr[i]) // 选择当前值
used[i] = true
loop(path, used, i+1) // 递归选择下一个元素
path.pop() // 回溯
used[i] = false
}
}
loop([], [], 0)
return result
}
subset([2,1,2])
看下图:
剪枝条件和全排列的剪枝条件是一样的,相信看过上一篇文章的小伙伴能轻松理解这里剪枝条件的意思,我就不再多赘述了。
其实上面的剪枝条件还可以写成if(i > count && arr[i] === arr[i-1]) continue这样的话函数中就可以省去used变量相关逻辑
,
因为每次递归循环i的值是从上一步的值+1的到的,所以我们只需要判断i > count && arr[i] === arr[i-1]就可以了,条件成立时当前的值arr[i]就需要被剪掉。
好了今天的两道子集相关的题就结束了,总体来说子集的题难点在于要判断出每次循环的i初始值不能为0了,有重复数的子集剪枝条件理解起来应该不难,对照图示多写几遍相信大家都可以掌握的。