問題
$N$個の貯水池が横一列に並んでいる。$T$回の降雨イベントが時刻$1,\dots,T$の順に発生し、時刻$t$のイベントは区間$[l_t,r_t)$の貯水池すべてに$x_t$だけ雨水を加える($x_t>0$)。
$Q$個の質問(貯水池$p_i$、しきい値$X_i$)それぞれについて、貯水池$p_i$の累積雨量が初めて$X_i$以上になる時刻を求めよ(届かなければ$-1$)。
入力形式
N T
l_1 r_1 x_1
...
l_T r_T x_T
Q
p_1 X_1
...
p_Q X_Q
制約
$1 \le N,T,Q \le 2\times10^3$
$0 \le l_t < r_t \le N$
$1 \le x_t \le 10$
$1 \le X_i \le 5000$
入出力例
入力例1
6 5
0 3 4
2 6 3
0 2 5
1 5 2
0 6 1
4
0 9
2 7
5 3
3 100出力例1
3
2
2
-1概念図:時刻軸を1ラウンドにつき1回だけ舐める
ヒント(段階的開示)
ヒント1(方向性)
質問ごとに素朴に「時刻について二分探索し、そのたびに時刻1からすべて再生」すると$O(QT\log T)$になり重い。二分探索の「時刻軸を舐め直す」部分を質問間で共有できないか考える。
ヒント2(アプローチ)
「答えの条件が時刻について単調」なとき、複数の質問の二分探索を同時に進められる。各質問は探索区間$[\mathrm{lo}_i,\mathrm{hi}_i]$を持ち、中間点$\mathrm{mid}_i$を知りたいだけ。時刻を$1$から$T$まで1回だけ舐めながら、通過中の時刻がどれかの質問の中間点と一致したらその質問だけ判定する——これを$\log T$ラウンド繰り返す。
ヒント3(誘導)
# 1ラウンドの処理
# 1. mid[i] = (lo[i]+hi[i])//2 を計算し mid の値でバケツにまとめる
# 2. 空のBITから時刻1..Tまで進め、イベント反映しつつ、
# tがバケツに入っていればそのバケツの質問すべてを判定してlo/hiを更新
模範解答 (Python)
import sys
class BIT:
def __init__(self, n):
self.n = n
self.tree = [0] * (n + 1)
def add(self, i, x):
i += 1
while i <= self.n:
self.tree[i] += x
i += i & (-i)
def prefix_sum(self, i):
i += 1
s = 0
while i > 0:
s += self.tree[i]
i -= i & (-i)
return s
class DiffBIT:
def __init__(self, n):
self.n = n
self.b1 = BIT(n)
self.b2 = BIT(n)
def range_add(self, l, r, x):
self.b1.add(l, x); self.b1.add(r, -x)
self.b2.add(l, x * l); self.b2.add(r, -x * r)
def prefix_sum(self, i):
return (i + 1) * self.b1.prefix_sum(i) - self.b2.prefix_sum(i)
def point_value(self, i):
return self.prefix_sum(i) - (self.prefix_sum(i - 1) if i > 0 else 0)
def solve():
data = sys.stdin.read().split()
idx = 0
n = int(data[idx]); idx += 1
T = int(data[idx]); idx += 1
events = {}
for t in range(1, T + 1):
l, r, x = int(data[idx]), int(data[idx+1]), int(data[idx+2]); idx += 3
events.setdefault(t, []).append((l, r, x))
Q = int(data[idx]); idx += 1
queries = []
for _ in range(Q):
p, X = int(data[idx]), int(data[idx+1]); idx += 2
queries.append((p, X))
lo = [1] * Q
hi = [T + 1] * Q
rounds = T.bit_length() + 2
for _ in range(rounds):
if all(lo[i] >= hi[i] for i in range(Q)):
break
buckets = {}
mid_of = [None] * Q
any_active = False
for i in range(Q):
if lo[i] < hi[i]:
any_active = True
mid = (lo[i] + hi[i]) // 2
mid_of[i] = mid
buckets.setdefault(mid, []).append(i)
if not any_active:
break
bit = DiffBIT(n)
for t in range(1, T + 1):
for (l, r, x) in events.get(t, []):
bit.range_add(l, r, x)
if t in buckets:
for i in buckets[t]:
p, X = queries[i]
if bit.point_value(p) >= X:
hi[i] = mid_of[i]
else:
lo[i] = mid_of[i] + 1
out = []
for i in range(Q):
out.append(str(lo[i]) if lo[i] <= T else "-1")
print('\n'.join(out))
solve()
Step-by-Step 解説
1単調性が前提
雨水しか加わらないため「累積値が$X$以上」は一度真になったらずっと真という単調性を持つ。この性質がないと二分探索が成立しない。
雨水しか加わらないため「累積値が$X$以上」は一度真になったらずっと真という単調性を持つ。この性質がないと二分探索が成立しない。
2質問ごと→時刻ごとへの転置
「時刻軸を舐める」作業をラウンドあたり1回にまとめ、その中で全質問の「今知りたい中間点」を一度に処理する。
「時刻軸を舐める」作業をラウンドあたり1回にまとめ、その中で全質問の「今知りたい中間点」を一度に処理する。
3バケツ分けの役割
同じ$\mathrm{mid}$を持つ質問をまとめておき、時刻$t$を通過するときに「$t$をちょうど知りたがっている質問」だけを判定する。
同じ$\mathrm{mid}$を持つ質問をまとめておき、時刻$t$を通過するときに「$t$をちょうど知りたがっている質問」だけを判定する。
4実測(本ページ作成時にPythonで実行)
| テスト内容 | 結果 |
|---|---|
| 入力例1 | 並列BS [3,2,2,-1] / 逐次再生 [3,2,2,-1] 一致 |
| ランダム150件 | 逐次再生との不一致 0件 |
| N=T=Q=2000 | 並列BS 0.099s / 逐次再生 12.612s(約127倍) |
迷った他案と採らなかった理由
| 案 | 採否 | 理由 |
|---|---|---|
| 質問ごとに逐次で二分探索(毎回時刻を再生) | 不採用 | $O(QT\log T)$。$N=T=Q=2000$で実測12.6秒、実用的でない |
| 並列二分探索 + Union-Find(Day028方式) | 不採用(今回は別対象) | 本問は「区間加算・点参照」の集計が必要で、Union-Findでは表現できない |
| 並列二分探索 + 差分BIT(本ページ採用) | 採用 | 区間加算・点参照が$O(\log N)$で行え、単調性のある閾値クエリと相性が良い |
よくあるミス
| ミス | 原因 | 正しい書き方 |
|---|---|---|
| ラウンドごとにBITを使い回す | 「前のラウンドの状態を引き継げば速そう」という誤解 | 各ラウンドの最初に必ず新しいBITを作り直す |
hiの初期値をTにする | 「見つからなかった」を表現できない | 番兵としてhi=T+1を使う |
| 同じmidの質問を1つずつ処理 | バケツ化を怠る | midでバケツ分けし、その場でまとめて処理 |
| 終了判定を怠り無限ループ | 収束した質問も毎回中間点を計算 | lo[i]<hi[i]のときだけ計算し早期終了する |
次のステップ
- 発展: イベントに負の量(蒸発)を許すと単調性が崩れる具体例を作る
- 発展2: BITの代わりにセグメント木(区間加算・区間最小値)に載せ替える
- 扱わなかった範囲: 負のイベントを含む一般化
- 次回予告: 並列二分探索 + Segment Tree Beats の組み合わせ