話題とする問題はこちら。
知らなかったので、細かいところの実装コストが膨れて解けなかったので備忘のため。
atcoder.jp
本記事の主張内容はおおむね解説にある通りなので、当記事はお手軽に使える python スクリプトがあるよ、ということでお願いします。
E 問題を解くにあたって、以下のような部分問題を解くことになります。
および、
が与えられる。
各について、以下を満たす最小の
を求めよ。
が問題に登場する「箱に入っているじゃがいもの数」となります。
これ単品であれば、ABC C 問題程度の難易度かなという印象です。
さて、思いつくのは累積和と二分探索による高速化ですが、悪い例(分岐が複雑)としてこんなスクリプトがあります。
import bisect # 累積和の計算。s[i] は [0, i) の区間を足し合わせたものとする。 # 番兵の都合でリスト長は n + 1 s = [0] * (n + 1) for k in range(n): s[k+1] = s[k] + w[k] all_w = sum(w) # i, i+1, ..., n-1, 0, ..., i-1 まで足した分は事前に足し上げておく ans = (x // all_w) * n # 事前に足し上げた分を取り除く rem = x % all_w if rem > all_w - s[i]: # n-1 まで足しても足りないので、とりあえず n-1 まで足す。 ans += n - i rem -= all_w - s[i] # 0 からどこまで足せばよいか二分探索。 j = bisect.bisect_left(s, rem, lo=0, hi=n+1) ans += j else: # i からどこまで足せばよいか二分探索 j = bisect.bisect_left(s, rem + s[i], lo=0, hi=n+1) ans += j - i print(ans)
個人的には、「n-1 まで足しても足りないので、とりあえず n-1 まで足す」付近の ans や rem を更新するあたりで「1足すべき? 足さないで OK?」など細かいことを考えないといけなくなり、時間がかかってしまいました。
解説の提案する手法はこちらです。
import bisect # 累積和の計算。s[i] は [0, i) の区間を足し合わせたものとする。 # リスト長を 2n にしておくのが肝。 s = [0] * (2 * n) for k in range(2*n): s[k+1] = s[k] + w[k % n] all_w = sum(w) # i, i+1, ..., n-1, 0, ..., i-1 まで足した分は事前に足し上げておく ans = (x // all_w) * n # 事前に足し上げた分を取り除く rem = x % all_w # i からどこまで足せばよいか二分探索。 # リスト長を2倍にしているので、 # 添え字が n-1 から 0 に戻ってしまうのが1回以内ならば # s の中で探しきることができる。 j = bisect.bisect_left(s, rem + s[i], lo=0, hi=n+1) ans += j - i print(ans)
コメントがごちゃごちゃしていてわかり辛いですが、分岐も減ってスクリプト量も減っており、何より二分探索の部分が のときに書くスクリプトと、ほぼ同じスクリプトで OK になります。
累積和を、リスト長超えてぐるぐる足し合わせるような問題のときに、ぜひご活用ください。