洪水染色
- 难度 提高+
- 时限 1s
- 空限 256m
- 区间 DP
数据规模:,。
思路
先合并连通块。 一次操作改变的是「包含起点的整个连通块」,永远不会把连在一起的两个同色方格拆开,所以相邻同色的方格可以视为一个整体。把输入压成 个颜色交替的块 (),答案不变。
一次操作等价于区间扩张。 设起点在第 个块内,并记「当前包含起点的块」覆盖的块下标区间为 ,初始 。由于它始终是一段连续区间,一次操作把它整体改成颜色 时只有下面几种结果:
- :左侧相邻块被吞并,区间变成 ;
- :右侧相邻块被吞并,区间变成 ;
- :两侧同色,一次改色同时吞并两格,区间变成 ;
- 其余情况区间没有变化,是纯粹的空耗,不会出现在最优方案里。
所以每次操作要么让区间向一侧长一格,要么在左右两个相邻块同色时长两格。目标是把区间扩张到 ,且起点可以任选,于是问题化为:用最少的扩张次数把 吞成一个块。
状态与转移。 定义 为把块 合并成一个块所需的最少操作数,起点在 内且只允许吞并 内的块,边界 。
考虑让区间变成 的最后一次操作,它只有三种可能:
- 只吞了 :此前是 且已合成一块,代价 ;
- 只吞了 :代价 ;
- 同时吞了 与 :一次改色要同时匹配两侧颜色,必须 ,此前区间是 ,代价 。
于是
当 时再与 取较小值。答案即 。
为什么同色两端能省一步。 两端颜色相同时,把它们中间的部分先合成一块,再一次改色就能同时吃掉左右两个端点块,把原本要分开做的两次吞并合并成一次。这也是样例 的答案:中间块改成 ,一步同时并掉左右两个 。
为什么拆成「最后一步」不重不漏。 任何合法方案都对应区间从中心向两侧逐格扩张的过程,把它的最后一次扩张删掉,剩下的仍是一个合法方案,且中间状态正是 、 或 三者之一;反过来,这三种子方案各自都能补上那最后一步得到 。归纳即可。
滚动数组。 只依赖长度为 与 的两层,按区间长度从小到大递推时保留这两层即可,空间降到 。长度为 的空区间取值 ,正好对应 且中间为空时一步吞掉两个块(此时 ,答案为 )。
小例子核对。 :,取 ;:两端不同,;全同色压缩后 ,答案为 。
复杂度
共 个状态、每个状态常数次转移,时间 ;滚动数组只保留两层,空间 。其中 是压缩后的块数。
仓颉实现
查看仓颉实现
import std.env.*
import std.convert.*
main() {
let reader = getStdIn()
let n = Int64.parse(reader.readln().getOrThrow())
let raw = reader.readln().getOrThrow().split(" ", removeEmpty: true).map({ p: String => Int64.parse(p) })
let blk = Array<Int64>(n, { _ => 0 })
var m: Int64 = 0
for (i in 0..n) {
if (m == 0 || blk[m - 1] != raw[i]) {
blk[m] = raw[i]
m += 1
}
}
if (m == 1) {
println(0)
} else {
var prev1 = Array<Int64>(m, { _ => 0 })
var prev2 = Array<Int64>(m, { _ => 0 })
var cur = Array<Int64>(m, { _ => 0 })
for (len in 2..=m) {
for (l in 0..=(m - len)) {
var best = prev1[l]
if (prev1[l + 1] < best) {
best = prev1[l + 1]
}
best += 1
if (blk[l] == blk[l + len - 1]) {
let v = prev2[l + 1] + 1
if (v < best) {
best = v
}
}
cur[l] = best
}
let tmp = prev2
prev2 = prev1
prev1 = cur
cur = tmp
}
println(prev1[0])
}
}