---
tags: 成大高階競技程式設計 2020, ys
---
:+1: [2020 競技程設教材 HackMD Book](https://hackmd.io/@nckuacm/ryLIV6BYI) :+1:
2020 Week 2: I/O & Standard Template Library
=
接下來的章節,採用 **C++** 作為主要的程式語言,原因是相對於其他主流的高階語言(e.g. Python, JavaScript...),C++ 作為編譯語言有相對快的執行速度,且在某些細節操作上更加自由。不過在部分時間或空間限制較寬鬆的題目中採用 Python 解題可以提升解題速度,這部分就交給讀者自行探索。
# Input/Output
在 C++ 中,主要有兩種教派(?),`std::cin`, `std::cout` 派與 `scanf`, `printf` 派
在一般情況,兩種輸出入函式可以混用,他們會**同步**使用同一塊緩衝區。
但這樣對於 `cin/cout` 的負擔很大[^sync],所以<B>==純用==</B> `cin/cout` 的話建議在使用前加入這行:
```cpp
std::ios_base::sync_with_stdio(false);
```
預設中在執行 `cin` 之前 `cout` 會直接 flush (將緩衝區內容輸出到螢幕),也建議關掉這功能:
> 有大量的 I/O 將會拖慢時間
```cpp
std::cin.tie(NULL);
```
而 C++ 有個換行操作是 `std::endl`,將會強制進行 flush,建議也用 `'\n'` 取代
# 資料型態的範圍
在撰寫程式時要估算資料大小,以選擇適合的資料型別,就算是身經百戰的選手也可能因沒有小心估算範圍而得到 WA 或 RE,以下給出 C++ 中常用型別以及**大約範圍**:
`int x`: $|x| \le 2 \times 10^9$
`long long x`: $|x| \le 9 \times 10^{18}$
`float x`: 共 6 位精確度
- 例如 $123.456789$ 後面的 $789$ 是不準確的
> 純整數若超過 $± 8 \times 10^{6}$ 會不精確
`double x`: 共 15 位精確度
> 純整數若超過 $± 4 \times 10^{15}$ 會不精確
`double` 與 `float` 的精確度取決**整數位數+小數位數**,若同時存過大且對小數精確度要高的數時,可能會出現誤差。
> 對浮點數實作及精確值有興趣的話可翻閱 [IEEE 754](https://zh.wikipedia.org/wiki/IEEE_754) 規範
第 15 週會討論到如何解決因精度不夠而產生的問題
#### 練習:
[CODEFORCES 913A Modular Exponentiation](https://codeforces.com/contest/913/problem/A)
# 演算法的效率
>演算法的設計,得建立在資料結構之上,並評估**時間**與**空間**上的效率
題目給定輸入規模通常很大,2 倍、3 倍、甚至 10 倍的常數倍優化其實不是競賽時考慮的要點。
我們所設計的演算法必須根據輸入規模 $N$ 而定。
* Big $O$ 表示法
$f(N) = O(g(N)) \iff \exists N_0,c>0. \forall N>N_0.|f(N)|\leq c\cdot |g(N)|$
意思是說在 $N$ 足夠大的時候,存在正數 $c$ 使得 $c\cdot |g(N)|$ 大於 $|f(N)|$
或是說 $g(N)$ 趨近無窮的成長速度不比 $f(N)$ 來得慢
假設寫的程式碼長這樣:
```cpp
for(int i = 0; i < n; i++)
dp[0][i] = dp[i][0] = 0;
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++)
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]) + a[i][j];
cout << dp[n][n] << "\n";
```
對應的時間估計函數就是 $f(x)=x^2 + x + 1$
在 $x$ 很大的時候,主要影響整個函數值的大小是平方項,這時可以說 $f(N) = O(N^2)$[^1]
假設輸入規模為 $N$,常見的複雜度有:
$O(1)\leq O(\log_2N)\leq O(N)\leq O(N\log_2N)\leq O(N^k)\leq O(k^N)\leq O(N!)\leq O(N^N)$
> $k$ 為不受輸入規模影響的常數
## 合理的複雜度
> 通常無特別強調,複雜度都暗指**時間**複雜度。但**空間**複雜度的估計也很重要
### 空間複雜度
記憶體空間的用量在各個比賽中規範都不相同
但有些基本的考慮因素,例如 遞迴深度,使用的變數多寡,程式碼長度[^sloc]
### 時間複雜度
普遍上題目都會以秒為單位去做時間限制 (e.g. 3 秒、10 秒)
但通常我們會直接考慮輸入資料的規模與計算出來的複雜度
有個傳統(?)的範圍:$10^7$ 左右
> 這只是大約的估計範圍
假設輸入規模為 $N$,演算法的複雜度為 $O(N^2\log_2 N)$
那麼需要滿足 $N^2\log_2 N \leq 10^7$
具體一點,上面如果 $N=10^5$,那你就得重新設計演算法了。
# 資料結構及 STL 介紹
資料結構 (data structure) 是種對資料的有系統的整理,
資料結構是為了令在其之上運作的演算法能更好的進行操作,或為了提昇演算法的效率,甚至是方便解釋演算法的運作(抽象化)。
每種資料結構通常是針對:**插入**,**刪除**,**修改**,**查詢**[^crud] 的效率及操作上的追求。
> 所以每個容器通常都有**相似**的函式能用,**函式名稱**或許也一樣。
STL 全名 Standard Template Library
由容器 (Containers)、迭代器 (Iterators)、演算法 (Algorithms)、函式 (Functions) 組成。
下面會陸續介紹幾個常用的 STL 裡的容器及函式
> 在使用前請先行測試過,以及了解其複雜度
絕大部分 STL 的東西只要涉及區間的操作,==區間表示一律為左閉右開==[^start_at_zero]
其中 `std::queue`, `std::stack`, `std::list` 先給出簡易實作,再介紹在 STL 裡的用法。
推薦的參考網站: [cplusplus.com](http://www.cplusplus.com/)、[C++ reference](https://en.cppreference.com)
## Iterator
假設容器 `C`,已經裝了一些元素,若想遍歷 `C` 中的所有元素,那要如何做到呢?
有些容器是沒有 index 可以隨機存取的 (例如: `std::list`),
為處理此問題,STL 為每個容器提供一個成員型別:**迭代器 (Iterator)**
可用"指標"的概念理解迭代器
>實際上,指標算是一種迭代器
假設迭代器 `it`,存取 `it` 所指向的內容,就用 `*it`
迭代器有 3 類:
- 隨機存取迭代器:能夠和整數的加減法,後移 $n$ 項、前移 $n$ 項
- 雙向迭代器:只能做遞增 (`++`)、遞減 (`--`)
- 單向迭代器:只能做遞增 (`++`)
STL 容器有 `begin()` 與 `end()` 成員函式,它倆都會回傳個迭代器。
> 由於左閉右開 `end()` 的迭代器位置落在容器**最尾端迭代器**的後面
## vector
vector 跟一般在使用的陣列很像,最大的特色就是他的儲存空間是動態增減的
### `std::vector`
```cpp
#include <vector>
using std::vector;
```
#### 宣告:
`vector<T> v`: `v` 為空的 vector,且各元素型別為 `T`
`vector<T> v(size_type a)`: `v` 長度為 `a`
`vector<T> v(size_type a, const T& b)`: `v` 會被 `a` 個 `b` 填滿
#### 函式:
`v.size()`: `v` 目前的長度
`v.push_back(T a)`: 在 `v` 的結尾加一個 `a`
`v.pop_back()`: 刪除 `v` 的最末項[^pop_back]
`v.empty()`: 是否 `v` 為空
```cpp
int a;
do {
cin >> a;
v.push_back(a);
} while (a);
cout << v.size() << '\n';
v.pop_back();
v.pop_back();
cout << v.size() << '\n';
cout << (v.empty()? "YES" : "NOT EMPTY");
```
```
> 5 2 8 9 1 2 0
< 7
< 5
< NOT EMPTY
```
`v[i]`: `v` 的第 `i` 項
`v.clear()`: 清空 `v`
`v.resize(size_type a)`: 將 `v` 的長度調至 `a`
```cpp
v.resize(4);
v.resize(8, 100);
v.resize(12);
for (int i = 0; i < v.size(); i++) cout << v[i] << ' ';
cout << '\n';
v.clear();
cout << "size = " << v.size();
```
```
< 5 2 8 9 100 100 100 100 0 0 0 0
< size = 0
```
`v.assign(InputIterator l, InputIterator r)`: 將指標 `l` 到 `r` 的內容覆蓋至 `v`
`v.assign(size_type n, const value_type& val)`: 覆蓋 `n` 個 `val` 至 `v`
```cpp
v.assign(7, 100); // 7 ints with a value of 100
cout << "#1: " << v.size() << '\n';
vector<int>::iterator it = v.begin() + 1;
v.assign(it, v.end()-1);
cout << "#2: " << v.size();
```
```
< #1: 7
< #2: 5
```
## string
字串 (string),顧名思義是很多個字它們串在一起
例如: "stringisastring",就是一堆字 's', 't', 'r', 'i', 'n', 'g', 'i', ..., 'g' 串在一起。
字串可像序列一樣表示, 例如長度為 $n$ 的字串 $A$ 為: $A_1, A_2, ... , A_{n-1}, A_n$
字串的問題與序列問題的區別在於字串通常探討的是字元前後**連續**的特性。
### `std::string`
string 的用法很像 `vector<char>`
但多了一些優化,以及功能、且 **`vector<char>` 有的 `string` 都有**
```cpp
#include <string>
using std::string;
```
若 `t` 是 `string` 或 [C style string](http://archive.oreilly.com/oreillyschool/courses/cplusplus1/cplusplus106.html) 則:
#### 運算:
`s = t`: 會將 `t` 複製給 `s`
`s += t`: 在 `s` 的尾端接上 `t`
#### 函式:
`cin >> s`: 輸入字串至 `s`
`cout << s`: 輸出 `s`
`getline(cin, s, char c)`: 輸入字串至 `s`,直到讀到 `c`。`c` 預設為 `'\n'`
```cpp
getline(cin, s, '\n');
t = " and"; // t is a string now
s += t;
s += " carry on";
cout << s;
```
```
> keep calm
< keep calm and carry on
```
`s == t`: 是否 `s` 跟 `t` 相同
`s.c_str()`: 回傳 `s` 的 C style string
```cpp
char cstr[100];
strcpy(cstr, s.c_str());
cstr[8] = 'l';
cstr[9] = '\0';
cout << cstr << '\n';
cout << (cstr == s? "YES" : "NO");
```
```
< keep call
< NO
```
#### 範例 [UVa 101 The Blocks Problem](https://uva.onlinejudge.org/external/1/101.pdf):
:::spoiler
>挺複雜的題目
將 4 道指令拆解成兩種指令的合成,也就是 `return_above` 以及 `move_to`
- `return_above(a)`: 將特定 block `a` 上方的 blocks 全數返回到原來的位置。
- `move_to(a, b)`: 將 block `a` 及其上所有 blocks 移動到 block `b` 所在的位置最上方。
宣告 `vector<int> pile[maxn];`
以 `pile[p]` 紀錄位置 `p` 上的所有 blocks (從 0 開始由下而上)
```cpp
#include<iostream>
#include<string>
#include<vector>
using namespace std;
const int maxn = 26;
int n, pos[maxn]; // pos := position
vector<int> pile[maxn];
int height(int obj) {
int i = 0, p = pos[obj];
for(; pile[p][i] != obj; i++);
return i + 1;
}
void return_above(int obj) {
int p = pos[obj], h = height(obj);
for(int i = h; i < pile[p].size(); i++) {
int w = pile[p][i]; // w := wood
pile[w].push_back(w);
pos[w] = w;
}
pile[p].resize(h);
}
void move_to(int a, int b) {
int ap = pos[a], bp = pos[b];
int ah = height(a);
for(int i = ah-1; i < pile[ap].size(); i++) {
int w = pile[ap][i]; // w := wood
pile[bp].push_back(w);
pos[w] = bp;
}
pile[ap].resize(ah-1);
}
int main() {
cin >> n;
for(int i = 0; i < n; i++) {
pos[i] = i;
pile[i].push_back(i);
}
int a, b;
string c1, c2;
while(cin >> c1 && c1 != "quit") {
cin >> a >> c2 >> b;
if(pos[a] == pos[b]) continue;
if(c1 == "move") return_above(a);
if(c2 == "onto") return_above(b);
move_to(a, b);
}
// output
for(int i = 0; i < n; i++) {
cout << i << ":";
for(int j = 0; j < pile[i].size(); j++) cout << " " << pile[i][j];
cout << endl;
}
return 0;
}
```
學習重點:
- 長度(或高度) 與最後一位 index 之間的關係 (尤其從 0 開始數)
- 化繁為簡,將重複性高的功能抽象(函式化)出來
:::
#### 練習:
[CODEFORCES 1287A Angry Students](https://codeforces.com/problemset/problem/1287/A)
[CODEFORCES 1301A Three Strings](https://codeforces.com/problemset/problem/1301/A)
[CODEFORCES 1144A Diverse Strings](https://codeforces.com/problemset/problem/1144/A)
[CODEFORCES 1243B1 Character Swap (Easy Version)](https://codeforces.com/problemset/problem/1243/B1)
\*[CODEFORCES 1304B Longest Palindrome](https://codeforces.com/problemset/problem/1304/B)
\*[CODEFORCES 1243B2 Character Swap (Hard Version)](https://codeforces.com/problemset/problem/1243/B2)
## sort
將一堆元素由"*給定規則*"排成一[順序](https://en.wikipedia.org/wiki/Order_theory),稱為排序 (sort)。
例如:
`5, 6, 9, 8, 2` 這五個元素由小到大排為 `2, 5, 6, 8, 9`
`a, bc, aa, ay` 這四個元素由字典順序排為 `a, aa, ay, bc`
### `std::sort`
STL 的 `sort` 裡有複雜的優化,**預設**將容器元素由==小排至大==
```cpp
#include <algorithm>
using std::sort;
```
假設有如下資料結構:
```cpp
struct T { int val, num; };
vector<T> v;
```
若想依 `num` 對 `v` 做排序,需寫**比較**函數
比較函數是在定義"**小於**"**運算**:
```cpp
bool cmp(const T &a, const T &b)
{ return a.num < b.num; }
```
> 或將小於運算子 (`operator<`) 重載也能達到一樣的效果
將 `cmp` 做為參數:
```cpp
sort(v.begin(), v.end(), cmp);
```
當然也可以直接把匿名函數寫進去:
```cpp
sort(v.begin(), v.end(), [](T a, T b) { return a.num < b.num; });
```
順帶一提,
若把小於行為內部定義為"大於":
```cpp
[](T a, T b) { return a.num > b.num }
```
則排序為從大至小。
#### 範例 [CODEFORCES 1114B Yet Another Array Partitioning Task](https://codeforces.com/contest/1114/problem/B):
:::spoiler
> 乍看之下有夠難欸
仔細觀察發現,
$k$ 個切出來的 subarray 中最大的 $m$ 個元素,收集起來恰好就是原 array 中前 $k\times m$ 大的元素
所以將它們加起來就能得到所有 beauty 的總和
```cpp
#include<bits/stdc++.h>
using namespace std;
int const maxn = 2e5 + 100;
int n, m, k, a[maxn];
vector<int> idx;
int main() {
scanf("%d%d%d", &n, &m, &k);
for(int i = 0; i < n; i++) {
scanf("%d", &a[i]);
idx.push_back(i);
}
long long sum = 0;
sort(idx.begin(), idx.end(), [&](int x, int y) { return a[x] > a[y]; });
for(int i = 0; i < m*k; i++) sum += a[idx[i]];
printf("%lld\n", sum);
:
.
```
因為要求輸出間隔的 index 位置,所以我們需要原本的 index 位置
將 `idx` 再排序一遍,就能穩穩輸出來了 :+1:
```cpp
:
.
sort(idx.begin(), idx.begin() + m*k);
for(int i = m-1; i < m*(k-1); i+=m) printf("%d ", idx[i]+1);
putchar('\n');
return 0;
}
```
學習重點:
- 凡事先試試 sort,想想有什麼好處,通常狀況只會變好
- `std::sort` 複雜度 $O(N\log_2 N)$,大部份競賽中並不會比 $O(N)$ 壞很多
- `for` 迴圈中大於 $1$ 以上的累加
:::
#### 練習:
[Kick start Round A 2019 A Training](https://codingcompetitions.withgoogle.com/kickstart/round/0000000000050e01/00000000000698d6)
[Kick start Round F 2018 A Common Anagrams](https://code.google.com/codejam/contest/3314486/dashboard#s=p0)
[CODEFORCES 1294B Collecting Packages](https://codeforces.com/problemset/problem/1294/B)
[CODEFORCES 1107C Brutality](https://codeforces.com/contest/1107/problem/C)
## queue
要搭公車之前,都是先*排隊*進入公車站,等到公車到來時,再從公車站出去搭上公車;
由此,公車站可以作為一種資料結構,人可類比為欲儲存之資料
![](https://i.imgur.com/zqr4zuM.png) [^4]
此種資料結構稱作*隊列* (Queue);擁有**先進先出 (First In, First Out)** 的特性。
例如在郵局要辦事,會先從機器拿號碼紙等叫號,這時候就是用隊列去儲存編號。
下面給出隊列的簡易實作程式碼:
```cpp
int Q[maxn], front_idx = 0, back_idx = 0;
void enqueue(int data)
{ Q[back_idx++] = data; }
int front()
{ return Q[front_idx]; }
bool empty()
{ return front_idx == back_idx; }
void dequeue()
{ if(!empty()) front_idx++; }
```
操作多次 `enqueue` 與 `dequeue`,會使得 `front_idx` 最終會碰到陣列邊界,這樣會導致能儲存的資料量變小。
觀察發現,當 `front_idx` 增加,相當於丟掉以前的資料,這時候就有舊的空間空出來了,該如何使用這塊舊空間呢?
直接的,讓 `back_idx` 碰到邊界後回去初始位置就可以了: 這種資料結構稱作*環狀隊列*
還有種變種隊列,叫做*雙端隊列*(**D**ouble **e**nded **que**ue),他可以從前面或後面 `enqueue` 或 `dequeue`。
### `std::queue`
```cpp
#include <queue>
using std::queue;
```
#### 宣告:
`queue<T> q`: `q` 為空的隊列,且各元素型別為 `T`
#### 函式:
`q.front()`: 第一個進入 `q` 的元素
`q.back()`: 最後一個進入 `q` 的元素
`q.push(T a)`: 將 `a` 加入 `q` 中 (enqueue)
`q.pop()`: 將第一個進入 `q` 的元素移除 (dequeue)
`q.empty(), q.size()`: `q` 當然也有這兩個函式
```cpp
queue<int> q; // []
q.push(1); // [1]
q.push(2); // [1, 2]
cout << q.front() << ' '; // 1
q.push(3); // [1, 2, 3]
cout << q.front() << ' '; // 1
q.pop(); // [2, 3]
q.pop(); // [3]
cout << q.front(); // 3
q.pop(); // []
```
```
< 1 1 3
```
#### 練習:
[UVa OJ 10935 Throwing cards away I](https://uva.onlinejudge.org/external/109/10935.pdf)
[UVa OJ 12100 Printer Queue](https://uva.onlinejudge.org/external/121/12100.pdf)
[Zero Judge c700 壞掉的隊列(queue)](https://zerojudge.tw/ShowProblem?problemid=c700) (建議讀完 stack 再來練習本題)
## stack
考慮將鬆餅每次只放一片疊到盤子上,則最後放上去的鬆餅將會是最上層的鬆餅,如果要吃會依序從最上層開始拿;
由此,盤子可以作為一種資料結構,鬆餅可類比為欲儲存之資料
![](https://i.imgur.com/iPdRCnM.png)[^5]
鬆餅(資料)的放、拿法,是種稱作*堆疊* (stack) 的資料結構;有**後進先出 (Last In, First Out)** 特性,
下面給出堆疊的簡易實作程式碼:
```cpp
int S[maxn], top_idx = -1;
void push(int data)
{ S[++top_idx] = data; }
int top()
{ return S[top_idx]; }
bool empty()
{ return top_idx == -1; }
void pop()
{ if(!empty()) top_idx--; }
```
相較於隊列的簡易實作版,堆疊不用擔心**已用過的**空間會永遠用不到
### `std::stack`
```cpp
#include <stack>
using std::stack;
```
#### 宣告:
`stack<T> st`: `st` 為空的堆疊,且各元素型別為 `T`
#### 函式:
`st.top()`: 存取最後一個進入 `st` 的元素
`st.push(T a)`: 將 `a` 加入 `st` 中
`st.pop()`: 將最後一個進入 `st` 的元素移除
```cpp
stack<int> st; // []
st.push(1); // [1]
st.push(2); // [1, 2]
st.push(3); // [1, 2, 3]
cout << st.top() << ' '; // 3
st.pop(); // [1, 2]
cout << st.top() << ' '; // 2
st.pop(); // [1]
cout << st.top(); // 1
st.pop(); // []
```
```
< 3 2 1
```
#### 練習:
[UVa OJ 514 Rails](https://uva.onlinejudge.org/external/5/514.pdf)
[UVa OJ 673 Parentheses Balance](https://uva.onlinejudge.org/external/6/673.pdf)
[UVa OJ 271 Simply Syntax](https://uva.onlinejudge.org/external/2/271.pdf)
[UVa OJ 11234 Expressions](https://uva.onlinejudge.org/external/112/11234.pdf)
## list
玩撲克牌遊戲時,常會有將牌**拿起**與將牌**插**到某個位置的動作
支援這兩個行為的資料結構稱為"連結串列 (Linked list)"[^doubly_linked_list]
![](https://i.imgur.com/6VeOzJd.png)[^6]
連結串列比較複雜點,需要造出兩種結構:
1. 定義一個結構叫做"節點(node)",它可儲存**資料**及下個節點的**位置**
```cpp
struct node {
int data;
node *prev, *next;
} *list[maxn];
```
2. 當資料都放進節點後,要將節點們串起來
其中 `list[0]` 不當一般資料 (`.data`) 使用,它用來**指向**連接串列的頭
```cpp
for (int i = 0; i < N; i++) {
node *p = new node;
if(i) { // 0 or others
list[i-1]->next = list[i] = p;
list[i]->prev = list[i-1];
} else list[0] = p;
}
list[0]->prev = list[N-1];
list[N-1]->next = list[0];
```
在最後將 `list[0]`(head pointer) 與串列尾部連接起來,是為了下面兩個函式寫法上的方便(?
當擁有這樣的結構,拔除節點和插入節點只需 $O(1)$:
```cpp
void remove(int idx) {
list[idx]->prev->next = list[idx]->next;
list[idx]->next->prev = list[idx]->prev;
list[idx]->next = list[idx]->prev = NULL;
}
void insert(int idx1, int idx2) {
list[idx2]->next = list[idx1]->next;
list[idx1]->next = list[idx2];
list[idx2]->next->prev = list[idx2];
list[idx2]->prev = list[idx1];
}
```
> 比起 queue 和 stack,從頭刻 linked list 真的挺累的
> 因應題目需求,有時候還是得自己刻
### `std::list`
```cpp
#include <list>
using std::list;
```
#### 宣告:
`list<T> ls`: `ls` 空的連接串列,且各元素型別為 `T`
#### 函式:
`insert`, `erase` 將回傳操作過後的迭代器位置 (最好自行實驗)
`ls.insert(iterator it, T a)`: 在 `it` 位置**前**插入 `a`
`ls.insert(iterator it, size_type n, T a)`: 在 `it` 位置**前**插入 `n` 個 `a`
`ls.erase(iterator it)`: 把 `it` 位置上的元素移除
`ls.erase(iterator l, iterator r)`: 把 $[l, r)$ 位置上的元素移除
```cpp
list<int> ls;
ls.push_back(1); // 1
ls.push_back(2); // 1 <-> 2
ls.push_front(3); // 3 <-> 1 <-> 2
cout << ls.front() << ' '; // 3
cout << ls.back(); // 2
```
```
< 3 2
```
```cpp
for (list<int>::iterator i = ls.begin(); i != ls.end(); i++)
cout << *i << ' ';
```
```
< 3 1 2
```
```cpp
list<int>::iterator it = ls.begin();
++it; ++it; // it points now to position 2 (start from 0)
ls.insert(it, 3, 100); // 3 <-> 1 <-> 100 <-> 100 <-> 100 <-> 2
for (list<int>::iterator i = ls.begin(); i != ls.end(); i++)
cout << *i << ' ';
```
```
< 3 1 100 100 100 2
```
此時 `it` 將指向第 $5 = 2 + 3$ 個位置
>$2$ 是還未 `ls.insert(it, 3, 100)` 前的位置
>$+3$ 是因為插入了 $3$ 個 `100`
```cpp
--it; --it;
ls.erase(it, ls.end());
for (list<int>::iterator i = ls.begin(); i != ls.end(); i++)
cout << *i << ' ';
```
```
< 3 1 100
```
#### 練習:
[UVa OJ 11988 Broken Keyboard (a.k.a. Beiju Text)](https://uva.onlinejudge.org/external/119/11988.pdf)
[CODEFORCES 1154E Two Teams](https://codeforces.com/contest/1154/problem/E)
[UVa OJ 245 Uncompress](https://uva.onlinejudge.org/external/2/245.pdf)
[Sprout OJ 21 陸行鳥大賽車](https://neoj.sprout.tw/problem/21/)
[Sprout OJ 20 中國人排隊問題](https://neoj.sprout.tw/problem/20/)
## inserter
介紹 `std::set` 之前,得先介紹一些集合操作
inserter 顧名思義,就是把一些東西插入到某個地方
```cpp
#include <iostream> // std::cout
#include <iterator> // std::inserter
#include <list> // std::list
#include <algorithm> // std::copy
using namespace std;
int main () {
list<int> foo, bar;
for (int i = 1; i <= 5; i++)
foo.push_back(i), bar.push_back(i*10);
list<int>::iterator it = foo.begin();
advance(it, 3);
copy(bar.begin(), bar.end(), inserter(foo, it));
for (auto it : foo) cout << it << ' ';
return 0;
}
```
```
< 1 2 3 10 20 30 40 50 4 5
```
## set_union
union 就是將兩個集合取聯集
```cpp
#include <iostream> // std::cout
#include <algorithm> // std::set_union, std::sort
#include <vector> // std::vector
using namespace std;
int main () {
int first[] = { 5, 10, 15, 20, 25 };
int second[] = { 50, 40, 30, 20, 10 };
vector<int> V(10); // 0 0 0 0 0 0 0 0 0 0
vector<int>::iterator it;
sort(first, first+5); // 5 10 15 20 25
sort(second, second+5); // 10 20 30 40 50
it = set_union(first, first+5, second, second+5, V.begin());
// 5 10 15 20 25 30 40 50 0 0
V.resize(it - V.begin()); // 5 10 15 20 25 30 40 50
for (auto it : V) cout << it << ' ';
return 0;
}
```
```
< 5 10 15 20 25 30 40 50
```
## set_intersection
取交集
```cpp
#include <iostream> // std::cout
#include <algorithm> // std::set_intersection, std::sort
#include <vector> // std::vector
using namespace std;
int main () {
int first[] = { 5, 10, 15, 20, 25 };
int second[] = { 50, 40, 30, 20, 10 };
vector<int> V(10); // 0 0 0 0 0 0 0 0 0 0
vector<int>::iterator it;
sort(first, first+5); // 5 10 15 20 25
sort(second, second+5); // 10 20 30 40 50
it = set_intersection(first, first+5, second, second+5, V.begin());
// 10 20 0 0 0 0 0 0 0 0
V.resize(it - V.begin()); // 10 20
for (auto it : V) cout << it << ' ';
return 0;
}
```
```
< 10 20
```
## set_difference
差集
```cpp
#include <iostream> // std::cout
#include <algorithm> // std::set_difference, std::sort
#include <vector> // std::vector
using namespace std;
int main () {
int first[] = { 5, 10, 15, 20, 25 };
int second[] = { 50, 40, 30, 20, 10 };
vector<int> V(10); // 0 0 0 0 0 0 0 0 0 0
vector<int>::iterator it;
sort(first, first+5); // 5 10 15 20 25
sort(second, second+5); // 10 20 30 40 50
it = set_difference(first, first+5, second, second+5, V.begin());
// 5 15 25 0 0 0 0 0 0 0
V.resize(it - V.begin()); // 5 15 25
for (auto it : V) cout << it << ' ';
return 0;
}
```
```
< 5 15 25
```
## set
集合(set) 是非常基礎的數學結構,對於資料結構也一樣基礎
特別注意:集合中的**元素不會重複**
### `std::set`
`std::set` 是使用紅黑樹實作的,插入、刪除、查詢都為 $O(\log_2 N)$,其中元素也不會重複。
且元素們在 `set` 容器中保持**排序的**(可比大小)
> 也就是說迭代器位置會優先從元素值小的排到大的
```cpp
#include <set>
using std::set;
```
#### 宣告:
`set<T> S`: 空的集合
#### 函式:
`S.insert(T a)`: 插入元素 `a`
`S.erase(iterator x)`: ,移除 $x$ 位置上的元素
`S.erase(iterator l, iterator r)`: 把 $[l, r)$ 位置上的元素移除
`S.erase(T a)`: 移除元素 `a`
`S.find(T a)`: 回傳指向元素 `a` 的迭代器;若 `a` 不存在,則回傳 `S.end()`
`S.count(T a)`: 元素 `a` 是否存在
> 在 C++11 後,erase 會回傳最後被移除元素下個元素的迭代器
#### 範例 [CODEFORCES 1157A Reachable Numbers](https://codeforces.com/contest/1157/problem/A):
:::spoiler
持續使用函數 $f$,除以 1 次 $10$ 前最多只會加 9 次 $1$,
於是對於數字 $n$ 最多只會使用 $O(\log n)$ 次 $f$
在這過程中若遇到**已經見過**的數字,表示不會再出現不曾見過的數字了
```cpp
set<int> S;
while(!S.count(n)) {
S.insert(n);
n++;
while(n % 10 == 0) n /= 10;
}
printf("%d\n", S.size());
```
:::
#### 練習:
[CODEFORCES 1238B Kill \`Em All](https://codeforces.com/contest/1238/problem/B)
[CODEFORCES 1253B Silly Mistake](https://codeforces.com/contest/1253/problem/B)
## key-value pairs (KVP)
鍵(key)值(value)對(pair) 是非常實用的資料結構
例如想表達每個人 $p_i$ 的身高 $h_i$ 可以寫:
$(\text{'john'}, 167), (\text{'aria'}, 145), (\text{'bob'}, 170)$
又或是表達有理數 $220\over 440$ 的分子分母:
$(220, 440), (22, 44), (2, 4), (1, 2)$
### `std::map`
`std::map` 的插入、刪除或查詢為 $O(\log_2 N)$
`map` 的每個元素結構為 `std::pair<T1, T2>` 構成的 KVP
且元素們在 `map` 容器中保持**排序的**
```cpp
#include <map>
using std::map;
```
#### 宣告:
`map<T1, T2> M`: 空的 KVP 集合,鍵型態 `T1`,值型態 `T2`
#### 函式:
`M[k] = a`: 修改鍵 `k` 對應的值為 `a`
`M[k]`: 存取鍵 `k` 對應的值
`M.insert(pair<T1, T2> P)`: 插入一個鍵值對 `P`
#### 範例 [CODEFORCES 1133C Balanced Team](https://codeforces.com/contest/1133/problem/C):
:::spoiler
每次將 skill 為 $a_i$ student 以上相差 $5$ 以內的 skill 都記錄下來就行
例如有 skill $1$、 $6$ 及 $4$ 的 student 存在
那麼首先記錄 $1$:
```
skill: 1 2 3 4 5 6 7 8 9 10 11
count: 1 1 1 1 1 1 0 0 0 0 0
```
接著 $4$
```
skill: 1 2 3 4 5 6 7 8 9 10 11
count: 1 1 1 2 2 2 1 1 1 0 0
```
然後 $6$
```
skill: 1 2 3 4 5 6 7 8 9 10 11
count: 1 1 1 2 2 3 2 2 2 1 1
```
所以對於 skill $a_i$:
```cpp
for(int i = 0; i < n; i++) {
scanf("%d", &a[i]);
for(int k = 0; k <= 5; k++) cnt[a[i]+k]++;
}
```
接著找出 $a_i$ **以下**相差為 $5$ 的 skill 數中的最大值:
```cpp
int best = 1;
for(int i = 0; i < n; i++) best = max(best, cnt[a[i]]);
```
且慢!
注意到一個限制: $1 \leq a_i \leq 10^9$
若把 `cnt` 陣列的空間開得這麼大,明顯的會 Runtime error[^CE]
所以把陣列改成 `map<int, int>`,就能避免空間上的龐大需求 :+1:
:::
#### 範例 [CODEFORCES 1255C League of Leesins](https://codeforces.com/contest/1255/problem/C):
:::spoiler
觀察題目中的例子 $(1,4,2),(4,2,3),(2,3,5)$
應該可以很自然地推得數列 $1, 4, 2, 3, 5$
原因是當**起頭確定**時,可得知下兩個數字是啥
確定是 $1$,那一定知道後面是 $4, 2$
確定是 $5$,那一定知道後面是 $2, 3$
並且若知道下兩個數字,又能再推得一個數字
$4, 2$ 的話能推得 $3$,因為有 $(4, 2, 3)$
依此類推,能夠把所有數字都算出來
且起頭的數字是統計所有 triple 中只**出現 1 次**的數字,如 $1, 5$
第二個數字則是統計所有 triple 中只**出現 2 次**的數字,如 $4, 3$
```cpp
#include<bits/stdc++.h>
using namespace std;
int const maxn = 1e5 + 10;
int n, x, y, z;
int main()
{
scanf("%d", &n);
int cnt[maxn] = {};
map<pair<int, int>, vector<int>> tp; // tp := triple
for(int i = 0; i < n-2; i++) {
scanf("%d%d%d", &x, &y, &z);
tp[{x, y}].push_back(z);
tp[{y, x}].push_back(z);
tp[{y, z}].push_back(x);
tp[{z, y}].push_back(x);
tp[{z, x}].push_back(y);
tp[{x, z}].push_back(y);
cnt[x]++;
cnt[y]++;
cnt[z]++;
}
int p1, p2;
for(int i = 1; i <= n; i++) if(cnt[i] == 1) p1 = i;
for(int i = 1; i <= n; i++) if(cnt[i] == 2 && tp.count({p1, i})) p2 = i;
bool vis[maxn] = {};
for(int i = 0; i < n; i++) {
printf("%d ", p1);
vis[p1] = true;
if(i == n-1) break;
auto v = tp[{p1, p2}];
p1 = p2;
p2 = vis[v[0]]? v[1] : v[0];
}
return 0;
}
```
:::
#### 練習:
[UVa OJ 11991 Easy Problem from Rujia Liu?](https://uva.onlinejudge.org/external/119/11991.pdf)
\*[CODEFORCES 1257C Dominated Subarray](https://codeforces.com/problemset/problem/1257/C)
\*\*[CODEFORCES 1230D Marcin and Training Camp](https://codeforces.com/contest/1230/problem/D)
## Priority queue
優先隊列 (priority queue) 是隊列 (queue) 的一個變種
- dequeue 會先選容器中**優先度最大**的元素
- front 也先選容器中**優先度最大**的元素
### `std::priority_queue`
每次進出的時間複雜度都為 $O(\log_2 N)$
> 對於數值型態的元素,數值越大優先度越大
```cpp
#include <queue>
using std::priority_queue;
```
#### 宣告:
`priority_queue<T> pq`: `pq` 為空的優先隊列,且各元素型別為 `T`
#### 函式:
`pq.top()`: `pq` 中優先度最大的元素
`pq.push(T a)`: 將 `a` 加入 `pq` 中 (enqueue)
`pq.pop()`: 將`pq` 中優先度最大的一個元素移除 (dequeue)
對於未定義**排序**關係的元素(型態),==要先定義**小於**運算子==,例如:
```cpp
struct XXX {
int code, weight;
bool operator<(const XXX &lhs) const
{ return weight < lhs.weight; }
};
```
```cpp
priority_queue<XXX> PQ; // []
PQ.push({2, 2147483647}) // [{2, 2147483647}]
PQ.push({3, -1}) // [{2, 2147483647}, {3, -1}]
PQ.push({2, 0}) // [{2, 2147483647}, {2, 0}, {3, -1}]
cout << PQ.top().weight << endl; // {2, 2147483647}
PQ.pop(); // [{2, 0}, {3, -1}]
puts(PQ.empty()? "yes" : "no"); // no
```
```
< 2147483647
< no
```
`priority_queue` 也能定義比較函數,可以自行實驗
#### 範例 [GCJ Kickstart Round E 2018 B Milk Tea](https://code.google.com/codejam/contest/4394486/dashboard#s=p1):
:::spoiler
簡單的,將所有 binary string (preferences) 都產出,並把 string 對應的抱怨 (complaint) 算出
接著找出最小抱怨且不屬於 forbidden types 的 binary string 就好。
用 `span` 保存將產出的 binary string:
```cpp
priority_queue<pair<int, string> > span;
span.emplace(0, "");
```
以 `seed` 作為基礎,再將 string 加上 `"0"` 或 `"1"`
並每次將當前的抱怨值算出後保存:
```cpp
for (int i = 0; i < P; i++) {
priority_queue<pair<int, string> > seed;
swap(seed, span);
while (!seed.empty()) {
int cp; // complaints
string bin; // binary string
tie(cp, bin) = seed.top(); seed.pop();
span.emplace(cp+one[i], bin+"0");
span.emplace(cp+zero[i], bin+"1");
}
}
```
其中 `one[i]` 與 `zero[i]` 為 Shakti 的朋友們對於第 $i$ 個 option 的加總 (選項只有 $0$ 和 $1$)
但光是產出所有 binary string 就足夠讓複雜度導致 TLE。
在上述==過程中,若把某些 string 去除掉,就能使效率增加不少==
合理的,答案只要最小抱怨值的 binary string,那在過程中移除抱怨值較大的 string 就行了:
```cpp
while (span.size() > M+1) span.pop();
```
因為共有 $M$ 個 forbidden type,所以至少要保存 $M+1$ 個 binary string
最後把 string 為 forbidden type (`ban`) 的濾掉,留下抱怨值最小的就好:
```cpp
int ans;
while (!span.empty()) {
tie(cp, bin) = span.top(); span.pop();
if (!ban.count(bin)) ans = cp;
}
```
:::
#### 練習:
[ICPC 3135 Argus](https://icpcarchive.ecs.baylor.edu/external/31/3135.pdf)
[UVa OJ 11997 K Smallest Sums](https://uva.onlinejudge.org/external/119/11997.pdf)
[^sloc]: 有時會想試試不靠儲存空間,使用大量的判定輸出,此時程式碼長度的估算也得考量
[^1]: 注意這邊的符號($=$)與數學上的用法"[等價](https://en.wikipedia.org/wiki/Equivalence_relation)"意義不同
[^pop_back]: 若 v 為空,會發生無法預期的結果
[^start_at_zero]: [Why numbering should start at zero](https://www.cs.utexas.edu/users/EWD/transcriptions/EWD08xx/EWD831.html)
[^crud]: [wikipedia/ 建立、讀取、更新、刪除](https://zh.wikipedia.org/wiki/%E5%BB%BA%E7%AB%8B%E3%80%81%E8%AE%80%E5%8F%96%E3%80%81%E6%9B%B4%E6%96%B0%E3%80%81%E5%88%AA%E9%99%A4)
[^doubly_linked_list]: 這裡介紹的是更為泛用的 doubly linked list 而非單純的 singly linked list
[^4]: [wikipedia/ Representation of a FIFO (first in, first out) queue](https://en.wikipedia.org/wiki/Queue_(abstract_data_type)#/media/File:Data_Queue.svg)
[^5]: [wikipedia/ Simple representation of a stack runtime with push and pop operations](https://en.wikipedia.org/wiki/Stack_(abstract_data_type)#/media/File:Lifo_stack.png)
[^6]: [wikimedia/ DoubleLinkedListHsrw](https://commons.wikimedia.org/wiki/File:DoubleLinkedListHsrw.png)
[^CE]: 不過實驗證明,提交上 Codeforces 會拿到 Compilation error
[^sync]: [GNU/ Interacting with C/ Performance](https://gcc.gnu.org/onlinedocs/libstdc++/manual/io_and_c.html#std.io.c.sync)