目录
题目链接:368. 最大整除子集 - 力扣(LeetCode)
题目链接:368. 最大整除子集 - 力扣(LeetCode)
注:下述题目描述和示例均来自力扣
题目描述
给你一个由 无重复 正整数组成的集合 nums
,请你找出并返回其中最大的整除子集 answer
,子集中每一元素对 (answer[i], answer[j])
都应当满足:
answer[i] % answer[j] == 0
,或answer[j] % answer[i] == 0
如果存在多个有效解子集,返回其中任何一个均可。
示例 1:
输入:nums = [1,2,3] 输出:[1,2] 解释:[1,3] 也会被视为正确答案。
示例 2:
输入:nums = [1,2,4,8] 输出:[1,2,4,8]
提示:
1 <= nums.length <= 1000
1 <= nums[i] <= 2 * 10^9
nums
中的所有整数 互不相同
解法一:动态规划ProMax
问题分析
给定一个无重复正整数数组 nums
,我们需要找到其中最大的一个子集,满足该子集中任意两个元素之间都能相互整除。这里所说的“相互整除”指的是对于子集中的任何一对元素 (a, b)
,要么 a % b == 0
,要么 b % a == 0
。
解决方案步骤
-
排序:
- 首先对输入数组进行升序排列。这样做的好处是在后续处理时,我们只需要考虑当前元素能否被之前出现过的元素整除,而不需要双向验证(即检查当前元素能否整除之后的元素)。
-
动态规划初始化:
- 创建一个与原数组等长的
dp
数组,用来记录以每一个元素结尾的最大整除子集的大小。初始状态下,每个元素至少可以单独构成一个子集,所以dp
数组的所有元素都初始化为 1。 - 同时创建一个
prev
数组来追踪每个元素在最大整除子集中的前驱元素索引,用于最后重构出完整的最大整除子集。
- 创建一个与原数组等长的
-
填充
dp
数组:- 对于排序后的数组中的每个元素
nums[i]
,遍历它之前的所有元素nums[j]
(j < i)。 - 如果
nums[i] % nums[j] == 0
,说明我们可以将nums[i]
加入到以nums[j]
结尾的整除子集中。如果这样做能让子集变大(即dp[j] + 1 > dp[i]
),则更新dp[i]
和prev[i]
。
- 对于排序后的数组中的每个元素
-
确定最大子集的末尾元素:
- 在填充完
dp
数组后,遍历dp
数组找出最大值及其对应的索引。这个索引就是最大整除子集最后一个元素的位置。
- 在填充完
-
回溯构造结果:
- 利用
prev
数组从最大子集的末尾元素开始回溯,逐步向前添加元素,直到到达序列的开头,从而得到整个最大整除子集。
- 利用
Java写法:
class Solution {
public List<Integer> largestDivisibleSubset(int[] nums) {
// 如果数组为空,直接返回空列表
if (nums == null || nums.length == 0) {
return new ArrayList<>();
}
// 对数组进行排序(虽然题目给的测试用例都是递增的,但是这都是骗局,整体测试用例有一个[3,4,16,8]这里就不是递增的)
Arrays.sort(nums);
int n = nums.length;
// dp[i] 表示以 nums[i] 结尾的最大整除子集的大小
int[] dp = new int[n];
// prev[i] 表示 nums[i] 在最大整除子集中的前驱元素的索引
int[] prev = new int[n];
// 初始化 dp 数组,每个元素至少可以单独成一个子集
Arrays.fill(dp, 1);
// 初始化 prev 数组,-1 表示没有前驱
Arrays.fill(prev, -1);
// 记录最大整除子集的最后一个元素的索引
int maxIndex = 0;
// 记录最大整除子集的大小
int maxSize = 1;
// 动态规划填表
for (int i = 1; i < n; i++) {
for (int j = 0; j < i; j++) {
// 如果 i 能整除之前的全部 元素而且数量增多了
// 如果 nums[i] 能被 nums[j] 整除,并且 dp[j] + 1 > dp[i]
if (nums[i] % nums[j] == 0 && dp[j] + 1 > dp[i]) {
// 写入dp数组
dp[i] = dp[j] + 1;
// 更新前驱索引
prev[i] = j;
}
}
// 更新最大子集的大小和对应的索引
if (dp[i] > maxSize) {
maxSize = dp[i];
maxIndex = i;
}
}
// 回溯构造结果子集
List<Integer> result = new ArrayList<>();
while (maxIndex != -1) {
result.add(nums[maxIndex]);
// 相当于链表往前找
maxIndex = prev[maxIndex];
}
// 返回结果(逆序)
return result;
}
}
C++写法:
#include <vector>
#include <algorithm>
using namespace std;
class Solution {
public:
vector<int> largestDivisibleSubset(vector<int>& nums) {
// 如果数组为空,直接返回空列表
if (nums.empty()) {
return {};
}
// 对数组进行排序
sort(nums.begin(), nums.end());
int n = nums.size();
// dp[i] 表示以 nums[i] 结尾的最大整除子集的大小
vector<int> dp(n, 1);
// prev[i] 表示 nums[i] 在最大整除子集中的前驱元素的索引
vector<int> prev(n, -1);
// 记录最大整除子集的最后一个元素的索引
int maxIndex = 0;
// 记录最大整除子集的大小
int maxSize = 1;
// 动态规划填表
for (int i = 1; i < n; ++i) {
for (int j = 0; j < i; ++j) {
// 如果 nums[i] 能被 nums[j] 整除,并且 dp[j] + 1 > dp[i]
if (nums[i] % nums[j] == 0 && dp[j] + 1 > dp[i]) {
dp[i] = dp[j] + 1;
prev[i] = j; // 更新前驱索引
}
}
// 更新最大子集的大小和对应的索引
if (dp[i] > maxSize) {
maxSize = dp[i];
maxIndex = i;
}
}
// 回溯构造结果子集
vector<int> result;
while (maxIndex != -1) {
result.push_back(nums[maxIndex]);
maxIndex = prev[maxIndex]; // 相当于链表往前找
}
// 返回结果(逆序)
reverse(result.begin(), result.end());
return result;
}
};
运行时间
时间复杂度和空间复杂度
![]() |
![]() |
总结
这种方法利用了动态规划的思想,通过预先计算和存储中间结果来避免重复计算,提高了算法效率。时间复杂度主要由排序操作(O(n log n))和动态规划填表过程(O(n^2))决定,整体时间复杂度为 O(n^2),空间复杂度为 O(n),因为需要额外的空间来存储 dp
和 prev
数组。这种方法能有效地找出符合条件的最大整除子集。