体育节
- 难度 提高
- 时限 1s
- 空限 256m
- 区间 DP
数据规模:,。
思路
先排序。 每个阶段的不协调值只取决于该阶段出场选手的最大速度与最小速度,与选手的原始编号无关。把速度升序排序为 ,题目只问出场顺序,所以排序不改变答案。
把答案拆到相邻空隙上。 对某阶段的出场集合 ,设其中最小、最大速度分别是排序后的 ,则
也就是说,相邻的每一对 贡献的空隙 ,只要该阶段同时出场了「编号 的选手」和「编号 的选手」就被计入一次。于是总代价为
其中 是「同时出场了空隙 两侧选手」的阶段个数。这等于 , 是空隙 两侧都被占满的那个阶段(即第 个阶段, 是排序后第 位选手的出场时刻)。要让总代价小,就要让每个空隙「填满」得尽量晚。
最优顺序可以取成区间扩张。 设第一个出场的是排序后的第 位()。
- 对 :空隙右侧已经含第 位,所以 ,,即这些空隙无论后面怎么排都是满代价 。
- 对 :空隙左侧含第 位,,只由右侧选手的出场时刻决定。
现在考虑「从第 位出发,先一直向左扩到 ,再一直向右扩到 」这种具体顺序:排在 号空隙两侧都出现的阶段时,左侧只需扩到 ,右侧需要扩到 ,于是 时空隙的代价是 , 时是 。和任意顺序比:
- 左半部分:,故 ,不会更差;
- 右半部分: 的位置共有 个,它们的出场时刻是 个互不相同的正整数,其中最小值至多为 ,即 ,故 ,也不会更差。
所以任何出场顺序都能被这个「区间扩张」顺序替代而不变差。反过来,任何区间扩张顺序都对应一个合法出场顺序,因此最优解恰是「从排序后某个位置开始,每次把区间向左或向右扩大一格」这类顺序的最优值。
状态与转移。 由于扩张过程中已出场的集合始终是排序后的一个连续区间 ,设 表示前 个阶段(即已出场集合依次扩张到 )的代价最小值。上一阶段要么是 ,要么是 ,而当前阶段的代价是 :
答案为 。按区间长度从小到大递推即可,等号说明重叠的速度值不影响正确性(对应空隙 ,本来就不产生代价)。
为什么用二维表就够。 转移只依赖长度少一的两个区间,所以按长度递增计算、用二维数组直接存是最省事的写法。 时状态数 ,每个状态只做两次查表和一次加法。答案上界约 ,需要 64 位整数。
复杂度
状态数 ,转移 ,时间 ;存储 。
仓颉实现
二维表压缩成一维数组,下标为 ,初始全为 ,正好对应 。
查看仓颉实现
import std.env.*
import std.convert.*
import std.sort.*
main() {
let n = Int64.parse(getStdIn().readln().getOrThrow())
let s = getStdIn().readln().getOrThrow().split(" ", removeEmpty: true).map({ p: String => Int64.parse(p) })
sort(s)
let w = n + 1
let dp = Array<Int64>(w * w, { _ => 0 })
for (len in 2..=n) {
for (l in 0..=(n - len)) {
let r = l + len - 1
let a = dp[(l + 1) * w + r]
let b = dp[l * w + (r - 1)]
var m = a
if (b < a) {
m = b
}
dp[l * w + r] = (s[r] - s[l]) + m
}
}
println(dp[n - 1])
}