[R53D] 数组积
数据规模:,,,。
思路
先计算初始数组积
记数组 的前缀和为
若选择下标 执行一次加法操作,只有 各增加 ,因此数组积增加 ;执行一次减法操作时,数组积增加 。
每次操作都可以独立选择加法或减法,所以选择下标 时,总能选取更优的方向,使这次操作对答案的增量为
这样,原问题转化为:第 类物品有 个,每个价值为 ,从所有物品中选择至多 个,使总价值最大。
将所有下标按照 从大到小排序,然后依次取出
次操作即可。如果所有下标的次数上限之和小于 ,就取完所有操作;由于每次操作的最优增量都非负,不会使答案变小。
正确性证明
对于任意下标 ,一次加法操作对数组积的改变量为 ,一次减法操作的改变量为 。算法选择其中较大的一个,因此每次选择下标 都能获得且最多获得 的增量。
考虑任意一个满足限制的最优方案。若它选择了下标 的一次操作,而某个下标 仍有可用次数且 ,则用下标 的一次操作替换它,不会改变操作总数,也不会违反任何下标的次数限制,却会使总增量增加 。这与原方案最优矛盾。
因此,存在一个最优方案会在仍有操作名额时,总是优先选择尚未达到次数上限且增量最大的下标。算法正是按照这个顺序贪心选择,并在达到 次或用完所有可用操作时停止,所以得到的额外增量最大。加上初始数组积 后,输出即为题目要求的最大数组积。
复杂度
计算前缀和需要 时间,排序需要 时间,因此总时间复杂度为 ,空间复杂度为 。
仓颉实现
import std.convert.*
import std.env.*
import std.sort.*
main() {
let reader = getStdIn()
let first = reader.readln().getOrThrow().split(" ", removeEmpty: true).map({ p: String => Int64.parse(p) })
let n = first[0]
var remaining = first[1]
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) })
let c = reader.readln().getOrThrow().split(" ", removeEmpty: true).map({ p: String => Int64.parse(p) })
var answer: Int64 = 0
let gain = Array<Int64>(n, { _ => 0 })
var prefix: Int64 = 0
for (i in 0..n) {
answer += a[i] * b[i]
prefix += b[i]
gain[i] = if (prefix >= 0) { prefix } else { -prefix }
}
let order = Array<Int64>(n, { i => i })
sort(order, key: { i: Int64 => gain[i] }, descending: true)
for (i in order) {
if (remaining == 0) {
break
}
let take = if (c[i] < remaining) { c[i] } else { remaining }
answer += take * gain[i]
remaining -= take
}
println(answer)
}