0. どんな問題?(問題の概要)
カラオケの得点計算を行い、参加者の中で最も高い得点を出力する問題です。
課題曲の正しい音程(Hz)と、参加者 $N$ 人が歌った各小節の音程が与えられます。
- 採点方法:100点満点からの減点方式(最低0点)
-
減点ルール(正しい音程と実際の音程の差の絶対値):
- 5 Hz 以内:減点なし
- 10 Hz 以内:1点減点
- 20 Hz 以内:2点減点
- 30 Hz 以内:3点減点
- 30 Hz 超:5点減点
1. 最初のアプローチとつまずいたポイント
自分でもわかってはいたのですが、エラーが出ないようにその場でエラーを修正するためのコードをごちゃごちゃ足していくだけのコードになってしまいました。
問題文からやるべきことを把握するのは比較的上手になったかと思いますが、高速で正確になるようにコードを洗練するのが下手すぎるなと自覚しました。
N, M = map(int, input().split())
pitch = [int(input()) for _ in range(M)]
#正しい基準となる音を入れるための配列です
sample = []
for i in range(N):
sample.append([int(input()) for _ in range(M)])
#歌った人たちの音の配列です
scores = [100 for _ in range(N)]
for k in range(N):
for l in range(M):
if abs(pitch[l] - sample[k][l]) <= 5:
scores[k] -= 0
elif abs(pitch[l] - sample[k][l]) <= 10:
scores[k] -= 1
elif abs(pitch[l] - sample[k][l]) <= 20:
scores[k] -= 2
elif abs(pitch[l] - sample[k][l]) <= 30:
scores[k] -= 3
else:
scores[k] -= 5
#基準と比較していき、scoresから減点方式で採点します
for p in range(N):
if (scores[p] < 0):
scores[p] = 0
#scoresの値がマイナスとならないようにしました
print(max(scores))
2. 発想
正解の数値を出すロジック自体は組めていましたが、コードの記述量と実行効率を見直し、以下のように改善を目指しました。
-
forループの多用を見直す
最初は入力の受け取り、減点計算、0点補正、最大値の選出までをすべて別々のforループで処理していましたが、組み込み関数や内包表記を使うことで大幅にコード量を削れます -
zip()関数とスライスの活用
正しい音程のリストと参加者の音程リストをzip()でペアにすることで、インデックス(iやj)を管理する二重ループを排除できます -
一括入力と即時計算(高速・省メモリ)
sys.stdin.read().split()で全データを一括取得し、1人分ずつスライスで切り出して処理することで、無駄な二重配列を作らずに高速処理できます
3. 解答コード(素直な実装版)
基本に忠実に、すべての処理を for ループと標準の input() で1ステップずつ記述した実装です。
# 1. 人数 N と小節数 M を取得
N, M = map(int, input().split())
# 2. 課題曲の正しい音程をリストに1つずつ追加
pitch = []
for i in range(M):
pitch.append(int(input()))
# 3. N人分の歌唱音程を取得(2次元配列を作成)
sample = []
for i in range(N):
person_pitch = []
for j in range(M):
person_pitch.append(int(input()))
sample.append(person_pitch)
# 4. 全員の初期スコア(100点スタート)を用意
scores = []
for i in range(N):
scores.append(100)
# 5. 二重 loop で全員の各小節の減点を計算
for k in range(N):
for l in range(M):
# 正しい音程と歌った音程の誤差(絶対値)
diff = abs(pitch[l] - sample[k][l])
if diff <= 5:
scores[k] -= 0
elif diff <= 10:
scores[k] -= 1
elif diff <= 20:
scores[k] -= 2
elif diff <= 30:
scores[k] -= 3
else:
scores[k] -= 5
# 6. マイナス点になった人を 0 点に補正
for p in range(N):
if scores[p] < 0:
scores[p] = 0
# 7. for ループで最高得点を探索
max_val = 0
for score in scores:
if score > max_val:
max_val = score
print(max_val)
4. さらに洗練されたコード
Pythonの組み込み機能(zip, sum, max)とスライスを活用し、ロジックをシンプルかつ短くまとめたコードです。
import sys
def get_penalty(diff):
"""音程の誤差(Hz)に応じた減点数を返す"""
if diff <= 5:
return 0
if diff <= 10:
return 1
if diff <= 20:
return 2
if diff <= 30:
return 3
return 5
def solve():
# 入力をすべて一括で読み込み、数値のリストに変換(高速化)
data = list(map(int, sys.stdin.read().split()))
if not data:
return
N, M = data[0], data[1]
correct = data[2 : 2 + M] # 課題曲の正解音程リスト
max_score = 0
# スライスを使って1人分(M個)ずつ音程データを切り出して計算
for i in range(N):
user = data[2 + M + i * M : 2 + M + (i + 1) * M]
# zip(correct, user) で正解と歌唱の音程をペアにし、減点合計を sum() で一括計算
penalty = sum(get_penalty(abs(c - u)) for c, u in zip(correct, user))
# 100点から減算し、max(0, ...) で0点未満を防止
score = max(0, 100 - penalty)
# 最高得点を更新
max_score = max(max_score, score)
print(max_score)
if __name__ == '__main__':
solve()
5. 学んだこと・まとめ
-
ステップを意識したリファクタリング:最初は for ループで手順をそのままコードに起こし、後から「このループはまとめられるか?」「組み込み関数で代用できるか?」と見直す流れが有効だと実感しました。
-
zip() と内包表記の強力さ:複数のリストを要素ごとに比較する際、インデックス変数を使った二重ループを書かずに zip() や sum() を使うと、コード量が劇的に減り読みやすくなります。
-
入力と計算の効率化:データを無駄に保持せず、その場で計算して max() で比較していく手法は、コードの短さだけでなく実行速度やメモリ節約にも直結することを学びました。