<style>
.reveal .slides {
text-align: left;
font-size:30px;
}
</style>
## Time Complexity & STL
9/22 preTrain
----
<div style="font-size:21px">
- Time Complexity
- How to estimate?
- Some Examples
</div>
----
<div style="font-size:21px">
- Commonly Used Function
- math related function
- min / max / swap
- fill
- sort
- compare function
- reverse
- next / prev_permutation
- islower / isupper / isdigit / isalpha / tolower / toupper
- nth_element
- lower_bound / upper_bound
- unique
- STL
- iterator
- pair / tuple
- vector
- priority_queue
- stack / queue
- set / multiset
- map / multimap
- bitset
- list
</div>
----
## 語法查詢網站
- [Cplusplus](https://cplusplus.com/)
- [Cppreference](https://en.cppreference.com/w/)
---
## 時間複雜度
### Time Complexity
- 與**輸入大小**有關的函數
- 可以用來估計程式執行大約的時間
- 用程式執行次數來去估算執行時間
- 通常以大 $O$ 符號表式,ex: $O(N)$、$O(N\log M)、O(k^3)$
----
### 計算大概的執行次數
執行次數無法完全精準的計算
可能受到編譯器優化影響
使得執行次數不是我們所計算的
而只需要計算大概的次數就可以去估算時間
----
### 表達時間複雜度
- 以大$O$符號表示,其中大$O$符號代表 "上限" 的意思
----
### 計算時間複雜度
1. 估計程式的運算執行次數 $\to$ 可能是個多項式 : $p=an^2+bn+c$
3. 將得到的函數最高階項以外的項全部刪掉 $\to an^2$
4. 把係數拿掉 $\to n^2$
----
### 迴圈的時間複雜度
迴圈的執行次數 $\times$ 每次迴圈的時間複雜度
```cpp
for(int i = 0; i < n; i++) { // n
arr[i] = arr[i - 1] * a + b; // O(1)
swap(a, b); // O(1)
}
```
$n\times (O(1)+ O(1))=n\times O(1)=O(n)$
----
```cpp
for(int i = 0; i < n; i++) { // n
for(int j = i + 1; j < n; j++) { // n - i
if(arr[i] > arr[j]) swap(arr[i], arr[j]); // O(1)
}
}
```
執行次數大約是 :
$$
\begin{split}
&(n-1)+(n-2)+\cdots+1\\
&=\frac{1}{2}n(n-1)\\
&=\frac{1}{2}n^2+\frac{1}{2}n=O(n^2)
\end{split}
$$
----
```cpp
for(int i = 1; i < n; i *= 2)
s += i;
```
設迴圈執行次數為 $k$
$$
\max i=2^k \lt n\quad \stackrel{取 \log_2}{\Longrightarrow}\quad k\lt \lg n
$$
因此是 $O(\log n)$
在算複雜度時,通常 $\log$ 代表以 $2$ 為底,即 $\log_2$ (有時寫 $\lg$)
----
### 遞迴函數的時間複雜度
- 簡單的你們應該會算(?)
- 其他方法就留給演算法必修課去學
- 比較難的等教分治法(Divide&Conquer)時再說
- 很複雜的需要用到很難的函數設計技巧 ~~我也不會~~
----
### 內建函數的時間複雜度
其實從函數的功能應該可以猜出其複雜度
不然的話就把它記起來(至少常用的函數要知道)
- memset : $O(N)$
- sort : $O(N\lg N)$
- \_\_gcd : $O(\log N)$
- lower_bound : $O(\log N)$
$\cdots$
等一下會講這些函數怎麼使用
----
## 為什麼要學時間複雜度
- 對演算法的**執行時間**與**資料量**的關係有個概念
- 判斷一個程式 (做法) 會不會 **TLE**
- 可以用測資的範圍去反推演算法
----
1. 把時間複雜度的大 $O$ 符號拿掉,並將題目給定的輸入範圍**上限**帶入函數,令得到的數值為 $T$
2. 一般而言假設 c++ 每秒能跑 $10^8$ 的數量級
3. 假設題目的時限是 $\hat{T}$ 秒,那麼
- 若 $T\lt \hat{T}\times 10^8$則通常不會TLE
- 若 $T\gt \hat{T}\times 5\times 10^8$ 則極有可能拿到TLE
----
### 時間複雜度的好處
- 當你想到一個做法後,在開始寫之前就能先用時間複雜度來判斷是否會 TLE,以避免浪費時間在寫必然會 TLE 的程式
- 在比賽中更容易作時間分配、難度分析
---
## 常用的函數
----
## math related function
- abs(x) 回傳x的絕對值
- sqrt(x) 回傳x的平方根 (浮點數)
- pow(a,x) 回傳a的x次方 (浮點數)
- ceil(x)/floor(x)/round(x) 回傳 x 向上取整/向下取整/四捨五入
----
## 三角函數
三角函數 $\cos(x)$, $\sin(x)$, $\tan(x)$
反三角函數 $\text{acos}(x)$, $\text{asin}(x)$, $\text{atan}(x)$, $\text{atan}2(y, x)$
數值單位全部都是弧度(radian) :
$\text{角度}=\cfrac{弧度\times 180}{\pi}$
常用 $\pi = \text{acos}(-1)$
```
#define PI acos(-1)
```
----
## 指對數系列
- exp(x) 自然底數 e 的 x 次方
- log(x) 以 e 為底的對數
- log10(x) 以 10 為底的對數
- log2(x) 以 2 為底的對數
要換底請用換底公式
----
## 比大小
### min/max
```cpp=
#include<algorithm>//min/max
min(a, b)
min({a, b, c})
max(a, b)
max({a, b, c, d});
```
----
## 交換值
```cpp=
#include<algorithm>
swap(a, b)
```
----
## GCD 最大公因數
(需 c++14 後才能使用)
```cpp
#includ<algorithm> //gcd
#include<iostream>
int main(){
int a, b; cin >> a >> b;
cout << __gcd(a, b) << endl;
}
```
$$
a \text{ 和 } b \text{ 的最小公倍數} = \cfrac{a \cdot b}{\gcd(a, b)}
$$
----
## fill
把區間 [l, r) 的值都設成 x,l, r 為指標或者迭代器
```
#include<iostream>
#include<algorithm>
int main(){
int n = 5;
int arr[8]={0,1,2,3,4,5,6,7};
fill(arr+1, arr+5, 10);
}
```
arr 的結果為 {0,10,10,10,10,5,6,7}
----
## sort 用法
排序區間 [l, r),l, r 為指標或者迭代器,複雜度為 $O(n\log n)$
```
sort(l, r);
```
### 範例
```cpp=
#include<iostream>
#include<algorithm> // sort
int arr[48763]={4, 8, 7, 6, 3};
int main(){
sort(arr,arr+5);
}
```
結果為 3, 4, 6, 7, 8
----
## 自訂比較關係
如果不想由小排到大,想由大排到小?
----
寫一個函式,
告訴編譯器你要怎麼排!
----
## compare function
傳入兩個參數 lhs, rhs,作為比較對象
lhs(left hand side) : 代表較左邊的元素
rhs(right hand side) : 代表較右邊的元素
可以想成希望在左邊的元素大於右邊的元素,
```cpp=
#include<iostream>
#include<algorithm> // sort
bool cmp(int lhs, int rhs){ // compare function
return lhs > rhs;
}
int arr[5] = {3, 2, 7};
int main(){
sort(arr, arr+3, cmp);
}
```
----
想省行數的話也可以寫在一起
```cpp
sort(arr, arr+3, [](int lhs, int rhs){
return lhs > rhs;
});
```
----
## 例題
### [UVA11321 — Sort! Sort!! and Sort!!!](https://onlinejudge.org/index.php?option=onlinejudge&page=show_problem&problem=2296)
給你 $n$ 個整數與整數 $m$,排序順序如下 :
1. 按照$\mod m$ 由小到大
2. 餘數相等則奇數在偶數前面
3. 如果同為奇數則由大到小
4. 如果同為偶數則由小到大
----
## 比較函式
1. 按照$\mod m$ 由小到大
2. 餘數相等則奇數在偶數前面
3. 如果同為奇數則由大到小
4. 如果同為偶數則由小到大
```cpp=
bool cmp(int lhs, int rhs){
if((lhs % m) != (rhs % m)) return (lhs % m) < (rhs % m);
if ((lhs%2) != (rhs%2)) return (lhs%2) > (rhs%2);
if(lhs%2) return lhs > rhs;
else return lhs < rhs;
}
```
----
## reverse
把區間 [l, r) 翻轉,l, r 為指標或迭代器
```cpp=
#include<algorithm> //reverse
int arr[5] = {1,2,3,4,5};
reverse(arr, arr+5);
```
arr 的結果為 \{5,4,3,2,1\}
----
## next / prev_permutation
- next_permutation 產出的下一個排列都會比原本的字典序還大,而 prev_permutation 則相反
- 如果一開始的排列是字典序最小的,就可以用 next_permutation 產生出全部的排列。
----
## 用法
產生區間 [l, r) 的下一個排列,若不存在則回傳 false,反之回傳 true
均攤複雜度 $\Omega(1)$,最差 $O(n)$
input
```
1 3 2
```
result
```
2 1 3
```
----
```cpp=
#include<algorithm>
int arr[4] = {3,2,4,1};
next_permutation(arr, arr+4);
```
result
```
3 4 1 2
```
----
### 產生所有排列
如果序列一開始為最小字典序,
透過 next_permutation 不斷變成下一大的順序,
可以產生所有排序
```cpp=
int n = 3;
int arr[3] = {1, 2, 3};
do{
for(int i = 0; i < n; i++) cout << arr[i] << " \n"[i+1==n];
} while(next_permutation(arr, arr + n));
```
#### 輸出:
```
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1
```
----
## 判斷/轉換字元
```cpp
#include<cctype>
```
- islower\(c\) 判斷字元 c 是否為小寫字母
- isupper\(c\) 判斷字元 c 是否為大寫字母
- isalpha\(c\) 判斷字元 c 是否為字母
- isdigit\(c\) 判斷字元 c 是否為數字
- tolower\(c\) 回傳字元 c 的小寫字母
- toupper\(c\) 回傳字元 c 的大寫字母
----
## nth_element
```cpp
nth_element(l, l+k, r)
```
將此區間 [l, r) 第 k 小的數放在左邊數來第 k 個位置,
且 [l , k) 皆比 k 小,(k , r) 皆比 k 大,
但這兩個區間內是無序的
----
## 範例
```cpp=
int arr[1e5+5]={};
bool cmp(int x,int y){
return x>y; //更改為降序排列
}
int main(){
for(int i=1;i<=100;i++) arr[i]=i;
nth_element(arr+1,arr+50+1,arr+100+1);
}
```
結果 : 1~49 (無序排列) , 50 , 51~100 (無序排列)
- 複雜度為平均 $O(n)$
- 可以加入第四個函數傳入自訂比較函數
```cpp
nth_element(l, l+k, r, cmp)
```
----
## 例題 --- [CF 412B](https://codeforces.com/problemset/problem/412/B)
題意:
給你 N 個數字,以及 K,求出第 K 大的數字是多少
```
6 4
100 20 40 20 50 50
```
答案為40
----
## 實作
```cpp=
#include <algorithm>
#include <iostream>
using namespace std;
int n, k, a[100005];
int main() {
cin >> n >> k;
for (int i = 0; i < n; i++) cin >> a[i];
nth_element(a, a + (k - 1), a + n, greater<int>());
cout << a[k - 1] << '\n';
return 0;
}
```
greater<> 為內建的大於比較函式
----
## lower_bound / upper_bound
### lower_bound(l, r, x)
找到區間 [l, r) 範圍內第一個**大於等於** x 的值的位置。
### upper_bound(l, r, x)
找到區間 [l, r) 範圍內第一個**大於** x 的值的位置。
<br>
以上兩個函數裡面所尋找的區間必須為**已排序**的才可以正常使用
如果沒找到則會回傳位置 r (最後一項的下一項)
----
## 範例
時間複雜度 $O(\log n)$
```cpp=
#include<iostream>
#include<algorithm>
using namespace std;
int a[100005];
int main(){
int n,x;
cin >> n;
for(int i = 0; i < n; ++i) cin >> a[i];
sort(a, a + n);
cin >> x;
cout << lower_bound(a, a + n, x) - a << '\n';
cout << upper_bound(a, a + n, x) - a << '\n';
// 通過找到的位置減去起始位置 begin, 得到索引值
return 0;
}
```
一樣可以使用自訂比較函式
lower_bound ( first , last , value , cmp)
upper_bound ( first , last , value , cmp)
----
## unique
把區間內連續相同元素刪除到只留一個,
回傳值為沒被刪除的元素的下一項指標/迭代器
複雜度 $O(n)$
input
```
4 2 1 1 1 5 2 2 4 4
```
result
```
4 2 1 5 2 4 x x x x
```
注意原陣列大小不會改變,相異元素之後的值無法保證為何
----
## unique 用法
unique(l, r) : 將區間 [l, r) 內連續相同元素刪除到只留一個
```cpp=
#include<iostream>
#include<algorithm>
int n = 10, arr[10] = {4, 2, 1, 1, 1, 5, 2, 2, 4, 4};
int main(){
int k = unique(arr, arr + n) - arr;
cout << k << endl;
for(int i = 0; i < k; i++)
cout << arr[i] << " \n"[i + 1 == k];
}
```
result
```
6
4 2 1 5 2 4
```
----
排序後再刪除,即可知曉整個陣列共出現幾種相異的數字
作法
1. sort 區間 [l, r)
2. unique [l, r) 並記錄回傳的指標/迭代器
----
## 找相異數字
結果為原始陣列**所有相異元素**並且**已排序**
```cpp=
#include<iostream>
#include<algorithm>
int n = 10, arr[10] = {4, 2, 1, 1, 1, 5, 2, 2, 4, 4};
int main(){
sort(arr, arr + n); // 1. sort 區間 [l, r)
int k = unique(arr, arr + n) - arr; // 2. unique [l, r) 並記錄回傳的指標/迭代器
cout << k << endl;
for(int i = 0; i < k; i++)
cout << arr[i] << " \n"[i + 1 == k];
}
```
result
```
4
1 2 4 5
```
----
此技巧常用於離散化 :
- 一種把資料的數值變小 (重新編號) 的技巧
- 不影響到任兩資料的大小關係
作法 :
1. 把一段的區間 [l, r) 的資料只留下相異的元素各一個
2. 再用原區間資料去做 lower_bound,並替換
----
## 陣列離散化
```cpp=
#include<iostream>
#include<algorithm>
int n = 10, arr[10] = {100, 5000, 100, 999999999, 42};
int tmp[10]; // 處理離散化的陣列
int main(){
for(int i = 0; i < n; i++) tmp[i] = arr[i]; // 把陣列站存在 tmp
sort(tmp, tmp + n);
int k = unique(tmp, tmp + n) - tmp; // 離散化後的長度
for(int i = 0; i < n; i++){
arr[i] = lower_bound(tmp, tmp + k, arr[i]) - tmp;
}
}
```
result
```
4
1 2 1 3 0
```
---
## Iterator & STL container
- pair / tuple
- iterator
- vector
- stack / queue
- set / multiset
- map / multimap
- priority_queue
- bitset
- list
----
### pair / tuple
分別在 \<utility\> 和 \<tuple\> 中
偷懶用資料結構,分別是可以用來宣告裝 2 個東西/多個東西的變數
----
### pair
常用來儲存二維平面上的點 (x, y) 之類
* 宣告
```cpp
pair<int,int>
pair<double,int>
```
* 取值
```cpp=
pair<int,int> a
a.first (可以取到第一個值)
a.second (可以取到第二個值)
```
----
對 pair 的元素 sort,會先比較 first 大小,相同再比較 second 的大小
```cpp=
#include<iostream>
#include<utility>
using namespace std;
pair<int, int> point[10];
int main(){
for(int i = 0; i < 5; i++) cin >> point[i].first >> point[i].second;
sort(point, point+5);
int a, b;
point[0] = make_pair(a, b);
}
```
----
### 使用範例:tuple
* 宣告
```cpp
tuple<int, int, int>
tuple<double, int, long double, ...(還可以塞很多東西)>
```
* 取值
```cpp=
tuple<int, int, int> a
get<0>(a) (可以取到第一個值)
get<2>(a) (可以取到第三個值)
```
----
make_tuple 可以把元素合併成 tuple
```cpp
#include<iostream>
#include<tuple>
using namespace std;
tuple<string, int, int, double> item[10];
int main(){
string s; int a, b; double d;
for(int i = 0; i < 5; i++){
cin >> s >> a >> b >> d;
item[i] = make_tuple(s, a, b, d);
}
sort(item, item+5);
tie(s, a, b, d) = item[0];
cout << s << " " << b << " " << d << " " << get<0>(item[1]) << endl;
}
```
----
### Iterator 迭代器
迭代器是用來遍歷容器的東西,由於有些容器不像陣列能用下標 (index) 遍歷,迭代器便能用來遍歷這類容器。
----
c++ 的迭代器皆被實作的與指標相似,用遞增運算子 (\+\+it \/ it\+\+) 來移動到下一項,且用反指標運算子(*it)來取得當前項的值。
多數還支援遞減運算子(\-\-it \/ it\-\-) 移動到前一項(稱作雙向迭代器)
有些支援隨機存取(random access), +\/- 運算子(it+k \/ it-k) 一次能移動多項
----
對於一個容器,分別有代表頭尾的迭代器,其中頭代表首項,而尾代表最後一項的下一項,因此遍歷方式通常是for(it=begin;it!=end;it++)
對於陣列 (C-style array),其迭代器就是指標,且頭尾分別是 arr/arr+n,對於 STL 內的容器,其迭代器是C::iterator,且頭尾分別是arr.begin() / arr.end()
此外STL內還有實作反向迭代器用來從最後一項開始往前遍歷,其形態是 C::reverse_iterator 頭尾分別是 arr.rbegin() / arr.rend()
----
### vector
```
#include<vector>
```
動態陣列。有時候我們無法預估陣列所需要宣告的長度,又必須宣告陣列,這時會選用 vector
----
- vector 的常用函式 : push_back , pop_back , size , empty , begin , end, clear, resize, assign
- 須注意的是若等到 vector 空間不夠裝,vector 會自動安排記憶體,但這麼做常數比較大,所以通常在使用前會先預設一些記憶體位置給 vector
* 我們會使用 reserve() 。e.g. `vector.reserve(100005)`
* 或是直接在宣告時用建構元 (constructor) 指定大小。 e.g. `vector<int> vec(n)`
- insert, erase 複雜度為 $O(n)$ ,請勿亂用
----
實際演練 : 常用函式
```cpp=
#include <iostream>
#include <vector> // vector 的標題檔
using namespace std;
int main() {
vector<int> Mega;
//宣告一個 vector 名稱叫 Mega 用來裝 int 元素
//裡面的容器是空的
for (int i=1; i<=5; i++) Mega.push_back(i);
for (int i=5; i>=1; i--) Mega.push_back(i);
cout << Mega.size() << "\n";
for (int i=1; i<=4; i++) Mega.pop_back();// 將 vector 尾端元素刪掉
cout << Mega.size() << "\n";
cout << Mega.empty() << "\n";
// empty() 函式為查詢 vector 裡面是否有元素
// 回傳值為 bool 1 裡面沒有元素 0 則是有
Mega.clear();
cout << Mega.empty() << "\n";
}
```
----
實際演練 : 使用 index 遍歷
```cpp=
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<char> roy;
for (int i=0; i<26; i++) {
char c = i + 'A';
roy.push_back(c);
}
for (int i=0; i<roy.size(); i++) {
cout << roy[i] << " ";
}
cout << roy[5] << "\n";
cout << roy[0] << "\n";
// 查詢單一個元素可用陣列方式查詢
}
```
----
實際演練 : 遍歷 iterator 遍歷
```cpp=
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> vt;
vector<int>::iterator iter;
for (int i=95; i<=100; i++) {
vt.push_back(i);
}
iter = vt.begin();
for(iter = vt.begin(); iter != vt.end(); iter++) {
cout << *iter << "\n";
// 一定要 「 * 」 號
}
}
```
----
### stack
```
#include<stack>
```
堆疊是一種先進後出的結構,像是疊盤子一樣,先放下去的會最慢被拿出來。
----
- stack的常用函式 : emplace(push) , size , empty , top , pop .
- 不支援隨機存取:O(N) 查找速度慢:O(N) 在集合中增刪元素很快:O(1)
----
實際演練:
```cpp=
#include <iostream>
#include <stack> // stack 的標題檔
using namespace std;
int main() {
stack<int> Stk;
//宣告一個 stack 名稱叫 stk 用來裝int元素
//一開始裡面的容器是空的
for (int i=4; i<=10; i++) {
Stk.push(i);
}
cout << Stk.top() << "\n";
// 輸出 stack 最上層的元素,所以會輸出 10
Stk.pop();
// 刪除 stack 最上層的元素
cout << Stk.top() << "\n";
// 輸出 stack 最上層的元素,所以會輸出 9
cout << "裡面有多少元素" <<Stk.size() << "\n";
// 查詢 stk 裡面有幾個元素,裡面元素有 4 5 6 7 8 9共六個,所以會輸出 6
cout << Stk.empty() << "\n";
// empty() 函式為查詢 stack 裡面是否有元素
// 回傳值為bool 1 裡面沒有元素 0 則是
//清除 stack的元素
while(Stk.size() != 0) { // 或是 while(Stk.empty == 0)
Stk.pop();
}
cout << "裡面有多少元素" << Stk.size() << "\n";
cout << Stk.empty() << "\n";
}
```
----
## 超經典例題 : 括弧匹配
**題目:**
給予一個字串並包含了括號 '[', ']', '(', ')' 和 '{', '}',請驗證該字串中的括號是否合法配對。也就是 "([])", "{()}", "[]" 為合法配對,但是 "(]", 或 "([)]" 就是不合法配對。
**規律:**
* 括號數量一定是偶數
* 有左邊系列括號,就一定有右邊系列括號
* 越慢出現的左邊括號,他的右邊括號就越早出現(順序相反)。
----
### queue
```cpp
#include<queue>
```
佇列是一種先進先出的結構,像是水管一樣的單向道,最早進去的會最先通出來。
----
- queue 的常用函式 : push , size , empty , front , pop .
- 不支援隨機存取:$O(N)$ 查找速度慢:$O(N)$ 在集合中增刪元素很快:$O(1)$
- 常用在 BFS
----
實際演練:
```cpp=
#include <iostream>
#include <queue> // queue 的標題檔
using namespace std;
int main() {
queue<int> que;
//宣告一個 queue 名稱叫 que 用來裝int元素
//一開始裡面的容器是空的
for (int i=4; i<=10; i++) {
que.push(i);
}
cout << que.front() << "\n";
// 輸出 queue 最上層的元素,所以會輸出 4
que.pop();
// 刪除 queue 最上層的元素
cout << que.front() << "\n";
// 輸出 queue 最上層的元素,所以會輸出 5
cout << "裡面有多少元素" <<queue.size() << "\n";
// 查詢 queue 裡面有幾個元素,裡面元素有 5 6 7 8 9 10共六個,所以會輸出 6
cout << que.empty() << "\n";
// empty() 函式為查詢 queue 裡面是否有元素
// 回傳值為bool 1 裡面沒有元素 0 則是
//清除 queue的元素
while(que.size() != 0) { // 或是 while(que.empty == 0)
que.pop();
}
cout << "裡面有多少元素" << que.size() << "\n";
cout << que.empty() << "\n";
}
```
----
### 經典例題環節: 用兩個 stack 去實作出 queue 的功能
**作法:**
- 始終維護 s1 作爲存儲空間,以 s2 作爲臨時緩衝區
- 入隊時,將元素壓入 s1
- 出隊時,將 s1 的元素逐個“倒入”(彈出並壓入)s2,將 s2 的頂元素彈出作爲出隊元素,之後再將 s2 剩下的元素逐個“倒回” s1

----
### priority_queue
可以很快很方便的加入元素 或 取出當前最優先的元素
常用於優化複雜度
----
* 在標頭檔 queue 裡面
* priority_queue 裡面可以塞 int , string , long long , 實作原理是 max_heap 在運作
* 為一序列容器 是一個帶優先級的 queue
* priority_queue 的常用函式 : push , size , empty , top , pop .
----
### priority_queue 複雜度分析
* push() 新增元素,複雜度 $O(\log n)$
* size() 取得目前元素數量,複雜度 $O(1)$
* top() 取得目前最高優先(數值最大)的元素(不會刪掉),複雜度 $O(1)$
* pop() 刪掉目前最高優先(數值最大)的元素,複雜度 $O(\log n)$
----
實際演練:
```cpp=
#include <queue>
priority_queue<int> pq;
priority_queue<int, vector<int>, greater<int> > pq1; //從小到大 變成min_heap
pq.push(5);
pq.push(8);
pq.push(4);
// 元素可重複,會個別保留
pq.push(5);
cout << pq.size() << "\n";
cout << pq.top() << "\n";
pq.pop();
cout << pq.size() << "\n";
cout << pq.top() << "\n";
cout << pq.empty() << "\n";
```
----
### 題目演練: 動態中位數
對每一個輸入,請輸出到現在為止已輸入的數的中位數。
```
輸入: 輸出:
1 1
3 2
4 3
60 3
70 4
50 27
2 4
```
----
## 中位數
維護兩個 pq(priority_queue),
其中一個 pq 維護大於等於中位數的數值
另一個 pq 維護小於中位數的數值
如此一來,中位數必定為大於等中位數那堆的最小值
----
## 取中位數
大於等中位數的 pq 每次取最小值
即為中位數
```cpp
priority_queue<int, vector<int>, greater<int>> big;
cout << big.top() << endl;
```
----
## 加入一個新元素
判斷當前加入的新元素 x 如果小於中位數,則加入維護小的 pq
否則加入維護大的 pq
```cpp
if(x < big.top()) small.push(x);
else big.push(x);
```
----
## 維護兩個 pq 的大小
為了讓中位數固定在比較大的那堆的最小值,
兩堆 pq 的大小至多只能差一
因此每次新增完元素,
如果小的那堆太多要把最大的元素丟過去大的那堆
反過來則大的最小丟去小的那堆
```cpp
priority_queue<int> small;
priority_queue<int, vector<int>, greater<int>> big;
while(small.size() >= big.size()) big.push(small.top()), small.pop();
while(small.size()+1 < big.size()) small.push(big.top()), big.pop();
```
----
### set (集合)
```
#include<set>
```
- 插入一個元素
- 從集合中刪除一個元素
- 查詢元素有沒有在集合裡面
- 查詢集合內最小/大值
- 查詢集合內 $> x$ 或 $\ge x$ 中的最小元素
----
- set 是有序的,對 set 遍歷時,會以定義的大小順序遍歷,預設是由小到大
- 大多數操作皆為 $O(\log N)$
- set 的常用函式:insert, erase, count, size, empty, begin, end, lower_bound, upper_bound
----
### 使用範例: 插入、刪除元素
```cpp
set<int> s; // []
s.insert(1); // s = [1]
s.insert(5); // s = [1, 5]
s.insert(3); // s = [1, 3, 5]
s.insert(2); // s = [1, 2, 3, 5]
s.insert(1); // s = [1, 2, 3, 5]
s.erase(3); // s = [1,2,5]
```
----
### 使用範例: 插入、查詢元素
```cpp
set<pair<int,int> > s; //[]
s.insert({1, 2}); // [{1, 2}]
s.insert({2, 1}); // [{1, 2}, {2, 1}]
s.insert({2, 1}); // [{1, 2}, {2, 1}]
s.insert({3, 5}); // [{1, 2}, {2, 1}, {3, 5}]
s.count({2, 1}); // return 1,代表 {2, 1} 有在集合內;
s.count({5, 3}); // return 0
```
----
### 使用範例: 插入元素、找最小/大值
```cpp
set<int> s; // []
s.insert(1); // s = [1]
s.insert(5); // s = [1, 5]
s.insert(3); // s = [1, 3, 5]
s.insert(2); // s = [1, 2, 3, 5]
cout << *s.begin() << endl; // 1
cout << *s.rbegin() << endl; // 5
```
----
### 使用範例: 插入元素、找第一個大於等於 x 的元素
如果沒找到則回傳 end()
```cpp
set<int> s; // []
s.insert(1); // s = [1]
s.insert(5); // s = [1, 5]
s.insert(3); // s = [1, 3, 5]
s.insert(2); // s = [1, 2, 3, 5]
if(s.lower_bound(3) != s.end())
cout << s.lower_bound(3) << endl;
else
cout << "NOT FOUND" << endl;
```
----
### 使用範例: 插入元素、找第一個大於 x 的元素
如果沒找到則回傳 end()
```cpp
set<int> s; // []
s.insert(1); // s = [1]
s.insert(5); // s = [1, 5]
s.insert(3); // s = [1, 3, 5]
s.insert(2); // s = [1, 2, 3, 5]
if(s.lower_bound(3) != s.end())
cout << s.upper_bound(3) << endl;
else
cout << "NOT FOUND" << endl;
```
* lower_bound(s.begin(), s.end(), x) **複雜度為 $O(n)$**
* s.lower_bound(x) 複雜度才是 $O(\log n)$
----
### map
```
#include<map>
```
映射,基本上就是多了個對應值的set,單位型態為 pair<key,value>
用法跟 set 很像,但多了 operator[] 可以取值或存值。
----
* 大多操作皆為 $O(\log n)$
* map 的常用函式:insert, erase, count, size, empty, begin, end, lower_bound, upper_bound.
----
### 使用範例:
```cpp
map<int, int> mp;
mp[3] = 1; // [3] = 1,
mp[2] = 2; // [2] = 2, [3] = 1;
mp[3] = 5; // [2] = 2, [3] = 5;
```
----
### 使用範例:
```cpp
map<string,int> mp; // map 還可以用 string 當 key
mp["qwer"] = 1;
mp["abc"] = 2;
```
----
### multiset / multimap
* key 可以重複的 set / map
* 由於 key 會重複,因此少了 [ ]operator
* 大多操作皆為 $O(\log n)$
* 函式與 set / map 類似
----
### 使用範例:
```cpp
multiset<int> s; // []
s.insert(1); // [1]
s.insert(2); // [1, 2]
s.insert(3); // [1, 2, 3]
s.insert(1); // [1, 1, 2, 3]
s.count(1); // return 2;
s.erase(1); // [2, 3]
```
使用 erase(x) 會把全部值為 x 的元素刪除
如果只要刪除一個可以使用 s.erase(s.find(x)) 刪除
----
### 使用範例:
```cpp
multimap<int, string> mp; // []
mp.insert({1, "c8763"});
mp.insert({1, "38763"});
mp.insert({2, "1919"});
mp.insert({4, "8100"});
mp.count(1); // return 2
```
----
### bitset
```cpp
#include<bitset>
```
可看作是 bool 陣列,支援位元運算,且成員函數多為位元相關函數,
用在位元運算常數非常小,
沒有要做位元運算別亂用(用 vector\<bool\> 就好)
----
- 但長度必須是**常數**
- 常用函式:count、size、test、any、none、all、set、reset、flip。
----
### 使用範例:
```cpp
#include<bitset> //bitset
#include<iostream>
bitset<10> a = 7;
bitset<10> b(10);
bitset<10> c("10010");
int main(){
b = 5; //101
b >>= 1; //10
cout << b << endl;
cout << (a|c) << " " << (a&c) << " " << (a^c) << endl;
}
```
----
## list
```cpp
#include<list>
```
C++ 內建的 linked-list,可以在 O(1) erase, insert
但沒有支援隨機存取(random access), 無法使用 index 查詢
常用函數: push_back, push_front, pop_back, pop_front, insert, erase, size
----
## 範例
```cpp
#include<list>
#include<iostream>
using namespace std;
int main(){
list<int> arr;
arr.push_back(3); // [3]
arr.push_back(4); // [3, 4]
list<int>::iterator iter = prev(arr.end()); // 儲存 arr 最後一個元素的iterator
arr.push_back(1); // [3, 4, 1]
arr.push_front(10); // [10, 3, 4, 1]
arr.erase(iter); //[10, 3, 1]
}
```
----
## 例題: 刪除元素
- 給一個長度為 $n$ 的排列,有 $q$ 筆操作,操作為每次把數字 x 以及左右相鄰的元素刪除,求最後序列的樣子
input
```
8 2
1 5 8 3 6 4 7 2
3
4
````
output
```
1 2
```
[1 5 8 3 6 4 7 2] $\to$ [1 5 4 7 2] $\to$ [1 2]
----
暴力找某個元素的位置最差情況需要遍歷整個序列 $O(n)$
但太慢了
因此通常會搭配 map,把每個元素儲存在 list 中的位置儲存在 map 中
```cpp
#include<list>
#include<iostream>
using namespace std;
int main(){
list<int> arr;
map<int, list<int>::iterator> mp;
int n; cin >> n;
for(int i = 0, x; i < n; i++){
cin >> x;
arr.push_back(x);
mp[x] = prev(arr.end());
}
}
```
----
### 移除操作: 把數字 x 以及左右相鄰的元素刪除
```cpp=
void remove(int x){
list<int>::iterator it = mp[x];
if(it != arr.begin()) arr.erase(prev(it));
if(next(it) != arr.end()) arr.erase(next(it));
arr.erase(it);
}
```
----
map 維護儲存、查詢為 $O(\log n)$,
$q$ 筆操作總複雜度為 $O(q\log n)$
---
## 時間複雜度範例分析
----
```cpp
for(int i = 10; i < n; i++){
for(int j = i - 100; j >= 1000; j--)
cout << i << " " << j << '\n';
}
```
$$
O(n^2)
$$
<!-- .element: class="fragment" data-fragment-index="1" -->
----
```cpp
for(int i = 0; i < 100000000; i++)
cin >> n, cout<< n * n << '\n';
```
$$
O(1)
$$
<!-- .element: class="fragment" data-fragment-index="1" -->
----
```cpp
for(int i = 1; i <= n; i++){
sort(arr, arr + i); // <algorithm> library 的通用排序
}
```
$$
\sum\limits_{k=1}^{n}k\lg k\approx n^2\lg n=O(n^2\lg n)
$$
<!-- .element: class="fragment" data-fragment-index="1" -->
這要視排序法而定,但是原函式還是 $O(n^2\lg n)$
<!-- .element: class="fragment" data-fragment-index="2" -->
----
```cpp
for(int i = 1; i < n; i = i * 2){
for(int j = i; j < n; j += i)
arr[j] += arr[j - i];
}
```
$$
n+\frac{n}{2}+\frac{n}{4}+...\approx 2n=O(n)
$$
<!-- .element: class="fragment" data-fragment-index="1" -->
----
```cpp
void f(int n){
if(n == 0) return 1;
return f(n - 1) * n;
}
```
$$
O(n)
$$
<!-- .element: class="fragment" data-fragment-index="1" -->
----
```cpp
void f(int i){
if(i==n) return;
v[i] = 0, f(i+1);
v[i] = 1, f(i+1);
v[i] = 2, f(i+1);
}
```
$$
O(3^n)
$$
<!-- .element: class="fragment" data-fragment-index="1" -->
----
```cpp
set<int> s;
for(int i = 0, tmp; i < n; i++){
cin >> tmp;
s.insert(tmp);
}
```
$$
\begin{split}
\sum_{k=1}^{n}\log k
&=\log 1+\cdots+\log n=\log (1\times\cdots\times n)\\
&\leq \log n^n=n\log n=O(n\log n)
\end{split}
$$
<!-- .element: class="fragment" data-fragment-index="1" -->
---
建議每種 STL 語法都套過題目一遍試試看,以免比賽中要用到不會用
不用硬背起來,要用到再查語法,多寫幾次自然後就記得了。
[Problem List](https://vjudge.net/contest/749722#overview)
{"description":"Commonly Used Function","title":"Time Complexity & STL","slideOptions":"{\"type\":\"slide\",\"slideOptions\":{\"transition\":\"fade;\"}}","image":"https://hackmd.io/_uploads/H1wFwARqxx.png","contributors":"[{\"id\":\"4f67a8cd-06ae-45dc-a8e3-62c6a41e5a37\",\"add\":26761,\"del\":3089,\"latestUpdatedAt\":1760622194864}]"}