---
# System prepended metadata

title: '第三章 :基礎資結'

---

# 第三章 :基礎資結
## STL 標準資料結構
### pair
一種存資料的方法，跟`struct`很像，優點是有自定義排序，會先依照前項排序再依照後項排序，||可以偷懶||
$e.g.$
```cpp=+
pair<int, int> p1; // p1 = {0 , 0}  
pair<int, int> p3 = {4, 8};
cout<<p3.first<<" "<<p3.second; //4 8

pair<pair<int, int>, int> p4 = {{4, 8}, 7};
cout<<p4.first.first<<" "<<p4.first.second<<" "<<p4.second; // 4 8 7
vector<pair<int, int>> v1(100, {1, 2});
cin>>v1[0].first;
vector<pair<int, int>> v2;
v2.push_back({1, 2});
```

### queue && stack (線性容器)
![](https://github.com/FliGhtzzz/ccirc/blob/main/pictures/q.png?raw=true)
**可以把`queue`想像成一個水管，只能右進左出，只能訪問最前面跟最後面
而`stack`則是像一個桶子，後進先出，只能訪問最上面**
```cpp=
stack<int> s;
s.push(val):將 val 加進 stack 的最上面,O(1)
s.pop():將 stack 最上面的元素刪除,O(1)
s.top():取得 stack 最上面的元素,O(1)
queue<int> q;
q.push(val):將 val 加進 queue 的最後面,O(1)
q.pop():將 queue 最前面的元素刪除,O(1)
q.front():取得 queue 最前面的元素,O(1)
```
### priority_queue
![](https://github.com/FliGhtzzz/ccirc/blob/main/pictures/pq.png?raw=true)
將最重要的元素放在最頂端，只能訪問最前項，預設是將最大當作最重要

$e.g.$
```cpp=+
priority_queue<int> pq
pq.push(val):將 val 加進 pq 裡面,O(log n)
• pq.pop():將 pq 最上面的元素刪除,O(log n)
• pq.top():取得 pq 最上面的元素,O(1)
/***************************************/
pq.push(1);
pq.push(2);
pq.push(2);
while(!pq.empty()) {
    cout<<pq.top()<<" ";
    pq.top();
}
//2 2 1
```
* **註 : `pq`的時間常數比`set`還要低**
* [進階語法](https://yuihuang.com/cpp-stl-priority-queue/)
### set&&multiset
$set$主要的功能有`排序`與`去重`：
把資料送入$set$時，若$set$內已有該數值就會被刪除，若沒有就會被排序
**即所有相同資料在$set$中只能存在一個**
```cpp=
set<int> s;
int val=1;
s.empty();  //回傳布林值：s是否為空
s.size();  //回傳整數：s的大小
s.begin();  //回傳迭代器：s最前面的位置
s.end();  //回傳迭代器：s最後位置的下一個位置
s.insert(val);  //插入數值val到s中 回傳pair<iterator,bool>插入位置和是否成功 O(log n)
s.count(val);  //查看s中val數值的數量（set中必為1或0）O(log n)
s.erase(val);  //刪除s中的val元素 
s.erase(iterator);  //刪除iterator迭代器位置的數值 O(1)
s.find(val);  //查找val數值的位置 回傳iterator O(logn)
```
$multiset$是允許多個相同數值存在的$set$
使用方法和$set$大致相同
```cpp=
multiset<int> ms;
int val=1;
ms.empty();  //回傳布林值：s是否為空
ms.size();  //回傳整數：s的大小
ms.begin();  //回傳迭代器：s最前面的位置
ms.end();  //回傳迭代器：s最後位置的下一個位置
ms.insert(val);  //插入數值val到s中  O(log n)
ms.count(val);  //查看s中val數值的數量 O((val在ms中的個數)log n)
ms.erase(val);  //刪除s中的所有val元素 回傳整數被刪除的元素數量 O((val在ms中的個數)log n)
ms.erase(iterator); //刪除iterator迭代器位置的數值 回傳iterator刪除元素的下一個位置 O(1)
ms.find(val);  //查找val數值的位置 若有多個數值 回傳最前面的那個 O(logn)
```
需要注意的是，如果想要只刪除$multiset$某個元素一次而不是刪掉所有的這個元素，可以像以下的做法。
```cpp=
multiset<int> ms;
ms.insert(1);
ms.insert(1);
ms.insert(1);
ms.erase(ms.find(1)); //只會刪掉一個
cout<<ms.size(); //2
ms.erase(1);
cout<<ms.size(); //0
```
### map
![](https://github.com/FliGhtzzz/ccirc/blob/main/pictures/mapit.png?raw=true)
用於儲存離散化的資料，像是一個陣列，但陣列索引可能是任何資料型態，特別是當需要開一個很大的陣列的時候可以考慮使用它，通常都以`map<int, int>`來做，每次對map上的東西進行操作時間複雜度都是$O(logn)$，$n$是目前在map上有多少開啟了多少個點
```cpp=
//map<key_type,value_type> mp;  這是宣告方式
map<char,int> mp;  //預設每一項都是0
mp['A'];  //在mp中宣告一個引索為A的數
mp['A']=1;
mp['A']+=5;
cout<<mp['A'];  //6
mp.erase(key);  //刪除該索引的
mp.find(key);  //回傳該索引的位置 找不到則回傳mp.end()
```

練習題 
> [set應用](https://judge.cchs.chc.edu.tw/ShowProblem?problemid=a155)
> [離散化](https://judge.cchs.chc.edu.tw/ShowProblem?problemid=a532)
> [map](https://judge.cchs.chc.edu.tw/ShowProblem?problemid=a177)
> [STL](https://cses.fi/problemset/task/1621)
> [pair排序](https://cses.fi/problemset/task/1629/)
## 並查集 Disjoint Set
[例題](https://judge.cchs.chc.edu.tw/ShowProblem?problemid=a531)
又稱互斥集或$DSU$，具有下列功能的資料結構。
> 所有元素 $P$ 編號為 $1,2,3,...,n$。
> 1. 對於$P$中的每個元素$t$都恰有一個集合包含該元素。
> 2. 把$A$,$B$兩個元素所在的集合合併到同個集合中。
> 3. 查詢$A$,$B$兩個元素所在的集合是否是同一個集合。
> 
由於$STL$並沒有支援所有我們需要自己實作，值得慶幸的是，這個資料結構實作比較輕鬆。
1. 首先，我們需要紀錄每個元素的$Boss$，每個元素的$Boss$一開始都是自己，如果一個元素的$Boss$=該元素，那這個元素我們就定義它是最大的$Boss$。
2. 如果需要判斷兩個元素是否在同一個集合，可以判斷兩個元素的最大的$Boss$是否相同。
3. 如果要合併兩個集合，就把其中一個元素的最大的$Boss$指定為另外一個元素最大的的$Boss$。

範例 : 
```cpp=
#include<bits/stdc++.h>
using namespace std;
const MAXN=2e5+5;
vector<int> boss(MAXN, 0);
void build_dsu() {
    for(int i=1;i<MAX;i++) boss[i]=i;
}
int find(int now) {
    if(boss[now]=now) return now;
    return find(boss[now]);
}
bool same(int x, int y) {
    return find(x)==find(y); 
}
void union(int x, int y) {
    int fx=find(x), fy=find(y);
    boss[fx]=fy;
}
```
這個程式的每次操作的時間複雜度是$O(n)$，因為可以觀察到查詢跟合併的操作都是依靠$find()$這個函式，而一個元素要找到它最大的$boss$最多需要執行$n$次，因此整體的時間複雜度式$O(Qn)$，$Q$是總共有幾次操作。
因此我們可以針對$find()$函式進行優化，我們可以發現一個元素的$Boss$的$Boss$一直到這個元素的最大的$Boss$之前，所有的$Boss$的最大的$Boss$都是相同的，因此我們可以把$find()$改成下面的程式，看起來差不多，但是每次查詢的時間複雜度根據均攤分析可以優化到$O(logn)$，這個優化我們稱之為路徑壓縮。
* [維基百科](https://en.wikipedia.org/wiki/Disjoint-set_data_structure#Time_complexity)
* [OI Wiki](https://oi-wiki.org/ds/dsu/)
```cpp=
int find(int now) {
    if(boss[now]=now) return now;
    return boss[now]=find(boss[now]);
}
```
快還要更快，一個很貪心且直覺的想法就是在合併兩個集合的時候，把比較小的集合的最大的$Boss$，設成比較大的集合的最大的$Boss$，一定比把比較大的集合的最大的$Boss$，設成比較小的集合的最大的$Boss$，因為很顯然的，比較大的集合在更新的最大的$Boss$時需要更多時間，因為元素更多，這樣可以把每次操作的時間優化成$O(\alpha(n))$。
> $\alpha(n)$，的中文是反阿克曼函數，增長很慢，比較直觀的是:$\alpha(2^{2^{2^{2^2}}})=4$，$2^{2^{2^{2^2}}}\approx 2\times10^{19728}$

$DSU .final$
```cpp=
#include<bits/stdc++.h>
using namespace std;
const MAXN=2e5+5;
vector<int> boss(MAXN, 0), sz(MAXN, 1); //一開始的集合大小都是1
void build_dsu() {
    for(int i=1;i<MAX;i++) boss[i]=i;
}
int find(int now) {
    if(boss[now]=now) return now;
    return boss[now]=find(boss[now]);
}
bool same(int x, int y) {
    return find(x)==find(y); 
}
void union(int x, int y) {
    int fx=find(x), fy=find(y);
    if(sz[fx]<sz[fy]) swap(fx, fy);
    sz[fx]+=sz[fy];
    boss[fy]=boss[fx];
}
```
練習題
> [DSU](https://atcoder.jp/contests/abc177/tasks/abc177_d)
> [DSU](https://cses.fi/problemset/task/1676/)
## BIT
又稱作樹狀數組或二元索引樹($Binary$ $Indexed$ $Tree$ $=>$ $BIT$ )，可以在$O(logn)$的時間做到單點改值以及前墜查詢，時間常數低，空間也只要$n$，實作也簡單。


BIT上每一個節點表示的值根據它的位置編號而定，$BIT_i$定義為:以$i$為結尾，長度為$lowbit(i)$的區間和，也就是$[i-lowbit(i), i]$的和。

$lowbit(i)$的定義:把$i$轉成二進制後把除了最低位的1以外的1都變成0，例如:

$$lowbit(26)_{10}=lowbit(11010)_{2}=(00010)_{2}=(10)_{2}=(2)_{10}$$
所以$BIT_{26}$表示的就是$[24, 26]$的區間和。

在程式中可以透過`i&-i`來取得$lowbit(i)$，因為`-x`會是`x`的補數+1，所以最低位的1會變成0，但右邊所有的0都會變成1，再+1之後又會進為讓原本最低位的1從0進位成1，右邊全部的1變回0，再透過`&`運算就可以發現`i&-i`=$lowbit(i)$

例如 $i=26$，$i=(11010)_{2}$，$-i=(1...11100101)_{2}+1=(1...11100110)_{2}$，`i&-i`=2

$BIT_0$代表空區間，$BIT_1$代表的是$[1, 1]$，所以序列的索引應該從1開始

可以把BIT畫成以下兩種形式。
* 圖一
![image](https://hackmd.io/_uploads/SJfxS61U1x.png)
* 圖二
![image](https://hackmd.io/_uploads/HJEWH6kIJe.png)

* 節點$i$的父節點是$i-lowbit(i)$
* 節點$i$的深度=2進制下$i$是1的位元數 
* 節點$i$的右兄弟節點 ( 如果有的話 ) 是$i+lowbit(i)$ 
### 單點改值
假設在使用$BIT$想要進行修改的動作，需要修改他的所有右兄弟節點，例如想要修改原陣列節點9的資訊，應該修改是$BIT$中包含節點9的區間:$[9, 9],[9, 10],[9, 12]$，你可以發現就是圖二的節點$9, 10, 12$，也就是節點9的右兄弟節點，實作如下。
```cpp=
void modify(int pos, int val) {
    //n是BIT大小
    for(;pos<=n;pos+=pos&-pos) 
        bit[pos]+=val;
}
```
可以發現$pos$會不斷增加，因此時間複雜度是$O(logn)$。
### 區間查詢
如果要查以$i$為結尾的前綴和，需要計算節點$i$與他的祖先節點的和，因為以$i$結尾的區間就是節點$i$ ( 可以去看圖一,圖二 ) ，而節點$i$所代表的區間會完全的接在節點$i$的父節點後，因此只要計算節點$i$與他的祖先節點的和，就能算出以$i$為結尾的前綴和，實作如下。
```cpp=
//以最基礎的BIT為例
int search(int pos) {
    int ret=0;
    for(;pos;pos-=pos&-pos) {
        ret+=bit[pos];
    }
    return ret;
}
```
可以發現父節點的大小會以2的冪次成長，所以時間複雜度是$O(logn)$。

### 蓋BIT
$modify$每個點，時間是$O(nlogn)$。
### 應用
1. 求區間和: 有了前綴和當然可以求區間和，$[x, y]$的和是$[1, y]-[1, x-1],x\le y$，可以在$O(logn)$做好。
2. 區間加值: 先把原陣列做差分，可以$O(logn)$區間修改，$O(logn)$單點查詢。
3. 如果原陣列每一項都是正數，那$BIT$會是遞增的，因此可以對$BIT$做二分搜，這個技巧也被稱為$BIT$上二分，時間複雜度是$O(lognlogn)$，優化後可以$O(logn)$。
4. 高維BIT: 把$BIT$模板化，$X$維的$BIT$的每個節點存一棵$X-1$維的$BIT$。
5. 求逆序數對: 可以對陣列蓋$BIT$，每次插入$v_i$前查詢$BIT$中大於$v_i$的數量。

練習題