題目的難度顏色使用 Luogu 上的分級,由簡單到困難分別為 🔴🟠🟡🟢🔵🟣⚫。
Problem Statement
題目簡述
在數線上給定 N 個區間,一開始第 i 個區間為 [0,Wi],且 W1<W2<⋯<WN。
給定 Q 次操作,有三種類型:
1 v:將編號 1∼v 的所有區間平移,使其左端點對齊目前第 v 個區間的左端點。
2 v:將編號 1∼v 的所有區間平移,使其右端點對齊目前第 v 個區間的右端點。
3 x:詢問目前共有多少個區間包含座標點 x+21。
Constraints
約束條件
- 1≤N,Q≤2×105
- 1≤Wi≤109
- W1<W2<⋯<WN
- 1≤v≤N
- 0≤x≤109
思路:Lazy Segment Tree
這題真正的核心不是一開始就想到 Lazy SegTree,而是需要先發覺一個非常強的結構性質:任何時刻都會有
[L1,R1]⊆[L2,R2]⊆⋯⊆[LN,RN]
也就是編號越大,區間越大,永遠是一層包一層。
1. 為什麼一直保持巢狀?
- 初始狀態:一開始都是左端點 0,而且 Wi 嚴格遞增,所以顯然 [0,W1]⊆[0,W2]⊆⋯⊆[0,WN]。
- 操作 1(左端點對齊):把前綴 (1∼v) 的左端點全部改成同一個 Lv。
- 前綴變成:[Lv,Lv+W1],[Lv,Lv+W2],…,[Lv,Lv+Wv]
- 因為長度 Wi 遞增,所以右端點也遞增,前綴內部仍然是巢狀。
- 而第 v 條區間本身沒變,原本就是被第 v+1 條包含,所以整體巢狀不變。
- 操作 2(右端點對齊):把前綴 (1∼v) 的右端點全部改成同一個 Rv。
- 前綴變成:[Rv−W1,Rv],[Rv−W2,Rv],…,[Rv−Wv,Rv]
- 因為 Wi 遞增,左端點 Rv−Wi 會越來越小,所以一樣是巢狀。
2. 巢狀性質如何簡化詢問 3?
若所有區間都是巢狀,對任意一個查詢點 p=x+21:
- 前面小區間可能不包含它
- 一旦某個區間開始包含,後面所有更大的區間一定都包含
包含 x+21 的區間分佈形如 0, 0, 0, 1, 1, 1, 1。因此,問題退化成:找到第一條「包含 x+21」的區間位置 r,那麼答案就是 N−r。
另外,因為區間端點都是整數,包含判斷可以完全免除浮點數:
- L≤x+21≤R⟺L≤x 且 R≥x+1
- 反之,「不包含」就是:L>x 或 R<x+1。
3. 線段樹的節點與延遲標記設計
理解了上面性質後,我們只需要用資料結構來處理這兩種「區間端點修改」的操作,因為只需查詢前綴/後綴屬性,用 Lazy Segment Tree 再適合不過。
一個區間節點所代表的 prefix,如果它最右邊那一條區間都不包含點,那整個 prefix 肯定全不包含(巢狀);如果最右邊那一條已經包含了,就表示邊界出現了。所以對於任意一個連續區段,只要存該段「最右邊的那個元素」的資訊即可。
於是,節點狀態宣告為 (sz, lr, wr):
sz:這段合併的區間數量。
lr:這段區間中,最右端那個元素的左端點。
wr:這段區間中,最右端那個元素的固定長度 W。
op 的合併邏輯:
因為我們只關注最右邊元素的資訊,合併兩段(左段 a、右段 b)時,直接取右段的最右邊資訊,並將長度相加:(sz_a + sz_b, lr_b, wr_b)。
F 標記的設計:
雖然操作 1 和 2 是對齊左/右端點,但若都轉換為「這條區間的左端點變成多少」,邏輯會更好統一:
(mode=1, c):左對齊。所有左端點直接賦值為 c。也就是 L←c。
(mode=2, c):右對齊。所有右端點對齊為 c,等價於左端點 L←c−W。
mapping (套用標記):
當把標記套用至狀態時,由於我們只記錄了最右邊的元素,且該元素長度為 wr:
- 若
mode == 1:回傳新的左端點 c,總長度 wr 不變。
- 若
mode == 2:回傳新的左端點 c−wr,總長度 wr 不變。
composition (標記合成):
這兩種更新本質都是「直接賦值整段重新計算」,不是加法或疊加,所以如果有新操作 f,它會直接覆蓋舊操作 g。
4. 透過 max_right 找出答案
查詢 3 x 給定點 x+21,我們可以套用 ACL 線段樹內建的 seg.max_right(0, check)。
這會找最大的 r,使得前綴 [0,r) 皆滿足 check 條件。我們的條件是:這條區間不包含 x+21。
1 2 3
| def check(s): _, lr, wr = s return not (lr <= x and x + 1 <= lr + wr)
|
這樣 max_right 回傳的 r,它的意義就是第一條包含目標點的區間下標。答案即為後綴長度 N−r。
(這正對應程式中的 not (lr <= x and x + 1 <= lr + wr) 即為 lr > x or lr + wr < x + 1。)
複雜度分析
- 時間複雜度:
- 初始化線段樹 O(N)
- 每次操作:修改前綴區間或使用
max_right 二分搜皆為 O(logN)
- 總時間複雜度 O((N+Q)logN)
- 空間複雜度:線段樹所需的節點陣列大小 O(N)
Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
| from atcoder.lazysegtree import LazySegTree
def solve(): n = int(input()) W = list(map(int, input().split()))
e = (0, 0, 0)
def op(a, b): sz_a, lr_a, wr_a = a sz_b, lr_b, wr_b = b if sz_b == 0: return a return (sz_a + sz_b, lr_b, wr_b)
id_ = (0, 0)
def mapping(f, s): mode, c = f sz, lr, wr = s if sz == 0 or mode == 0: return s return (sz, c if mode == 1 else c - wr, wr)
def composition(f, g): return f if f[0] else g
data = [(1, 0, w) for w in W] seg = LazySegTree(op, e, mapping, composition, id_, data)
ans = []
q = int(input()) for _ in range(q): t, *args = map(int, input().split())
if t == 1: v = args[0] - 1 lv = seg.get(v)[1] seg.apply(0, v + 1, (1, lv)) elif t == 2: v = args[0] - 1 lv = seg.get(v)[1] rv = lv + W[v] seg.apply(0, v + 1, (2, rv)) else: x = args[0]
def check(s): _, lr, wr = s return not (lr <= x and x + 1 <= lr + wr)
r = seg.max_right(0, check) ans.append(n - r)
print(*ans, sep="\n")
if __name__ == "__main__": solve()
|