C - バッテリー残量 / Battery Remaining Editorial by admin
Gemini 3.0 Flash概要
\(N\) 台のスマートフォンの初期バッテリー残量に対し、指定された範囲 \([L, R]\) の残量を \(1\) 減らす操作を \(Q\) 回行った後の最終的な残量を求める問題です。ただし、残量は \(0\) 未満にはなりません。
考察
素朴なアプローチ
各通知イベントごとに、範囲 \([L, R]\) に含まれるすべてのスマートフォンの値を \(1\) ずつ減らす方法が考えられます。しかし、この方法では \(1\) 回のイベントにつき最大で \(N\) 回の更新が必要になります。 最悪の場合の計算量は \(O(NQ)\) となり、今回の制約(\(N, Q \leq 2 \times 10^5\))では \(4 \times 10^{10}\) 回程度の計算が必要になるため、実行時間制限に間に合いません(TLEとなります)。
効率的なアプローチの検討
この問題の重要なポイントは以下の2点です。 1. 減少量は常に \(1\) である:各イベントで減る量は固定です。 2. 「\(0\) 未満にならない」の処理:各イベントごとに \(0\) かどうかを判定して更新しなくても、最終的な減少回数の合計がわかれば、最後にまとめて「初期値 \(-\) 減少回数」を計算し、結果がマイナスなら \(0\) とすることで正しい答えが得られます。
したがって、この問題は「各スマートフォンが合計で何回の通知イベントに含まれたか」を高速に数え上げる問題に言い換えられます。
いもす法(imos法)の利用
特定の範囲 \([L, R]\) に対して一括で加算(今回は通知回数のカウント)を行う場合、「いもす法(imos法)」というアルゴリズムが非常に有効です。
- 長さ \(N+1\) の差分配列 diff を用意します。
- 範囲 \([L, R]\) に \(1\) を加える操作を、diff[L] += 1 と diff[R+1] -= 1 という 2 点の更新に置き換えます。
- すべてのクエリを処理した後、左から累積和をとることで、各地点の最終的な加算合計(通知回数)が \(O(N+Q)\) で求められます。
アルゴリズム
- 長さ \(N+2\) の配列
diffを \(0\) で初期化します。 - \(Q\) 個の通知イベント \([L_i, R_i]\) について、以下を行います:
diff[L_i] += 1diff[R_i + 1] -= 1
- 変数
current_events(現在の累積和)を \(0\) で初期化し、スマートフォンの番号 \(i = 1\) から \(N\) まで順番に以下を繰り返します:current_events += diff[i]- \(i\) 番目のスマートフォンの最終残量を \(\max(0, S_i - current\_events)\) として計算します。
- 計算した結果をすべて出力します。
計算量
- 時間計算量: \(O(N + Q)\)
- 入力の読み込みに \(O(N + Q)\)、いもす法の加算操作に \(O(Q)\)、累積和の計算と最終的な残量の算出に \(O(N)\) かかります。
- 空間計算量: \(O(N)\)
- 初期残量を保持する配列と、いもす法のための差分配列で \(O(N)\) のメモリを使用します。
実装のポイント
大量の入出力: Python の場合、
input()やprint()を繰り返すと時間がかかるため、sys.stdin.read().split()で一括で読み込み、sys.stdout.write()で一括で出力することで高速化を図っています。インデックスの扱い: 問題文は \(1\)-indexed(1番目から始まる)ですが、プログラム内での配列の扱いや範囲外参照(
R+1)に注意して実装する必要があります。ソースコード
import sys
def main():
# 標準入力からすべてのデータを取得し、スペース区切りで分割します
# 大量の入力を処理するため、sys.stdin.read().split() を使用します
input_data = sys.stdin.read().split()
if not input_data:
return
N = int(input_data[0])
Q = int(input_data[1])
# スマートフォンの初期バッテリー残量
S = list(map(int, input_data[2:N+2]))
# いもす法(imos法)を用いて、各スマートフォンが何回の通知イベントに含まれるかを計算します
# diff配列は、各インデックスでの変化量を記録します
diff = [0] * (N + 2)
# 通知イベントの読み込みと差分配列の更新
idx = N + 2
for _ in range(Q):
L = int(input_data[idx])
R = int(input_data[idx+1])
diff[L] += 1
diff[R+1] -= 1
idx += 2
# 各スマートフォンの最終的なバッテリー残量を計算します
# 各イベントで減少する量は 1 であり、0 未満にはならないため、
# 最終的な減少量は「カバーされたイベントの総数」と「初期残量」の小さい方になります
ans = [0] * N
current_events = 0
for i in range(N):
# 累積和をとることで、i番目のスマートフォンがカバーされた回数を求めます
current_events += diff[i+1]
# 残量 = 初期値 - 通知回数(ただし 0 未満にはならない)
res = S[i] - current_events
if res < 0:
res = 0
ans[i] = res
# 結果をスペース区切りで出力します
sys.stdout.write(" ".join(map(str, ans)) + "\n")
if __name__ == "__main__":
main()
この解説は gemini-3-flash-preview によって生成されました。