D - Raise Minimum Editorial
by
harurun4635
観察
配列 \(A = (1, 2, 3)\) が与えられた時 、\(K\) 回操作したときの \(ans\) はどうなるでしょうか?( \(ans\) が達成される \(A\) の一例も挙げますが、一例であることに注意してください)
- \(K = 1\) では \(A = (2, 2, 3)\) で \(ans = 2\)
- \(K = 2\) では \(A = (3, 2, 3)\) で \(ans = 2\)
- \(K = 3\) では \(A = (3, 4, 3)\) で \(ans = 3\)
- \(K = 4\) では \(A = (4, 4, 3)\) で \(ans = 3\)
- \(K = 5\) では \(A = (4, 4, 6)\) で \(ans = 4\)
このように \(K\) に対して \(ans\) が「単調増加」であるような性質があります。これを「解が単調」などと呼ぶことがあります。
解の二分探索
「解が単調」である問題では、しばしば 解の二分探索 が有用です。 \(ans\) を直接求めるのではなく、「 \(ans\) を \(X\) 以上にすることが可能ですか?」という質問で解を二分探索することで、解を特定するというテクニックです。
これは多くの問題において、いわゆる「 \(ans\) を \(X\) 以上にすることが可能ですか?」などの 判定問題 のほうが解くほうが(直接元の問題を解くより)簡単であることが多いことを利用しています。
判定問題を解く
今回の問題で解くべき判定問題は
「( \(K\) 回の操作で)\(ans\) を \(X\) 以上にできるか?」
です。これは
「 \(ans\) を \(X\) 以上にするのに、必要な操作回数は \(K\) 回以下か?」
と同じことですし、
「すべての要素を \(X\) 以上にするのに、必要な操作回数は \(K\) 回以下か?」
と同じことです。
最後の問題であれば簡単に解けます。すべての \(i ~(1 \le i \le N)\) について
- \(A_i \ge X\) のとき、 \(0\)
- \(A_i\lt X\) のとき、 \(\displaystyle \Big\lceil \frac{X - A_i}{i} \Big\rceil\)
を足したものが最小の操作回数です。これが \(K\) 以下か判定すればよいです。
解法
今回であれば、必ず \(ans\) は \(1\) 以上になります。また \(A_1 + K + 1\) 未満です。( \(A_1\) に \(K\) 回すべて操作を行っても届かないためです)よって、この範囲で解の二分探索をすればよいです。
上の判定問題の計算量は \(O(N)\) です。判定問題を \(O( \log (A_1 + K))\) 回解けばよいので、計算量は \(O(N \log(A_1 + K))\) となります。
実装例
n, k = map(int, input().split())
a = [0] + list(map(int, input().split())) # 1-indexed で扱えるようにする
def isok(x):
# ans を x 以上にすることができますか?
nk = 0
for i in range(1, n + 1):
if a[i] < x:
nk += (x - a[i] + i - 1) // i # ceil((x - a[i]) / i)
if nk > k:
return False
return True
ok = 1
ng = a[1] + k + 1
while ng - ok > 1:
m = (ok + ng) // 2
if isok(m):
ok = m
else:
ng = m
print(ok)
注意
ceilを小数で計算しようとすると、その精度によってはWAとなります。基本的には整数範囲で行うことを意識するよいでしょう。overflow も注意してください。
ngも実装方針によってはしますし、判定問題内のnkの overflow は気が付きにくいです。
posted:
last update:
