問題
$N$ 個の整数列 $a_1, \ldots, a_N$(初期値すべて $0$)に対して以下の $Q$ クエリを処理せよ:
1 l r v:$a_l, \ldots, a_r$ に整数 $v$ を加算する2 l r v:$a_l, \ldots, a_r$ の各要素を $\min(a_i, v)$ に更新する(区間 chmin)3 l r:$\sum_{i=l}^{r} a_i$ を出力する
制約
$1 \le N \le 10^5$
$1 \le Q \le 10^5$
$-10^9 \le v \le 10^9$
$1 \le l \le r \le N$
時間制限: 3秒
入出力例
入力例 1
5 5
1 1 5 3
2 2 4 4
3 1 5
1 3 5 2
3 1 5
出力例 1
14
18
概念図: Segment Tree Beats のノード構造
ヒント(段階的開示)
ヒント1: 方向性
区間加算と区間 chmin が混在する問題は通常の遅延セグメント木では対処できません。「Segment Tree Beats(Ji Driver Segmentation)」という拡張手法を考えましょう。各ノードに最大値・最大値カウント・第二最大値・区間和・遅延加算値・遅延chmin値を持たせます。
ヒント2: アプローチ
- 各ノードに保持:
max_val(区間最大値),max_cnt(最大値の個数),second_max(第二最大値),sum_val(区間和),lazy_add(遅延加算),lazy_chmin(遅延chmin、初期値+∞) - 区間 chmin(v): $v \ge \text{max\_val}$ なら変化なし;$v > \text{second\_max}$ なら最大値のみ $v$ に変更 $O(1)$;それ以外は子に分割伝播
- 区間加算: 通常の遅延加算と同様、ただし
lazy_chminにも加算する
ヒント3: 実装骨格
def apply_add(self, i, v, length):
nd = self.node[i]
nd.sum_val += v * length
nd.max_val += v
if nd.second_max != -INF:
nd.second_max += v
nd.lazy_add += v
if nd.lazy_chmin != INF:
nd.lazy_chmin += v
def apply_chmin(self, i, v):
nd = self.node[i]
if v >= nd.max_val:
return
nd.sum_val -= (nd.max_val - v) * nd.max_cnt
nd.max_val = v
nd.lazy_chmin = min(nd.lazy_chmin, v)
模範解答 (Python)
import sys
input = sys.stdin.readline
INF = float('inf')
class Node:
__slots__ = ('max_val','max_cnt','second_max','sum_val','lazy_add','lazy_chmin')
def __init__(self, size=0):
self.max_val = 0
self.max_cnt = size
self.second_max = -INF
self.sum_val = 0
self.lazy_add = 0
self.lazy_chmin = INF
class SegTreeBeats:
def __init__(self, n):
self.n = n
self.size = 1
while self.size < n:
self.size <<= 1
self.node = [Node() for _ in range(2 * self.size)]
for i in range(self.size):
self.node[self.size + i].max_cnt = 1
def push_up(self, i):
l, r = 2*i, 2*i+1
nd = self.node
nd[i].sum_val = nd[l].sum_val + nd[r].sum_val
if nd[l].max_val == nd[r].max_val:
nd[i].max_val = nd[l].max_val
nd[i].max_cnt = nd[l].max_cnt + nd[r].max_cnt
nd[i].second_max = max(nd[l].second_max, nd[r].second_max)
elif nd[l].max_val > nd[r].max_val:
nd[i].max_val = nd[l].max_val
nd[i].max_cnt = nd[l].max_cnt
nd[i].second_max = max(nd[l].second_max, nd[r].max_val)
else:
nd[i].max_val = nd[r].max_val
nd[i].max_cnt = nd[r].max_cnt
nd[i].second_max = max(nd[l].max_val, nd[r].second_max)
def apply_add(self, i, v, length):
nd = self.node[i]
nd.sum_val += v * length
nd.max_val += v
if nd.second_max != -INF:
nd.second_max += v
nd.lazy_add += v
if nd.lazy_chmin != INF:
nd.lazy_chmin += v
def apply_chmin(self, i, v):
nd = self.node[i]
if v >= nd.max_val:
return
nd.sum_val -= (nd.max_val - v) * nd.max_cnt
nd.max_val = v
nd.lazy_chmin = min(nd.lazy_chmin, v)
def push_down(self, i, length):
nd = self.node
half = length >> 1
if nd[i].lazy_add != 0:
self.apply_add(2*i, nd[i].lazy_add, half)
self.apply_add(2*i+1, nd[i].lazy_add, half)
nd[i].lazy_add = 0
if nd[i].lazy_chmin != INF:
self.apply_chmin(2*i, nd[i].lazy_chmin)
self.apply_chmin(2*i+1, nd[i].lazy_chmin)
nd[i].lazy_chmin = INF
def _update_add(self, node, l, r, ql, qr, v, length):
if qr < l or r < ql:
return
if ql <= l and r <= qr:
self.apply_add(node, v, length)
return
self.push_down(node, length)
mid = (l + r) >> 1
self._update_add(2*node, l, mid, ql, qr, v, length >> 1)
self._update_add(2*node+1, mid+1, r, ql, qr, v, length >> 1)
self.push_up(node)
def _update_chmin(self, node, l, r, ql, qr, v, length):
if qr < l or r < ql or v >= self.node[node].max_val:
return
if ql <= l and r <= qr and v > self.node[node].second_max:
self.apply_chmin(node, v)
return
self.push_down(node, length)
mid = (l + r) >> 1
self._update_chmin(2*node, l, mid, ql, qr, v, length >> 1)
self._update_chmin(2*node+1, mid+1, r, ql, qr, v, length >> 1)
self.push_up(node)
def _query_sum(self, node, l, r, ql, qr, length):
if qr < l or r < ql:
return 0
if ql <= l and r <= qr:
return self.node[node].sum_val
self.push_down(node, length)
mid = (l + r) >> 1
return (self._query_sum(2*node, l, mid, ql, qr, length >> 1) +
self._query_sum(2*node+1, mid+1, r, ql, qr, length >> 1))
def add(self, l, r, v):
self._update_add(1, 1, self.size, l, r, v, self.size)
def chmin(self, l, r, v):
self._update_chmin(1, 1, self.size, l, r, v, self.size)
def query(self, l, r):
return self._query_sum(1, 1, self.size, l, r, self.size)
def main():
N, Q = map(int, input().split())
seg = SegTreeBeats(N)
out = []
for _ in range(Q):
q = list(map(int, input().split()))
if q[0] == 1:
seg.add(q[1], q[2], q[3])
elif q[0] == 2:
seg.chmin(q[1], q[2], q[3])
else:
out.append(seg.query(q[1], q[2]))
print('\n'.join(map(str, out)))
main()
Step-by-Step 解説
1Segment Tree Beats の基本思想
通常の遅延セグメント木は「各要素に同一変換を適用する」場合に機能する。区間 chmin は要素により変化量が異なるため直接遅延できない。「第二最大値より大きい chmin クエリは最大値のみを変化させる」性質を利用して $O(1)$ 処理。
通常の遅延セグメント木は「各要素に同一変換を適用する」場合に機能する。区間 chmin は要素により変化量が異なるため直接遅延できない。「第二最大値より大きい chmin クエリは最大値のみを変化させる」性質を利用して $O(1)$ 処理。
2ノードの設計
各ノードに
各ノードに
max_val, max_cnt, second_max, sum_val を保持する。sum_val の変化量は (max_val - v) * max_cnt で計算できる。
3遅延伝播の設計
加算 lazy と chmin lazy を組み合わせる。push_down では加算を先に伝播してから chmin を伝播する順序を守る。
加算 lazy と chmin lazy を組み合わせる。push_down では加算を先に伝播してから chmin を伝播する順序を守る。
4計算量解析
区間 chmin の amortized 計算量は勢位関数解析で $O(N \log^2 N)$。全体 $O(Q \log^2 N)$。
区間 chmin の amortized 計算量は勢位関数解析で $O(N \log^2 N)$。全体 $O(Q \log^2 N)$。
5push_down 時の length 管理
セグメント木の各ノードが管理する区間長を引数で渡す。apply_add では
セグメント木の各ノードが管理する区間長を引数で渡す。apply_add では
length を使い sum を更新する。
計算量
構築: $O(N)$
区間加算: $O(\log N)$ amortized
区間 chmin: $O(\log^2 N)$ amortized
区間和クエリ: $O(\log N)$
全体: $O(Q \log^2 N)$
区間加算: $O(\log N)$ amortized
区間 chmin: $O(\log^2 N)$ amortized
区間和クエリ: $O(\log N)$
全体: $O(Q \log^2 N)$
よくあるミス
| ミス | 原因 | 正しい書き方 |
|---|---|---|
| second_max の初期値が 0 | 全要素 0 の時に chmin が誤動作 | second_max = -INF |
| 加算後に lazy_chmin を更新しない | chmin 値がずれる | lazy_chmin += v も忘れずに |
| push_down の順序を誤る | chmin を先に伝播すると加算後に chmin が正しくない | 加算 → chmin の順で伝播 |
| chmin で second_max < v < max_val の判定ミス | 子に分割せず誤って適用 | v > second_max の条件を厳密に |
次のステップ
- 発展問題: 区間 chmax も加えた「Ji Driver Segmentation」完全版
- 関連: Day035 Q1(Segment Tree Beats 実際更新カウント)の復習
- 応用: 機械学習の特徴量クランプ処理の高速化