區間資結的應用
建國中學 賴昭勳
區間詢問,區間修改
之前介紹了懶標(lazy tag),說了這句話
懶標分成兩種:
- 不用下推操作的
\(seg[cur]\)代表當前節點經過子節點修改後的答案
- 需要下推操作的
該節點的懶標對\(seg[cur]\)做事,遇到的時候再處理。
這份講義會討論第二種
用例題來解釋吧!
給你一個長度為\(n\)的序列,維護以下操作:
1. 把區間\([l, r]\)的數字全部改成\(x\)
2. 詢問\([l, r]\)的總和
\(n, q \leq 2 * 10^5\)
之前的做法是不是不太可行了?
假設這兩個地方都有標記...
5
8
標記的先後會互相影響!
把懶標下推(push)
5
5
5
5, 5, 5, 5, 5...
- 用懶標上的東西更新當前節點
- 更新左右子樹上的懶標
- 清空當前節點上的 tag
上拉資訊(pull)
X
4
\(seg[cur]\)紀錄的是「左右子節點更新完答案之後的結果」
合併時考慮左右節點與他們的懶標
2, 1, 4, 7
4, 4, 4, 4
相加
重新看看modify在幹嘛
#include <iostream>
#include <algorithm>
#define maxn 100005
using namespace std;
int seg[4 * maxn], lazy[4 * maxn];
void modify(....) {
if (r <= l || ql >= r || qr <= l) return;
if (ql <= l && qr >= r) {
PUT_TAG;
return;
}
int mid = (l + r) / 2;
modify(LEFT_CHILD);
modify(RIGHT_CHILD);
PULL seg[cur];
}
應該在哪裡加上push操作?
PUSH(cur);
PUSH(LEFT);
PUSH(RIGHT);
重新看看modify在幹嘛
#include <iostream>
#include <algorithm>
#define maxn 100005
using namespace std;
int seg[4 * maxn], lazy[4 * maxn];
void modify(....) {
if (r <= l || ql >= r || qr <= l) return;
if (ql <= l && qr >= r) {
PUT_TAG;
return;
}
int mid = (l + r) / 2;
modify(LEFT_CHILD);
modify(RIGHT_CHILD);
PULL seg[cur];
}
應該在哪裡加上push操作?
PUSH(cur);
PUSH(LEFT);
PUSH(RIGHT);
重新看一下query
int query(....) {
if (r <= l || ql >= r || qr <= l) return 0;
if (ql <= l && qr >= r) {
return seg[cur];
}
int mid = (l + r) / 2;
return COMBINE(query(LEFT) + query(RIGHT));
}
push(cur);
應該在哪裡加上push操作?
重新看一下query
int query(....) {
if (r <= l || ql >= r || qr <= l) return 0;
if (ql <= l && qr >= r) {
return seg[cur];
}
int mid = (l + r) / 2;
return COMBINE(query(LEFT) + query(RIGHT));
}
push(cur);
應該在哪裡加上push操作?
回頭看一下複雜度
整體執行次數和原本的做法一樣!每次push, pull 只有常數次
修改,詢問皆為\(O(logn)\)
push 的實作細節
注意:如果當前節點是葉節點的話,不要把懶標下推,要不然會有各種怪事發生
void push(int cur, int l, int r) { //[l, r)
seg[cur] = (r - l) * tag[cur];
if (r - l > 1) {
tag[cur * 2] = tag[cur];
tag[cur * 2 + 1] = tag[cur];
}
tag[cur] = 0;
}
還有更複雜的
剛剛的問題,再加上區間加值操作!
給你一個長度為\(n\)的序列,維護以下操作:
1. 把區間\([l, r]\)的數字全部改成\(x\)
2. 把區間\([l, r]\)的數字加上\(x\)
3. 詢問\([l, r]\)的總和
這也太難寫...
紀錄兩種不同的懶標,同時在push的時候處理兩個操作就好了啊!
但是在push裡面要怎麼寫...
修改操作的優先序
如果在同一個區間,我之前有加值的標記,現在新增一個改值操作,那改值操作會把加值覆蓋掉
因此,實作的時候要保證:如果一個節點同時有加值和改值標記,那一定代表是先改值後加值。
寫push 的時候:
- 先處理改值,並在下推標記時把左右節點的加值tag清空
- 正常處理加值操作
困難練習題..IOI 2014 - Wall
這邊的東西在之後的題目可能會用到
給你一個正整數序列\(A\),支援兩種操作
1. 把區間\([l, r]\)的數字\(a_i\)全部改成\(min(a_i, x)\)
2. 把區間\([l, r]\)的數字\(a_i\)全部改成\(max(a_i, x)\)
最後請輸出整個序列
\(n \leq 2 \times 10^6, q \leq 5 \times 10^5\)
線段樹+特殊的合併操作
線段樹不是只有加加減減最大最小這麼死板的..
所有有某種結合律的函數都可以套線段樹!
區間最大連續和
給你一個序列,支援兩種操作:
1. 單點修改某個元素的值
2. 詢問區間\([l, r]\)的最大連續和
用分治的角度看區間連續和
現在有兩個相鄰的區間,你需要知道這些區間的什麼資訊才能找到最大連續和?
有三種可能:
- 答案在左半
- 答案在右半
- 答案橫跨左右邊
不能只存一個數字
對於每個區間,我們需要知道:
- 最大連續和
- 最大前綴和
- 最大後綴和
- (整個區間的和)
答案會是:\(\max(left.all, right.all, left.suf + right.pref)\)
給你一個序列,支援兩種操作:
1. 把\([l, r]\)的值加上\(k\)
2. 詢問區間\([l, r]\)的最大公因數
\(n, q \leq 10^5\)
給你一個序列,支援兩種操作:
1. 把\(a_i\)加上\(k\)
2. 詢問整個序列,選取任意多個互不相鄰元素的最大和
\(n \leq 40000, \ q \leq 50000\)
線段樹+二分搜
假設你想要做這樣的事...
給你一個序列,支援兩種操作:
1. 單點改值
2. 詢問總和\(\leq x\)的最長前綴長度(aka對前綴取lower_bound)
\(n, q \leq 5 \times 10^5, a_i \geq 0\)
可以二分搜!
對位置(答案)二分搜
每次詢問一個位置,看那個位置的前綴和是否超過 \(O(logn)\)
總複雜度:\(O(n + qlog^2n)\)
可不可以更好?
利用線段樹的特質
如果左半的總和超過\(x\),答案就在左半,否則就在右半。
Query: 30
Sum: 13
Query: 17
Sum: 20
你要從(0, 0)走到(n-1, n-1),只能往上或往右。每一橫排都有一個區間,走過那個區間就會得到一分(但同一個區間走兩次只有一分)。求最大得分 。
\(n \leq 8 \times 10^5\)
Hint: 先想想\(O(n^2)\)的DP解...
掃描線技巧
還記得矩形覆蓋面積的做法嗎?
大致想法:把資料的兩個維度畫在空間平面上。枚舉一個維度,並用線段樹解決掉另一個維度。
不管是原本的資料,修改,或是詢問都可以對應到空間上的點、線、或平面!
也就是說,往往是離線處理詢問
給你一個兩個1~n的排列\(a, b\),有\(q\)筆詢問。每次詢問\(a[l_1...r_1]\)和\(b[l_2...r_2]\)的組成元素是否相同
\(n, q \leq 10^5\)
IZHO Sortbooks
動態開點
當值域很大,然後無法壓縮的時候...
直接開整個範圍的線段樹會MLE
但我們需要用到所有的節點嗎?
動態開點的想法
每次修改時只會動到\(O(logn)\)個節點,那我要修改節點的時候再新增就好了
動態開點的實作
改寫指標型線段樹。在遞迴下去之前特判該節點是不是Null。
畈勵:No Judge
給你一個序列,支援兩種操作:
1. 單點改值
2. 給你一個\(x\),詢問\(max(a_i \oplus x)\)
\(n, q \leq 2 \times 10^5, 0 \leq a_i \leq 10^9\)
謝謝大家
祝各位魔境加油 ;P
區間資結的應用 (資讀)
By justinlai2003
區間資結的應用 (資讀)
- 1,605