[R1F] 重复度不超过 k 的数

  • 难度 普及/提高-
  • 时限 1s
  • 空限 512m
  • 动态规划组合数学

数据规模:1kn1001 \le k \le n \le 1001m1001 \le m \le 100

思路

「重复度不超过 kk」即每种数字在 nn 个位置中出现的次数都不超过 kk,问题等价于:用 mm 种数字填满 nn 个有标号的位置,且每种数字最多出现 kk 次,求方案数。

dp[i][j]dp[i][j] 为前 ii 种数字填了 jj 个位置的方案数,转移时枚举第 ii 种数字填 pp 个位置(0pmin(k,j)0 \le p \le \min(k,j)):

dp[i][j]=p=0min(k,j)dp[i1][jp]×(n(jp)p)dp[i][j] = \sum_{p=0}^{\min(k,j)} dp[i-1][j-p] \times \binom{n-(j-p)}{p}

其中 (n(jp)p)\binom{n-(j-p)}{p} 是从剩余空位中选出 pp 个放第 ii 种数字的方案数。边界 dp[0][0]=1dp[0][0]=1,答案 dp[m][n]dp[m][n]

组合数用杨辉三角预处理(模 998244353998244353)。k=nk=n 时答案即为 mnm^n,该 DP 自然覆盖此情况。

复杂度:时间 O(mnk)O(mnk),空间 O(mn)O(mn)

仓颉实现

import std.env.*
import std.convert.*

const MOD = 998244353

main(): Int64 {
    let reader = getStdIn()
    let nmk = reader.readln().getOrThrow().split(" ", removeEmpty: true).map({ p => Int64.parse(p) })
    let n = nmk[0]
    let m = nmk[1]
    let k = nmk[2]
    let nn = n
    var C = Array<Array<Int64>>(nn + 1, { _ => Array<Int64>(nn + 1, { _ => 0 }) })
    for (i in 0..=nn) {
        C[i][0] = 1
        for (j in 1..=i) {
            C[i][j] = (C[i - 1][j - 1] + C[i - 1][j]) % MOD
        }
    }
    var dp = Array<Array<Int64>>(m + 1, { _ => Array<Int64>(nn + 1, { _ => 0 }) })
    dp[0][0] = 1
    for (i in 1..=m) {
        for (j in 0..=nn) {
            var p: Int64 = 0
            let maxp = if (j < k) { j } else { k }
            while (p <= maxp) {
                dp[i][j] = (dp[i][j] + dp[i - 1][j - p] * C[nn - (j - p)][p]) % MOD
                p += 1
            }
        }
    }
    println(dp[m][nn])
    return 0
}

要点:

  • n100n \le 100,组合数只需 (0..n0..n)\binom{0..n}{0..n} 的表,杨辉三角即可;取模要应用到每一步加法与乘法。
  • 两个模数下的乘积约为 101810^{18},在 Int64 范围内,不会溢出。