目录

题目描述

673. 最长递增子序列的个数

image-20230312222311403

题意分析

给定一个整数数组,要求返回「最长严格递增子序列」有多少条,而不是它有多长。子序列允许跳着取,但下标必须保持原有先后顺序,且相邻取出的元素必须严格递增,相等的两个数不能同时出现在一条子序列里。

需要数清楚的只是长度恰好等于最大值的那些子序列,比最长短一截的一概不计。两条子序列只要下标集合不同就算两条,哪怕它们的数值序列完全一样。以 [1, 2, 4, 3] 为例,最长长度是 3,[1, 2, 4][1, 2, 3] 都达到这个长度,答案是 2。

约束里数组长度不超过 2000,这个规模明确允许一个平方级的双重循环,不必一上来就追求更优的树状数组或线段树写法;同时题目保证答案能用 32 位整数表示,说明计数虽然可能很大但不需要额外做取模。

边界上要留意:数组长度可能为 1,此时答案是 1;数组里可能全是相同的数,例如 [2, 2, 2, 2],最长长度是 1,而每一个元素各自构成一条,答案是 4。

解法:长度与数量双状态动态规划

核心思路

只记录 LIS 长度会丢失“有多少种达到该长度”的信息,因此对每个结尾下标同时维护两个状态:

  • lengths[i]:以 nums[i] 结尾的最长严格递增子序列长度;
  • counts[i]:以 nums[i] 结尾、长度恰为 lengths[i] 的子序列数量。

枚举合法前驱 j < inums[j] < nums[i]。若 lengths[j] + 1 更长,旧方案已经不是最优,覆盖长度并将数量重置为 counts[j];若与当前最长长度相等,说明找到了另一批互不重复的下标序列,累加 counts[j]

这个转移完整且不重不漏:任何以 i 结尾的递增子序列都有唯一的倒数第二个下标 j,删掉 nums[i] 后恰好归入对应前驱的方案集合。最后,最长子序列可能结束在不同位置,需要汇总所有 lengths[i] 等于全局最大值的 counts[i]

解题步骤

  1. 每个位置先初始化 lengths[i] = counts[i] = 1,表示只选择当前元素。
  2. 枚举左侧所有严格更小的前驱 j,比较候选长度 lengths[j] + 1
  3. 候选更长时覆盖长度和数量;候选同长时只累加数量。
  4. i 的状态确定后,用它更新全局最长长度和答案:更长则重置答案,同长则累加答案。

[1,3,5,4,7] 为例,结尾为 5 和 4 时都得到 (长度 3, 数量 1);处理 7 时,这两批方案都能延长到长度 4,于是数量相加为 2。相反,全相等的 [2,2,2] 没有合法前驱,每个位置都保持 (1,1),最终答案为 3。

代码实现

class Solution {
    public int findNumberOfLIS(int[] nums) {
        int n = nums.length;
        int[] lengths = new int[n];
        int[] counts = new int[n];
        int maxLen = 0;
        int ans = 0;

        for (int i = 0; i < n; i++) {
            lengths[i] = 1;
            counts[i] = 1;
            for (int j = 0; j < i; j++) {
                if (nums[j] >= nums[i]) {
                    continue;
                }
                // 更长则覆盖方案数,同长则累加方案数。
                if (lengths[j] + 1 > lengths[i]) {
                    lengths[i] = lengths[j] + 1;
                    counts[i] = counts[j];
                } else if (lengths[j] + 1 == lengths[i]) {
                    counts[i] += counts[j];
                }
            }

            if (lengths[i] > maxLen) {
                maxLen = lengths[i];
                ans = counts[i];
            } else if (lengths[i] == maxLen) {
                ans += counts[i];
            }
        }
        return ans;
    }
}
func findNumberOfLIS(nums []int) int {
    n := len(nums)
    lengths := make([]int, n)
    counts := make([]int, n)
    maxLen := 0
    ans := 0

    for i := 0; i < n; i++ {
        lengths[i] = 1
        counts[i] = 1
        for j := 0; j < i; j++ {
            if nums[j] >= nums[i] {
                continue
            }
            // 以 j 为前驱时,根据新长度决定覆盖还是累加。
            if lengths[j]+1 > lengths[i] {
                lengths[i] = lengths[j] + 1
                counts[i] = counts[j]
            } else if lengths[j]+1 == lengths[i] {
                counts[i] += counts[j]
            }
        }

        if lengths[i] > maxLen {
            maxLen = lengths[i]
            ans = counts[i]
        } else if lengths[i] == maxLen {
            ans += counts[i]
        }
    }
    return ans
}

复杂度分析

  • 时间复杂度:$O(n^2)$,每个位置枚举其左侧前驱。
  • 空间复杂度:$O(n)$,使用两个长度为 n 的状态数组。

关键点总结

  • “更优则覆盖、同优则累加”是最优方案计数的核心模式。
  • counts[i] 只统计与当前 lengths[i] 匹配的方案;长度刷新时旧数量必须作废。
  • 本题是严格递增,前驱条件必须是 nums[j] < nums[i]
  • $n \le 2000$ 时 $O(n^2)$ 动态规划清晰且足够;树状数组方案更复杂,不是面试首选。

易错点总结

  • 候选更长时若写成 counts[i] += counts[j],会把较短方案混入最长方案;此时必须覆盖。
  • 候选同长时若写成赋值,会漏掉其他前驱贡献;[1,2,4,3,5] 应得到 2。
  • 使用 nums[j] <= nums[i] 会把相等元素接起来;[2,2,2] 的答案应是 3,而不是长度为 3 的一条。
  • 只返回某个最长结尾的数量会漏解;[1,2,4,3] 的两条 LIS 结束位置不同。

相似题目

题目 难度 考察点
300. 最长递增子序列 中等 本题的基础版,只求最长长度,可额外用贪心加二分做到 $O(n \log n)$
354. 俄罗斯套娃信封问题 困难 二维偏序,难点在先按宽升序、等宽时高降序排序后转化为一维 LIS
368. 最大整除子集 中等 转移条件从大小比较换成整除关系,且要回溯输出方案本身而非条数
1048. 最长字符串链 中等 偏序关系是「删一个字符可得」,需按长度分组并用哈希表加速前驱查找
面试题 08.13. 堆箱子 困难 三维严格偏序,三个维度必须同时严格递减才能堆叠
面试题 17.08. 马戏团人塔 中等 与 354 同型但数据量更大,几乎强制使用二分优化的 LIS