[R9F] 硬币问题
- 难度 提高
- 时限 1s
- 空限 512m
- 背包计数分块
数据规模:。
思路
第 种硬币有 枚、每枚价值 ,求总价值恰为 的方案数。这是带数量上限的多重背包计数。以 为界把硬币分成小()、大()两类分别处理,最后按总价值卷起来。
小硬币():物品 至多用 枚,多重背包用「定长滑动窗口」优化到 。记 为用前若干种小硬币凑出 的方案数,转移到第 种时维护 ,于是
即窗口长度恰好为枚举数 的 项。滚动数组空间 。
大硬币():一枚大硬币价值至少 ,凑出 最多用 枚,所以数量上限自动满足,可视为无限制。记 为恰好用 枚大硬币凑成 的方案数,按「至少一枚最小大硬币 / 每枚都减一」二分:
只到 ,空间 ,时间 。
最后答案为 ,即大硬币凑 、小硬币凑 的方案乘积之和。
复杂度:时间 ,空间 (可滚动到 )。
仓颉实现
import std.convert.*
import std.env.*
let MOD: Int64 = 998244353
main(): Int64 {
let reader = getStdIn()
let n = Int64.parse(reader.readln().getOrThrow())
var B: Int64 = 1
while ((B + 1) * (B + 1) <= n) {
B += 1
}
let SQ = B
// 小硬币 f[j]
var f = Array<Int64>(n + 1, { _ => 0 })
f[0] = 1
for (i in 1..=SQ) {
let nf = Array<Int64>(n + 1, { _ => 0 })
var s = Array<Int64>(n + 1, { _ => 0 })
for (j in 0..=n) {
let a = f[j]
let b = if (j >= i) { s[j - i] } else { 0 }
s[j] = (a + b) % MOD
var val = s[j]
let lim = (i + 1) * i
if (j >= lim) {
val = (val - s[j - lim] + MOD) % MOD
}
nf[j] = val
}
f = nf
}
// 大硬币 g[i][j]: 恰好 i 枚大硬币凑成 j
let g = Array<Array<Int64>>(SQ + 1, { _ => Array<Int64>(n + 1, { _ => 0 }) })
g[0][0] = 1
for (i in 1..=SQ) {
let gi = g[i]
let gim1 = g[i - 1]
for (j in 0..=n) {
var v: Int64 = 0
if (j >= SQ + 1) {
v = gim1[j - (SQ + 1)]
}
if (j >= i) {
v = (v + gi[j - i]) % MOD
}
gi[j] = v
}
}
var ans: Int64 = 0
for (i in 0..=SQ) {
let gi = g[i]
for (j in 0..=n) {
if (gi[j] != 0) {
ans = (ans + gi[j] * f[n - j]) % MOD
}
}
}
println(ans)
return 0
}
要点:
- 按价值 切分:小硬币数量限制关键但种类少,用滑动窗口多重背包;大硬币数量限制天然松,用「数枚数」的完全背包。
- 小硬币转移维护 让等差数列求和变成 ,是把 降到 的关键。