最大整除子集
题目要求
解题思路
来自[宫水三叶]
根据题意:对于符合要求的[整除子集]中的任意两个值,必然满足[较大数]是[较小数]的倍数
数据范围是
1
0
3
10^3
103,我们不可能采取获取所有子集,再检查子集是否合法的暴力搜解法。
通常递归做不了,我们就往[递推]方向取考虑。
由于存在[整除子集]中任意两个值必然存在倍数/约数关系的性质,我们自然会想到对nums
进行排序,然后从集合nums
中从大到小进行取数每次取数只考虑到当前决策的数是否与[整除子集]中的最后一个数成倍数关系即可。
这时候你可能会想枚举个数作为[整除子集]的起点,然后从前往后遍历一遍,每次都将符合[与当前子集最后一个元素成倍数]关系的数加入答案。
但是这样子的做法只能确保获得[合法解],无法确保得到的是[最长整除子集]
得到这个反例:[9,18,54,90,108,180,360,540,720],如果按照我们上述逻辑,我们得到的是 [9,18,54,108,540] 答案(长度为 5),但事实上存在更长的「整除子集」: [9,18,90,180,360,720](长度为 6)。
其本质是因为同一个数的不同倍数之间不存在必然的[倍数/约数关系],而是存在[具有公约数]的性质,这会导致我们[模拟解法]错过最优解
因此当我们决策到某个数nums[i]
时(nums
已排好序),我们无法直接将nums[i]
直接接在符合[约数关系]的,最靠近位置i
的数后面,而是要检查位置i
前面的所有符合[约数关系]的位置,找到一个已经形成[整除子集]长度最大的数。换句话说,当我们对nums
排好序号并从前往后处理时,已经形成的[整数子集]长度是多少,然后从中选一个最长的[整除子集],将nums[i]
接在后面(前提是符合[倍数关系])
动态规划
基于上述分析,我们不难发现这其实是一个序列DP问题:某个状态的转移依赖于与前一个状态的关系。即nums[i]
能否接在nums[j]
后面,取决于是否满足nums[i] % nums[j] == 0
条件
可以看作是[最长上升子序列]问题的变形题。
定义f[i]为考虑前i
个数字,且以第i
个数为结尾的最长[整数子集]长度。
我们不失一般性的考虑任意位置i
,存在两种情况:
- 如果在
i
之前找不到符合条件nums[i]%nums[j]==0
的位置j
,那么nums[i]
不能接在位置i
之前的任何数的后面,只能自己独立作为[整除子集]的第一个数,此时状态转移方程为f[i]=1; - 如果在
i
之前能够找到符合条件的位置j
,则取所有符合条件的f[i]
的最大值,代表如果希望找到以nums[i]
作为结尾的最长[整除子集],需要将nums[i]
接到符合条件的最长的nums[j]
后面,此时状态转移方程为f[i]=f[j]+1
同时由于我们需要输出具体方案,需要额外使用g[]
数组来记录每个状态是由哪个状态转移而来的。
定义g[i]为记录f[i]是由哪个下标的状态转移而来的,如果f[i] = f[j] + 1,则有g[i] = j
对于求方案数的题目,多开一个数组来记录状态从何转移而来是常见的手段。当我们求得所有的状态值止呕,可以对f[]
数组进行遍历,取得最长[整除子集]长度和对应下标,然后使用g[]
数组进行回溯,取得答案。
代码
class Solution:
def largestDivisibleSubset(self, nums: List[int]) -> List[int]:
nums.sort()
n = len(nums)
f,g = [0] *n,[0]*n
for i in range (n):
#至少包含自身一个数,因此起始长度为1,由自身转移而来
length,prev =1,i
for j in range(i):
if nums[i] %nums[j] ==0:
# 如果能接在更长的序列后面,则更新[最大长度]&[从何转移而来]
if f[j] +1>length:
length +=1
prev =j
# 记录【最终长度】& 【从何转移而来】
f[i] =length
g[i] =prev
#遍历所有的f[i],取得[最大长度]和【对应下标】
max_len = idx = -1
for i in range(n):
if f[i] >max_len:
idx =i
max_len =f[i]
#使用g[]数组回溯出具体方案
ans=[]
while len(ans)<max_len:
ans.append(nums[idx])
idx = g[idx]
ans.reverse()
return ans
复杂度分析
时间复杂度:
O
(
n
2
)
O(n^2)
O(n2)
空间复杂度:
O
(
n
)
O(n)
O(n)