[R46G]贪吃鬼
数据规模:,。
思路
首先刻画哪些序列能够被吃完。
一个序列能够被吃完,当且仅当它能被划分成若干个连续段,并且每一段的首尾颜色相同。
- 如果已经有这样的划分,逐段选择首尾两个糖果,就能直接吃掉整段。
- 反过来,考虑某种合法操作方案的最后一次操作。设它选择了原序列中位置 的两个糖果,那么 。位置 左侧和位置 右侧的糖果都必须在此之前分别被吃完,否则最后一次操作无法吃掉它们;任何更早的操作也不能跨过 或 ,否则会提前吃掉最后一次操作的端点。因此左右两侧都是合法序列,递归划分后,再加入区间 ,便得到上述划分。
于是问题变成统计有多少个序列能划分成若干个首尾同色的连续段。直接按划分位置计数会重复统计,需要为每个序列选取唯一状态。
对于一个已经处理的后缀 ,定义集合 :若 中某个位置的颜色为 ,并且该位置之后的后缀合法,就把颜色 加入 。空序列视为合法。
在 前面加入一个颜色为 的糖果:
- 新序列 合法,当且仅当 。因为第一段必须从新加入的糖果开始,并在某个同色位置结束,而该位置之后还必须是合法后缀。
- 如果 合法,新加入的位置也能作为以后某一段的右端点,所以 ;如果 不合法,则 。
颜色的具体编号不重要,只需记录集合大小。令:
- 表示长度为 、自身合法且 的后缀数量;
- 表示长度为 、自身不合法且 的后缀数量。
初始时空后缀合法,故 。从长度 转移到 时,若新颜色属于 ,有 种选择;否则有 种选择:
最终答案为 。使用滚动数组保存相邻两层即可。
复杂度
时间复杂度 ,空间复杂度 。
仓颉实现
查看仓颉实现
import std.env.*
import std.convert.*
const MOD: Int64 = 1000000007
main() {
let reader = getStdIn()
let input = reader.readln().getOrThrow().split(" ", removeEmpty: true).map({ p: String => Int64.parse(p) })
let n = input[0]
let m = input[1]
let size = n + 2
// good[k] / bad[k]:当前后缀合法 / 不合法,且接续颜色集合大小为 k 的方案数
var good = Array<Int64>(size, { _ => 0 })
var bad = Array<Int64>(size, { _ => 0 })
var nextGood = Array<Int64>(size, { _ => 0 })
var nextBad = Array<Int64>(size, { _ => 0 })
good[0] = 1
var len: Int64 = 0
while (len < n) {
let limit = if (len < m) { len } else { m }
var k: Int64 = 0
while (k <= limit + 1) {
nextGood[k] = 0
nextBad[k] = 0
k = k + 1
}
k = 0
while (k <= limit) {
let inside = k
let outside = m - k
nextGood[k] = (nextGood[k] + (good[k] + bad[k]) % MOD * inside) % MOD
nextBad[k] = (nextBad[k] + bad[k] * outside) % MOD
nextBad[k + 1] = (nextBad[k + 1] + good[k] * outside) % MOD
k = k + 1
}
let tempGood = good
good = nextGood
nextGood = tempGood
let tempBad = bad
bad = nextBad
nextBad = tempBad
len = len + 1
}
let limit = if (n < m) { n } else { m }
var answer: Int64 = 0
var k: Int64 = 0
while (k <= limit) {
answer = (answer + good[k]) % MOD
k = k + 1
}
println(answer)
}