D - 荷物運びの道 / The Path of Carrying Luggage 解説 by admin
Gemini 3.0 Flash (Thinking)概要
一列に並んだ倉庫から荷物を運び、合計重量が制限 \(K\) を超えた(または終点に達した)時点で停止するとき、各スタート地点 \(s\) からの停止位置 \(f(s)\) の区間和を求める問題です。
考察
1. \(f(s)\) の性質
高橋君は倉庫 \(s\) から出発し、倉庫 \(s, s+1, \dots, t\) と進んでいきます。停止する条件は以下の通りです。 - \(A_s + A_{s+1} + \dots + A_t > K\) となる最小の \(t\) - 上記を満たす \(t\) が存在しない場合は \(t = N\)
ここで重要なのは、スタート地点 \(s\) が右に移動すれば、停止位置 \(f(s)\) も右に移動(または同じ位置に留まる)するという「単調性」です。 例えば、倉庫 \(1\) から始めて倉庫 \(5\) で重さを超えたなら、倉庫 \(2\) から始めた場合に倉庫 \(5\) より手前で重さを超えることはありません(荷物の重さ \(A_i\) がすべて正であるため)。
2. 素朴な解法と課題
各クエリ \((L_j, R_j)\) に対して、毎回 \(f(L_j), f(L_j+1), \dots, f(R_j)\) を計算し、さらにそれぞれの \(f(s)\) を求めるために倉庫を走査すると、最悪の場合 \(O(Q \times N^2)\) の時間がかかってしまい、実行時間制限に間に合いません。
この問題を解決するために、以下の2段階の高速化を行います。 1. 尺取り法を用いて、すべての \(s\) に対する \(f(s)\) をまとめて \(O(N)\) で計算する。 2. 累積和を用いて、クエリごとの和を \(O(1)\) で計算する。
アルゴリズム
ステップ1:尺取り法による \(f(s)\) の列挙
「合計重量が \(K\) を超えるまで右端 \(t\) を伸ばす」という操作を、左端 \(s\) を \(1\) から \(N\) まで動かしながら行います。 - \(s\) を \(1\) つ進める際、前回の \(t\) の位置から再開できるため、\(t\) が戻ることはありません。 - 区間の和を高速に計算するために、あらかじめ荷物の重さ \(A_i\) の累積和をとっておきます。
ステップ2:累積和によるクエリ回答
すべての \(s\) について \(f(s)\) が求まったら、次は \(f(s)\) 自体の累積和 \(P\) を作成します。 - \(P[i] = f(1) + f(2) + \dots + f(i)\) これを用意しておくことで、任意の区間 \([L, R]\) における \(f(s)\) の総和は \(P[R] - P[L-1]\) として、たった 1 回の計算で求められます。
計算量
- 時間計算量: \(O(N + Q)\)
- 荷物の累積和の計算に \(O(N)\)
- 尺取り法による \(f(s)\) の計算に \(O(N)\) (\(s\) と \(t\) がそれぞれ最大 \(N\) 回しか動かないため)
- \(f(s)\) の累積和の計算に \(O(N)\)
- \(Q\) 個のクエリ回答に \(O(Q)\)
- 空間計算量: \(O(N)\)
- 累積和や配列 \(f(s)\) の格納に \(N\) に比例したメモリを使用します。
実装のポイント
尺取り法の条件: 合計重量が \(K\) 以下である限り \(t\) を進めます。ループを抜けたときの \(t\) が「初めて \(K\) を超えた場所」または「終端」になります。
高速な入出力: \(N\) や \(Q\) が \(2 \times 10^5\) と大きいため、Pythonでは
sys.stdin.read().split()やsys.stdout.write()を使うことで実行時間を短縮できます。インデックスの管理: 累積和を利用する際、配列のサイズを \(N+1\) にして先頭に \(0\) を入れると、範囲和の計算が扱いやすくなります。
ソースコード
import sys
# Increase recursion depth just in case, though the solution is iterative
sys.setrecursionlimit(200005)
def solve():
# Read all input at once for faster processing
try:
input_data = sys.stdin.read().split()
except EOFError:
return
if not input_data:
return
# N: number of warehouses, K: weight limit, Q: number of queries
N = int(input_data[0])
K = int(input_data[1])
Q = int(input_data[2])
# A: weights of the warehouses 1 to N
A = list(map(int, input_data[3:3+N]))
# S: prefix sums of A to calculate range sums in O(1)
# S[i] = A_1 + A_2 + ... + A_i
S = [0] * (N + 1)
for i in range(N):
S[i+1] = S[i] + A[i]
# f[s]: the warehouse where Takahashi stops if he starts at warehouse s.
# Takahashi visits s, s+1, ..., t until:
# 1. The total weight A[s] + ... + A[t] > K, OR
# 2. He reaches the last warehouse (t = N)
f = [0] * (N + 1)
t = 1
for s in range(1, N + 1):
# Ensure t is at least the current warehouse s
if t < s:
t = s
# Use a two-pointer approach: as s increases, the stopping point t also increases.
# We move t forward as long as we haven't reached the end and the cumulative weight is within K.
while t <= N and S[t] - S[s-1] <= K:
t += 1
# If the loop ended because t > N, the sum never exceeded K, so he stopped at warehouse N.
if t > N:
f[s] = N
else:
# If the loop ended because the weight exceeded K, he stopped at warehouse t.
f[s] = t
# P: prefix sums of f to answer range sum queries for f in O(1).
# P[i] = f[1] + f[2] + ... + f[i]
P = [0] * (N + 1)
for i in range(1, N + 1):
P[i] = P[i-1] + f[i]
# Process each of the Q queries.
# Convert query range strings to integers efficiently.
queries = list(map(int, input_data[3+N:]))
results = []
for j in range(Q):
L = queries[2*j]
R = queries[2*j+1]
# The sum f(L) + f(L+1) + ... + f(R) is calculated as P[R] - P[L-1].
results.append(str(P[R] - P[L-1]))
# Output all query results joined by newlines for efficiency.
if results:
sys.stdout.write('\n'.join(results) + '\n')
if __name__ == '__main__':
solve()
この解説は gemini-3-flash-thinking によって生成されました。