这次我想给大家分享的内容是动态规划Subset Sum (子集之和)问题 。其实挺枯燥的,涉及到的是数学和编程方面的内容,如果不感兴趣请直接退出阅读。
本文概要:
- Subset Sum 问题描述
- 问题求解思路
- 递归法求解
- 重点:动态规划法求解
Subset Sum 问题
假设有一串 { 1,2, 4,5,6} , 是否存在任意数字之和为 7 , 55 。
通过目测 { 1, 2,4} {5, 2} 之和为7,所以答案为存在,因为所有元素之和小于55,所以答案为不存在。
问题:
假设有一个非负整数的集合 Set ,求是否存在一个组合使得其中任意元素之和为一个给定的整数 sum ?
A = { X1,X2,....Xn}, sum = Y
subset(A,Y) = True or False ?
求解思路
我们立马能想到第一个方法是先求解集合A 所有的排列组合,一共有2的5次方种可能,然后逐一对组合求和判断是否等于6。这种思路属于暴利求解法,当集合元素非常多的时候,计算时间会指数级增长,该算法的时间复杂度为O(2^n)。
第二种求解方法的思想是 把大事化小,小事化无的。
如: A = { 1,2, 4,5,6} 可以简化为 { 2, 4,5,6 } 这个子集合元素是否有sum = 6 的解,或者{ 2, 4,5,6 } 是否存在相加之和为5 的解 。依次类推可以将问题一层一层缩小,利用上篇文章提到的递归思想。
递归求解法
arr = [1,2 ,4,5,6]
def subset(arr , i , s):
if i == 0:
return s == arr[0]
elif s == 0:
return True
elif arr[i] > s :
return rec_subset(arr, i-1 , s)
else:
A = subset(arr , i-1 ,s - arr[i])
B = subset(arr, i-1 , s)
return A or B
print( subset(arr ,len(arr )-1, 7))
out: True
通过递归方式来求解必须遍历所有的子问题,但其中子问题有重叠的部分,也就是说存在重复计算的情况,算法时间复杂度为O(2^n)。对于规模比较大的问题这是无法容忍的。接下来通过动态规划的方法 ,来避免重复计算来优化程序的性能。
动态规划求解法
动态规划是从下往上(bottom-up)的求解方法,并且把中间结果缓存起来避免重复计算。
例如:把{ 1,2, 4,5,6} 分解成 空集 0 { } ,1 { 1 } ,2 { 1,2 } ,3 { 1,2, 4 }, ,4 { 1,2, 4,5 } ,5 { 1,2, 4,5,6} 分别来计算,具体流程如下。
- 构建一个二维数组Subset[i,j] 来存放子集合相加和的所有可能情况,True(以下记为T)表示存在解,False(以下记为F)表示不存在解。
如图1: 行为子集相加为0-7所有的情况,列为0-5个元素的所有情况。
第一行表示: 除了第一列为0存在解标记为T 之外,其余1-7都为 F 。
第一列表示:各个子集相加之和为0的情况。例如 {1 } 的情况显然如果 不取元素1 空集{}之和存在0的解,类似{1,2} 也有存在0的解,因此第一列都为T。
第二行第二列:表示{1} 之和为1,显然为T 。
第二行其余列:因为最大为1,所以余下都为F。
中间任意一个格子计算方法,分两种情况。
如果格子上方为T,当前格子也为T。实际意义为:当前集合的子集如果有解那么,当前集合也有解,如{1,2,4} 之和3的情况,因为 {1,2} 存在之和为3的解。
因此得出结论,只要T下方的格子都可以设置为T。
如果格子上方为F, 则求 sum - A 余下子集合的解。例如上图绿色标记的位置 Subset[2,4] , 可以简化为求解 { 1 } 是否有 1的解,查表值为T,当前表格值则 为T。
用此算法,把表格填满。
表格最后一个元素就是 { 1,2, 4,5,6} 是否存在子集合之和为7 这个问题的解。
具体的代码实现:
import numpy as np
def dp_subset(arr,S):
subset = np.zeros((len(arr),S+1),dtype=bool) #构造二维数组
subset[:,0] = True # 第一列 设为True
subset[0,: ] = False #第一列 设为 False
subset[0,arr[0]] = True
for i in range(1,len(arr)):
for s in range(1, S+1):
if arr[i] > s:
subset[i , s] = subset[i-1 , s]
else:
A = subset[i-1 , s ]
B = subset[i-1 , s - arr[i]]
subset[i,s] = A or B
row ,cell = subset.shape
return subset[row-1,cell-1]
arr = [1,2, 4,5,6]
dp_subset(arr,7)
out:True