資料結構

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
  1. \([5,8)\)
  2. \([2,4)\)
  3. \([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

「查詢區間」與該節點「維護區間」的關係必為下列其中一種:

  1. 兩者無交集
  2. 維護區間\(\subseteq\)查詢區間
  3. 兩者有交集,但不滿足第二種

查詢SOP

「查詢區間」與該節點「維護區間」的關係必為下列其中一種:

  1. 兩者無交集
  2. 維護區間\(\subseteq\)查詢區間
  3. 兩者有交集,但不滿足第二種

\(\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;
}

優化方案

  1. 指標\(\Rightarrow\)陣列:每個\(id\)為一個節點;可利用\(id\times2+1\)和\(id\times2+2\)當作左、右節點
  2. 每個\(id\)對應到的左右節點可以再呼叫函式前再算,省去儲存空間
  3. 其實不需要將陣列大小補至\(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));
    }
};

更多線段樹資源

Made with Slides.com