LoginSignup
1
1

More than 5 years have passed since last update.

AtCoder Beginner Contest 124 D 解説動画(尺取法、累積和)をPythonでやってみる

Posted at

概要

2度目のAtCoder Beginner Contestで、今回もD問題が解けず悔しい思いをしました。
今回は解説PDFで実装が省略されていたこともあり、C++での解説動画をPythonでやってみることを通じ、アルゴリズムを理解しようということでやってみました。。
尚、解説動画は生配信で色々とアクシデントがあり面白いので、この記事は見なくても、解説動画を見て、いいねボタンを押しましょう。

AtCoder Beginner Contest 124 D - Handstand
AtCoder Beginner Contest 124 解説配信
AtCoder Beginner Contest 124 解説PDF

TLE (Time Limit Exceeded)になるやり方

直大さん(@chokudai)くらいのスーパープログラマーがキレイにコードを書くと通ってしまうことがありますが、普通は通らない実装方法です。

N, K = map(int, input().split())
S = input()
Nums = []
now = 1 # 今見ている数
cnt = 0 # nowがいくつ並んでいるか
for i in range(N):
    if S[i] == str(now):
        cnt += 1
    else:
        Nums.append(cnt)
        now = 1 - now   # 0と1を切り替えるときの計算 now ^= 1
        cnt = 1
if cnt != 0:
    Nums.append(cnt)
# 1-0-1-0-1-0-1 って感じの配列が欲しい
# 1-0-1-0-1-0 みたいに0で終わっていたら適当に1つ足す
if len(Nums) % 2 == 0:
    Nums.append(0)

Add = 2 * K + 1
ans = 0
# 1-0-1... の1から始めるので、偶数番目だけ見る
for i in range(0, len(Nums), 2):
    tmp = 0
    left = i
    right = min(i + Add, len(Nums))
    for j in range(left, right):
        tmp += Nums[j]
    ans = max(tmp, ans)

print(ans)

image.png

for文が2重になっていることから計算回数が$O(N^2)$になるため、PythonではしっかりTLEになりました。
個人的には now = 1 とすることで、0から始まる時にNums[0]に0を入れられるところ、now = 1 - now で0と1を交互にできるところ、偶数番目だけを見るなどで、なるほど!となってしまいました。
だいたい末端処理の実装でやらかして、デバックで疲弊してしまうので、こういうテクニックを自然に使えるようになりたいです。

尺取法

N, K = map(int, input().split())
S = input()
Nums = []
now = 1 # 今見ている数
cnt = 0 # nowがいくつ並んでいるか
for i in range(N):
    if S[i] == str(now):
        cnt += 1
    else:
        Nums.append(cnt)
        now = 1 - now   # 0と1を切り替えるときの計算 now ^= 1
        cnt = 1
if cnt != 0:
    Nums.append(cnt)
# 1-0-1-0-1-0-1 って感じの配列が欲しい
# 1-0-1-0-1-0 みたいに0で終わっていたら適当に1つ足す
if len(Nums) % 2 == 0:
    Nums.append(0)

Add = 2 * K + 1
ans = 0
# 尺取り法 forループの外側にleft, rightを持つ
left = 0
right = 0
tmp = 0 # [left, right)のsum
# 1-0-1... の1から始めるので、偶数番目だけ見る
for i in range(0, len(Nums), 2):
    # 次のleft, rightを計算する
    Nextleft = i
    Nextright = min(i + Add, len(Nums))
    # 左端を移動する
    while Nextleft > left:
        tmp -= Nums[left]
        left += 1
    # 右端を移動する
    while Nextright > right:
        tmp += Nums[right]
        right += 1
    ans = max(tmp, ans)

print(ans)

image.png

尺取法というのは実は解説動画で初めて聞いたのですが、右側と左側の動きが尺取虫のように動くということでしょうね。
計算回数が$O(N)$になるので、Pythonでも余裕をもって通ります。C++では5msということで、差は大きいですね
理論は分かっても実装となると苦労しそうなのですが、left, right, Nextleft, Nextrightという使い方が分かりやすく、メンテナンス性も高いコードで、こういうコードを書きたいですね。

累積和

N, K = map(int, input().split())
S = input()
Nums = []
now = 1 # 今見ている数
cnt = 0 # nowがいくつ並んでいるか
for i in range(N):
    if S[i] == str(now):
        cnt += 1
    else:
        Nums.append(cnt)
        now = 1 - now   # 0と1を切り替えるときの計算 now ^= 1
        cnt = 1
if cnt != 0:
    Nums.append(cnt)
# 1-0-1-0-1-0-1 って感じの配列が欲しい
# 1-0-1-0-1-0 みたいに0で終わっていたら適当に1つ足す
if len(Nums) % 2 == 0:
    Nums.append(0)

Add = 2 * K + 1
# 累積和を作る
# 0 1 2 3 4 5 6
#  0 1 2 3 4 5
sum = [0] * (len(Nums) + 1)
for i in range(len(Nums)):
    sum[i + 1] = sum[i] + Nums[i]
ans = 0
# 1-0-1... の1から始めるので、偶数番目だけ見る
for i in range(0, len(Nums), 2):
    # 次のleft, rightを計算する [left, right)
    left = i
    right = min(i + Add, len(Nums))
    tmp = sum[right] - sum[left]
    ans = max(tmp, ans)

print(ans)

image.png

累積和という言葉は聞いたことはありましたが、実装はしたことが無いと思います。
計算回数が$O(N)$になり尺取法と同程度ということですが、実行時間が少しだけ尺取法より早くなりました。c++では同じ5msでしたね。
実装は非常に分かりやすいコードで、直大さんも累積和を推奨しますと言っていたので、次に累積和を使う問題が出たらスムーズな実装を目指したいと思います。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1