鼠标输入密码
- 难度 提高
- 时限 1s
- 空限 256m
- 线性 DP
数据规模:,,,,,。
思路
最终形态只由粘贴次数决定。 粘贴永远是把完整的 追加到末尾,滚动只改某个元素的值,删除只去掉某个元素、其余元素相对顺序不变。所以粘贴 次之后,编辑区必然是 连写 遍;要让它变成 ,等价于说 是这份长序列的子序列:删掉其余 个元素,再把保留下来的每个元素各自滚动到对应的 值。一份方案的费用是
其中 是保留下来的元素在「 重复 遍」中的位置, 表示它被用来配 的第 项。
若某份拷贝一个元素都没留下,删掉它的全部 个元素再取消这次粘贴,能省下 ,所以最优解中每份拷贝至少留下一个元素,即 ;同时 。
处理顺序可以重排。 每份拷贝在编辑区里的位置只取决于它是第几次粘贴,与何时粘贴无关;滚动和删除又都只作用于单个元素。所以总可以规定:先把当前副本从左到右处理完,再粘贴下一份,费用不变。
状态。 设 表示已经配好 的前 个元素、当前这份拷贝的前 个元素都已处理完(每个要么被删除、要么被匹配)时的最小费用,粘贴费与到此刻为止的删除、滚动费都已付清。 表示这份拷贝刚粘贴、一个元素都还没处理。
在同一份拷贝内有三种动作:
- 时删掉第 个元素,花 ;
- 把第 个元素滚动成 并与之匹配,花 ,同时 增加 ;
- 当前副本用完()后换一份,花 。
层内成环的处理。 直接按上面三条递推,层 内会形成 的环。但从任意 绕回 一圈的增量是 ,绕圈只会更贵,所以最优路径里不会出现绕圈,可以只在「不绕圈」的范围内计算。
固定层 ( 的前 个已配好,待配的是 ,下标从 起),先算「本层不再新粘贴」的值:
第一项是删掉第 个元素;第二项是把第 个元素改成 并配掉它,于是从上一层「处理到 」的状态过来。这两项穷尽了 的来源,所以 就是本层不含新粘贴的真实值。
本层新粘贴的起点。 想在本层再粘贴一份,必须先把当前副本剩下的元素删完:若停在 ,要付 的删除费再加 。于是
从 取起: 意味着本层一个元素都没处理就去了 ,那正是 递推里已经包含的「从上一层粘贴过来」。而 本身等于 ( 是逐位加 递推出来的),所以代码里也可以直接取 。有了 后
第二项是「本层刚粘贴一份,把它前 个元素直接删掉」。
为什么两项就够。 对 归纳即可:若 ,则
因为 恰好就是右边括号里前两项的合并。基例 也吻合。特别地,「新粘贴一份、删掉前 个、再在第 个位置匹配」这种走法并没有被漏掉:它先以 的形式进入 ,再由下一层的 递推接到匹配动作上。
边界与答案。 第 层还没配任何 ,只能先粘贴再删,。配完 个元素后,当前副本剩下的 个元素必须删掉,所以
其中 那一项对应「配完之后又粘贴一份、再整份删掉」,其值 不小于直接收尾的走法,一并参与取最小不影响结果。
都可能为 ,推导中只用到 来排除绕环变优,没有假设费用为正。
复杂度
每组数据时间 ,总时间 。滚动数组加一个临时数组,额外空间 。
仓颉实现
用两个长度 的数组滚动保存相邻两层:先把 就地算进当前层的数组,再扫一遍求出 ,最后与 取较小值合并成新的 ,然后交换两个数组。最大费用约为 ,必须用 64 位整数, 取 ,加上 也不会溢出。
查看仓颉实现
import std.env.*
import std.convert.*
func solve(reader: ConsoleReader) {
let INF: Int64 = 4000000000000000000
let head = reader.readln().getOrThrow().split(" ", removeEmpty: true).map({ p: String => Int64.parse(p) })
let n = head[0]
let m = head[1]
let x = head[2]
let y = head[3]
let z = head[4]
let a = reader.readln().getOrThrow().split(" ", removeEmpty: true).map({ p: String => Int64.parse(p) })
let b = reader.readln().getOrThrow().split(" ", removeEmpty: true).map({ p: String => Int64.parse(p) })
// prev[j]:已配好前 i 个 b,当前副本处理完前 j 个元素的最小费用
var prev = Array<Int64>(n + 1, { k: Int64 => x + k * z })
var cur = Array<Int64>(n + 1, { _ => 0 })
for (i in 0..m) {
let bi = b[i]
// 先在 cur 里算出「本层不新粘贴」的代价 A[j]
var last: Int64 = INF
cur[0] = INF
for (j in 1..=n) {
let d = a[j - 1] - bi
let diff = if (d >= 0) { d } else { -d }
let matched = prev[j - 1] + y * diff
let deleted = last + z
let now = if (matched < deleted) { matched } else { deleted }
cur[j] = now
last = now
}
// 本层新粘贴一份:先删掉当前副本剩下的元素,再付粘贴费
var best: Int64 = INF
for (j in 1..=n) {
let cand = cur[j] + (n - j) * z
if (cand < best) {
best = cand
}
}
let start = x + best
cur[0] = start
for (j in 1..=n) {
let straight = start + j * z
if (straight < cur[j]) {
cur[j] = straight
}
}
let tmp = prev
prev = cur
cur = tmp
}
var ans: Int64 = INF
for (j in 0..=n) {
let cand = prev[j] + (n - j) * z
if (cand < ans) {
ans = cand
}
}
println(ans)
}
main() {
let reader = getStdIn()
let t = Int64.parse(reader.readln().getOrThrow())
for (_ in 0..t) {
solve(reader)
}
}