目次

AtCoder Beginner Contest 128 D,E,F問題メモ

AtCoder Beginner Contest 128

回を重ねる毎に解けた問題数が減っていくのは何故だ。

D - equeue

D - equeue

問題

解法

Dequeを筒に見立てているが、特に解法にDequeが必要なわけではない。

なるべく負の価値を持つ宝石は取りたくないが、筒の奥にある価値の高い宝石を得るために一時的に取った方が得になることもある。

-2  -1  -3  100000  -5  -10

その際、取った負の宝石は筒に戻したいが、これは $K$ 回の内ならいつ戻してもよいので、必要なだけ取り出してから、戻せる分を戻す、という操作の順番に固定して考えてよい。

以下のデータを作る。

すると $k=0,1,2,...,K$ の中で、$DP_L[k]+DP_R[K-k]$ の最大値が答えとなる。

ただし、$DP_L$ と $DP_R$ の最大値が、ともに同じ宝石を取ってしまっているケースが考えられる。 操作回数が十分だと、得られる価値の最大値は正の価値を持つ宝石だけの和なので、超えている場合はその上界に上書きする。

データの作り方は、もっと上手い方法はあるとは思うが、制約が小さいので

というのを $t=0~N$ までシミュレーションして間に合う。ただし $\leftarrow$ は、現在値と比較して大きければ更新という操作を示す。

また、あくまで上で埋められるのは「$K$ 回ちょうど」の最大値であり、答えるのは「最大 $K$ 回」なので、最後に累積MAXを取る。

from itertools import accumulate


def make_dp(vvv, n, k):
    dp = [0] * (k + 1)

    for i in range(min(n, k)):
        i += 1
        vv = vvv[:i]
        vv.sort(reverse=True)
        sm = sum(vv)
        dp[i] = max(dp[i], sm)
        while i < k and vv and vv[-1] < 0:
            sm -= vv.pop()
            i += 1
            dp[i] = max(dp[i], sm)

    return list(accumulate(dp, func=max))


n, k = list(map(int, input().split()))
vvv = list(map(int, input().split()))

acl = make_dp(vvv, n, k)
vvv.reverse()
acr = make_dp(vvv, n, k)
acr.reverse()

ans = max(map(sum, zip(acl, acr)))
ans = min(ans, sum(v for v in vvv if v > 0))
print(ans)

E - Roadwork

E - Roadwork

問題

解法

区間Updateセグ木問題、のはずが、PythonだとPyPyでも時間的に厳しくてTLE。 セグ木を諦めて汎用性はなくなるが高速な解法にすると、とりあえずは間に合う。 セグ木でも間に合ってる人もいるっぽいので、実装をちょっと頑張れば通せるはず。(自分の実装が悪い気がする)

問題文で時刻に0.5を引いているのは「開始ちょうど、終了ちょうどに到達した場合がどうなるかのあやふやさを無くすため」だと思う。 「$S_i$ ちょうどに到着したら既に始まってるので引っかかる」 「$T_i$ ちょうどに到着したら既に終わってるので引っかからない」と解釈すればよい。

ここから $X_i$ までの移動にかかる時間を差し引くと「この工事に引っかかる可能性のある人の、地点0を出発した時刻の範囲」がわかり、$[S_i-X_i,T_i-X_i)$ となる。

2つ以上の工事に該当する人は座標が小さい方の工事に最初に引っかかるので、 一度Updateされた区間は上書きされないようなセグ木で、座標が小さい方から順に区間Updateを行えばよい。

その際、$D_i$ の取り得る範囲は $10^9$ と大きいので、以下のいずれかで座標変換を行う。

$Q=10^5$ の場合 $\log{Q}=16.6$ なので毎回二分探索にそれだけの計算量がかかることを考えると、下の方がいいのかな。

高速な実装

この問題のように、Updateする優先順位が決まっていて、一度Updateした部分はもう変更されない場合、セグ木よりもっと簡単な実装がある。

$[s_i,t_i)$ を $x_i$ にする更新クエリが来たら、

i = si
while i < ti:
    if ans[i] == -1:  # まだ手前の工事に引っかかってない
        ans[i] = xi
        skip[i] = ti
        i += 1
    else:             # 手前で引っかかり済み
        i = skip[i]

全体で $ans$ の各要素には1回しか代入されないので、単純ながらも高速になる。

高速な実装2

解説PDFの方法。「今出発したら引っかかる工事リスト」を作る。

道路工事開始イベント、道路工事終了イベント、$i$ 番目の人出発イベントを、出発時刻順にソートする。

終了、開始、出発イベントの順に取り出されるようにする。

開始イベントでは工事リストに $X_i$ を追加、終了イベントでは削除する。 出発イベントが取り出された時点の工事リストの最小値が、その人が最初に直面する工事の座標となる。

