2025-01-04 ABC007

はじめに

1月4日っていつも仕事はじめなので、なんか延長戦を満喫している感がすごい。

AtCoder Beginner Contest 007

A - 植木算
# 入力された整数 n から 1 を引いた値を出力する
input_number = int(input())
print(input_number - 1)

1引いた数を出力するだけなので、こんなものでしょうか。

B - 辞書式順序
# 入力を取得
# 入力された文字列が "a" なら -1 を、それ以外なら "a" を出力する
input_string = input()
print("a" if input_string != "a" else -1)

"a"が辞書順最速なので、だいたい"a"を出しておけばOK。
ただし、入力が"a"だとそれより強いものがないので"-1"を出力する必要があります。
そのとおりの実装なので特に言うことはない。

C - 幅優先探索
# 入力を取得
from collections import deque

# 入力の取得
rows, cols = map(int, input().split())
start_y, start_x = map(int, input().split())
goal_y, goal_x = map(int, input().split())
maze = [input() for _ in range(rows)]

# 初期設定
steps = [[-1 for _ in range(cols)] for _ in range(rows)]
steps[start_y - 1][start_x - 1] = 0  # スタート地点の歩数を 0 に設定
queue = deque([(start_y - 1, start_x - 1)])  # BFS キュー

# 移動方向の定義 (上下左右)
dy = [1, 0, -1, 0]
dx = [0, 1, 0, -1]

# 幅優先探索 (BFS)
while queue:
    y, x = queue.popleft()
    # ゴール地点に到達したら終了
    if (y, x) == (goal_y - 1, goal_x - 1):
        break
    # 上下左右への移動を試す
    for i in range(4):
        ny, nx = y + dy[i], x + dx[i]
        # 移動可能かのチェック
        if 0 <= ny < rows and 0 <= nx < cols and steps[ny][nx] == -1 and maze[ny][nx] == ".":
            steps[ny][nx] = steps[y][x] + 1
            queue.append((ny, nx))

# ゴール地点の歩数を出力
print(steps[goal_y - 1][goal_x - 1])

ド定番問題のためか、めちゃくちゃ破壊的にリフォームが行われました。
もとは「deque」など導入せずリストで管理したり、上下左右への移動も4方向に場合分けしたりとかなり原始的なつくりをしていました。
移動方向の定義はお上手ですね。これなら確かにfor文で綺麗に書けるのか。

D - 禁止された数字
def adjust_digit(digit):
    """
    桁の値を調整する:
    - 4 または 9 を含まない場合の有効な桁数に変換。
    """
    if digit <= 3:
        return digit + 1
    elif digit <= 8:
        return digit
    else:
        return digit - 1

def count_valid_numbers(num_str):
    """
    数字の文字列を受け取り、その範囲内で「4」または「9」を含まない
    有効な数字の総数を計算。
    """
    digits = list(map(int, num_str))
    valid_count = 0
    for idx, current_digit in enumerate(digits):
        if current_digit == 0:
            continue
        # 有効な数字を加算
        valid_count += adjust_digit(current_digit - 1) * (8 ** (len(digits) - idx - 1))
        # 現在の桁が「4」または「9」の場合、以降の計算を終了
        if current_digit == 4 or current_digit == 9:
            break
    # 総数から有効な数字を引く
    return int(num_str) - valid_count

# 入力と範囲内の有効な数字を計算
x, y = map(int, input().split())
result = count_valid_numbers(str(y + 1)) - count_valid_numbers(str(x))
print(result)

場合の数の問題なので、机上でガリガリ解き方を考えたものを実装したのであまり競技プログラミングって感じがしてない……。
「count_valid_numbers」関数の中身をwhile文からfor文に書き換えてくれました。
見てる桁の番目「idx」とその数字「current_digit」の両方を保持しなきゃいけないので書くときに結構頭を悩ませましたが、
enumerate関数などというものがあるのですねぇ。初めて知りました。

おわりに

今日までABC001から順番に全問見ていきましたがこのあたりでストックが尽きたので、
次回からはだいぶ飛び飛びになります。また、解けたもののみの掲載になっていきます。

2025-01-03 ABC006

はじめに

箱根も終わったし、ぼちぼち正月気分も抜けてくる頃ですね。
今回はD問地力で解けなかったのでカンニングしました。もう当初のモチベーション関係なくなってるじゃん……。

AtCoder Beginner Contest 006

A - 世界のFizzBuzz
# 入力を取得
n = int(input())

