题目描述

牛客原题: ✅ 补充题 189. 非降序数组的填充方案数

数组中的 0 表示被删除的数。将每个 0 填成 1…k 中的整数,使整个数组非降序,已有非零数不变。

返回方案数模 1000000007。

示例 1:

输入: nums = [1,0,0], k = 3
输出: 6
解释: 后两个数可填为 (1,1)、(1,2)、(1,3)、(2,2)、(2,3)、(3,3)。

示例 2:

输入: nums = [0,4,5], k = 6
输出: 4
解释: 第一个位置可填 1、2、3、4,才能保持整个数组非降序。

提示:

  • 0 表示待填位置。
  • 填入的值属于 1…k,已有非零值不能修改。
  • 整个数组必须非降序。
  • 答案对 1000000007 取模。

题意分析

已有非零值不能改变,因此它们必须本身保持非降序;否则任何填法都无法修复。合法的已知值把所有零分成独立连续段,每段只受左右已知值以及填入范围 1~k 约束。

固定一个零段的可选值范围后,非降序要求让每种值的出现次数唯一决定排列,不应按逐位置任意选择来计数。用可重复组合计算每段方案数,再将独立段的结果相乘。

解法:按零段用可重复组合计数

核心思路

[!blue]

先按已有非零值把待填的零分段。某段两侧的已知值确定可填区间,再与 [1,k] 取交集得到 [low,high];开头没有左邻、结尾没有右邻时,分别只受 1 和 k 限制。已有非零值若下降,填零无法修复。

设一段有 m 个零,可选值有 V = high-low+1 种。非降序要求决定了各值的排列顺序,所以只需决定每种值出现多少次,即求 V 个非负计数之和等于 m 的方案数。插板法得到 C(V+m-1,m)。例如两个零可填 1 到 3 时,结果是 11、12、13、22、23、33,共 C(4,2)=6 种。

各零段的边界已经固定,选择互不影响,因此将段数相乘。代码用 count 保存段长,previous 保存左侧已知值;choose(high-low+count, count) 对应上面的组合数。

组合数的计算与分段思路分开讲:上界小于模数时,用分子乘积和分母逆元;上界跨过模数时,choose 按模数逐位拆分(Lucas),逐位计算并相乘。先解释为何要数非降序填法,再展开这部分数论实现。

解题步骤

  1. 顺序扫描非零值,若已有值出现下降则直接返回 0。
  2. 对中间、开头和结尾的每段零,把已知上下界与 [1,k] 相交。
  3. 非空零段范围为空则无解,否则乘上 C(high-low+count,count)。
  4. 用 Lucas 分解与快速幂逆元计算模组合数,跳过长度为零的段。

代码实现

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

    private long power(long a, long n) {
        long r = 1;

        while (n > 0) {
            if ((n & 1) != 0) {
                r = r * a % MOD;
            }

            a = a * a % MOD;
            n >>= 1;
        }

        return r;
    }

    private long choose(long n, long r) {
        long answer = 1;

        while (n > 0 || r > 0) {
            long a = n % MOD;
            long b = r % MOD;

            if (b > a) {
                return 0;
            }

            b = Math.min(b, a - b);
            long up = 1;
            long down = 1;

            for (long i = 1; i <= b; i++) {
                up = up * (a - b + i) % MOD;
                down = down * i % MOD;
            }

            answer = answer * up % MOD * power(down, MOD - 2) % MOD;
            n /= MOD;
            r /= MOD;
        }

        return answer;
    }

    public int fillWays(int[] a, int k) {
        long answer = 1;
        long previous = Long.MIN_VALUE;
        int start = 0;

        for (int i = 0; i <= a.length; i++) {
            if (i < a.length && a[i] == 0) {
                continue;
            }

            if (i < a.length && a[i] < previous) {
                return 0;
            }

            int count = i - start;

            if (count > 0) {
                long low = Math.max(1L, previous);
                long high = i == a.length ? k : Math.min(k, a[i]);

                if (low > high) {
                    return 0;
                }

                answer = answer * choose(high - low + count, count) % MOD;
            }

            if (i < a.length) {
                previous = a[i];
            }

            start = i + 1;
        }

        return (int) answer;
    }
}
func fillWays(a []int, k int) int {
    const mod int64 = 1_000_000_007
    power := func(a, n int64) int64 {
        r := int64(1)
        for n > 0 {
            if n&1 != 0 {
                r = r * a % mod
            }
            a = a * a % mod
            n >>= 1
        }
        return r
    }
    choose := func(n, r int64) int64 {
        answer := int64(1)
        for n > 0 || r > 0 {
            x, y := n%mod, r%mod
            if y > x {
                return 0
            }
            y = min(y, x-y)
            up, down := int64(1), int64(1)
            for i := int64(1); i <= y; i++ {
                up = up * (x - y + i) % mod
                down = down * i % mod
            }
            answer = answer * up % mod * power(down, mod-2) % mod
            n /= mod
            r /= mod
        }
        return answer
    }
    answer, previous, start := int64(1), int64(-1<<63), 0
    for i := 0; i <= len(a); i++ {
        if i < len(a) && a[i] == 0 {
            continue
        }
        if i < len(a) && int64(a[i]) < previous {
            return 0
        }
        count := i - start
        if count > 0 {
            low, high := max(int64(1), previous), int64(k)
            if i < len(a) {
                high = min(high, int64(a[i]))
            }
            if low > high {
                return 0
            }
            answer = answer * choose(high-low+int64(count), int64(count)) % mod
        }
        if i < len(a) {
            previous = int64(a[i])
        }
        start = i + 1
    }
    return int(answer)
}

复杂度分析

  • 时间复杂度:总乘积循环不超过零元素数量;含模幂时,时间 $O(n \log P)$,P=1000000007。
  • 空间复杂度:额外空间 $O(1)$。

关键点总结

[!green]

已知值把零段分开后,各段选择互不影响;每段用每种值的出现次数唯一表示非降序填法,所以是可重复组合而不是排列。

易错点总结

[!yellow]

  • 只有填入值受 [1,k] 限制,已有非零值保持不变;例如 [5]、k=3 没有零且已非降序,答案为 1。
  • 相邻已知值逆序时无解;非空零段的上下界与 [1,k] 取交集后为空也无解。
  • 零段内部必须非降序,不能按 V 的 m 次方计数。
  • 组合数上界可能跨越 P,直接对含 P 因子的分母取逆元会错,需先按 Lucas 分解。

相似题目

题目 难度 关联与区别
62. 不同路径 中等 非降序选数可转为隔板法,与网格路径一样归结为组合数。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2026/11875861
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!