题目描述

✅ 368. 最大整除子集

image-20260929094908122

题意分析

从互不相同的正整数中选择尽量多的元素,使选出元素的任意一对都满足一方能整除另一方。要求返回具体子集,而不只是长度;若有多个同样大的合法子集,返回任意一个即可。

解法:排序 + DP 记录前驱

核心思路

[!blue]

先把数组升序排序。对于两个不同的正整数,只有较小者可能整除较大者,所以合法子集排序后一定从小到大连接。又因为整除具有传递性,只要链上相邻元素满足前者整除后者,更早的每个元素也都能整除更后的元素。于是“两两满足整除”就转成了“寻找最长整除链”。

定义 dp[i] 为以 nums[i] 作为最大元素、也就是链尾的最长链长度。单个元素始终合法,因此初始 dp[i] = 1。枚举所有 j < i,若 nums[i] % nums[j] == 0,就可以把 nums[i] 接到以 nums[j] 结尾的最长链后面,得到候选长度 dp[j] + 1。

接入新元素时,只检查链尾就足够:前面所有元素已经整除 nums[j],而 nums[j] 又整除 nums[i],传递性保证新元素与整条链都满足要求。反过来,一条以 nums[i] 结尾且长度超过一的最优链,倒数第二项一定是某个满足条件的 j,因此枚举所有 j 不会遗漏最优方案。

为返回具体子集,prev[i] 记录当前最优链中 i 的前驱下标,初始为 $-1$,表示这条链只有自身。只有候选长度严格更大时,才同时更新 dp[i] 和 prev[i],保证长度与回溯路径来自同一个选择;等长时保留任意一条即可。

一边完成各个 dp[i],一边记录全局最长长度 bestLen 及末尾 bestEnd。最优链不一定包含数组最大值,所以不能固定从最后一个位置还原。应从 bestEnd 沿 prev 回溯,直到 $-1$;前驱下标始终更小,回溯一定结束,取得的元素正好构成对应最长链。当前实现再反转一次,使结果按升序展示。

只有一个元素或没有任何两个元素能整除时,所有候选都保持单元素链,依然可以从全局最佳末尾返回一个合法最大子集。

解题步骤

  1. 排序,所有长度初始化为一,前驱初始化为 -1。
  2. 枚举 i 及此前的 j,满足整除且链更长时更新。
  3. 记录最长链末尾。
  4. 沿前驱还原并返回结果。

代码实现

class Solution {
    public List<Integer> largestDivisibleSubset(int[] nums) {
        Arrays.sort(nums);
        int n = nums.length;
        int[] dp = new int[n];
        int[] prev = new int[n];

        for (int i = 0; i < n; i++) {
            dp[i] = 1;
            prev[i] = -1;
        }

        int bestLen = 0;
        int bestEnd = -1;

        for (int i = 0; i < n; i++) {
            for (int j = 0; j < i; j++) {
                if (nums[i] % nums[j] == 0 && dp[j] + 1 > dp[i]) {
                    // 更长链出现时同步保存长度和对应前驱。
                    dp[i] = dp[j] + 1;
                    prev[i] = j;
                }
            }

            if (dp[i] > bestLen) {
                bestLen = dp[i];
                bestEnd = i;
            }
        }

        List<Integer> res = new ArrayList<>();
        // 从全局最佳末尾沿前驱还原,不一定从数组最后一项开始。
        int cur = bestEnd;

        while (cur != -1) {
            res.add(nums[cur]);
            cur = prev[cur];
        }

        Collections.reverse(res);

        return res;
    }
}
import "sort"

func largestDivisibleSubset(nums []int) []int {
    sort.Ints(nums)
    n := len(nums)
    dp := make([]int, n)
    prev := make([]int, n)
    for i := 0; i < n; i++ {
        dp[i] = 1
        prev[i] = -1
    }

    bestLen := 0
    bestEnd := -1
    for i := 0; i < n; i++ {
        for j := 0; j < i; j++ {
            if nums[i]%nums[j] == 0 && dp[j]+1 > dp[i] {
                // 更长链出现时同步保存长度和对应前驱。
                dp[i] = dp[j] + 1
                prev[i] = j
            }
        }
        if dp[i] > bestLen {
            bestLen = dp[i]
            bestEnd = i
        }
    }

    res := make([]int, 0)
    // 从全局最佳末尾沿前驱还原,不一定从数组最后一项开始。
    cur := bestEnd
    for cur != -1 {
        res = append(res, nums[cur])
        cur = prev[cur]
    }

    for i, j := 0, len(res)-1; i < j; i, j = i+1, j-1 {
        res[i], res[j] = res[j], res[i]
    }
    return res
}

复杂度分析

  • 时间复杂度:$O(n²)$,双层转移占主导。
  • 空间复杂度:$O(n)$,保存长度、前驱及结果;输入数组会被排序。

关键点总结

[!green]

  • 相邻整除通过传递性保证两两整除。
  • 长度和前驱必须对应同一条选中的链。
  • 最长链末尾不一定是数组最大值。

易错点总结

[!yellow]

  • 不排序仍只查此前元素:可能遗漏数值更小但位置更后的前驱。
  • 前驱默认零:回溯可能停在零下标形成自环。
  • 只要整除就覆盖前驱:较短链可能覆盖更好的方案。
  • 直接从最后一项回溯:数组最大值不一定能接在最优链后面,应使用单独记录的 bestEnd。

相似题目

题目 难度 关联与区别
300. 最长递增子序列 中等 排序后同样用以某项结尾的最长链DP,本题转移条件是整除而不是仅数值递增。
873. 最长的斐波那契子序列的长度 中等 同样要记录可延续的序列状态,本题只依赖末项,斐波那契条件则依赖最后两项。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2023/82766898
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!