## 處理四捨五入 一般題目都會特別規範四捨五入的位置與自行定義"新的四捨五入",這裡使用常見的四捨五入,在小數點後一位 簡單來說就是四捨五入到整數,最後的結果不要出現小數 最基本的方式就是將結果先 $\times 10$ 假設結果是 $43.1$,那在計算的過程中先 $\times 10$ 之後只要做 `if (n % 10 < 5)` 就可以當作四捨五入,不過記得判斷完要 $\div 10$ ## 大數處理 (Python 也可以用) 在面對某些題目時會遇到位數非常大的數字,比如 $1000$ 位就遠超 long long 能儲存的範圍 這時候我們就需要使用大數處理的技巧,大數處理通常會依照以下的順序 1. 用 string 輸入數字 2. 將 string 每一個字元一一儲存進 array ```cpp= string s ; cin >> s ; int n = s.size() ; for (int i=0;i<n;i++) A[i] = s[n-i-1] - '0' ; // 順序要顛倒 ``` 之所以順序要顛倒是因為如果遇到加減乘除運算,可能會讓數字位數變大 所以讓位數越大的放越右邊有助於空間利用 不過在大部分情況,只要確定數字在 long long 內,直接用 long long 取代 int 除非在一些考慮速度的競賽,因為 long long 相較 int 會更慢一點 ## 二維矩陣的邊界 對於部分題目,二維矩陣的邊界判斷不可避免的,一般會用以下方式執行 ```cpp= // 假設 (x,y) 為當前位置,0 <= x <= n-1, 0 <= y <= m-1 if (x >= 0 && x < n && y >= 0 && y < m) { // 程式片段 } ``` 不過其實也可以用"不存在的元素"當作邊界,比如常見的有 $-1$ 不過要記得開多一點空間,實際的作法會像是這樣 ```cpp= // 注意從 1~n, 1~m for (int i=1;i<=n;i++) { for (int j=1;j<=m;j++) { cin >> A[i][j] ; } } for (int j=0;j<=m+1;j++) { // 上下作邊界 A[0][j] = A[n+1][j] = -1 ; } for (int i=0;i<=n+1;i++) { // 左右作邊界 A[i][0] = A[i][m+1] = -1 ; } ``` 不過這個要不要使用就因人而異,也有人認為前面的作法就很好用了 ## 簡易方向表達 一般在考慮上下左右的時候,都會需要用 $(x\pm1, y)$ 或 $(x,y\pm1)$ 如果一個方向需要寫一段程式碼,那四個方向就需要做四次,這時可以用 for+array 取代 ```cpp= int mm[4][2] = {{1,0},{-1,0},{0,1},{0,-1}} ; for (int i=0;i<4;i++) { int nx = x + mm[i][0], ny = y + mm[i][1] ; // 上下左右的座標 // 程式碼片段 } ``` 通常會在有二維陣列的題目裡使用,不過方向還有另一種可能,就是順/逆時針移動 這時候如果用 $0$ 表示上、$1$ 表示右、$2$ 表示下、$3$ 表示左 ```cpp= int to = 0 ; for (int i=0;i<n;i++) { // 程式碼片段 to = (to+1) % 4 ; } ``` 只要重複取餘 $4$ 就可以讓左 $\rightarrow$ 上順利轉向 ## 簡易演算法函式庫 C++ 當中有提供一些滿不錯的函式庫,由於我們都用 `#include<bits/stdc++.h>` 所以不需要特別去記下方的函式從哪些函式庫出來的,只要記住用法即可 第一個就是最大值或最小值,通常會用 `max(), min()`,不過這只能一次判斷兩個值 如果我們想一次判斷多個值或一個陣列等等,那就要用到這些方法 ```cpp= // 一次判斷多個值 min({a,b,c,d,...}) ; max({a,b,c,d,...}) ; // 一次判斷陣列 a 的所有元素 int min_v = *min_element(a, a+n) ; int max_v = *max_element(a, a+n) ; // 最大值的"位置" int min_p = min_element(a, a+n) ; int max_p = max_element(a, a+n) ; ``` 比較特殊的就是找第 $k$ 大的值,如果用排序的那就是 $a[k-1]$ 但如果沒有排序的話,那用 `nth_element()` 通常會快很多 ```cpp= int kth_v = *nth_element(a, a+n) ; int kth_p = nth_element(a, a+n) ; ``` --- 第二個就是排序 `sort()` 跟交換 `swap()` `sort()` 可以處理陣列或 vector 非常方便 ```cpp= int a[101] ; vector<int> b(101) ; // 程式碼片段 sort(a, a+n, cmp) ; // cmp 是比較函數,預設是由小到大 sort(b.begin(), b.end(), cmp) ``` ```cpp= int a = 3, b = 5 ; swap(a, b) ; // a == 5, b == 3 ``` --- 第三個 `unique()` 是在做資料離散(相同的資料去除留下剩一個)的時候很方便 一般我們的操作會像是下面這樣 ```cpp= sort(a, a+n) ; vector<int> v = {a[0]} ; for (int i=1;i<n;i++) if (a[i] != a[i-1]) v.push_back(a[i]) ; ``` 排序過後,同樣的值會被排在一起,所以相鄰不相同的值就是新的值,自然要加到 v 當中 那 `unique()` 就是直接幫你省去了這個過程,並且還能不用開新的容器 ```cpp= sort(v.being(), v.end()) ; int pos = unique(v.begin(), v.end()) ; // 假設 1 1 2 2 2 4 5 // unique 後會變成 1 2 4 5 x ... // pos 就是 x 位置的 iterator // x 的值不重要,因為前面的數字已經去除重複了 ``` 當然用 set 也可以做到同樣的事情,這個就看用途去做選擇 `unique()` 本身的功能是將相鄰的同值元素消到只剩一個,不過通常用在排序後 所以我們就不去探討其他的用途了,萬一會用到也可以用最初的做法去做 --- 第四個是 `accumulate()` 和 `iota()`,這兩個的基礎使用場景比較少,自己做也不難 `accumulate()` 可以計算 $a[i]\sim a[j]$ 的總和 ```cpp= cout << accumulate(a+i, a+j) << '\n' ; ``` 不過這個可以自己手搓一個出來,並不難,如果多次查詢還是用前綴和會比較快 `iota()` 的用處就是在 $a[x] \sim a[y]$ 的部分放從 $z$ 開始一次加一的值 ```cpp= // 自己手搓 for (int i=x;i<=y;i++) a[i] = z+(i-x) ; // 用 iota() iota(a+x, a+y+1, z) ; ``` 這個自己手搓也不難,大部分會用到都是用下面這種方式,自己寫就好 ```cpp= for (int i=0;i<n;i++) a[i] = i ; ``` ## Reference 參考 在競技程式中,Reference 通常拿來簡化許多問題,算是在 C++ 特有的語法糖 首先的功能就是用地址去實現類似 global 變數的情況 一般在不同 function 中,local 變數不可以在另一個 function 被改變 比如如果要交換兩個變數,我們如果這樣寫 ```cpp= void swap(int a, int b) { int tmp = a ; a = b ; b = t ; } swap(x, y) ; ``` 這樣交換的是 swap 內部的 local 變數 $a,b$,而不是 $x,y$,應該這樣寫 ```cpp= void swap(int &a, int &b) { int tmp = a ; a = b ; b = t ; } swap(x, y) ; ``` 因為這樣 $x,y$ 的地址也會被傳入 swap,更改的時候會一起更改 不過要注意,因為"地址"也要被傳入,所以這裡的 swap 只能傳整數變數,整數是不行的 --- 第二個功能也是在 function,傳入 vector 等資料結構的時候很好用 ```cpp= void sort_and_print(vector<int> &v, int n) { sort(v.begin(), v.end()) ; for (int i=0;i<n;i++) cout << v[i] << ' ' ; cout << '\n' ; } ``` 如果沒加入 & 的話,傳入 function 一次就需要複製一次 vector 如果像是 DFS 這種需要一邊更改陣列內容,一邊遞迴的情況,多次複製會大幅度降低效率 這種方式只要注意,function 內部跟外部是同時改變的(用指標也可以做到同樣的事情) --- 最後一種功能就是用參考精簡變數,通常用在需要 array、map、string 等多層堆疊的變數 ```cpp= for (int i=0;i<n;i++) { find_root(s[a[idx[i]]]) ; if (check(s[a[idx[i]]])) s[a[idx[i]]] += cul(s[a[idx[i]]]) ; else if (s[a[idx[i]]]-'a' < 26 && s[a[idx[i]]]-'a' >= 0) s[a[idx[i]]] = 'a' ; else s[a[idx[i]]] = 'A' ; } ``` 這段程式碼用到很多 `s[a[idx[i]]]`,雖然在實作中可以用複製貼上 但是如果這之間的順序錯了,比如 `s[a[idx[i]]]` 應該是`s[idx[a[i]]]` 這時候就需要一次改變所有同樣位置,如果這時候可以用一個簡單的變數取代,改起來就很輕鬆 ```cpp= for (int i=0;i<n;i++) { int &tmp = s[a[idx[i]]] ; find_root(tmp) ; if (check(tmp)) tmp += cul(tmp) ; else if (tmp-'a' < 26 && tmp-'a' >= 0) tmp = 'a' ; else tmp = 'A' ; } ``` 當然這裡要注意幾個問題,大致上就是要記得別用下面幾種寫法 ```cpp= int &tmp ; // 沒指定 -> 無法做參考 int &tmp = 100 ; // 無法做純整數的參考 int &tmp = a+b ; // 兩整數變數相加後是純整數,無法做參考 ``` ## 變數的初始化與特性 通常變數的初始化都是我們額外去指定的,但大致上有幾個特性可以幫助我們解題 首先就是整數全域變數會被初始化為 $0$,因為這個特性所以通常在以下情況會用到 比如在定義一個一維矩陣或二維矩陣時,如果要多寫一個 for 初始化有點麻煩 ```cpp= int G[101][101] ; // 預設初始化為 0 int main() { // 程式碼片段 return 0 ; } ``` 在只需要一次初始化的時候滿好用的,但是如果要多次初始化就要用其他方式了 至於如果需要一個 local 的區域變數初始化,就直接用原本的方式即可 `int G[101][101] = {} ;` 還有一個有關 global 的特性,就是拿來存放超大的陣列 --- 第二個就是 struct 的使用,通常在需要一次存兩個以上變數時會用到 比如圖論常會用到 (點, 權重) 之類的做法,這時候用 struct 就會滿方便的 不過 struct 初始化有一個特性,就是 struct 內部初始化後,使用時都會自動初始化 ```cpp= struct node { int n = 7, w = 1 ; } ; node global_node ; int main() { node local_node ; cout << global_node.n << ' ' << global_node.w << '\n' ; // 7 1 cout << local_node.n << ' ' << local_node.w << '\n' ; // 7 1 return 0 ; } ``` --- 第三個就是 auto 的使用,auto 的特性讓我們在一些情況很方便 比如最簡單的就是在處理一些 STL 的資料結構時,for 迴圈跑過需要用到 iterator ```cpp= map<int, int> mp ; for (auto it=mp.begin();it!=mp.end();it++) { // 程式碼片段 } ``` 還有在一些情況,需要用到以下作法,解釋起來太麻煩,只要在實作上能用就可以了 ```cpp= auto cmp[](int a, int b) { return a < b ; } ; ``` --- 第四個是 static,這是一種拿來更好地區隔 global 和 local 的方式 如果多次執行同一個函式,並且輸出是第幾次執行這個函式 這時候你可以傳入與回傳紀錄次數,或是用一個 globla var 紀錄次數 ```cpp= // 傳入與回傳紀錄次數 int f(int times) { // 程式碼片段 return times+1 } int global_t = 0 ; void g() { // 程式碼片段 global_t++ ; } int main() { int times = 0 ; times = f(times) ; // times = times+1 g() ; cout << '\n' << times << ' ' << global_t << '\n' ; return 0 ; } ``` 這兩種的缺點也很明顯,第一個方法的問題是如果函式需要回傳其他值,就會很麻煩 第二個方法就是因為 global var 有可能會不小心被其他地方改掉 這時候如果用 static local var 就可以有一個不會被重製的 local var ```cpp= void f() { static int times = 0 ; times++ ; } void g() { static int times = 0 ; times-- ; } int main() { f(); // times in f: 1 f(); // times in f: 2 g(); // times in g: -1, times in f: 2 f(); // times in g: -1, times in g: 3 g(); // times in g: -2, times in g: 3 return 0 ; } ``` 這兩個函式內的 times 都是 local,兩個變數互不干涉 static 還有一個很方便的功能,就是讓 local var 初始化 比如我需要一個在 function 內寫全 $0$ 的陣列 通常需要一個 for-loop,但用 static array 就可以省下麻煩 不過還是要記得上面說的 static 的功能,不然可能會誤用 --- 最後就是 const,主要的功能就是讓變數不能修改,然後執行速度變快 會用在只能來讀取但不修改的變數,比如大量取某數的餘數或四個方向移動陣列 ```cpp= const int MOD = 10000000007 ; const int mm[4][2] = {{1,0},{-1,0},{0,1},{0,-1}} ; ``` 還可以用在函式傳遞某些變數,只要不修改就可以使用 ```cpp= void f(const vector<int> &a) { // 程式碼片段 } ``` 還有一個額外的好處,就是 reference 可以傳入非變數,比如上方可以用 `f({1,2,3})` ### 用數字表示選擇 一般來說,我們在表示某個有規律的資料中,元素是否被選擇的時候,會用 bool 陣列去做 但是如果我們也可以用另外一個方式表示,使用數字去儲存是否選這個元素,或是否存在該元素 這樣的好處就是在處理 $n$ 個由資料內元素組成的集合時,我們不需要用 $n$ 個 bool 陣列 只需要 $n$ 個數字就可以處理,至於實作的方式,是考慮到數字本身由位元組成 二進制的數字剛好和 bool 陣列的概念類似,都是一串 $0$ 和 $1$ ```cpp= #include<bits/stdc++.h> using namespace std ; int main() { int A[10] = {9,8,7,6,5,4,3,2,1,0} ; int pick_odd = 0, pick_even = 0 ; // 根據 index 選 for (int i=0;i<10;i++) { if (i % 2) // 奇數 pick_odd += 1 ; else // 偶數 pick_even += 1 ; // 進到下一個選擇 pick_odd <<= 1 ; pick_even <<= 1 ; } int len = 10 ; for (int i=len-1;i>=0;i--) { pick_odd >>= 1 ; if (pick_odd % 2) // 輸出選擇的數字 cout << A[i] << ' ' ; } cout << '\n' ; for (int i=len-1;i>=0;i--) { pick_even >>= 1 ; if (pick_even % 2) // 輸出選擇的數字 cout << A[i] << ' ' ; } cout << '\n' ; return 0 ; } ```