题目描述

✅ 1420. 生成数组

image-20260929082546245

image-20260929082546379

题意分析

统计长度恰好为 n、每项取值在 1 到 m 的数组数量,要求从左到右扫描时,当前最大值恰好被刷新 k 次。只有遇到严格更大的元素才刷新,遇到相等或更小的元素不增加次数。

数组非空且元素为正,首个元素一定刷新一次。这里统计的是有顺序的数组方案,同一批数字不同排列可能产生不同刷新次数;最终方案数对 10^9 + 7 取模。

解法:DP + 前缀和优化

核心思路

[!blue]

逐个追加元素时,未来只需要知道当前最大值和已经刷新几次,不需要保留完整前缀。因此定义 dp[maximum][cost]:在当前已处理长度下,最大值恰为 maximum、刷新次数恰为 cost 的数组数量。长度一时,每个最大值只对应单元素数组,所以 dp[maximum][1] = 1。

要追加一个元素得到新状态 (maximum, cost),最后一步只有两类互斥来源:

  • 不刷新最大值:旧状态已经是 (maximum, cost),末项可以选一到 maximum 中任何值,共 maximum 种,因此贡献 dp[maximum][cost] * maximum。
  • 刷新最大值:末项必须正好取 maximum,旧最大值严格小于它,旧刷新次数为 cost - 1,因此贡献所有较小旧最大值状态的数量之和。

这两类按最后一个元素是否刷新来划分,既不会重复,也覆盖每个合法数组。直接求第二类贡献会再枚举一次旧最大值,可以用递增扫描时的前缀和消掉这一层循环。

固定 cost 后,从小到大枚举 maximum,令 smaller 保存上一长度中、刷新次数为 cost - 1、最大值严格小于当前位置的方案总和。先用它计算当前 next[maximum][cost],再把 dp[maximum][cost - 1] 加进去,供下一位置使用。这个先后顺序保证相等最大值不会被误当成一次刷新。

每次增加长度都新建 next,所有来源只读取旧 dp,完成整层后再替换,避免同一轮连续追加多次。计数及时取模,不刷新分支的乘法先使用 64 位,以免取模之前就溢出。

刷新次数至少为一,且不超过数组长度,也不超过可用值的种数,所以 k == 0、k > n 或 k > m 都可直接返回零。生成完整长度后,再把所有最终最大值下刷新次数恰好为 k 的状态相加。

解题步骤

  1. 排除不可能的 k,初始化长度一时各个最大值的方案数。
  2. 逐步增加数组长度,为下一长度创建全零状态表 next。
  3. 枚举可行刷新次数,每次将严格前缀和 smaller 重置为零。
  4. 递增枚举最大值,用“不刷新分支乘选择数 + smaller”计算当前状态并取模。
  5. 当前状态算完后,才将旧表中当前最大值、少一次刷新的方案加入 smaller。
  6. 用 next 替换旧表,最后汇总所有最大值对应的 k 次刷新方案。

代码实现

class Solution {
    private static final int MOD = 1_000_000_007;

    public int numOfArrays(int n, int m, int k) {
        if (k == 0 || k > n || k > m) {
            return 0;
        }

        int[][] dp = new int[m + 1][k + 1];

        for (int maximum = 1; maximum <= m; maximum++) {
            // 单元素数组首次刷新最大值,搜索代价恰为一。
            dp[maximum][1] = 1;
        }

        for (int length = 2; length <= n; length++) {
            int[][] next = new int[m + 1][k + 1];

            for (int cost = 1; cost <= Math.min(k, length); cost++) {
                // 每个 cost 单独累积严格更小的旧最大值。
                int smaller = 0;

                for (int maximum = 1; maximum <= m; maximum++) {
                    // 不刷新时末项有 maximum 种选择,乘法先使用 64 位。
                    long keepMaximum = (long) dp[maximum][cost] * maximum;

                    next[maximum][cost] = (int) ((keepMaximum + smaller) % MOD);

                    // 当前转移完成后再加入,下一轮前缀才包含这个旧最大值。
                    smaller += dp[maximum][cost - 1];

                    if (smaller >= MOD) {
                        smaller -= MOD;
                    }
                }
            }

            dp = next;
        }

        int answer = 0;

        for (int maximum = 1; maximum <= m; maximum++) {
            answer += dp[maximum][k];

            if (answer >= MOD) {
                answer -= MOD;
            }
        }

        return answer;
    }
}
func numOfArrays(n int, m int, k int) int {
    const mod = 1_000_000_007
    if k == 0 || k > n || k > m {
        return 0
    }

    dp := make([][]int, m+1)
    for maximum := 0; maximum <= m; maximum++ {
        dp[maximum] = make([]int, k+1)
    }
    for maximum := 1; maximum <= m; maximum++ {
        // 单元素数组首次刷新最大值,搜索代价恰为一。
        dp[maximum][1] = 1
    }

    for length := 2; length <= n; length++ {
        next := make([][]int, m+1)
        for maximum := 0; maximum <= m; maximum++ {
            next[maximum] = make([]int, k+1)
        }

        for cost := 1; cost <= k && cost <= length; cost++ {
            // 每个 cost 单独累积严格更小的旧最大值。
            smaller := 0
            for maximum := 1; maximum <= m; maximum++ {
                // 不刷新时末项有 maximum 种选择,乘法先使用 64 位。
                keepMaximum := int64(dp[maximum][cost]) * int64(maximum)
                next[maximum][cost] = int((keepMaximum + int64(smaller)) % int64(mod))

                // 当前转移完成后再加入,下一轮前缀才包含这个旧最大值。
                smaller += dp[maximum][cost-1]
                if smaller >= mod {
                    smaller -= mod
                }
            }
        }
        dp = next
    }

    answer := 0
    for maximum := 1; maximum <= m; maximum++ {
        answer += dp[maximum][k]
        if answer >= mod {
            answer -= mod
        }
    }
    return answer
}

复杂度分析

  • 时间复杂度:通过不可能情况的检查后为 $O(nmk)$。枚举长度、最大值和刷新次数,严格前缀和使每次状态转移为常数时间;提前判无解时为常数时间。
  • 辅助空间复杂度:$O(mk)$,只保存相邻两种长度的状态表,不保存全部长度。

关键点总结

[!green]

  • 最大值与刷新次数都表示恰好,不是上界。
  • 不刷新时有 maximum 种末项,刷新时末项固定、旧最大值严格更小。
  • 前缀和先使用后累加,保证只包括严格更小的旧最大值。
  • 新旧长度分表,宽整数乘法和及时取模保证数值正确。

易错点总结

[!yellow]

  • 把首元素的刷新次数设为零,会让全部状态偏少一次。
  • 不刷新分支乘 maximum - 1 会遗漏末项等于当前最大值的合法选择。
  • 先把当前最大值加入 smaller 再计算,会把相等值也算成刷新。
  • 每个 cost 都需要重新初始化 smaller,不同刷新次数的前缀不能混用。
  • 原地混用当前长度和上一长度,会在一次转移中重复追加元素。
  • 先用 32 位做乘法再取模,无法恢复已经溢出的正确计数。

相似题目

题目 难度 关联与区别
920. 播放列表的数量 困难 同样把加入一项分成产生新状态与复用旧状态两类,本题关注是否刷新最大值,原题关注是否首次播放新歌。
629. K 个逆序对数组 困难 同样按长度与累计指标计数,转移中的一段前驱求和可用前缀和加速。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2021/98039080
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!