力扣每日一题——最大整除子集(难度:中等?)

目录

题目链接:368. 最大整除子集 - 力扣(LeetCode)

题目描述

解法一:动态规划ProMax

问题分析

解决方案步骤

​编辑

Java写法:

C++写法:

运行时间

时间复杂度和空间复杂度

总结



题目链接: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

解决方案步骤

  1. 排序

    • 首先对输入数组进行升序排列。这样做的好处是在后续处理时,我们只需要考虑当前元素能否被之前出现过的元素整除,而不需要双向验证(即检查当前元素能否整除之后的元素)。
  2. 动态规划初始化

    • 创建一个与原数组等长的 dp 数组,用来记录以每一个元素结尾的最大整除子集的大小。初始状态下,每个元素至少可以单独构成一个子集,所以 dp 数组的所有元素都初始化为 1。
    • 同时创建一个 prev 数组来追踪每个元素在最大整除子集中的前驱元素索引,用于最后重构出完整的最大整除子集。
  3. 填充 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]
  4. 确定最大子集的末尾元素

    • 在填充完 dp 数组后,遍历 dp 数组找出最大值及其对应的索引。这个索引就是最大整除子集最后一个元素的位置。
  5. 回溯构造结果

    • 利用 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),因为需要额外的空间来存储 dpprev 数组。这种方法能有效地找出符合条件的最大整除子集。