[R40E] Yet another counting problem
- 难度 提高
- 时限 1s
- 空限 512m
- 动态规划组合计数
数据规模:,。
思路
先把 中的点按 从小到大排序,排序后的编号越大,能连的范围越大。枚举 :排序后编号最大(即 最大)的不在匹配中的 点。此时点分成 4 类:
- 集合 1:;
- 集合 2:;
- 集合 3:;
- 集合 4:。
由极大性可知集合 1 和集合 4 中的点全部在匹配中:若集合 1 有未匹配点,则未匹配的 可以连它;若集合 4 有未匹配点,则 就不是编号最大的未匹配点。于是匹配中的边只可能是三类:集合 1 ↔ 集合 3、集合 1 ↔ 集合 4、集合 2 ↔ 集合 4。
集合 3 与集合 1 之间的匹配:设 表示排序后前 个 点中恰好匹配了 个 点的方案数,转移为
不匹配贡献 ;若匹配,它可连 ,其中 个已被前面的点占用,故有 种选择。枚举集合 1 与集合 3 的连边数 ,方案数为 。
填补集合 1 剩余空位:集合 1 共 个位置,被集合 3 占去 个后还剩 个,必须由集合 4 完全覆盖。集合 4 共 个点,其中 个去匹配集合 2,剩下 个填入集合 1 的空位,这部分是全排列 。
集合 4 匹配集合 2:设 表示从 中选 个点匹配到 的方案数。固定 时 点能连的范围不同( 只能连到 ),所以不能直接套组合数,需要递推:当 减小 1 时,集合 2 新增 个空位 ,这些空位的下标都不超过 ,可被集合 4(,共 个点)中任意点连接。把 个空位逐个加入,每个空位要么不用,要么选一个未匹配的 点占用:
因此固定 对答案的贡献为
最后, 全部匹配即完美匹配的情况单独计入 。
复杂度:时间 (各次转移中 的总和不超过 ),空间 。
仓颉实现
import std.convert.*
import std.env.*
import std.sort.*
main(): Int64 {
let MOD: Int64 = 1000000007
let reader = getStdIn()
let n = Int64.parse(reader.readln().getOrThrow())
let nn = n
let a = reader.readln().getOrThrow().split(" ", removeEmpty: true).map({ p: String => Int64.parse(p) })
// 将 B 按 a 升序排序
sort(a)
// 阶乘
let fact = Array<Int64>(nn + 1, { _ => 0 })
fact[0] = 1
for (i in 1..nn + 1) {
fact[i] = fact[i - 1] * Int64(i) % MOD
}
// g[k][z]:从排序后 B_{k+1..n} 中选 z 个点匹配到 A_{a_k+1..n} 的方案数(0 基下标,g[k] 即题解 g[k])
let g = Array<Array<UInt32>>(nn + 1, { _ => Array<UInt32>(nn + 1, { _ => 0 }) })
g[nn][0] = 1
var k: Int64 = nn
while (k >= 2) {
let L = a[k - 1] - a[k - 2] // k 减小时集合 2 新增的 A 空位数
let M = nn - k + 1 // 集合 4(B_k..B_n)的大小
var cur = g[k]
var step: Int64 = 0
while (step < L) {
let nxt = Array<UInt32>(nn + 1, { _ => 0 })
nxt[0] = cur[0]
for (z in 1..nn + 1) {
let rest = M - z + 1 // 新空位被使用时可选用的未匹配 B 点数
var v: Int64 = Int64(cur[z])
if (rest > 0) {
v += Int64(cur[z - 1]) * rest
}
nxt[z] = UInt32(v % MOD)
}
cur = nxt
step += 1
}
g[k - 1] = cur
k -= 1
}
// 枚举 k:排序后编号最大(a 最大)的未匹配 B 点;f 逐行维护 f[k-1]
var ans: Int64 = 0
var fprev = Array<UInt32>(nn + 1, { _ => 0 })
fprev[0] = 1
for (k in 1..nn + 1) {
let ak = a[k - 1]
// x:集合 1(A_1..A_{a_k})与集合 3(B_1..B_{k-1})之间的边数
var lo: Int64 = ak + k - nn
if (lo < 0) {
lo = 0
}
var hi: Int64 = k - 1
if (hi > ak) {
hi = ak
}
var x: Int64 = lo
while (x <= hi) {
let z = nn - k - ak + x // 集合 4 中剩余需要与集合 2 匹配的点数
let ways = Int64(fprev[x]) * Int64(g[k][z]) % MOD * fact[Int64(ak - x)] % MOD
ans = (ans + ways) % MOD
x += 1
}
// 更新 f 到 f[k]:B_1..B_k 中恰好匹配 j 个 A 点的方案数
let fnext = Array<UInt32>(nn + 1, { _ => 0 })
fnext[0] = 1
var jmax: Int64 = k
if (jmax > ak) {
jmax = ak
}
for (j in 1..jmax + 1) {
let v = Int64(fprev[j]) + Int64(fprev[j - 1]) * (ak - j + 1)
fnext[j] = UInt32(v % MOD)
}
fprev = fnext
}
// 所有 B 点都匹配的完美匹配
ans = (ans + Int64(fprev[nn])) % MOD
println(ans)
return 0
}
要点:
- 的下界为 ,保证 ; 的上界为 。
- 只需逐行维护:枚举 时恰好用到 整行,配合 计算贡献后再推进一行。
- 数组全部预先算好( 从大到小),因为枚举 时每个 都要用到。