[R59F] 数汉堡

  • 难度 普及+/提高
  • 时限 2s
  • 空限 512m
  • 数位 DP

数据规模:n102000n \le 10^{2000}(最多 2000 位),tn|t| \le n 的位数,tt 不含数字 0

思路

要统计 1n1 \sim n 中十进制表示以 tt子序列的数的个数。nn 可达 10200010^{2000},必须把 nn 当作字符串做数位 DP

贪心匹配自动机:处理一个数字串时,只需记录 jj = 已匹配的 tt 的最长前缀长度。读入数字 cc 时,若 j<tj < |t|c=t[j]c = t[j],则 jj 加一;否则 jj 不变。正确性:任何匹配 t[0..k]t[0..k] 的方式若最后一位使用 cc,则要求 c=t[k]c = t[k]t[0..k1]t[0..k-1] 已经匹配;而贪心已经匹配了尽可能长的前缀,所以只有 c=t[j]c = t[j] 时才能推进。

数位 DP:把所有数看作等长的 LL 位串(LLnn 的位数),高位补 0。由于 tt 不含数字 0,前导 0 永远不会推进匹配,因此补零不影响结果;x=0x = 0 也不可能匹配,无需额外处理。按位从高位到低位枚举:

  • 前缀与 nn 完全一致(紧贴)的路径只有一条,用标量 jtj_t 维护,当前位只能选 ddnn 的这一位);
  • 前缀已严格小于 nn(松绑)的路径用数组 dp[j]dp[j] 维护方案数。

对松绑状态,转移可以整段合并成 O(1)O(1) 公式:状态 j<mj < m 时,除了数字 t[j]t[j] 会推进到 j+1j+1,其余 9 个数字都保持 jj;状态 mm 已完全匹配,10 个数字都保持 mm。于是每位的松绑转移为

dp[j]=9dp[j]+dp[j1](1j<m)dp'[j] = 9 \cdot dp[j] + dp[j-1] \quad (1 \le j < m) dp[0]=9dp[0],dp[m]=10dp[m]+dp[m1]dp'[0] = 9 \cdot dp[0], \qquad dp'[m] = 10 \cdot dp[m] + dp[m-1]

紧贴路径的贡献:当前位 dd,若 jt<mj_t < m,选 c[0,d1]c \in [0, d-1] 时,恰有一个 c=t[jt]c = t[j_t](当 t[jt]<dt[j_t] < d 时存在)进入 jt+1j_t+1,其余 d1d-1dd 个进入 jtj_t;若 jt=mj_t = m,则 dd 个数字全部进入状态 mm。紧贴路径自身选 c=dc = d:若 t[jt]=dt[j_t] = d,则 jtj_t 加一。

最终答案为 dp[m]+[jt=m]dp[m] + [j_t = m](后者对应 nn 本身)。

复杂度:时间 O(Lm)O(L \cdot m)L,m2000L, m \le 2000),空间 O(m)O(m)

仓颉实现

import std.env.*

main(): Int64 {
    let reader = getStdIn()
    // n 可达 10^2000,按字符串读入
    let ns = reader.readln().getOrThrow()
    let t = reader.readln().getOrThrow()
    let L = ns.size
    let m = t.size
    let mod = 998244353
    let size = m + 1
    // 各位数字
    let nd = Array<Int64>(L, { i: Int64 => Int64(ns[i]) - 48 })
    let td = Array<Int64>(m, { i: Int64 => Int64(t[i]) - 48 })
    var loose = Array<Int64>(size, { _: Int64 => 0 })
    // jt:与 n 前缀完全一致(紧贴)的路径上,已匹配 t 的最长前缀长度
    var jt: Int64 = 0
    for (i in 0..L) {
        let d = nd[i]
        var nxt = Array<Int64>(size, { _: Int64 => 0 })
        // 已松绑(前缀严格小于 n)的状态转移:
        // 状态 j(j < m)只有一位数字 t[j] 能推进到 j+1,其余 9 位保持 j;
        // 状态 m 已完全匹配,任何数字都保持 m
        for (j in 0..size) {
            if (j < m) {
                nxt[j] = (9 * loose[j] + (if (j > 0) { loose[j - 1] } else { 0 })) % mod
            } else {
                nxt[j] = (10 * loose[j] + loose[j - 1]) % mod
            }
        }
        // 紧贴路径选择比 d 小的数字,进入松绑状态
        if (jt < m) {
            let adv = if (td[jt] < d) { 1 } else { 0 }
            nxt[jt] = (nxt[jt] + d - adv) % mod
            if (adv == 1) {
                nxt[jt + 1] = (nxt[jt + 1] + 1) % mod
            }
        } else {
            nxt[m] = (nxt[m] + d) % mod
        }
        // 紧贴路径自身选数字 d
        if (jt < m && td[jt] == d) {
            jt = jt + 1
        }
        loose = nxt
    }
    let ans = (loose[m] + (if (jt == m) { 1 } else { 0 })) % mod
    println(ans)
    return 0
}

要点:

  • 紧贴路径只有一条,无需 DP 数组,用标量 jtj_t 模拟即可;松绑路径按位整体转移,把每位 10 次状态枚举压成 O(m)O(m)