数据规模:1≤n≤2×105,1≤k≤2×1014,1≤ai≤109。
思路
记当前盘中剩余为 r,一位需求为 x 的客人取完后,剩余变成
r−min(x,⌊2r⌋)=max(r−x,⌈2r⌉).
把这一变换记作 fx(r)=max(r−x,⌈r/2⌉)。总共取走的数量是 k−r末,所以 最大化取走量等价于最小化最终剩余;又因为最终剩余只取决于每步变换的复合,顺序的影响完全体现在这个复合结果上。
贪心:按 ai 从大到小安排客人。
相邻交换证明。 固定当前剩余 r,取相邻的两位客人,需求 a≥b。记 R(a,b) 为「先 a 后 b」两步之后的剩余。利用 ⌈max(u,v)/2⌉=max(⌈u/2⌉,⌈v/2⌉) 与 max(u,v)−b=max(u−b,v−b),可以把两步复合展开成
R(a,b;r)=max(r−a−b, ⌈2r⌉−b, ⌈2r−a⌉, ⌈4r⌉),
同理
R(b,a;r)=max(r−a−b, ⌈2r⌉−a, ⌈2r−b⌉, ⌈4r⌉).
逐项对比第一式的每一项都不超过第二式的最大值:
- r−a−b 与 ⌈r/4⌉ 两项两边相同;
- ⌈(r−a)/2⌉≤⌈(r−b)/2⌉,因为 a≥b;
- ⌈r/2⌉−b≤⌈(r−b)/2⌉,这由 ⌈(r−b)/2⌉+⌈b/2⌉≥⌈r/2⌉ 与 ⌈b/2⌉≤b 得到。
因此 R(a,b;r)≤R(b,a;r):把需求更大的客人排在前面,两步之后盘中剩余不会更多。又因为 fx 关于 r 单调不减,剩余更少只会让后面每一步都不劣,所以这次交换不会使答案变差。反复消去「前小后大」的相邻对,最优顺序就是需求降序。
模拟。 把 a 降序排序后从左到右依次让客人取:每步 r←max(r−ai,⌈r/2⌉),最后输出 k−r。由于 ai≥1,一旦 r≤1 后面每位客人都只能取 0,可以提前结束。
注意 ⌈r/2⌉=r−⌊r/2⌋,实现里直接用整数除法得到 ⌊r/2⌋,取走的数量就是 min(ai,⌊r/2⌋)。
复杂度
排序 O(nlogn),模拟 O(n),总时间 O(nlogn);空间 O(n)(存储 a)。
仓颉实现
查看仓颉实现
import std.env.*
import std.convert.*
import std.sort.*
main() {
let reader = getStdIn()
let head = reader.readln().getOrThrow().split(" ", removeEmpty: true)
let k = Int64.parse(head[1])
let a = reader.readln().getOrThrow().split(" ", removeEmpty: true).map({ p: String => Int64.parse(p) })
sort(a, descending: true)
var r = k
for (x in a) {
let half = r / 2
if (x < half) {
r -= x
} else {
r -= half
}
if (r <= 1) {
break
}
}
println(k - r)
}