回溯算法(二)

161 阅读3分钟

上篇文章讲了一下回溯算法的基本定义、代码模版和两道全排列相关的算法题,这次来看几道其他类型的回溯算法题。

子集 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了,有重复数的子集剪枝条件理解起来应该不难,对照图示多写几遍相信大家都可以掌握的。