# 3の倍数かどうかを判定して出力
result = "YES" if n % 3 == 0 else "NO"
print(result)

3で割り切れるor3がつくかどうかを判定する問題だが、何故かN≦9なので後者の判定が不要になっている。
一旦resultで置く形に直された。print文に関数を書くのはお気に召さないようで。

B - トリボナッチ数列
# 入力を取得
n = int(input())

# 定数の設定
MOD = 10007

# 初期値の設定
a, b, c = 0, 0, 1

# n が小さい場合は直接出力
if n <= 3:
    print([0, 0, 1, 1][n - 1])
else:
    # n が 4 以上の場合はループで計算
    for _ in range(n - 3):
        a, b, c = b % MOD, c % MOD, (a + b + c) % MOD
    print(c)

a_n=a_{n-1}+a_{n-2}(a_1=0、a_2=0、a_3=0)のa_n(mod 10,007)を求める問題。
もとはn≧4の時に退避用に「d」という変数がいましたが、直接置き換えることによって無くなっています。
プログラム眺めてて思いましたが、n≦3のところにある[0,0,1,1]は[0,0,1]でよさそうですね。読むことがないので。

C - スフィンクスのなぞなぞ
# 入力を取得
n, m = map(int, input().split())

# 解の初期値を設定
result = [-1, -1, -1]

# ループで条件を満たす値を探す
for z in range(n + 1):
    x = 3 * n - m + z
    y = m - 2 * n - 2 * z
    if x >= 0 and y >= 0:
        result = [x, y, z]
        break

# 結果を出力
print(result[0], result[1], result[2])

x+y+z=n…①、2x+3y+4z=m…②を満たす整数解x,y,zを求める問題。不定方程式なので文字を消すことを考えて、
②-3×①からx=-m+3n+z、②-2×①からy=m-2n-2zなので、zを決めればxとyが決まってくれます。
よってzを走査して、x,yがともに0以上になる場合を出力すればOKです。
これは特に何も無し。今までの傾向からするとprint文をlist内包表記にするかとも思いましたが、愚直にやってますね。

D - トランプ挿入ソート
from bisect import bisect_left

def longest_increasing_subsequence_length(sequence):
    lis = [sequence[0]]
    for num in sequence[1:]:
        if num > lis[-1]:
            lis.append(num)
        else:
            lis[bisect_left(lis, num)] = num
    return len(lis)

n = int(input())
sequence = [int(input()) for _ in range(n)]
lis_length = longest_increasing_subsequence_length(sequence)
print(n - lis_length)

最長増加部分列を出せばいいということまでわかりましたが、そこからは地力で解けなかったので解法をカンニングしました。二分探索……なるほど……。
ということで自力の実装ではないため元々プログラムが整っており、変数や関数名が置き換えられただけです。

2025-01-02 ABC005

はじめに

元旦2日目―――――。(?)

AtCoder Beginner Contest 005

A - おいしいたこ焼きの作り方
# 2つの整数を入力として受け取る
divisor, dividend = map(int, input().split())

# ゼロ除算を防ぐ
if divisor == 0:
    print("Error: Division by zero is not allowed.")
