上一篇博客【Leetcode】从 2sum 到 3sum, 4sum, Ksum 中介绍了Ksum,就是 “一个数组中的K个数之和等于给定数” 的问题。如果进一步来说,给定一个数组(元素不重复),请问是否存在某些数之和等于给定数?
Ksum 是固定了元素的数量为K个,很容易去思考,至少知道一个暴力解的K层循环。但是如果并不给定K,即参与求和的元素个数不确定(uncertain),只是某些元素和为给定的 target,这又该怎么做呢?其实这就是很经典的 “子集和问题”。
(注: 可参考 Leetcode中的 Combination sum I II III IV(Leetcode 39, 40,216, 377))
子集和问题
已知集合 S = { a 1 , a 2 , a 3 , ⋯   , a n } S=\{a_1,a_2,a_3,\cdots,a_n\} S={a1,a2,a3,⋯,an} 中的元素都是正整数且互不相同,给定另外一个正整数 M M M,试问是否存在 S S S 的一个子集使得其中的元素之和等于 M M M?
对于 “子集和问题”,常规的做法就是动态规划法和递归法,下面详细叙述一下这两种方法。
用动态规划法 (DP) 解 “子集和问题”
我们定义一个布尔变量 dp[ i i i][ j j j],它表示集合 S S S 中前 i i i 个元素里 (包括第 i i i 个) 是否存在某些元素之和等于 j j j。若存在,则 dp[ i i i][ j j j] = 1 =1 =1;若不存在,则 dp[ i i i][ j j j] = 0 =0 =0。
先考虑边界情况,即
i
=
0
i=0
i=0 和
j
=
0
j=0
j=0 的情况。
(1) 对于
j
=
0
j=0
j=0,此处规定空集的元素和为0,而每个集合都有子集为空集,所以一定有 dp[
i
i
i][
0
0
0] = 1;
(2) 对于
i
=
0
i=0
i=0,它对应的是前 0 个元素,即空集。那么只会有 dp[
0
0
0][
0
0
0] = 1,其余
j
j
j 的取值都是得到: dp[
0
0
0][
j
j
j] = 0 (
j
≠
0
j\neq 0
j̸=0)
再考虑常规情况,即 dp[
i
i
i][
j
j
j] (
i
≠
0
i\neq 0
i̸=0,
j
≠
0
j\neq 0
j̸=0)。
首先,如果 dp[
i
−
1
i-1
i−1][
j
j
j] = 1,那么必有 dp[
i
i
i][
j
j
j] = 1。因为前
i
−
1
i-1
i−1 个元素存在子集和为
j
j
j 的话,那么前
i
i
i 个元素肯定存在子集和为
j
j
j 的子集,原因是前
i
i
i 个元素包含前
i
−
1
i-1
i−1 个元素。
那么,如果 dp[
i
−
1
i-1
i−1][
j
j
j] = 0 呢?此种情况下,需要考虑到第
i
i
i 个元素值
a
i
a_i
ai。为什么这么说呢?因为 dp[
i
−
1
i-1
i−1][
j
j
j] = 0 表示前
i
−
1
i-1
i−1 个元素里面没有子集和为
j
j
j 的子集存在,但是如果前
i
−
1
i-1
i−1 个元素里面有子集和为
j
−
a
i
j-a_i
j−ai 的子集存在的话,那么前
i
i
i 个元素中不就有了子集和为
j
j
j 的子集存在了吗?
将上面的讨论情况总结一下,可以写成下面的公式:
d
p
[
i
]
[
j
]
=
{
d
p
[
i
−
1
]
[
j
]
,
如
果
c
o
n
d
i
t
i
o
n
1
d
p
[
i
−
1
]
[
j
]
∣
∣
d
p
[
i
−
1
]
[
j
−
a
i
]
,
如
果
c
o
n
d
i
t
i
o
n
2
dp[i][j]=\left\{ \begin{array}{l} dp[i-1][j], \hspace{2.8cm}如果 \ condition1 \\ dp[i-1][j] || dp[i-1][j-a_i], \ \hspace{0.14cm} 如果\ condition2 \end{array} \right.
dp[i][j]={dp[i−1][j],如果 condition1dp[i−1][j]∣∣dp[i−1][j−ai], 如果 condition2
上面式子中的符号 “||” 是真假值运算的 “或” ,
c
o
n
d
i
t
i
o
n
1
condition1
condition1 是指的是 “前
i
i
i 个元素中和为
j
j
j的子集不包括第
i
i
i 个元素”,
c
o
n
d
i
t
i
o
n
2
condition2
condition2 是指的是 “前
i
i
i 个元素中和为
j
j
j的子集包括第
i
i
i 个元素”。
Python 代码如下:
# nums是给定的集合(这里用数组形式表示),其中每个元素都是正整数
# M是给定的正整数,需要找出nums中的子集和等于它
def func(nums, M):
dp = [[0 for _ in range(M+1)] for _ in range(len(nums)+1)]
for i in range(len(nums)+1):
dp[i][0] = True
for j in range(1,M+1):
dp[0][j] = False
for i in range(1,len(nums)+1):
for j in range(M+1):
dp[i][j] = dp[i-1][j]
if j>= nums[i-1]: # 注:由于上面做了padding,所以此处的第i个元素即为nums[i-1]
dp[i][j]=dp[i-1][j]|dp[i-1][j-nums[i-1]]
return dp[len(nums)][M]
当然,上面这个动态规划也有个不好的地方,就是要创建一个记忆性的 dp 二维数组,用于存储数据,可以知道这个 dp 二维数组的维度是 (len(nums)+1) × \times × (M+1) 维的。当给定的 M 非常大时,那么这个二维数组将非常占用内存空间。
用递归法解 “子集和问题”
用递归法求解,其思路与上面一样,不同的是递归法是 “自上而下”,而动态规划法是 “自下而上”。
Python 代码如下:
def func(nums, M):
return isSubset(nums, len(nums), M)
def isSubset(nums, k, c):
if c==0:
return True
if k==0 and c!=0:
return False
if c>=nums[k-1]:
return isSubset(nums, k-1, c)|isSubset(nums, k-1, c-nums[k-1])
else:
return isSubset(nums, k-1, c)
通过上面的总结,可以知道虽然 Ksum 和 Uncertain sum 的题目求解问题描述差不多,但是解法上却相差很多(Ksum 的解法可以参见我上一篇博客【Leetcode】从 2sum 到 3sum, 4sum, Ksum)。
经常总结,经常思考,这样才会慢慢有进步。