資料結構
20230922 校隊培訓簡報
高翊恩
關於講師
- OJ handle: Pring, PringDeSu
- 左閉右開教
- 每次校內複賽都燒雞
資料結構 preview
什麼是資料結構?
- 給定一正整數陣列\(a[1..n]\),接下來有\(q\)筆詢問,每筆詢問會有兩個數字\(l,r\)
- 對於每筆詢問,輸出\(\\a[l]+a[l+1]+a[l+2]+...+a[r-1]+a[r]\)
- \(n,q\leq10^5\)
- \(1\leq l,r\leq n\)
- \(a[i]\leq1000\)
法一:暴力
- 對於每筆詢問,用一個迴圈暴力掃過去
...
while (q--) {
cin >> l >> r;
int sum = 0;
for (int i = l; i <= r; i++) {
sum += a[i];
}
cout << sum << endl;
}
...
複雜度評估
...
while (q--) {
cin >> l >> r;
int sum = 0;
for (int i = l; i <= r; i++) {
sum += a[i];
}
cout << sum << endl;
}
...
複雜度評估
...
while (q--) {
cin >> l >> r;
int sum = 0;
for (int i = l; i <= r; i++) {
sum += a[i];
}
cout << sum << endl;
}
...
\(\Omicron(nq)\)
法二:前綴和
構造一個新陣列\(p[0..n]\),使得
\(p[0]=0\)
\(p[1]=a[1]\)
\(p[2]=a[1]+a[2]\)
\(p[3]=a[1]+a[2]+a[3]\)
...
\(p[k]=a[1]+a[2]+\dots+a[k-1]+a[k]\)
...
\(p[n]=a[1]+a[2]+a[3]+\dots+a[n-1]+a[n]\)
\(p[0]=0\)
\(p[1]=a[1]\)
\(p[2]=a[1]+a[2]\)
\(p[3]=a[1]+a[2]+a[3]\)
...
\(p[k]=a[1]+a[2]+\dots+a[k-1]+a[k]\)
...
\(p[n]=a[1]+a[2]+a[3]+\dots+a[n-1]+a[n]\)
這樣可以做什麼?
\(p[0]=0\)
\(p[1]=a[1]\)
\(p[2]=a[1]+a[2]\)
\(p[3]=a[1]+a[2]+a[3]\)
...
\(p[k]=a[1]+a[2]+\dots+a[k-1]+a[k]\)
...
\(p[n]=a[1]+a[2]+a[3]+\dots+a[n-1]+a[n]\)
這樣可以做什麼?
可以發現對於一筆詢問\((l,r)\)的解就是
\(p[r]-p[l-1]\)!
如何蓋 \(p\) 陣列?
...
p[0] = 0;
for (int i = 1; i <= n; i++) {
p[i] = p[i - 1] + a[i];
}
...
如何蓋 \(p\) 陣列?
...
p[0] = 0;
for (int i = 1; i <= n; i++) {
p[i] = p[i - 1] + a[i];
}
...
\(\Omicron(n)\)
如何蓋 \(p\) 陣列?
...
p[0] = 0;
for (int i = 1; i <= n; i++) {
p[i] = p[i - 1] + a[i];
}
...
最終複雜度:\(\Omicron(n+q)\)
資料結構
- 使用特定的方式儲存(大量)資料的結構
- 特定的資料結構可以「支援」某些特定的操作
- 特定的資料結構可以「維護」資料的某些特性
「支援」
請支援收銀- 在此資料結構的幫助下,特定操作會變得更簡單(複雜度變小)
\(a\)
\(p\)
- 支援「區間查詢總和」
- 支援「單點修改」
「維護」
- 透過資料結構的運作方式,可以保留住元素之間的某些性質
\(a\)
\(p\)
- 維護「各元素之間的獨立性」
資料結構基礎篇
STL
vector
array
set
stack
deque
queue
multiset
priority_queue
map
Pair
pair<int, int> p;
pair<int, int>(int x, int y) | 初始化p | O(1) |
int first; | pair的第一項 | O(1) |
int second; | pair的第二項 | O(1) |
pair<type_A, type_B> make_pair(type_A a, type_B b);
瘋狂用Pair
...
#define fs first
#define sc second
#define mp make_pair
typedef pair<int, int> pii;
void DIJKSTRA(int id) {
priority_queue<pii, vector<pii>,greater<pii>> pq;
fill(dis, dis + n, INF);
visited.reset();
pq.push(mp(0, id));
while (pq.size()) {
pii now = pq.top();
pq.pop();
if (visited[now.sc]) continue;
dis[now.sc] = now.fs;
visited[now.sc] = true;
for (auto &i : edge[now.sc]) {
if (visited[i.sc]) continue;
pq.push(mp(now.fs + i.fs, i.sc));
}
}
}
...
陣列型
- queue
- stack
- vector
- array
- deque
- bitset
Queue
queue<int> q;
unsigned size() | 回傳q目前裝了幾個人 | O(1) |
void push(int x) | 將x放入q的最末端 | O(1) |
void pop() | 將q的最前端元素移除 | O(1) |
int &front() | 存取最前端元素 | O(1) |
Stack
stack<int> st;
unsigned size() | 回傳st目前裝了幾個人 | O(1) |
void push(int x) | 將x放入st的最末端 | O(1) |
void pop() | 將st的最末端元素移除 | O(1) |
int &top() | 存取最後一個被放入的元素 | O(1) |
Vector
vector<int> v;
vector<int>(int n, int x) | 開一個大小為n,每項初始值為x的陣列 | O(n) |
unsigned size() | 回傳v目前裝了幾個人 | O(1) |
void push_back(int x) | 將x放入v的最末端 | O(1) |
void pop_back() | 將v的最末端元素移除 | O(n) |
void clear() | 清除v的所有元素 | O(n) |
int &operator[](int id) | 存取從頭數來的第id項 | O(1) |
Array
array<int, 10> arr;
看起來比較酷的陣列?
Deque
deque<int> dq;
unsigned size() | 回傳dq目前裝了幾個人 | O(1) |
void push_front(int x) | 將x放入dq的最前端 | O(1) |
void pop_front() | 將dq的最前端元素移除 | O(1) |
void push_back(int x) | 將x放入dq的最末端 | O(1) |
void pop_back() | 將dq的最末端元素移除 | O(1) |
int &front() | 存取dq最前端元素 | O(1) |
int &back() | 存取dq最末端元素 | O(1) |
void clear() | 清除所有元素 | O(n) |
int &operator[](int id) | 存取從頭數來的第id項 | O(1) |
Bitset
bitset<1000> b;
void reset() | 將b的每個元素變成false | O(n) |
bool &operator[](int id) | 存取b的第id個bit | O(1) |
高級型
- priority_queue
- set
- multiset
- map
Priority Queue
priority_queue<int> pq;
unsigned size() | 回傳pq目前裝了幾個人 | O(log n) |
void push(int x) | 將x放入pq | O(log n) |
void pop() | 將pq的最頂端元素移除 | O(log n) |
int &top() | 存取目前最大的元素 | O(1) |
void clear() | 清除pq的所有元素 | O(n) |
Set
set<int> S;
unsigned size() | 回傳目前裝了幾個人 | O(1) |
pair<iterator,bool> insert(int x) | 將x放入S中 | O(log n) |
iterator find(int x) | 回傳指向x的iterator | O(log n) |
void erase(iterator it) | 將it指向的元素移除 | O(log n) |
void clear() | 清除S的所有元素 | O(n) |
Multiset
multiset<int> MS;
unsigned size() | 回傳目前裝了幾個人 | O(1) |
iterator insert(int x) | 將x放入MS中 | O(log n) |
iterator find(int x) | 回傳指向x的iterator | O(log n) |
void erase(iterator it) | 將it指向的元素移除 | O(log n) |
void clear() | 清除MS的所有元素 | O(n) |
Map
map<int, int> M;
unsigned size() | 回傳目前裝了幾組人 | O(1) |
int &opreator[](int key) | 存取key對應到的值 | O(log n) |
iterator find(int key) | 回傳指向key的iterator | O(log n) |
void erase(iterator it) | 將it指向的元素移除 | O(log n) |
void clear() | 清除M的所有元素 | O(n) |
iterator是啥?
- 在STL中與容器內的元素互動的「指標」
- 有時候不能暴力用[]存取時就要用iterator
int32_t main() {
int a[10];
for (int *p = a; p < a + 10; p++) {
cin >> *p;
}
}
int32_t main() {
vector<int> v(10);
for (auto it = v.begin(); it != v.end(); it++) {
cin >> *it;
}
}
iterator比較
隨機存取iterator:
雙向存取iterator:
沒有iterator:
- vector
- array
- deque
- set
- multiset
- map
- queue
- stack
- priority_queue
(bitset支援隨機存取,但沒有iterator)
iterator常用語法
iterator data_str.begin() | 回傳該資料結構的「開頭」iterator |
iterator data_str.end() | 回傳該資料結構的「結尾」iterator |
iterator next(iterator it) | 回傳「下一個」iterator |
iterator prev(iterator it) | 回傳「上一個」iterator |
*it | 存取it指到的位址的那個人 |
it -> func(); it -> attr; | (*it).func(); (*it).attr; |
it + 3 | 回傳「下3個」iterator |
it - 3 | 回傳「上3個」iterator |
it_1 - it_0 | 回傳兩iterator間的距離 |
iterator常用操作
※vector排序
int32_t main() {
int n;
cin >> n;
int vector<int> v(n);
for (auto &i : v) cin >> i;
sort(v.begin(), v.end());
...
}
iterator常用操作
※set遍歷
int32_t main() {
int n, br;
set<int> S;
cin >> n;
for (int i = 0; i < n; i++) {
cin >> br;
S.insert(br);
}
for (auto it = S.begin(); it != S.end(); it++) {
cout << *it << ' ';
}
cout << endl;
...
}
iterator常用操作
※set找人
int32_t main() {
int n, br;
set<int> S;
cin >> n;
for (int i = 0; i < n; i++) {
cin >> br;
S.insert(br);
}
cin >> br;
auto it = S.find(br);
if (it == S.end()) {
cout << "YES" << endl;
} else {
cout << "NO" << endl;
}
...
}
iterator常用操作
※map遍歷
int32_t main() {
int n, key, val;
map<int, int> M;
cin >> n;
for (int i = 0; i < n; i++) {
cin >> key >> val;
M[key] += val;
}
for (auto it = M.begin(); it != M.end(); it++) {
cout << it -> first >> ": " >> it -> second << endl;
}
}
iterator常用操作
※map找人
map<int, int> M;
inline int advenced_find(int key) {
auto it = M.find(key);
if (it == M.end()) return -1;
return it -> second;
}
練習題時間
Restaurant Customers
- 你開了一家餐廳,接下來有\(n\)位客人來訪
- 給定每位客人的進入時間與離開時間,即每位客人在「第\(l_i\)時間單位初」時進入餐廳,在「第\(r_i\)時間單位初」時離開餐廳
- 輸出這間餐廳裡「有最多人」的時候有幾人
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|
- \([5,8)\)
- \([2,4)\)
- \([3,9)\)
觀察性質
「人數只有在『有人進入』或『有人離開』時才會發生改變。」
Solution #1
- 記錄每個人數發生變化的位置以及人數的變化量
- 將所有「事件」依照時間順序排序
- 每次讀入一個「事件」,依照人數變化量改變現有人數
- 時間複雜度:\(\Omicron(n\log n)\)
Code
雖然AC了…
雖然AC了…
- 在同一時刻有可能發生多個「事件」
- 但是我們只能算一次
Solution #1-1
- 用一招酷酷的方式遍歷所有事件,讓所有同時的事件都發生後再寫入答案。
Solution #1-2
- 注意到同時有多個事件發生時,只要先讓要離開的人離開再讓要進來的人進來,就不會影響最後答案的正確性。
Solution #1-3
- 試著用pair<int, int>表示事件,則可以利用pair的特性直接做排序,省去寫cmp函式的時間。
Solution #1-4
- 發現可以把同個時間的所有事件壓成一個大事件,且找到一個很讚的STL能夠滿足所有需求。
Solution #2
- 試試看另一種想法!
- 其實只有在「有人進入」時才需要寫入答案
- 試著讓時間只流經那些「有人進來的點」
- 要計算答案之前,檢查目前在店裡的所有客人,如果目前時間已經超過他的離開時間就把他趕出去
- 試試看另一種想法!
- 其實只有在「有人進入」時才需要寫入答案
- 試著讓時間只流經那些「有人進來的點」
- 要計算答案之前,檢查目前在店裡的所有客人,如果目前時間已經超過他的離開時間就把他趕出去
- 試試看另一種想法!
- 其實只有在「有人進入」時才需要寫入答案
- 試著讓時間只流經那些「有人進來的點」
- 要計算答案之前,不斷檢查目前在店裡且離開時間最早的客人,如果目前時間已經超過他的離開時間就把他趕出去,直到所有人都被趕出去或是所有人的離開時間都比現在晚
- 有沒有哪個資料結構可以支援「加入時間點」和「移除目前最早的時間點」?
Code
為什麼事情變得這麼複雜
進階資料結構
- Sparse Table
- BIT
- Segment Tree
Sparse Table
從問題開始
- 給定一個陣列\(a[0..n)\)
- 接下來有\(q\)筆詢問,每筆詢問包含兩個數字\(l,r\)
- 對於每筆詢問,輸出\(\\\max(a[l],a[l+1],a[l+2],\dots,a[r-2],a[r-1])\)
※請設計一個資料結構使得可以在\(\Omicron(1)\)的時間內回答此種詢問
極值的特性
2 | 0 | 0 | 8 | 3 | 9 | 0 | 3 | 7 | 1 |
\(\max[2,9)\\=\max(\max[2,5),\max[5,9))\)
極值的特性
2 | 0 | 0 | 8 | 3 | 9 | 0 | 3 | 7 | 1 |
\(\max[2,9)\\=\max(\max[2,5),\max[5,9))\)
極值的特性
2 | 0 | 0 | 8 | 3 | 9 | 0 | 3 | 7 | 1 |
稍微取長一點沒差吧?
\(\max[2,9)\\=\max(\max[2,7),\max[4,9))\)
就是有偷懶的想法!
- 每次詢問會問一個「區間」的極值
- 這個「區間」會有一個大小(假設叫\(n\))
- 如果新開一個陣列\(p_k[0..n-k]\),令\(\\p_k[i]=max(a[i..i+k))\),那麼我們可以做什麼?
- 每次詢問會問一個「區間」的極值
- 這個「區間」會有一個大小(假設叫\(n\))
- 如果新開一個陣列\(p_k[0..n-k]\),令\(\\p_k[i]=max(a[i..i+k))\),那麼我們可以做什麼?
2 | 0 | 0 | 8 | 3 | 9 | 0 | 3 | 7 | 1 |
- 可以發現只要詢問區間的\(n=k\),就可以\(\Omicron(1)\)回答
- 每次詢問會問一個「區間」的極值
- 這個「區間」會有一個大小(假設叫\(n\))
- 如果新開一個陣列\(p_k[0..n-k]\),令\(\\p_k[i]=max(a[i..i+k))\),那麼我們可以做什麼?
2 | 0 | 0 | 8 | 3 | 9 | 0 | 3 | 7 | 1 |
- 可以發現只要詢問區間的\(n=k\),就可以\(\Omicron(1)\)回答
- 那如果\(n\)稍微大一點,可以怎麼做?
2 | 0 | 0 | 8 | 3 | 9 | 0 | 3 | 7 | 1 |
\(p_4[0..6]:\)
\([8,8,9,9,9,9,7]\)
想求\(\max[3,8)\)
\((n=5)\)
2 | 0 | 0 | 8 | 3 | 9 | 0 | 3 | 7 | 1 |
\(p_4[0..6]:\)
\([8,8,9,9,9,9,7]\)
想求\(\max[3,8)\)
\((n=5)\)
- 只要是這麼長的區間最大值,我們都可以用查表的找到
2 | 0 | 0 | 8 | 3 | 9 | 0 | 3 | 7 | 1 |
\(p_4[0..6]:\)
\([8,8,9,9,9,9,7]\)
想求\(\max[3,8)\)
\((n=5)\)
2 | 0 | 0 | 8 | 3 | 9 | 0 | 3 | 7 | 1 |
\(p_4[0..6]:\)
\([8,8,9,9,9,9,7]\)
想求\(\max[3,8)\)
\((n=5)\)
2 | 0 | 0 | 8 | 3 | 9 | 0 | 3 | 7 | 1 |
\(p_4[0..6]:\)
\([8,8,9,9,9,9,7]\)
2 | 0 | 0 | 8 | 3 | 9 | 0 | 3 | 7 | 1 |
\(p_4[0..6]:\)
\([8,8,9,9,9,9,7]\)
想求\(\max[3,9)\)
\((n=6)\)
2 | 0 | 0 | 8 | 3 | 9 | 0 | 3 | 7 | 1 |
\(p_4[0..6]:\)
\([8,8,9,9,9,9,7]\)
想求\(\max[3,9)\)
\((n=6)\)
2 | 0 | 0 | 8 | 3 | 9 | 0 | 3 | 7 | 1 |
\(p_4[0..6]:\)
\([8,8,9,9,9,9,7]\)
想求\(\max[3,9)\)
\((n=6)\)
- 在 \(4<n\leq4\times2\) 時,只要用兩個藍線就可以「包住」整個紅線
2 | 0 | 0 | 8 | 3 | 9 | 0 | 3 | 7 | 1 |
\(p_4[0..6]:\)
\([8,8,9,9,9,9,7]\)
想求\(\max[3,9)\)
\((n=6)\)
- 在 \(4<n\leq4\times2\) 時,只要用兩個藍線就可以「包住」整個紅線
- 我們說\(p_4\)的「可處理區間」為\([4,8]\)(利用\(p_4\)即可\(\Omicron(1)\)回答的\(n\)的區間)
That's the way!
- \(p_k\)可以維護\(n\in[k,2k]\)的答案
- 則我們需要哪些\(p_k\)就可以維護所有\(n\)的答案?
That's the way!
- \(p_k\)可以維護\(n\in[k,2k]\)的答案
- 則我們需要哪些\(p_k\)就可以維護所有\(n\)的答案?
\(p_1:n\in[1,2]\)
\(p_2:n\in[2,4]\)
\(p_4:n\in[4,8]\)
\(p_8:n\in[8,16]\)
\(\vdots\)
\(p_1[\dots]\)
\(p_2[\dots]\)
\(p_4[\dots]\)
\(p_8[\dots]\)
\(\vdots\)
- 這張表被我們稱作「稀疏表」(Sparse Table)
把陣列放在一起
Code
struct SPT {
int n, p[__lg(MXN)][MXN];
void init(int _n, int *a) {
n = _n;
for (int i = 0; i < n; i++) p[0][i] = a[i];
for (int i = 1, k = 2; i <= __lg(n); i++, k *= 2) {
for (int j1 = 0, j2 = k / 2, j1 <= n - k; j1++, j2++) {
p[i][j1] = max(p[i - 1][j1], p[i - 1][j2]);
}
}
}
int query(int l, int r) {
// [l, r)
int n = r - l;
int k_lg = __lg(n);
return max(p[k_lg][l], p[k_lg][r - (1 << k_lg)]);
}
};
Binary Indexed Tree
從問題開始
- 給定一個陣列\(a[1..n]\)
- 接下來有\(q\)筆詢問,每筆詢問會是下列兩種中的其中一種:
- 給定\(id,val\),將\(a[id]\)的值改成\(val\)
- 給定\(pos\),求\(a[1]+a[2]+...+a[pos]\)
※請設計一個資料結構使得可以支援改值操作,並利用某種方法維護前綴和
直接爆雷
2 | 0 | 0 | 5 | 1 | 0 | 1 | 8 |
b[1]: |
b[2]: |
b[3]: |
b[4]: |
b[5]: |
b[6]: |
b[7]: |
b[8]: |
(1-indexed)
直接爆雷
2 | 0 | 0 | 5 | 1 | 0 | 1 | 8 |
b[1]:2 |
b[2]:2 |
b[3]:0 |
b[4]:7 |
b[5]:1 |
b[6]:1 |
b[7]:1 |
b[8]:17 |
(1-indexed)
2 | 0 | 0 | 5 | 1 | 0 | 1 | 8 |
b[1]:2 |
b[2]:2 |
b[3]:0 |
b[4]:7 |
b[5]:1 |
b[6]:1 |
b[7]:1 |
b[8]:17 |
(1-indexed)
2 | 0 | 0 | 5 | 1 | 0 | 1 | 8 |
b[1]:2 |
b[2]:2 |
b[3]:0 |
b[4]:7 |
b[5]:1 |
b[6]:1 |
b[7]:1 |
b[8]:17 |
(1-indexed)
求前綴和?
2 | 0 | 0 | 5 | 1 | 0 | 1 | 8 |
b[1]:2 |
b[2]:2 |
b[3]:0 |
b[4]:7 |
b[5]:1 |
b[6]:1 |
b[7]:1 |
b[8]:17 |
(1-indexed)
求前綴和?
2 | 0 | 0 | 5 | 1 | 0 | 1 | 8 |
b[1]:2 |
b[2]:2 |
b[3]:0 |
b[4]:7 |
b[5]:1 |
b[6]:1 |
b[7]:1 |
b[8]:17 |
(1-indexed)
2 | 0 | 0 | 5 | 1 | 0 | 1 | 8 |
b[1]:2 |
b[2]:2 |
b[3]:0 |
b[4]:7 |
b[5]:1 |
b[6]:1 |
b[7]:1 |
b[8]:17 |
(1-indexed)
單點改值?
+2
2 | 0 | 0 | 5 | 1 | 0 | 1 | 8 |
b[1]:2 |
b[2]:2 |
b[3]:0 |
b[4]:7 |
b[5]:1 |
b[6]:1 |
b[7]:1 |
b[8]:17 |
(1-indexed)
單點改值?
+2
2 | 0 | 0 | 5 | 1 | 0 | 1 | 8 |
b[1]:2 |
b[2]:2 |
b[3]:0 |
b[4]:7 |
b[5]:1 |
b[6]:1 |
b[7]:1 |
b[8]:17 |
(1-indexed)
單點改值?
+2
+2
+2
+2
所以它到底是啥?
- 「感覺很像前綴和陣列」
- 「但是有些人儲存的範圍沒有像前綴和一樣拉到頭」
前綴和 v.s. BIT
- 求前綴和:\(\Omicron(1)\)
- 單點修改:\(\Omicron(n)\)
- 求前綴和:\(\Omicron(\log n)\)
- 單點修改:\(\Omicron(\log n)\)
一般陣列 v.s. BIT
- 求前綴和:\(\Omicron(n)\)
- 單點修改:\(\Omicron(1)\)
- 求前綴和:\(\Omicron(\log n)\)
- 單點修改:\(\Omicron(\log n)\)
BIT的真面目
- 「一般陣列與前綴和陣列的折衷方案」
- 第\(i\)格存儲範圍會往回延伸\(x_i\)格:
- \(x_i\)是\(i\)的因數中,能表示成\(2^a\)的數中的最大者
- \(x_i=\operatorname{lowbit}(i)=i\&(-i)\)
\(6_{10}=110_2\ \Rightarrow\ \operatorname{lowbit}(6)=2\)
遍歷與存取
在接到操作時,如何遍歷過所有需要存取的\(id\)?
for (; id <= n; id += (id & -id)) {
}
for (; id > 0; id -= (id & -id)) {
}
- 對第\(i\)項改值:
- 對前\(i\)項求和:
Code
struct BIT {
int n, val[MXN];
void init(int _n) {
n = _n;
for (int i = 1; i <= n; i++) val[i] = 0;
}
void modify(int id, int x) {
for (; id <= n; id += (id & -id)) val[id] += x;
}
int query(int id) {
// [1, id]
int ans = 0;
for (; id > 0; id -= (id & -id)) ans += val[id];
return ans;
}
};
Segment Tree
從問題開始
- 給定一個陣列\(a[1..n]\)
- 接下來有\(q\)筆詢問,每筆詢問會是下列兩種中的其中一種:
- 給定\(id,val\),將\(a[id]\)的值改成\(val\)
- 給定\(l,r\),求\(\max(a[l],a[l+1],\dots,a[r-1],a[r])\)
※請設計一個資料結構使得可以支援改值操作,並利用某種方法維護區間最大值
BIT的進化之路
「如果做得到\(n=2^a\)的詢問,會讓查詢複雜度降低」
「維護住『小部分』的區間,會讓改值複雜度降低」
Sparse Table:
BIT:
維護\(n=2^a\)的「小部分區間」
struct SMG_NODE {
int l, r, val;
SMG_NODE *L, *R;
};
2 | 0 | 0 | 5 | 1 | 0 | 1 | 8 |
8
5
8
2
5
1
8
2
0
0
5
1
0
1
8
2 | 0 | 0 | 5 | 1 | 0 | 1 | 8 |
8
5
8
2
5
1
8
2
0
0
5
1
0
1
8
改值
/ 3
2 | 0 | 0 | 5 | 1 | 0 | 1 | 8 |
8
5
8
2
5
1
8
2
0
0
5
1
0
1
8
改值
/ 3
2 | 0 | 0 | 5 | 1 | 0 | 1 | 8 |
8
5
8
2
5
1
8
2
0
0
5
1
0
1
8
改值
/ 3
2 | 0 | 0 | 5 | 1 | 0 | 1 | 8 |
8
5
8
2
5
1
8
2
0
0
5
1
0
1
8
改值
/ 3
2 | 0 | 0 | 5 | 1 | 0 | 1 | 8 |
8
5
8
2
5
1
8
2
0
0
5
1
0
1
8
改值
/ 3
2 | 0 | 0 | 5 | 1 | 0 | 1 | 8 |
8
5
8
2
5
1
8
2
0
0
5
1
0
1
8
改值
/ 3
2 | 0 | 0 | 5 | 1 | 0 | 1 | 8 |
8
5
8
2
5
1
8
2
0
0
5
1
0
1
8
改值
/ 3
2 | 0 | 0 | 5 | 1 | 0 | 1 | 8 |
8
5
8
2
5
1
8
2
0
0
5
1
0
1
8
改值
/ 3
2 | 0 | 0 | 5 | 1 | 0 | 1 | 8 |
8
5
8
2
5
1
8
2
0
0
5
1
0
1
8
改值
/ 3
2 | 0 | 0 | 5 | 1 | 0 | 1 | 8 |
8
5
8
2
5
1
8
2
0
0
5
1
0
1
8
改值
/ 3
2 | 0 | 0 | 5 | 1 | 0 | 1 | 8 |
8
5
8
2
5
1
8
2
0
0
5
1
0
1
8
改值
/ 3
2 | 0 | 0 | 5 | 1 | 0 | 1 | 8 |
8
5
8
2
5
1
8
2
0
0
5
1
0
1
8
改值
/ 3
2 | 0 | 0 | 5 | 1 | 0 | 1 | 8 |
8
5
8
2
5
1
8
2
0
0
3
1
0
1
8
改值
/ 3
2 | 0 | 0 | 5 | 1 | 0 | 1 | 8 |
8
5
8
2
5
1
8
2
0
0
3
1
0
1
8
改值
/ 3
2 | 0 | 0 | 5 | 1 | 0 | 1 | 8 |
8
5
8
2
5
1
8
2
0
0
3
1
0
1
8
改值
/ 3
2 | 0 | 0 | 5 | 1 | 0 | 1 | 8 |
8
5
8
2
5
1
8
2
0
0
3
1
0
1
8
改值
/ 3
2 | 0 | 0 | 5 | 1 | 0 | 1 | 8 |
8
5
8
2
3
1
8
2
0
0
3
1
0
1
8
改值
/ 3
2 | 0 | 0 | 5 | 1 | 0 | 1 | 8 |
8
5
8
2
3
1
8
2
0
0
3
1
0
1
8
改值
/ 3
2 | 0 | 0 | 5 | 1 | 0 | 1 | 8 |
8
5
8
2
3
1
8
2
0
0
3
1
0
1
8
改值
/ 3
2 | 0 | 0 | 5 | 1 | 0 | 1 | 8 |
8
5
8
2
3
1
8
2
0
0
3
1
0
1
8
改值
/ 3
2 | 0 | 0 | 5 | 1 | 0 | 1 | 8 |
8
3
8
2
3
1
8
2
0
0
3
1
0
1
8
改值
/ 3
2 | 0 | 0 | 5 | 1 | 0 | 1 | 8 |
8
3
8
2
3
1
8
2
0
0
3
1
0
1
8
改值
/ 3
2 | 0 | 0 | 5 | 1 | 0 | 1 | 8 |
8
3
8
2
3
1
8
2
0
0
3
1
0
1
8
改值
/ 3
2 | 0 | 0 | 5 | 1 | 0 | 1 | 8 |
8
3
8
2
3
1
8
2
0
0
3
1
0
1
8
改值
/ 3
2 | 0 | 0 | 5 | 1 | 0 | 1 | 8 |
8
3
8
2
3
1
8
2
0
0
3
1
0
1
8
改值
/ 3
2 | 0 | 0 | 5 | 1 | 0 | 1 | 8 |
8
3
8
2
3
1
8
2
0
0
3
1
0
1
8
改值
/ 3
2 | 0 | 0 | 3 | 1 | 0 | 1 | 8 |
8
3
8
2
3
1
8
2
0
0
3
1
0
1
8
改值
改值SOP
- 確認好往左走還是往右走
- 修改左/右節點
- 將此節點的答案利用兩個子節點的答案更新
實作簡化
- 確認好往左走還是往右走
- 修改左/右節點
- 將此節點的答案利用兩個子節點的答案更新
\(\Rightarrow\)兩個子節點都修改
- 如果這個點不需要更新就直接return
// in struct:
void modify(int _pos, int _val) {
if (!(l <= _pos && _pos < r)) return;
if (L == nullptr) val = _val;
else {
L -> modify(_pos, _val);
R -> modify(_pos, _val);
val = max(L -> val, R -> val);
}
}
查詢SOP
「查詢區間」與該節點「維護區間」的關係必為下列其中一種:
- 兩者無交集
- 維護區間\(\subseteq\)查詢區間
- 兩者有交集,但不滿足第二種
查詢SOP
「查詢區間」與該節點「維護區間」的關係必為下列其中一種:
- 兩者無交集
- 維護區間\(\subseteq\)查詢區間
- 兩者有交集,但不滿足第二種
\(\Rightarrow\) return -INF;
\(\Rightarrow\) return val;
\(\Rightarrow\) 遞迴後比較兩子節點回傳值
查詢Code
// in struct:
int query(int _l, int _r) {
if (_r <= l || r <= _l) return -INF;
if (_l <= l && r <= _r) return val;
return max(L -> query(_l, _r), R -> query(_l, _r));
}
2 | 0 | 0 | 3 | 1 | 0 | 1 | 8 |
8
3
8
2
3
1
8
2
0
0
3
1
0
1
8
查詢
2 | 0 | 0 | 3 | 1 | 0 | 1 | 8 |
8
3
8
2
3
1
8
2
0
0
3
1
0
1
8
查詢
2 | 0 | 0 | 3 | 1 | 0 | 1 | 8 |
8
3
8
2
3
1
8
2
0
0
3
1
0
1
8
查詢
2 | 0 | 0 | 3 | 1 | 0 | 1 | 8 |
8
3
8
2
3
1
8
2
0
0
3
1
0
1
8
查詢
2 | 0 | 0 | 3 | 1 | 0 | 1 | 8 |
8
3
8
2
3
1
8
2
0
0
3
1
0
1
8
查詢
2 | 0 | 0 | 3 | 1 | 0 | 1 | 8 |
8
3
8
2
3
1
8
2
0
0
3
1
0
1
8
查詢
2 | 0 | 0 | 3 | 1 | 0 | 1 | 8 |
8
3
8
2
3
1
8
2
0
0
3
1
0
1
8
查詢
2 | 0 | 0 | 3 | 1 | 0 | 1 | 8 |
8
3
8
2
3
1
8
2
0
0
3
1
0
1
8
查詢
2 | 0 | 0 | 3 | 1 | 0 | 1 | 8 |
8
3
8
2
3
1
8
2
0
0
3
1
0
1
8
查詢
2 | 0 | 0 | 3 | 1 | 0 | 1 | 8 |
8
3
8
2
3
1
8
2
0
0
3
1
0
1
8
查詢
2 | 0 | 0 | 3 | 1 | 0 | 1 | 8 |
8
3
8
2
3
1
8
2
0
0
3
1
0
1
8
查詢
2 | 0 | 0 | 3 | 1 | 0 | 1 | 8 |
8
3
8
2
3
1
8
2
0
0
3
1
0
1
8
查詢
2 | 0 | 0 | 3 | 1 | 0 | 1 | 8 |
8
3
8
2
3
1
8
2
0
0
3
1
0
1
8
查詢
2 | 0 | 0 | 3 | 1 | 0 | 1 | 8 |
8
3
8
2
3
1
8
2
0
0
3
1
0
1
8
查詢
2 | 0 | 0 | 3 | 1 | 0 | 1 | 8 |
8
3
8
2
3
1
8
2
0
0
3
1
0
1
8
查詢
2 | 0 | 0 | 3 | 1 | 0 | 1 | 8 |
8
3
8
2
3
1
8
2
0
0
3
1
0
1
8
查詢
2 | 0 | 0 | 3 | 1 | 0 | 1 | 8 |
8
3
8
2
3
1
8
2
0
0
3
1
0
1
8
查詢
2 | 0 | 0 | 3 | 1 | 0 | 1 | 8 |
8
3
8
2
3
1
8
2
0
0
3
1
0
1
8
查詢
2 | 0 | 0 | 3 | 1 | 0 | 1 | 8 |
8
3
8
2
3
1
8
2
0
0
3
1
0
1
8
查詢
2 | 0 | 0 | 3 | 1 | 0 | 1 | 8 |
8
3
8
2
3
1
8
2
0
0
3
1
0
1
8
查詢
2 | 0 | 0 | 3 | 1 | 0 | 1 | 8 |
8
3
8
2
3
1
8
2
0
0
3
1
0
1
8
查詢
2 | 0 | 0 | 3 | 1 | 0 | 1 | 8 |
8
3
8
2
3
1
8
2
0
0
3
1
0
1
8
查詢
2 | 0 | 0 | 3 | 1 | 0 | 1 | 8 |
8
3
8
2
3
1
8
2
0
0
3
1
0
1
8
查詢
2 | 0 | 0 | 3 | 1 | 0 | 1 | 8 |
8
3
8
2
3
1
8
2
0
0
3
1
0
1
8
查詢
Code
struct SMG_NODE {
int l, r, val;
SMG_NODE *L, *R;
SMG_NODE(int _l, int _r, int _val) : l(_l), r(_r), val(_val), L(nullptr), R(nullptr) {}
void modify(int _pos, int _val) {
if (!(l <= _pos && _pos < r)) return;
if (L == nullptr) val = _val;
else {
L -> modify(_pos, _val);
R -> modify(_pos, _val);
val = max(L -> val, R -> val);
}
}
int query(int _l, int _r) {
if (_r <= l || r <= _l) return -INF;
if (_l <= l && r <= _r) return val;
return max(L -> query(_l, _r), R -> query(_l, _r));
}
};
SMG_NODE *build(int _l, int _r, int *a) {
if (_l + 1 == _r) return new SMG_NODE(_l, _r, a[_l]);
SMG_NODE *ans = new SMG_NODE(_l, _r, -INF);
int mid = (_l + _r) >> 1;
ans -> L = build(_l, mid, a);
ans -> R = build(mid, _r, a);
ans -> val = max(L -> val, R -> val);
return ans;
}
優化方案
- 指標\(\Rightarrow\)陣列:每個\(id\)為一個節點;可利用\(id\times2+1\)和\(id\times2+2\)當作左、右節點
- 每個\(id\)對應到的左右節點可以再呼叫函式前再算,省去儲存空間
- 其實不需要將陣列大小補至\(2^a\)
Code[陣列型]
struct SMG {
int val[MXN * 4];
void modify(int id, int l, int r, int _pos, int _val) {
if (!(l <= id && id < r)) return;
if (l + 1 == r) {
val[id] = _val;
return;
}
int mid = (l + r) >> 1;
modify(id * 2 + 1, l, mid, _pos, _val);
modify(id * 2 + 2, mid, r, _pos, _val);
val[id] = max(val[id * 2 + 1], val[id * 2 + 2]);
}
int query(int id, int l, int r, int _l, int _r) {
if (_r <= l || r <= _l) return -INF;
if (_l <= l && r <= _r) return val[id];
int mid = (l + r) >> 1;
return max(query(id * 2 + 1, l, mid, _l, _r), query(id * 2 + 2, mid, r, _l, _r));
}
};
更多線段樹資源
資料結構
By pringdesu
資料結構
- 494