else:
    # 商を計算して出力
    print(dividend // divisor)

割り算の商を求める問題。ゼロ除算を回避するようにしてくれました(入力制約からして、気にしなくてもよいはよいですが)。

B - おいしいたこ焼きの食べ方
n = int(input())
min_value = float('inf')  # 初期値を無限大に設定

for _ in range(n):
    current = int(input())
    min_value = min(min_value, current)  # 最小値を更新

print(min_value)
n = int(input())
values = [int(input()) for _ in range(n)]  # 入力をリストに格納
print(min(values))  # リストの最小値を出力

珍しく2通り出力してきました。前者がわたしのプログラム準拠で、後者は一発で出すにはこうする、というものですね。
まあ確かに後者で十分かも。

C - おいしいたこ焼きの売り方
t = int(input())  # たこ焼きの提供可能時間
n = int(input())  # たこ焼きの数
cooking_times = list(map(int, input().split()))  # たこ焼きが焼き上がる時刻
m = int(input())  # 顧客の数
customer_times = list(map(int, input().split()))  # 顧客が訪れる時刻

seller_index = 0
for customer_time in customer_times:
    # 顧客が来た時刻に対応可能なたこ焼きを探す
    while seller_index < n and cooking_times[seller_index] < customer_time - t:
        seller_index += 1
    # 顧客に対応できない場合
    if seller_index >= n or cooking_times[seller_index] > customer_time:
        print("no")
        break
    # 対応可能なたこ焼きを使用
    seller_index += 1
else:
    print("yes")

冒頭で「このコードは、たこ焼きの販売に関する問題を解決するプログラムです。」とか言い始めて死ぬほど笑ってしまった。
言うほどたこ焼きかどうか自明か?お主知っているな?
もともとは大きなwhile文を書いて、インデックスがたこ焼き側・顧客側のどちらかでオーバーしたらbreakさせる処理にしてましたが、
全体はfor文に書き換えられていますね。たこ焼きの検索にだけwhile文使う形になっています。
GPTくん、結構while文を避けがちな気がします。

D - おいしいたこ焼きの焼き方
n = int(input())

# 累積和の準備
row_sum = []
for _ in range(n):
    row = list(map(int, input().split()))
    tmp = []
    cumulative = 0
    for value in row:
        cumulative += value
        tmp.append(cumulative)
    row_sum.append(tmp)

# 二次元累積和の計算
cumulative_sum = [[0] * n for _ in range(n)]
for i in range(n):
    for j in range(n):
        cumulative_sum[i][j] = row_sum[i][j]
        if i > 0:
            cumulative_sum[i][j] += cumulative_sum[i - 1][j]

# 面積ごとの最大値を格納するリスト
max_area_sums = [0] * (n * n + 1)

# 全区間を探索して各面積の最大値を計算
for i in range(n):
    for j in range(n):
        for k in range(i, n):
            for l in range(j, n):
                current_sum = cumulative_sum[k][l]
                if i > 0:
                    current_sum -= cumulative_sum[i - 1][l]
                if j > 0:
                    current_sum -= cumulative_sum[k][j - 1]
                if i > 0 and j > 0:
                    current_sum += cumulative_sum[i - 1][j - 1]
                area = (k - i + 1) * (l - j + 1)
                max_area_sums[area] = max(max_area_sums[area], current_sum)

# 面積以下の最大値を計算
max_cumulative_sums = [0] * (n * n + 1)
for i in range(1, len(max_area_sums)):
    max_cumulative_sums[i] = max(max_cumulative_sums[i - 1], max_area_sums[i])

# クエリの処理
m = int(input())
for _ in range(m):
    query = int(input())
    print(max_cumulative_sums[query])

2次元累積和をもとにして、一定面積以下での合計値の最大を求める問題。
とにかく元の変数が終わっていた(累積和を「rui」、二次元累積和を「rui2」とか書いてた)ので、それだけでもかなり見やすくなった。
答えを求めに行くパートは、4か所の長方形の面積を求めて足し引きを頑張るだけなので、結局は場合分けをセコセコやるしかないですね。
それでも不要な条件分岐とかも全部無くしてくれているのでかなりおいしくなっている気がする。

2025-01-01 ABC004

はじめに

あけましておめでとうございます。正月休みは時間があるのでなんとか毎日書けそうです。

AtCoder Beginner Contest 004

A - 流行
# 入力を取得
n = int(input())

# 結果を計算して出力
result = 2 * n
print(result)

まあ2倍するだけなのでね。一度「result」に置きなおすのが読みやすいと認識している節があるようだ。

B - 回転
# 4x4 の入力を取得
grid = [input().split() for _ in range(4)]

# 反転したグリッドを出力
for i in range(3, -1, -1):
    for j in range(3, -1, -1):
        print(grid[i][j], end=" ")
    print()

もとはfor文のインデックスを正順に回して、[3-i][3-j]を出力させていたが、
rangeのインデックスを逆回しにして対応した方がよいと言われた。そういうものですか。

C - 入れ替え
n = int(input())
cards = [1, 2, 3, 4, 5, 6]

# 必要な交換回数
swaps = n % 30

# 交換をシミュレート
for i in range(swaps):
    idx = i % 5
    cards[idx], cards[idx + 1] = cards[idx + 1], cards[idx]

# 結果を出力
print("".join(map(str, cards)))

まずswap=n%30やidx=i%5と一度別変数で置くように修正が入った。後者は確かにいちいち計算することになって面倒ですね。
print部分も雑にfor文で1つずつ出力するのではなく、join(map(~の形に変更された。

D - マーブル
a, b, c = map(int, input().split())

# 区間 [x, y] の整数の総和を計算する関数
def sum_of_integers(x, y):
    return (y + 1) * abs(y) // 2 - (x - 1) * abs(x) // 2

# j の範囲を計算
j_start = min(100 - c - b + 1, -b + 1)
j_end = max(-100 + a, 0) + 1

# 最小コストの初期値を非常に大きな値に設定
min_cost = float("inf")

# 各 j の値に対してコストを計算
for j in range(j_start, j_end):
    # i の計算
    if j <= -100 + (a - 1) // 2:
        i = j - a
    else:
        i = -100 - (a - 1) // 2

    # k の計算
    if j + b - 1 >= 100 - (c - 1) // 2:
        k = j + b
    else:
        k = 100 - (c - 1) // 2

    # 各区間の総和を計算
    cost_a = sum_of_integers(i + 100, i + 100 + a - 1)
    cost_b = sum_of_integers(j, j + b - 1)
    cost_c = sum_of_integers(k - 100, k - 100 + c - 1)

    # 合計コストを計算し、最小値を更新
    total_cost = cost_a + cost_b + cost_c
    min_cost = min(min_cost, total_cost)

# 最小コストを出力
print(min_cost)

自分で解いておいてどうしてこれで解けるのかが思い出せない……。
緑マーブルを置く位置の左端はj_start~j_endの範囲だけ考えればよくて(これはなんでだっけ……?)、
ひとつのjに対して赤・青マーブルを置く位置の左端も決まるから、
(赤を均等に広げて緑と重なるなら赤の右端が緑と隣接するように置く、青も均等に広げて緑と重なるなら青の左端が緑と隣接するように置く)
後はその配置にするための移動回数をtotal_costとして計算させる、という流れだった気がします。
これはただの数的処理なので、変数を整えてコメントを付してくれているだけですね。
地味なところだと初期値が「float("inf")」になってますね。普段はサボって10**20とかで初期値を置いてしまうので、サボりがとがめられている気分。

2024-12-31 ABC003

はじめに

よいお年を。とりあえずABC006までは全部やりつつ、それ以降は解けた問題だけ放り込んでいく。
昔解いた問題の推敲をさせるのが目的なので、それ以降はナンバリング順にはならず飛び飛びになる予定。
筆者のレーティングからすると、緑問題は半々+αくらい、水色はかなり微妙、青以上は無理って感じになるはず。

AtCoder Beginner Contest 003

A - AtCoder社の給料
n = int(input())  # 入力時のプロンプトを追加
total_cost = (n + 1) * 5000  # 計算を変数に格納
print(total_cost)

A問題は特に言うことないですね。
GPTくんはコストの問題だと思ったようですが、確かに固定費5000円でユニットコスト5000円のものをn個買う、みたいな処理に見えないこともない。
題意は給料をもらう側でした。

B - AtCoderトランプ
s = input()
t = input()

# '@' が置き換え可能な文字のセット
allowed_chars = {"a", "c", "d", "e", "o", "r", "t", "@"}

# 勝利判定
can_win = True
for sc, tc in zip(s, t):  # 文字列を一度に走査
    if sc == "@":
        if tc not in allowed_chars:
            can_win = False
            break
    elif tc == "@":
        if sc not in allowed_chars:
            can_win = False
            break
    elif sc != tc:
        can_win = False
        break

# 結果出力
print("You can win" if can_win else "You will lose")

sc,tcという適当に置いた変数はそのまま放置された。
元はfor文で書いていたところがzipに置き換わったり、勝ち負け判定がT/Fになったりしている。
zipも挙動よくわかってなくて普段使うことがないので、これを機に覚えておきたい。

C - AtCoderプログラミング講座
n, k = map(int, input().split())
m = sorted(map(int, input().split()))  # 入力を受け取って即ソート
average = 0

# 最大値を持つ k 個の要素で平均を計算
for i in range(n - k, n):
    average = (average + m[i]) / 2

print(average)

入力の一発ソート、できるんだ……。
こういう「愚直に書いて困ったことはないだろうけど一文で当たり前にできますよ」みたいな提案がガリガリ出てくるのもGPTくんの良いところ。
「最大値を持つ k 個の要素で平均を計算」ってコメントが付いているが、若干ミスリーディングな気も……?
逐次的に平均を作りに行っているだけで、k個の平均を取っているわけではないので。

D - AtCoder社の冬
import math

MOD = 10**9 + 7

# 組み合わせの計算
def comb(n, k):
    if n < k or k < 0:
        return 0
    return math.factorial(n) // (math.factorial(n - k) * math.factorial(k)) % MOD

# 特定の条件での組み合わせ計算
def comb2(total_cells, desks, lockers):
    if total_cells >= desks + lockers:
        return comb(total_cells, desks) * comb(total_cells - desks, lockers) % MOD
    return 0

# 入力処理
r, c = map(int, input().split())
x, y = map(int, input().split())
d, l = map(int, input().split())

# 部屋の配置可能なパターン数
room_patterns = (r - x + 1) * (c - y + 1) % MOD

# 各配置パターンにおける組み合わせ数
total = comb2(x * y, d, l)
subtract_row = comb2(x * max(0, y - 1), d, l)
subtract_col = comb2(max(0, x - 1) * y, d, l)
subtract_row2 = comb2(x * max(0, y - 2), d, l)
subtract_row_col = comb2(max(0, x - 1) * max(0, y - 1), d, l)
subtract_col2 = comb2(max(0, x - 2) * y, d, l)
add_row_col2 = comb2(max(0, x - 1) * max(0, y - 2), d, l)
add_col_row2 = comb2(max(0, x - 2) * max(0, y - 1), d, l)
add_row2_col2 = comb2(max(0, x - 2) * max(0, y - 2), d, l)

# 配置方法を計算
valid_arrangements = (
    total
    - 2 * (subtract_row + subtract_col)
    + subtract_row2
    + 4 * subtract_row_col
    + subtract_col2
    - 2 * (add_row_col2 + add_col_row2)
    + add_row2_col2
) % MOD

# 結果を出力
print(room_patterns * valid_arrangements % MOD)

なんでこれ「部屋」への「机とラック」の「配置方法」を求めてるってわかるんだろう……?AtCoder学習しているのかな。
当初出力は変数が適当なまま(a1とかa22とかつけてた)だったので「もうちょっと何とかなりません?」と聞いてみたら、row、colを使った変数に書き換えてきた。
subtractは分かる(列や行を「引く」の意でしょう)けど、なぜadd……?

2024-12-30 ABC002

はじめに

前回の続き。

AtCoder Beginner Contest 002

A - 正直者
a, b = map(int, input().split())
print(max(a, b))

二値の大きい方を返すだけなので、特に変わらず。

B - 罠
w = input()
vowels = {"a", "i", "u", "e", "o"}  # 母音を集合で定義
result = "".join(char for char in w if char not in vowels)  # リスト内包表記で簡潔に

print(result)

今回最も学びがあったのはこれ。
元は1文字ずつ読んでいって、その文字が母音集合にいなければ回答文字列に1文字付け足すのを繰り返すプログラムを書いていたが、
一発でリスト内包表記で書くように改善された。リスト内包表記すごく苦手なので勉強になる……。

C - 直訴
a, b, c, d, e, f = map(int, input().split())

# ベクトルの差を計算
p, q = c - a, d - b
r, s = e - a, f - b

# 三角形の面積を計算
area = abs(p * s - q * r) / 2

print(area)

これはヒント通りに単純なプログラム書くだけなので、さほど大きな変更点は無し。

D - 派閥
n, m = map(int, input().split())

# 隣接行列の初期化
adj_matrix = [[0] * n for _ in range(n)]
max_clique_size = 1

# 隣接行列の更新
for _ in range(m):
    x, y = map(int, input().split())
    adj_matrix[x - 1][y - 1] = 1
    adj_matrix[y - 1][x - 1] = 1

# すべての部分集合を探索
for bit_mask in range(1 << n):
    subset = [i for i in range(n) if bit_mask & (1 << i)]  # 部分集合の生成
    is_clique = True

    # 部分集合がクリークか判定
    for i in range(len(subset)):
        for j in range(i + 1, len(subset)):
            if adj_matrix[subset[i]][subset[j]] == 0:
                is_clique = False
                break
        if not is_clique:
            break

    # クリークなら最大サイズを更新
    if is_clique:
        max_clique_size = max(max_clique_size, len(subset))

print(max_clique_size)

やるべきことは「取ってきた頂点集合の全てに枝があるかを隣接行列で判定」ってのを、全頂点集合に対して行うってだけですね。
問題として典型的&元のプログラムがはちゃめちゃの両方が効いたためか、部分集合探索のパートが原型をとどめていない。
「'range(1 << n)’」というのは不勉強で初めて知った。こんな簡単にビット全探索できるんだ……。

2024-12-29 ABC001

コードをGPTに綺麗にしてもらう試み

一時期AtCoderにハマっていた時期があった。土曜日の一定の時間に家に居なければならない制約を嫌って手を引いてしまったが、近年久しぶりに細々と過去問を解きなおしている。
さて、普段プログラムを書く職には就いていないので、こういう機会がないとコードを書くという機会がない。
よって、過去のACしたプログラムを読みに行こうとしても、年単位でブランクがある状態でぐちゃぐちゃに書いたプログラムを読むという気力は正直湧かない。
そこで、ChatGPTを頼って、過去のコードを読みやすくしてもらうようにしてみた。これで、ある程度自分の書いたプログラムのニュアンスは残しつつも、読み解きやすくなるだろう、という試みである。
もしかしたら解説として欠片くらいは有用かもしれないし、綺麗にしたものを順に張り付けていってみようかと思う。
バージョンは4o。また、GPTが吐いたものが正しく動作しないことがあるため、その場合はこちらで手直ししている。

AtCoder Beginner Contest 001

A - 積雪深差
hour1 = int(input())
hour2 = int(input())
print(hour1 - hour2)

GPTくん、なんか時間のことだと思ったらしい。実際の題意は積雪の差を求めるもの。Heightとかの想定なんでしょうか。

B - 視程の通報
m = int(input())

if m <= 5000:
    result = f"{m // 100:02}"
elif m <= 30000:
    result = m // 1000 + 50
elif m <= 70000:
    result = (m // 1000 - 30) // 5 + 80
else:
    result = 89

print(result)

元はif文内に直接print関数を書いていたが、一旦resultにおいて最後に出力する形にされた。また、format関数だったところがf"{value:02}"の形に書き直された。

C - 風力観測
deg, dis = map(int, input().split())

# 風向リスト
directions = ["N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE", "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW", "N"]
dir_index = (deg + 112) // 225
wind_direction = directions[dir_index]

# 風速のしきい値リスト
thresholds = [0, 15, 93, 201, 327, 477, 645, 831, 1029, 1245, 1467, 1707, 1959]
wind_power = next((j-1 for j in range(13) if dis < thresholds[j]), 12)

# 風速が0の場合、風向を "C" に変更
if wind_power == 0:
    wind_direction = "C"

print(wind_direction, wind_power)

風向と風速を適切に変換して出力できますか?という問題。もともとはそれぞれ処理が分かれているので読みやすい。
変数の書き直しやコメントアウトで処理の説明もしてくれているので(そう指示した)、後から見たとき相当読みやすくなるのが嬉しいですね。
ちなみに初期出力はnext関数の中身が「j for j in range(13)~」となっていて1つズレてしまっていたため、手修正した。

D - 感雨時刻の整理
n = int(input())
timeline = [0] * (12 * 24 + 1)

for _ in range(n):
    time_range = input()
    start = time_range[:4]
    end = time_range[5:]

    # 時刻をインデックスに変換
    start_idx = int(start[:2]) * 12 + int(start[2:]) // 5
    end_idx = int(end[:2]) * 12 + int(end[2:]) // 5
    if end[3] in {"0", "5"}:  # 終了時刻調整
        end_idx -= 1

    for i in range(start_idx, end_idx + 1):
        timeline[i] = 1

# タイムラインを走査し、変化点を検出
start_time = 0
for i in range(len(timeline) - 1):
    if timeline[i] == 0 and timeline[i + 1] == 1:
        start_time = i + 1
    if timeline[i] == 1 and timeline[i + 1] == 0:
        end_time = i + 1

        # インデックスを時刻表記に変換
        start_h = f"{start_time // 12:02}"
        start_m = f"{(start_time % 12) * 5:02}"
        end_h = f"{end_time // 12:02}"
        end_m = f"{(end_time % 12) * 5:02}"

        # 結果の出力
        print(f"{start_h}{start_m}-{end_h}{end_m}")

入力時刻を5分ごとのグリッドに分けて雨が降っていればフラグを立てて、全部入力が終わったら00:00から順に読んでいって連続する時間を出力する、という処理を行えばよい。
綺麗にしてくれた後のプログラムを見ればそれがすぐわかるのでこの一文目もすらすら書けたが、元のプログラムは変数も処理もぐちゃぐちゃなのでそう簡単に読み取ることはできなかっただろう。
それにしても何も問題文の条件を与えていないし変数も適当な文字列にしていたのに、どういう問題を解くためのプログラムなのかをある程度読み取れているのがすごいよなあ。
ちなみに、当初出力には変化点検出1行目に「start_time = 0」の記載がなく、入力に0000から始まる記録がある場合に初期化に失敗していたため補記した。