ただし、工事リストのデータ構造がPythonでは難しい。 追加、削除に加え、最小値の取得をそれぞれ $O(\log{N})$ 程度で行いたいが、setでは最小値の取得が $O(N)$ となる(? 少なくともlogではないっぽい)し、heapqでは要素の削除が出来ない。 二分探索木などを実装すれば良いが、ちょっと過剰。

副次的な方法として、setは検索は速いので、heapqとsetを2つ持ち、最小値の取得をheapq、それが有効であるかどうかをsetで確かめるようにすると、そこそこ速くなる。

import sys
from bisect import bisect_left

n, q = map(int, input().split())
lines = sys.stdin.readlines()
kkk = []
for line in lines[:n]:
    s, t, x = map(int, line.split())
    kkk.append((x, s, t))
kkk.sort()
ddd = list(map(int, lines[n:]))

ans = [-1] * q
skip = [-1] * q
for x, s, t in kkk:
    ss = bisect_left(ddd, s - x)
    tt = bisect_left(ddd, t - x)
    while ss < tt:
        if skip[ss] == -1:
            ans[ss] = x
            skip[ss] = tt
            ss += 1
        else:
            ss = skip[ss]
print('\n'.join(map(str, ans)))

F - Frog Jump

F - Frog Jump

問題

N=11
番号  0   1   2   3   4   5   6   7   8   9  10
得点  0  -4   0 -99  31  14 -15 -39  43  18   0

A=3, B=2
移動  0→3→1→4→2→5→(3)
次に3に移動しようとするが、既に沈んでいるため、移動終了

A=6, B=4
移動  0→6→2→8→4→10
無事10の葉にたどり着く。得点は 0+0+31-15+43 = 59

解法

カエルの行動から水前寺清子氏を連想した。

前提として、水に落ちてはいけない。マイナス $10^{100}$ 点とか、制約上どう見ても取り返せない。 落ちずに $N-1$ に到達する移動を考える。

前方向に移動するのを移動A、後方向に移動するのを移動Bとする。

$A,B$ の持つ性質を考えてみる。

移動Bで $N-1$ に着地することはない。 なぜなら、移動の直前には $N-1$ より先の葉にいなければいけないが、そんな葉は無い。 $N-1$ へは移動Aで到達する。

ここで $A$ を固定すると、$N-1$ 直前にいる葉は $N-1-A$ である。

また、各移動Bの後にいる葉の番号は $A-B$ の倍数となる。よって $C=A-B$ として、$C$ は $N-1-A$ の約数である。

$A$ と $C$ を決めれば移動が特定され、得点が計算できる。

さて、しかしここで 「$A$ を決める」→「$N-1-A$ の約数を列挙して $C$ を決める」 →「同じ葉を踏まないか確認する」→「得点を計算する」とするのは、上手くいかない。 $A$ の列挙が $O(N)$、約数列挙が試し割りで $O(\sqrt{N})$、得点の計算が $O(N)$、合計 $O(N^2 \sqrt{N})$ くらいかかってしまう。

ならばと、$C$ を固定して考えてみる。

すると、移動Bを行った回数を $k$ とし、$k$ で場合分けすると、 不可能なパターンを探索しなくて済むと同時に、過去の計算結果を綺麗に活用できる。

N=16  C=2
k=0  0 → 15
k=1  0 → ⑬ → ② → 15
k=2  0 → ⑪ →  2 → 13 → ④ → 15
k=3  0 → ⑨ →  2 → 11 →  4 → 13 → ⑥ → 15
k=4  0 → ⑦ →  2 →  9 →  4 → 11 →  6 → 13 → ⑧ → 15
...

$k$ が1増えると、順番は変わっているものの 直前の $k$ で踏んだ葉はそのまま、ある特定の2枚の葉を新しく踏むことになっている。

その2枚とは、$N-1-kC$ と $kC$ である。なお、$N-1-kC=A$ である。

これなら $C$ と $k$ の列挙に $O(\sum_C{\frac{N}{C}})$ で調和級数となるので、$O(N \log{N})$ くらいに収まる。 切り口次第でがらっと計算量が変わるのが美しい。

ただ、上の例では終了条件をきちんと考えていない。満たすべき条件は以下の通りである。

コードはかなりシンプル。便宜上'k'の指す意味が $kC$ となっている。

n = int(input())
sss = list(map(int, input().split()))

ans = 0
for c in range(1, n // 2):
    a = n - 1 - c
    k = c
    tmp = 0
    while a > c and a != k and a + c != k:
        tmp += sss[a] + sss[k]
        ans = max(ans, tmp)
        a -= c
        k += c
print(ans)