[R16E] 数组问题
- 难度 普及+/提高
- 时限 1s
- 空限 512m
- 动态规划分拆数预处理
数据规模:,。答案对 取模。
思路
合法数组 且 ,。这恰好是 的 部分拆数,即把 拆成 个正整数之和(不计顺序)的方案数。
设 表示把 拆成 个正整数之和的方案数。按划分中最小值是否为 分类:
- 划分里含至少一个 :去掉一个 ,剩下的就是把 拆成 个正整数,方案数 ;
- 划分里所有数都大于 :把 个数各减 ,等价于把 拆成 个正整数,方案数 。
于是转移方程为
边界 ,且只有 时状态有意义。注意第二种转移要求 (即 , 为行内下标),否则第二项为 。
由于 高达 ,必须离线预处理整张表,查询时 查表。预处理时间 ,查询时间 ,总复杂度 。
仓颉实现
import std.console.*
import std.convert.*
main(): Int64 {
let MOD: UInt32 = 998244353
let MAX: Int64 = 8000
// dp[i][j] = number of ways to partition j into i positive parts.
// Only j >= i is meaningful. Use a triangular layout to save memory:
// for row i, store values for j in [i, MAX] in length (MAX - i + 1),
// i.e. index k corresponds to j = i + k.
// MOD < 2^31, so MOD+MOD < 2^32 fits in UInt32 without overflow.
var dp = Array<Array<UInt32>>(MAX + 1, { _ => Array<UInt32>(0, { _ => UInt32(0) }) })
// dp[0][0] = 1 (empty partition of 0); all other dp[0][k>0] = 0.
let row0 = Array<UInt32>(MAX + 1, { _ => UInt32(0) })
row0[0] = UInt32(1)
dp[0] = row0
var i: Int64 = 1
while (i <= MAX) {
let len: Int64 = MAX - i + 1
let row = Array<UInt32>(len, { _ => UInt32(0) })
var k: Int64 = 0
while (k < len) {
// dp[i][j] = dp[i-1][j-1] + dp[i][j-i], where j = i + k.
// part 1: dp[i-1][j-1] -> previous row index k (since (j-1)-(i-1)=k).
var v = dp[i - 1][k]
// part 2: dp[i][j-i] -> current row index k-i, valid only if k-i >= 0.
if (k >= i) {
v = v + row[k - i]
if (v >= MOD) {
v = v - MOD
}
}
row[k] = v
k = k + 1
}
dp[i] = row
i = i + 1
}
let reader = Console.stdIn
let t = Int64(reader.readln().getOrThrow().split(" ", removeEmpty: true).map({ p => Int64.parse(p) })[0])
var out = StringBuilder()
var count: Int64 = 0
while (count < t) {
count = count + 1
let parts = reader.readln().getOrThrow().split(" ", removeEmpty: true).map({ p => Int64.parse(p) })
let n = parts[0]
let m = parts[1]
// dp[n][m]: row n index for value m is m - n.
let ans = Int64(dp[n][m - n])
out.append(ans)
out.append('\n')
}
print(out.toString())
return 0
}
要点:
- 分拆数递推:把合法序列重述为「 拆成 个正整数」,立刻得到经典分拆数递推 ,两个加法项分别对应「划分里有一个 」「所有数都减 」两种情形。
- 三角化存储省内存:只有 时状态非零,第 行只存下标 ,下标换算 。完整 的
Int64表约 512MB,三角化后约 3200 万项。 - 用
UInt32存值:模数 ,两项之和 不会溢出,用v + row[k-i]后比较减模即可。约 3200 万项的UInt32表约 128MB,实测峰值 160MB,远低于 512MB 上限。逐行只引用「上一行」与「本行左侧」,递推方向天然无依赖冲突。 - 预处理后查询 : 达 时不能每次重算;整张表离线建好后,每问只查
dp[n][m-n]一次。输出用StringBuilder汇总再一次性打印,避免逐行 I/O 的开销。