K - 商店街の区画選び / Choosing Blocks in a Shopping Street 解説 by admin
gemini-3.5-flash-thinking概要
本問題は、長さ \(N\) の数列 \(A\)(賃料)と \(C\)(駐車スペースの有無)が与えられたとき、長さ \(K\) の連続する部分列(区画の選び方)について、駐車スペースの数が \(X\) 個以上であるもののうち、賃料の合計の最小値を求める問題です。 ただし、クエリによって途中で賃料 \(A_i\) が変更されるため、動的に値を管理する必要があります。
考察
連続する \(K\) 個の区画の選び方は、左端のインデックスを \(l\) (\(1 \leq l \leq N-K+1\)) とすると、全部で \(M = N-K+1\) 通り存在します。 各選び方 \(l\) について、以下の2つの値を定義します。 - \(P_l\): 駐車スペースの個数(\(C_l + \dots + C_{l+K-1}\)) - \(S_l\): 賃料の合計(\(A_l + \dots + A_{l+K-1}\))
質問クエリでは、「\(P_l \ge X\) を満たす \(l\) のうち、\(S_l\) の最小値」を求める必要があります。 ここで、重要な性質として駐車スペースの有無 \(C_i\) は変更されないため、各選び方 \(l\) に対する \(P_l\) の値は不変(固定)です。 一方で、賃料の変更クエリによって \(A_x\) が \(y\) に変更されると、その区画 \(x\) を含む選び方 \(l\)(すなわち \(x-K+1 \le l \le x\))の \(S_l\) の値がすべて \(y - A_x\) だけ変化します。これは \(S_l\) に対する「区間加算」に相当します。
もし愚直に毎回 \(S_l\) を更新して最小値を求めると、1回のクエリに \(O(N)\) かかり、全体で \(O(QN)\) となって実行時間制限に間に合いません(TLE)。 この「固定されたキー \(P_l\) に対する条件 \(P_l \ge X\)」と「動的に変化する値 \(S_l\) の区間加算・最小値取得」を効率的に処理するために、平方分割(Square Root Decomposition)を適用します。
アルゴリズム
\(M\) 個の選び方をサイズ \(B\) のバケット(グループ)に分割します。バケットの個数は \(D = \lceil M / B \rceil\) 個になります。
1. 初期化
各バケット内で、要素を \(P_l\) の昇順にソートします。
- ソートされたインデックスを sorted_l、対応する駐車スペース数を sorted_P とします。
- さらに、ソートされた順における \(S_l\) の「後ろからの累積最小値(suffix minimum)」を保持する配列 suffix_min を構築します。
- suffix_min[i] = \(\min_{j \ge i} S_{\text{sorted\_l}[j]}\)
- これにより、あるバケットにおいて \(P_l \ge X\) となる最小のインデックス \(idx\) を二分探索で見つければ、そのバケット内での条件を満たす \(S_l\) の最小値は suffix_min[idx] として \(O(1)\) で得られます。
2. 賃料の変更クエリ(\(T_j = 1\))
区画 \(x\) の賃料が \(y\) に変更されたとき、差分 \(d = y - A_x\) を計算します。
影響を受ける \(S_l\) の範囲は連続する区間 \([QL, QR] = [\max(0, x-K), \min(M-1, x-1)]\) (0-indexed) です。
平方分割の一般的な区間更新と同様に処理します。
- 完全に含まれるバケット: バケット全体の加算値を表す変数 add[bid] に \(d\) を加算します。
- 一部だけ含まれるバケット(端のバケット):
- 範囲内の要素 \(l\) に対して、個別に \(S_l \leftarrow S_l + d\) と更新します。
- その後、そのバケットの suffix_min を再構築(rebuild)します。\(P_l\) の順序は変わらないため、ソートし直す必要はなく、累積最小値の再計算だけで済むため \(O(B)\) で行えます。
3. 質問クエリ(\(T_j = 2\))
すべてのバケット(\(D\) 個)に対して以下を行います。
- バケットの sorted_P において、値が \(X\) 以上となる最小のインデックス \(idx\) を二分探索(bisect_left)で求めます。
- \(idx\) がバケットのサイズ未満であれば、そのバケット内での最小値 suffix_min[idx] + add[bid] を候補とし、全体の最小値を更新します。
- すべてのバケットを探索し終えたときの最小値が答えとなります。一度も候補が見つからなかった場合は IMPOSSIBLE を出力します。
計算量
バケットサイズを \(B \approx \sqrt{M}\) とします。
- 時間計算量: \(O(M \log M + Q \sqrt{M} \log M)\)
- 初期化: \(D\) 個のバケットでそれぞれサイズ \(B\) のソートを行うため、全体で \(O(D \cdot B \log B) = O(M \log B)\) です。
- 賃料の変更クエリ: 完全に含まれるバケットの更新に \(O(D)\)、端のバケットの更新と再構築に \(O(B)\) かかるため、1クエリあたり \(O(D + B) = O(\frac{M}{B} + B)\) です。
- 質問クエリ: \(D\) 個のバケットそれぞれで二分探索を行うため、1クエリあたり \(O(D \log B) = O(\frac{M}{B} \log B)\) です。
- \(B \approx \sqrt{M}\) と調整することで、各クエリを約 \(O(\sqrt{M} \log M)\) で高速に処理できます。
- 空間計算量: \(O(N + Q)\)
- 元の配列やバケットごとの情報を保持するために必要なメモリは、要素数 \(N\) およびクエリ数 \(Q\) に対して線形です。
実装のポイント
バケットサイズ \(B\) の選定:
- 理論上は \(B = \sqrt{M}\) 付近が最適ですが、Pythonのオーバーヘッドを考慮すると \(B = 500\) 程度に固定して設定することで、二分探索の回数(バケット数 \(D\))と再構築のコスト(バケットサイズ \(B\))のバランスが最も良く取れ、高速に動作します。
再構築(
rebuild)の高速化:端のバケットを更新する際、バケット内のソート順(
sorted_l)は変わりません。そのため、ソート処理を完全にスキップし、累積最小値の配列suffix_minのみを後ろから走査して更新することで、定数倍を大幅に削減しています。ソースコード
import bisect
import sys
def solve():
input = sys.stdin.read
data = input().split()
if not data:
return
N = int(data[0])
K = int(data[1])
Q = int(data[2])
A = [0] + [int(x) for x in data[3 : 3 + N]]
C = [0] + [int(x) for x in data[3 + N : 3 + 2 * N]]
queries = []
idx = 3 + 2 * N
for _ in range(Q):
t = int(data[idx])
x = int(data[idx + 1])
y = int(data[idx + 2])
queries.append((t, x, y))
idx += 3
pref_C = [0] * (N + 1)
for i in range(1, N + 1):
pref_C[i] = pref_C[i - 1] + C[i]
pref_A = [0] * (N + 1)
for i in range(1, N + 1):
pref_A[i] = pref_A[i - 1] + A[i]
M = N - K + 1
P = [0] * M
S = [0] * M
for l in range(M):
P[l] = pref_C[l + K] - pref_C[l]
S[l] = pref_A[l + K] - pref_A[l]
B = 500
num_buckets = (M + B - 1) // B
bucket_start = [i * B for i in range(num_buckets)]
bucket_end = [min(M, (i + 1) * B) for i in range(num_buckets)]
sorted_l = [[] for _ in range(num_buckets)]
sorted_P = [[] for _ in range(num_buckets)]
suffix_min = [[] for _ in range(num_buckets)]
add = [0] * num_buckets
for bid in range(num_buckets):
start = bucket_start[bid]
end = bucket_end[bid]
l_list = list(range(start, end))
l_list.sort(key=lambda l: P[l])
sorted_l[bid] = l_list
sorted_P[bid] = [P[l] for l in l_list]
sz = end - start
s_min = [0] * sz
if sz > 0:
s_min[-1] = S[l_list[-1]]
for i in range(sz - 2, -1, -1):
val = S[l_list[i]]
nxt = s_min[i + 1]
s_min[i] = val if val < nxt else nxt
suffix_min[bid] = s_min
INF = 10**18
def rebuild(bid, sorted_l_bid, suffix_min_bid, S):
sz = len(sorted_l_bid)
if sz > 0:
suffix_min_bid[-1] = S[sorted_l_bid[-1]]
for i in range(sz - 2, -1, -1):
val = S[sorted_l_bid[i]]
nxt = suffix_min_bid[i + 1]
suffix_min_bid[i] = val if val < nxt else nxt
out = []
bisect_left = bisect.bisect_left
for t, x, y in queries:
if t == 1:
d = y - A[x]
A[x] = y
QL = max(0, x - K)
QR = min(M - 1, x - 1)
if QL <= QR:
bid_L = QL // B
bid_R = QR // B
if bid_L == bid_R:
for l in range(QL, QR + 1):
S[l] += d
rebuild(bid_L, sorted_l[bid_L], suffix_min[bid_L], S)
else:
for l in range(QL, bucket_end[bid_L]):
S[l] += d
rebuild(bid_L, sorted_l[bid_L], suffix_min[bid_L], S)
for bid in range(bid_L + 1, bid_R):
add[bid] += d
for l in range(bucket_start[bid_R], QR + 1):
S[l] += d
rebuild(bid_R, sorted_l[bid_R], suffix_min[bid_R], S)
else:
ans = INF
for bid in range(num_buckets):
s_P = sorted_P[bid]
idx = bisect_left(s_P, x)
if idx < len(s_P):
val = suffix_min[bid][idx] + add[bid]
if val < ans:
ans = val
if ans == INF:
out.append("IMPOSSIBLE")
else:
out.append(str(ans))
print("\n".join(out))
if __name__ == "__main__":
solve()
この解説は gemini-3.5-flash-thinking によって生成されました。
投稿日時:
最終更新: