# 前言
本文章宗旨為帶領已有部分程式基礎的學生,學習更多高深語法、函式、演算法等。此篇文章建議有三級分以上的程度再來閱讀,如果需要基礎篇的同學,不妨參考我的上一篇講義(下方連結),能夠幫你很好的入門C++,最後祝福大家,在這篇講義中能有所收穫。
* 上一篇講義在這裡→[C++新手入門](https://hackmd.io/@37aitHUHRnKghKiwEbbHmg/SJHsKyJkp)
# 進階架構
對於三級分以上的題目,有些時候會出現TLE(Time Limit Exceeded,超出時間限制)的狀況,而除了程式本身的複雜度之外,有一部分的原因是輸入輸出操作(I/O)的效率過低。為了解決這個問題,我們會在程式碼開頭額外加上這兩行。
```cpp=1
#include <bits/stdc++.h>
using namespace std;
//副函式撰寫區
int main() {
ios::sync_with_stdio(0); // 關閉 C++ 流和 C 流的同步
cin.tie(0); // 取消 cin 和 cout 的綁定
return 0;
}
```
## ios::sync_with_stdio(0)
C++ 的 `iostream`(如 `cin` , `cout`)和 C 標準庫中的 I/O 函數(如 `scanf`, `printf`)默認是同步的,也就是說,在C++中,你仍然可以使用 `scanf`, `printf` 。不過,這樣的同步機制會增加不必要的性能開銷。
## cin.tie(0)
默認情況下,C++ 的 `cin` 和 `cout` 是互相綁定的。每當你使用 `cin` 進行輸入時,C++ 會自動刷新 `cout` 的緩衝區,以確保所有尚未顯示的內容會在輸入操作之前顯示出來。這樣的設計可以保證輸出和輸入的順序,但同時也會增加性能開銷。
# char/string
在先前的學習中,我們大多數時間都是進行數字運算,而對於字串和字元的學習基本沒有。事實上,字串和字元擁有許多功能,可以幫助我們解決問題,特別是在處理輸入測試資料時。以下是 `char` 和 `string` 的語法。
```cpp=1
// char(字元)
char c1;
char c2 = 'R';
// string(字串)
string s1;
string s2 = "Hello";
string s3 = "World";
string s4 = s2 + " " + s3; // s4="Hello World"
```
你知道嗎,其實字串就像是字元陣列一樣,是可以用 s[n] 來調用其中一個字元的。
```cpp
string s="Hello World"
```

## ASCII
ASCII(美國標準信息交換碼,American Standard Code for Information Interchange)是一種字符編碼標準,在計算機和通信設備中表示文本和符號。
* 字符集:ASCII 定義了128個字符的集合,包括英文字母(大小寫)、數字、標點符號和控制字符。
* 編碼範圍:每個字符都由一個7位的二進制數表示,範圍從0到127。這使得 ASCII 能夠表示128個不同的字符。
* 數字(48-57):從0到9。
* 大寫字母(65-90):從A到Z。
* 小寫字母(97-122):從a到z。
* 標點符號和其他符號:如逗號、句號、驚嘆號、問號等。

圖片來源:https://www.sciencebuddies.org/science-fair-projects/references/ascii-table
這裡我們試著思考一下,如果今天我有一個字串 "123456" ,實際上在 ASCII 中對應到的編號分別是 31 32 33 34 35 36,若我想將這個字串轉換成整數,這時候我該怎麼做呢?
答案其實不難,我們只需要將數字平移就好了,原本對應的區間是31~36,只要我通通減去30,區間就會變成1~6了,這時候就和原本的數字相對應了,於是查找表中編號30的字元,發現是 '0' ,至此我們可以將字串轉換成整數的程式碼完成了。
```cpp=1
string s="123456";
int n=0;
// 利用 for 迴圈遍歷字串 s
for(char c : s){
n = n * 10 + (c - '0'); // 乘以10是為了進位,因為我們是從最大的那個位數開始讀取的
}
cout << n;
```
### 輸出結果
```
123456
```
---
## 小試身手
* 題目:子柚是個很喜歡數學的學生,尤其喜歡計算非常大的數字,現在他希望可以進行20位數以上的乘法運算,請幫助他實現這個願望。
* 輸入說明:第一行為一個整數 $\text{n}$,第二行為一個個位數 $\text{m}$。$\left(20 \leq \log n \leq 100\right)$
* 輸出說明:請輸出 $\text{n × m}$ 的結果。
### 範例輸入1
```
600000000000000000000000000000000000000
2
```
### 範例輸出1
```
1200000000000000000000000000000000000000
```
---
### 參考解答
```cpp=1
#include <bits/stdc++.h>
using namespace std;
int main() {
string n;
int m;
string result = ""; // 儲存乘法結果
int carry = 0; // 進位
cin >> n >> m;
// 從最後一位開始逐位相乘
for (int i = n.size() - 1; i >= 0; --i) {
int digit = (n[i] - '0') * m + carry; // 計算每位數的乘積加上進位
result = char(digit % 10 + '0') + result; // 將當前位的結果添加到結果字符的前面
carry = digit / 10; // 更新進位
}
// 如果還有進位,繼續處理
while (carry) {
result = char(carry % 10 + '0') + result; // 將進位添加到結果字符的前面
carry /= 10;
}
cout << result << endl;
return 0;
}
```
## 練習題
[a021. 大數運算](https://zerojudge.tw/ShowProblem?problemid=a021)
[q182. 2. 字串操作](https://zerojudge.tw/ShowProblem?problemid=q182)
# 常用函式
以下是常用函式,熟記的話可以節省不少時間,且標頭檔都用 `#include<bits/stdc++.h>` 即可。
| 函式 | 用途 | 範例 |
|---------------|------------------------------|------------------------------------------|
| `abs(x)` | 取絕對值 | `abs(-5) = 5` |
| `min(a, b)` | 取最小值 | `min(3, 7) = 3` |
| `max(a, b)` | 取最大值 | `max(3, 7) = 7` |
| `sqrt(x)` | 取平方根 | `sqrt(16) = 4` |
| `pow(x, y)` | 指數運算 | `pow(2, 3) = 8` |
| `floor(x)` | 向下取整 | `floor(3.7) = 3` |
| `ceil(x)` | 向上取整 | `ceil(3.2) = 4` |
| `round(x)` | 四捨五入 | `round(3.5) = 4` |
| `log(x)` | 取自然對數(ln) | `log(2.71828) = 約 1` |
| `log10(x)` | 取以 10 為底的對數 | `log10(100) = 2` |
| `sin(x)` | 計算正弦 | `sin(3.14159 / 2) = 約 1` |
| `cos(x)` | 計算餘弦 | `cos(0) = 1` |
| `tan(x)` | 計算正切 | `tan(3.14159 / 4) = 約 1` |
| `hypot(x, y)`| 計算歐幾里得距離 √(x²+y²) | `hypot(3, 4) // 5` |
| `gcd(a, b)` | 計算最大公因數 | `gcd(12, 18) // 6` |
| `lcm(a, b)` | 計算最小公倍數 | `lcm(12, 18) // 36` |
| `swap(a, b)` | 交換兩個值 | `swap(x, y)` |
| `reverse(begin, end)` | 反轉範圍內元素(陣列) | `reverse(v.begin(), v.end())` |
# 指標
我們每個人的家都有地址,當然,變數也有,而我們要怎麼取得變數的地址,或者怎麼靠地址找到變數,就得依賴指標。
```cpp=1
#include <bits/stdc++.h>
using namespace std;
int main() {
int a = 10; // 一個普通的整數變數
int *p = &a; // 指標 p 儲存變數 a 的地址
cout << "a 的地址: " << &a << endl; // 顯示 a 的地址
cout << "p 指向的地址: " << p << endl; // 顯示指標 p 儲存的地址
cout << "p 指向的值: " << *p << endl; // 解引用指標,顯示 p 指向的值(即 a 的值)
return 0;
}
```
# 迭代器
迭代器就樣郵差一樣,根據信件的地址,去訪問那位住戶。
## 宣告
```cpp=1
vector<int>::iterator it // 宣告迭代器 it
```
## 移動
```cpp=1
it++ // 迭代器向前一格
++it // 迭代器向前一格
it-- // 迭代器向後一格
--it // 迭代器向後一格
```
## 常用迭代器
```cpp=1
v.begin() // v陣列的起點
v.end() // V陣列的終點
```
## 公式解-遍歷陣列
```cpp=1
#include <bits/stdc++.h>
using namespace std;
int main() {
vector<int> v={1,2,3,4,5};
for(vector<int>::iterator it=v.begin();it!=v.end();it++){
cout << *it << " ";
}
return 0;
}
```
## 進階操作-auto
`auto` 是一種透過編譯器來自動推測變數型態的關鍵字,好處是可以化簡代碼。
```cpp=1
#include <bits/stdc++.h>
using namespace std;
int main() {
vector<int> v={1,2,3,4,5};
for(auto it=v.begin();it!=v.end();it++){
cout << *it << " ";
}
return 0;
}
```
## 大師操作-range-based
```cpp=1
#include <bits/stdc++.h>
using namespace std;
int main() {
vector<int> v={1,2,3,4,5};
for(auto it : v){
cout << *it << " ";
}
return 0;
}
```
# 副函式
以往,我們的程式都寫在 `main` 當中,但有些重複出現的功能,像是判斷二維陣列的邊界條件,若是每個 `if` 都重寫會導致程式碼可閱讀性降低,如下方範例。
```cpp=1
#include <bits/stdc++.h>
using namespace std;
int main() {
int rows = 3;
int cols = 3;
// 迷宮陣列:0代表可以走,1代表牆壁
int grid[3][3] = {
{0, 1, 0},
{0, 0, 0},
{0, 1, 0}
};
// 可移動的四個方向:上、下、左、右
int directions[4][2] = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
string direction_names[4] = {"Up", "Down", "Left", "Right"};
// 起點為 (1, 1) 這是中間的位置
int startX = 1, startY = 1;
// 顯示起始位置
cout << "Starting at: (" << startX << ", " << startY << ")\n";
// 遍歷四個方向,看看哪些方向是可以走的
for (int i = 0; i < 4; i++) {
int newX = startX + directions[i][0];
int newY = startY + directions[i][1];
// 檢查新位置是否合法:在邊界內,且不是牆壁
if (newX >= 0 && newX < rows && newY >= 0 && newY < cols && grid[newX][newY] == 0) {
// 輸出可以走的方向
cout << "You can move " << direction_names[i] << " to: (" << newX << ", " << newY << ")\n";
}
}
return 0;
}
```
### 輸出結果
```
Starting at: (1, 1)
You can move Left to: (1, 0)
You can move Right to: (1, 2)
```
看完上面的,有沒有覺得頭昏眼花, `if` 裡面太多東西了吧!所以當我們把這個判斷的條件從主函式 `main` 提出來後,整個程式碼就可以變得精簡許多。
```cpp=1
#include <bits/stdc++.h>
using namespace std;
// 全域變數
int rows = 3;
int cols = 3;
int grid[3][3] = {
{0, 1, 0},
{0, 0, 0},
{0, 1, 0}
};
// 副函式:判斷新位置是否合法
bool canMove(int x, int y) {
return x >= 0 && x < rows && y >= 0 && y < cols && grid[x][y] == 0;
}
int main() {
// 可移動的四個方向:上、下、左、右
int directions[4][2] = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
string direction_names[4] = {"Up", "Down", "Left", "Right"};
// 起點為 (1, 1) 這是中間的位置
int startX = 1, startY = 1;
// 顯示起始位置
cout << "Starting at: (" << startX << ", " << startY << ")\n";
// 遍歷四個方向,看看哪些方向是可以走的
for (int i = 0; i < 4; i++) {
int newX = startX + directions[i][0];
int newY = startY + directions[i][1];
// 使用 canMove 函式檢查新位置是否合法
if (canMove(newX, newY)) {
cout << "You can move " << direction_names[i] << " to: (" << newX << ", " << newY << ")\n";
}
}
return 0;
}
```
### 輸出結果
```
Starting at: (1, 1)
You can move Left to: (1, 0)
You can move Right to: (1, 2)
```
可以看得出來,兩者的功能是完全相同的,但使用副函式可以讓程式碼可閱讀性及可維護性變高許多。
看了這兩個範例後,我們就可以來學習如何將主函式 `main` 中的程式打包成副函式了。
## 副函式基本守則
1. 如果非全域變數,就必須要傳遞變數給副函式。(忘記的可以去複習變數範圍)
```cpp=1
#include <bits/stdc++.h>
using namespace std;
// 傳遞變數,位置一一對應,資料型態也要相同
// 整數 x 對應到整數 a ,整數 y 對應到整數 b
void add(int a, int b) {
// 可以在這裡進行某些操作
}
int main() {
int x = 5, y = 3;
add(x, y); // 傳遞變數 x 和 y 到 add 函式
return 0;
}
```
2. 傳遞陣列時,直接使用陣列的名字當指標就可以了。
```cpp=1
#include <bits/stdc++.h>
using namespace std;
// 一維整數陣列 nums 對應到 一維整數陣列 arr
// 可以不用給定大小
void sumArray(int arr[]) {
// 可以在這裡進行某些操作
}
int main() {
int nums[] = {1, 2, 3, 4, 5};
sumArray(nums); // 傳遞陣列到副函式
return 0;
}
```
3. 如果是二維陣列,則必須將大小先訂好至少一個。
```cpp=1
#include <bits/stdc++.h>
using namespace std;
// 二維整數陣列 grid 對應二維整數陣列 arr
// 需先給定一個維度的大小
void sum2DArray(int arr[][3]) {
// 可以在這裡進行某些操作
}
int main() {
int grid[2][3] = {
{1, 2, 3},
{4, 5, 6}
};
sum2DArray(grid); // 傳遞二維陣列
return 0;
}
```
4. 若是希望傳遞過去的東西不要被更改,可以加上 `const` 來保護變數。
```cpp=1
// 確保陣列操作時不會更改其內容
void Array(const int arr[]) {
// 可以在這裡進行某些操作
}
```
## int
`int` 代表此副函式最後會回傳一個整數,常見的使用方法為「遞迴」,我們在下一個章節會提到。
```cpp=1
#include <bits/stdc++.h>
using namespace std;
// 副函式:計算兩個數字的和,並返回結果
int sum(int a, int b) {
return a + b;
}
int main() {
int x, y;
cin >> x >> y;
// 調用 sum 函式,並將結果存儲在 result 中
int result = sum(x, y);
cout << "The sum of " << x << " and " << y << " is " << result << endl;
return 0;
}
```
## void
`void` 代表「無」,即該函式不返回任何資料或結果。通常用於僅執行某些操作(如打印訊息、修改參數等)而不需要返回結果的函式。特別注意,如果需要終止 `void` 類型的副函式的話,仍然可以使用 `return` ,只不過不需要加入回傳值(就寫一個 `return` 就好了)。
```cpp=1
#include <bits/stdc++.h>
using namespace std;
// 副函式:檢查數字是否為正數,若是則打印,否則提前退出
void printPositiveNumber(int num) {
if (num <= 0) {
cout << "The number is not positive. Exiting function." << endl;
return; // 提前結束函式執行
}
cout << "The number is positive: " << num << endl;
}
int main() {
int num;
cin >> num;
// 呼叫函式處理數字
printPositiveNumber(num);
return 0;
}
```
## bool
`bool` 會回傳 `true` 或者 `false`,通常用於判斷此操作是否合法,前面的迷宮就是用到這個類型的副函式。
```cpp=1
#include <bits/stdc++.h>
using namespace std;
// 副函式:檢查一個數字是否是 2 的倍數
bool isMultipleOfTwo(int n) {
return n % 2 == 0;
/* 可以看成下方這樣子
if(n % 2 == 0){
return true;
}
else{
return false;
}*/
}
int main() {
int num;
cin >> num;
// 調用 isMultipleOfTwo 函式,檢查 num 是否是 2 的倍數
if (isMultipleOfTwo(num)) {
cout << num << " is a multiple of 2." << endl;
} else {
cout << num << " is not a multiple of 2." << endl;
}
return 0;
}
```
# 遞迴
在高一上的數學,我們學習到了遞迴這個觀念,透過一定的運算規則,例如費氏數列:
* f(0)=0
* f(1)=1
* f(n)=f(n-1)=f(n-2) n>=2
而這種東西透過副函式反覆呼叫自己就能很好的實現,以費氏數列舉例如下。
```cpp=1
#include <bits/stdc++.h>
using namespace std;
// 遞迴函式:計算費氏數列的第 n 項
int fibonacci(int n) {
if (n == 0) {
return 0; // f(0) = 0
}
else if (n == 1) {
return 1; // f(1) = 1
}
else {
return fibonacci(n - 1) + fibonacci(n - 2); // f(n) = f(n-1) + f(n-2) n>=2
}
}
int main() {
int n;
cin >> n;
// 輸出第 n 項費氏數列的值
cout << "Fibonacci number at position " << n << " is: " << fibonacci(n) << endl;
return 0;
}
```
## 練習題
[c002. 10696 - f91](https://zerojudge.tw/ShowProblem?problemid=c002)
[f640. 函數運算式求值](https://zerojudge.tw/ShowProblem?problemid=f640)
# sort(排序)
`sort` 是一個內建比較函式,可以幫助我們將陣列的資料排序,以下是 `sort` 的語法。
```cpp=1
sort(arr.begin(), arr.end()); // 升序排序
sort(arr.begin(), arr.end(), compare); // 使用自定義的排序
```
關於自定義排序,你需要寫一個副函式來判斷,以下是參考寫法。
```cpp=1
bool compare(int a, int b) {
return a > b; // a 大於 b 時返回 true,實現降序排序
}
```
# vector(動態陣列)
`vector` 是一種動態陣列,相較我們之前學習的陣列,`vector` 提供了「不需要預先設定大小」的優勢,並且可以很方便的插入或刪除陣列中任何位置的資料,讓我們看看 `vector` 的語法吧!
## 宣告
```cpp=1
// 一維陣列
vector<int> v1; // 空的 vector,元素類型為 int
vector<int> v2(10); // 大小為 10 的 vector,元素初始值為 0
vector<int> v3(10, 5); // 大小為 10 的 vector,所有元素初始值為 5
vector<int> v4 = {1, 2, 3, 4, 5}; // 初始化列表
// 二維陣列
vector<vector<int>> v5; // 空的二維 vector,元素類型為 int
```
## 插入資料
```cpp=1
v.push_back(5); // 在 vector 的尾端插入元素 5
v.insert(v.begin(), 3); // 在 vector 的開頭插入元素 3
v.insert(v.begin() + 2, 7); // 在 vector 的第三個位置插入元素 7
```
## 刪除資料
```cpp=1
v.pop_back(); // 刪除 vector 最尾端的元素
v.erase(v.begin()); // 刪除 vector 開頭的元素
v.erase(iterator); // 刪除 vector 中 iterator 所指的元素
v.erase(v.begin()+1, v.begin() + 4); // 刪除 vector 中第 2 ~ 4個元素
v.erase(iterator1, iterator2); // 刪除 vector 中 iterator1 到 iterator2 的元素
v.clear(); // 刪除所有元素
```
## 其它
```cpp=1
v.size(); // 回傳 vector 目前的大小
v.resize(5); // 調整 vector 大小為 5
swap(v1, v2); // 交換兩個 vector 的元素
v.empty(); // 回傳一個 bool ,如果 vector 為空回傳 True,反之 False
```
---
以下是幾種比較常用到的模板寫法。
## 公式解-輸入總數已知
* 當題目敘述有「第一行輸入一個整數 $\text{n}$ ,接下來有 $\text{n}$ 行的測試資料」時可以使用下方寫法。
```cpp=1
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;
cin >> n; // 資料總數
vector<int> v(n); // 宣告一個大小為 n 的 vector
for (int i = 0; i < n; i++) {
cin >> v[i]; // 讀取使用者輸入的數字
}
for (int i = 0; i < n; i++) {
cout << v[i] << " "; // 輸出每個數字
}
return 0;
}
```
## 公式解-輸入終止條件已知
* 當題目敘述有「測試資料有數筆,當輸入 0 時終止」時可以使用下方寫法。
```cpp=1
#include <bits/stdc++.h>
using namespace std;
int main() {
vector<int> v; // 宣告一個空的 vector
int n;
while (cin >> n) {
if(n == 0){ // 預設終止條件為 0 (此處可以根據題目條件更改)
break;
}
v.push_back(n); // 將輸入的數字加入 vector
}
for (int i = 0; i < v.size(); i++) { // 用 v.size() 來取得陣列大小
cout << v[i] << " ";
}
return 0;
}
```
## 進階操作-pair
正常來說,陣列中的每一格只能儲存一個資料,但有些時候測試資料會存在前後關聯性,例如說該產品的「成本」和「利益」,這時候分成兩個 `vector` 儲存,在進行某些操作時可能會出問題,像是常見的 `sort` ,如果對「成本」進行小到大排序,利益就無法對齊原本的成本,但若使用 `pair` 來處理,就可以把這個產品的「成本」和「利益」綁在一起,移動的時候也一起搬,這樣就不會亂掉了!
```cpp
vector<int> v;
```

```cpp
vector<pair<int, int>> v;
```

可以看到 `pair` 的每個房間都可以儲存兩個資料 { first, second },調用方式如上圖,先指定房間後加上 .first 或.second 就可以調用數值了,記得一定要加「.」喔!
---
## 小試身手
* 題目:子柚擁有多間工廠,分別生產不同的產品。由於近期資金短缺,公司決定優先生產成本較低的產品。請根據產品的生產成本由低到高的順序,輸出對應產品的利益。
* 輸入說明:第一行為一個整數 $\text{n}$,表示產品的數量。接下來的 $\text{n}$ 行,每行包含兩個整數,分別代表該產品的成本和對應的利益,以空格分隔。
* 輸出說明:請根據成本從低到高的順序,輸出每個產品的利益,數字之間用空格分隔。
### 範例輸入1
```
7
50 60
60 40
30 50
10 80
80 90
40 30
90 10
```
### 範例輸出1
```
80 50 30 60 40 90 10
```
---
### 參考解答
你可以把資料用兩個 `vector` 儲存起來,並且對儲存成本的那個陣列進行 `sort` ,但這時候會發現,成本由小排到大了,但利益「並沒有跟著動」,而你又不知道現在的產品編號順序是多少,也就無法正確的照順序輸出利益。
(請注意,下方的產品編號只是輔助說明,並沒有被存在 `vector` 中,所以無法調用。)
| vector編號 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| -------- | --- | --- | --- | --- | --- | --- | --- |
| 產品編號 | 4 | 3 | 6 | 1 | 2 | 5 | 7 |
| 成本 | 10 | 30 | 40 | 50 | 60 | 80 | 90 |
| vector編號 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| -------- | --- | --- | --- | --- | --- | --- | --- |
| 產品編號 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 利益 | 60 | 40 | 50 | 80 | 90 | 30 | 10 |
所以這時候我們就要使用 `pair` 來幫助我們把兩個資料儲存成「一組」,放在一起。
(請注意,現在只有一個 `vector` !但是這個 `vector` 的每一個格子可以存放兩個整數。)
| vector編號 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| -------- | --- | --- | --- | --- | --- | --- | --- |
| 產品編號 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 成本 | 50 | 60 | 30 | 10 | 80 | 40 | 90 |
| 利益 | 60 | 40 | 50 | 80 | 90 | 30 | 10 |
這時候再進行 `sort` 就會發現!成本跟利益一起動了!
| vector編號 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| -------- | --- | --- | --- | --- | --- | --- | --- |
| 產品編號 | 4 | 3 | 6 | 1 | 2 | 5 | 7 |
| 成本 | 10 | 30 | 40 | 50 | 60 | 80 | 90 |
| 利益 | 80 | 50 | 30 | 60 | 40 | 90 | 10 |
此時將 `vector` 輸出就會得到題目所求了!
```
輸出結果:80 50 30 60 40 90 10
```
以下是參考代碼。
```cpp=1
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;
cin >> n;
vector<pair<int, int>> vp; // 宣告一個 pair<first, second> 的 vector
for (int i = 0; i < n; i++) {
int a, b;
cin >> a >> b; // a 是成本, b 是利益
vp.push_back({a, b});
}
// 按成本 (first) 進行排序
sort(vp.begin(), vp.end());
for (int i = 0; i < n; i++) {
cout << vp[i].second << " ";
}
cout << endl;
return 0;
}
```
# set
`set` 是 STL 中一個用來儲存元素的容器,具有以下特性
* 元素不重複
* 自動排序(小到大)
* 搜尋效率高(底層邏輯基於紅黑數)
## 宣告
```cpp=1
set<int> a // 宣告一個名稱為 a,存放整數的 set
set<int> b = {10, 20, 30, 40, 50}; // 初始化列表
```
## 插入資料
```cpp=1
a.insert(num) // 插入 num 到 set 之中
// 由於會自動排序,所以不需要指定插入位置
```
## 刪除資料
```cpp=1
a.erase(sum) // 將 sum 從 set 之中刪除
a.erase(iterator) // 刪除 iterator 所指位置的元素
a.clear() // 清空 set
```
## 其他
```cpp=1
a.size() // 回傳整數表示 set 的大小
a.empty() // 回傳一個 bool ,如果 vector 為空回傳 True,反之 False
```
## 進階操作-find
有些時候,我們希望知道一個元素是否在容器之中,如果存在的話又是在哪個位置,這時候我們就可以用 `find` 來幫助我們搜尋。
```cpp=1
#include <bits/stdc++.h>
using namespace std;
int main() {
set<int> numbers = {10, 20, 30, 40, 50};
int target = 30;
auto it = numbers.find(target);
// it 當指標,指向numbers 中 target 的位置
// 若沒有找到 則 it 指向 numbers 的尾端
if (it != numbers.end()) {
cout << "Found " << target << " in the set." << endl;
}
else {
cout << target << " not found in the set." << endl;
}
return 0;
}
```
---
## 小試身手
* 題目:子柚的班上有 $\text{n}$ 名學生,在一場考試中,子柚得到的成績為 $\text{x}$ ,他想知道自己的成績在班上的排名是多少。(同分者排名相同,其他人不需調整名次)
* 輸入:第一行輸入兩個整數 $\text{n}$ 和 $\text{m}$ ,輸入用空格分開,接著輸入 $\text{n}$ 個整數,代表班上每個人的成績(包含子柚本人)。$\left(2\leq \text{n}\leq 100\right)\left(0\leq \text{m}\leq 100\right)\left(0\leq \text{x}\leq 100\right)$
* 輸出:輸出子柚在班上的排名,輸出格式請參照範例輸出。
### 範例輸入1
```
5 90
60 70 80 90 100
```
### 範例輸出1
```
子柚在這場考試中排名第 2 名
```
測資說明:成績高到低為 100 90 80 70 60 ,第 1 名 100 分,第 2 名 90 分,第 3 名 80 分,以此類推,故 90 分排名第 2 名,輸出「子柚在這場考試中排名第 2 名」。
### 範例輸入2
```
7 42
87 36 58 42 91 86 87
```
### 範例輸出2
```
子柚在這場考試中排名第 5 名
```
測資說明:成績高到低為 91 87 87 86 58 42 36 ,其中 87 分的兩位並列第 2 名,第 3 名為 86 分,第 4 名為 58 分,依此類推,故 42 分排名第 5 名,輸出「子柚在這場考試中排名第 5 名」。
---
### 參考解答
```cpp=1
#include <bits/stdc++.h>
using namespace std;
int main() {
int n, m;
cin >> n >> m;
set<int> scores;
for (int i = 0; i < n; ++i) {
int score;
cin >> score;
scores.insert(score);
}
// 使用反向迭代器遍歷 set,從最大分數開始計算排名
int rank = 1; // 排名從 1 開始
for (auto it = scores.rbegin(); it != scores.rend(); ++it) {
// 如果當前分數等於子柚的成績,則輸出排名
if (*it == m) {
cout << "子柚在這場考試中排名第 " << rank << " 名" << endl;
break;
}
rank++;
}
return 0;
}
```
---
# map
`map` 是 STL 中一個用來儲存鍵值對(key-value pair)的容器,具有以下特性:
* 鍵(`key`)不重複,每個鍵對應一個值(`value`)
* 鍵自動排序(默認按鍵由小到大排序)
* 搜尋效率高(底層邏輯基於紅黑樹)
## 宣告
```cpp=1
map<int, string> a; // 宣告一個名稱為 a,存放「整數」鍵對應「字串」的 map
map<int, string> b = {{1, "apple"}, {2, "banana"}, {3, "cherry"}}; // 初始化
```
## 插入資料
```cpp=1
a.insert({key, value}); // 插入一個鍵值對 (key, value) 到 map 中
a[key] = value; // 也可以用這種方式來插入鍵值對
```
* 如果插入的 `key` 已經存在,則 `insert` 不會成功,且不會覆蓋現有的 `value`,但使用 `a[]` 則會覆蓋先前結果。
## 刪除資料
```cpp=1
a.erase(key); // 刪除指定 key 的元素
a.erase(iterator); // 刪除 iterator 所指位置的元素
a.clear(); // 清空 map
```
## 其他
```cpp=1
a.size(); // 回傳整數,表示 map 的大小
a.empty(); // 回傳布林值,若 map 為空則返回 true,否則返回 false
iterator->first // 取得 value 對應的 key
iterator->second // 取得 key 對應的 value
```
---
## 小試身手
* 題目:在數學中,班佛定律(Benford's law)描述了真實數字數據集中首位數字的頻率分布。首位數字 $a \left( a = 1, 2, 3, 4, \ldots, 8, 9 \right)$ 出現的概率為 $\log(1+\dfrac{1}{a})$ 。它可用於檢查各種數據是否有造假。現在子柚正在稽查公司的財務報表,請替他透過班佛法則來檢查數據是否有造假。
* 輸入:輸入一個整數 $\text{t}$ 代表接下來會有 $\text{t}$ 筆測資。每筆測資輸入多個正整數 $\text{n}$,若 $\text{n}$ 等於 $-1$ 代表輸入結束。
* 輸出:若數字 $a$ 為首的出現概率偏離理論值 $7\%$ (含)以上時,則此財務報表的數據有造假,輸出 $\text{fake}$。反之輸出 $\text{real}$。
* 第一子題(20%):$\text{t} = 1 , \text{n} \leq 10^3$
第二子題(40%):$\text{t} \leq 10 , \text{n} \leq 10^6$
第三子題(40%):$\text{t} \leq 100 , \text{n} \leq 10^{10}$
### 範例輸入1
```
1
12 16 54 87 54 95 3 2 65 7 57 95 12 58 100 16 57 8 4 5 9 17 54 21 53 -1
```
### 範例輸出1
```
fake
```
測資說明:數字分布如下:
| 數字 | 實際值 | 理論值 |
|------|--------|--------|
| 1 | 24% | 30% |
| 2 | 8% | 18% |
| 3 | 4% | 12% |
| 4 | 4% | 10% |
| 5 | 32% | 8% |
| 6 | 4% | 7% |
| 7 | 4% | 6% |
| 8 | 8% | 5% |
| 9 | 12% | 5% |
### 範例輸入2
```
1
10 20 14 40 49 6 13 58 39 23 79 12 1 31 100 30 38 2 15 54 39 20 98 1 40 2 68 94 59 14 16 11 71 18 36 9 50 55 54 1 26 73 19 48 20 23 11 97 75 16 38 2 22 26 13 19 21 48 98 2 36 56 66 14 4 25 22 19 53 100 37 28 41 69 23 55 23 34 14 43 96 19 19 90 22 12 19 1 30 28 25 72 13 58 13 34 16 58 100 21 -1
```
### 範例輸出2
```
real
```
測資說明:數字分布如下:
| 數字 | 實際值 | 理論值 |
|------|--------|--------|
| 1 | 31% | 30% |
| 2 | 22% | 18% |
| 3 | 12% | 12% |
| 4 | 8% | 10% |
| 5 | 11% | 8% |
| 6 | 4% | 7% |
| 7 | 5% | 6% |
| 8 | 0% | 5% |
| 9 | 7% | 5% |
### 參考解答
```cpp=1
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
int t;
cin >> t;
while (t--) {
map<int, int> freq; // 記錄每個首位數字的出現次數
int total = 0; // 數據的總數
long long n;
while (cin >> n && n != -1) {
// 找到首位數字
int first_digit = n;
while (first_digit >= 10) {
first_digit /= 10;
}
freq[first_digit]++; // 此數字對應的計數器+1
total++;
}
bool fake = false;
for (int a = 1; a <= 9; a++) {
double observed = (double)freq[a] / total; // 實際值
double expected = log10(1.0 + (1.0 / a)); // 理論值
cout << a << " " << observed << " " << expected << endl;
// 若偏離超過 7%,則認定為 fake
if (abs(observed - expected) > 0.07) {
fake = true;
break;
}
}
if (fake) {
cout << "fake" << endl;
} else {
cout << "real" << endl;
}
}
return 0;
}
```
---
# struct
在某些時候,`int` `float` 等宣告方式可能無法滿足我們的需求,例如座標點,需要同時儲存兩個資料,這時候我們就可以用 `struct` 來自定宣告型式,方法如下:
```cpp=1
#include <bits/stdc++.h>
using namespace std;
// struct必須宣告在副函式的位置
// 定義一個struct來表示座標點,類型名稱叫Point
struct Point {
float x; // x 座標
float y; // y 座標
}; // 這裡要分號
int main() {
// 宣告一個 Point 類型的變數
Point p;
// 輸入座標
cin >> p.x >> p.y;
// 輸出座標
cout << "輸入的座標是: (" << p.x << ", " << p.y << ")" << endl;
return 0;
}
```
`struct` 好用的地方就在,不需要用 `pair` ,或者多個陣列來儲存輸入資料,對於維護性和可讀性有顯著提升。
---
# typedef
`typedef` 主要用來為既有的資料型別取別名,使程式碼更簡潔、可讀性更高,特別是在多重 `pair`、數字與字串交錯使用時,能讓程式碼結構更加清晰。
## 基本用法
```cpp
typedef 現有型別 新名稱;
```
例如,我希望記錄每位同學的姓名(字串)、身高(整數)、體重(浮點數),此時我可以這樣宣告:
```cpp
typedef pair<string,pair<int,float>> data;
data students[n]; // 直接用data當資料型別
```
如果不使用 `typedef`,就要在宣告陣列的時候完整的寫出來:
```cpp
pair<string,pair<int,float>> students[n]
```
看似相同,但若有多個陣列要宣告,那版面的整潔就會有差別。
---
# defind
`defind` 也是用來宣告資料型別,但通常用於簡稱。同時 `defind` 也可以用來宣告變數的值,在某些觀念題可能會看到這種用法。
## 基本用法
```cpp
#define MAXN 1000
#define long long = LL
LL arr[MAXN]; // 宣告一個存放 long long ,陣列大小為 1000 的陣列
```
# queue
`queue` 是 C++ 標準模板庫(STL)中的一種容器,是一個 FIFO(First In, First Out) 結構,具有以下特性:
* 元素只能從尾端插入(push),從前端移除(pop)。
* 不允許隨機訪問元素,只能訪問前端(front)或者尾端(back)。
## 宣告
```cpp=1
queue<int> q; // 宣告一個存放整數的 queue
```
## 插入資料
```cpp=1
q.push(value); // 將元素 value 從 queue 尾端插入
```
## 移除資料
```cpp=1
q.pop(); // 移除 queue 前端的元素
```
## 存取資料
```cpp=1
q.front(); // 取得 queue 前端的元素
q.back(); // 取得 queue 尾端的元素
```
## 其他操作
```cpp=1
q.size(); // 回傳整數,表示 queue 的大小
q.empty(); // 回傳布林值,若 queue 為空則返回 true,否則返回 false
```
## 圖解程式
```cpp
queue<int> q; // 創建一個儲存整數的 queue
q.push(10); // 從 queue 的尾端插入 10
```

```cpp
q.push(20); // 從 queue 的尾端插入 20
```

```cpp
q.push(30); // 從 queue 的尾端插入 30
```

```cpp
cout << q.front() << " "; // 輸出 queue 前端的元素
q.pop(); // 刪除 queue 前端的元素
```
```
輸出:10
```

```cpp
cout << q.front() << " "; // 輸出 queue 前端的元素
q.pop(); // 刪除 queue 前端的元素
```
```
輸出:20
```

```cpp
cout << q.front() << " "; // 輸出 queue 前端的元素
q.pop(); // 刪除 queue 前端的元素
```
```
輸出:30
```
### 範例程式
```cpp=1
#include <bits/stdc++.h>
using namespace std;
int main() {
queue<int> q;
q.push(10);
q.push(20);
q.push(30);
while(!q.empty()){
cout << q.front() << " ";
q.pop();
}
return 0;
}
```
### 輸出結果
```
10 20 30
```
---
## 小試身手
* 題目:子柚在和水木目玩桌遊,規則是抽取將撲克牌洗好後堆一疊放在桌上,每人輪流抽取撲克牌最上方的一張,大聲唸出數字後,將對應數量的牌從牌堆最上方拿到底部(一次只能拿一張),直到只剩下一張牌在桌上為止。
* 輸入:輸入一串字串,代表牌堆由上而下的所有數字。(J代表11、Q代表12、K代表13)(保證數字為非負整數)
* 輸出:輸出最後一張牌的數字。(11請輸出J、12請輸出Q、13請輸出K)
* 第一子題(20%):字串長度不超過10,數字在1~9之間。
第二子題(40%):字串長度不超過50,數字保證沒有10。
第三子題(40%):無額外限制。
### 範例輸入1
```
123456789
```
### 範例輸出1
```
5
```
### 範例輸入2
```
13579JK246810Q
```
### 範例輸出2
```
6
```
:::spoiler 參考解答
```cpp=1
#include<bits/stdc++.h>
using namespace std;
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
string s;
cin>>s;
queue<int>q;
for(int i=0;i<s.size();i++){
if(s[i]=='1'){
if(i==s.size()-1)q.push(1);
else if(s[++i]=='0')q.push(10);
else{
q.push(1);
q.push(s[i]-'0');
}
}
else if(s[i]=='J')q.push(11);
else if(s[i]=='Q')q.push(12);
else if(s[i]=='K')q.push(13);
else q.push(s[i]-'0');
}
int t;
while(q.size()!=1){
t=q.front();q.pop();
for(int i=0;i<t;i++){
q.push(q.front());
q.pop();
}
}
switch(q.front()){
case 11:cout<<'J';break;
case 12:cout<<'Q';break;
case 13:cout<<'K';break;
default:cout<<q.front();
}
}
```
:::
---
# priority_queue
`priority_queue` 是一個基於堆(heap)的容器,具有以下特性:
* 元素按照優先級排序,預設為大頂堆(降序)。
* 元素只能從尾端插入(push),從前端移除(pop)。
## 宣告
```cpp=1
priority_queue<int> pq; // 宣告一個存放整數的大頂堆
priority_queue<int, vector<int>, greater<int>> min_pq; // 宣告一個存放整數的小頂堆
```
## 插入資料
```cpp=1
pq.push(value); // 將元素 value 插入到隊列中
```
## 移除資料
```cpp=1
pq.pop(); // 移除隊列中優先級最高的元素
```
## 存取資料
```cpp=1
pq.top(); // 取得隊列中優先級最高的元素
```
## 其他操作
```cpp=1
pq.size(); // 回傳整數,表示隊列的大小
pq.empty(); // 回傳布林值,若隊列為空則返回 true,否則返回 false
```
## 圖解程式
```cpp
priority_queue<int> pq; // 宣告一個最大堆
pq.push(10); // 插入 10 (自動照優先級排列)
```

```cpp
pq.push(20); // 插入 20(自動照優先級排列)
```

```cpp
pq.push(15); // 插入 15(自動照優先級排列)
```

```cpp
cout << pq.top() << " "; // 輸出優先級最高的元素
pq.pop(); // 移除優先級最高的元素
```
```
輸出結果:20
```

```cpp
cout << pq.top() << " "; // 輸出優先級最高的元素
pq.pop(); // 移除優先級最高的元素
```
```
輸出結果:15
```

```cpp
cout << pq.top() << " "; // 輸出優先級最高的元素
pq.pop(); // 移除優先級最高的元素
```
```
輸出結果:10
```
### 範例程式
```cpp=1
#include <bits/stdc++.h>
using namespace std;
int main() {
priority_queue<int> pq;
pq.push(10);
pq.push(20);
pq.push(15);
while(!pq.empty()){
cout << pq.top() << " ";
pq.pop();
}
return 0;
}
```
### 輸出結果
```
20 15 10
```
---
## 小試身手
* 題目:子柚開了一間理髮廳,標榜「笑著進來,笑著出去」,吸引了許多人前來朝聖。近日因排隊人龍過長,子柚決定實施 VIP 制度,VIP 等級越高的人可以越優先理髮,VIP等相同的則需要照先來後到的順序排隊。這是預計到訪的客人名單(已照時間順序排列),請替子柚計算接待完所有顧客所需花費的時間。
* 輸入:第一行輸入一個整數 $\text{n}$ ,代表理髮店的座位總數。接下來每行輸入兩個整數,代表每位顧客的 $\text{VIP}$ 等級和理髮所需時間 $\text{t}$ (單位:分鐘),若 $\text{VIP}$ 等級為 $-1$ 但 $\text{t}$ 不為 $0$ ,則代表經過 $\text{t}$ 分鐘(沒有新客人拜訪)。輸入直到 $\text{VIP}$ 等級和 $\text{t}$ 皆為 $-1$ 結束。
* 輸出:輸出所需時間(格式:dd/hh/mm)(詳見範例輸出,dd 位數可大於 $2$)
* 第一子題(10%):$\text{n} = 1, \text{m} \leq 10, \text{VIP} = 0$
第二子題(20%):$\text{n} \leq 3, \text{m} \leq 20, -1 \leq \text{VIP} \leq 0$
第三子題(30%):$\text{n} \leq 3, \text{m} \leq 20, -1 \leq \text{VIP} \leq 5$
第四子題(40%):無額外限制
### 範例輸入1
```
1
0 10
0 20
0 30
0 40
0 50
-1 -1
```
### 範例輸出1
```
00/02/30
```
### 範例輸入2
```
3
0 10
2 20
4 15
3 30
-1 5
5 25
3 20
2 10
-1 10
5 5
-1 -1
```
### 範例輸出2
```
00/00/50
```
### 參考解答
```cpp=1
#include<bits/stdc++.h>
#define ff first
#define ss second
using namespace std;
int main(){
ios_base::sync_with_stdio(0);
cin.tie(0);
int n;
cin>>n;
int ans=0;
int a[n]={};
priority_queue<pair<int,int>>pq;
int v=0,t=0;
while(cin>>v>>t&&(v!=-1&&t!=-1)){
if(v!=-1)pq.push({v,t});
if(v==-1){
ans+=t;
for(int i=0;i<n;i++){
a[i]-=t;
while(a[i]<=0){
a[i]+=pq.top().ss;
pq.pop();
}
}
}
}
while(!pq.empty()){
auto p=pq.top();pq.pop();
}
}
```
---
# deque
deque(double-ended queue)是 C++ 標準模板庫(STL)中的一種容器,具有以下特性:
* 支援從前端與尾端插入與移除元素
* 允許隨機訪問元素(類似 vector)
* 插入與刪除操作在兩端的效率都很高
## 宣告
```cpp=1
deque<int> dq; // 宣告一個存放整數的 deque
```
## 插入資料
```cpp=1
dq.push_back(value); // 將元素 value 插入到 deque 尾端
dq.push_front(value); // 將元素 value 插入到 deque 前端
```
## 移除資料
```cpp=1
dq.pop_back(); // 移除 deque 尾端的元素
dq.pop_front(); // 移除 deque 前端的元素
```
## 存取資料
```cpp=1
dq.front(); // 取得 deque 前端的元素
dq.back(); // 取得 deque 尾端的元素
dq[i]; // 以隨機存取方式取得第 i 個元素(類似陣列或 vector)
```
## 其他操作
```cpp=1
dq.size(); // 回傳整數,表示 deque 的大小
dq.empty(); // 回傳布林值,若 deque 為空則返回 true,否則返回 false
dq.clear(); // 移除所有元素,使 deque 變為空
```
## 圖解程式
```cpp
deque<int> dq; // 宣告一個存放整數的 deque
dq.push_front(10); // 將元素 10 插入到 deque 前端
```

```cpp
dq.push_front(20); // 將元素 20 插入到 deque 前端
```

```cpp
dq.push_back(30); // 將元素 30 插入到 deque 尾端
```

```cpp
cout << dq.front() << " ";
dq.pop_front(); // 移除 dq 前端的元素
```
```
輸出結果:20
```

```cpp
cout << dq.back() << " ";
dq.pop_back(); // 移除 dq 尾端的元素
```
```
輸出結果:30
```

```cpp
cout << dq.front() << " ";
dq.pop_front(); // 移除 dq 前端的元素
```
```
輸出結果:10
```
## 範例程式
```cpp=1
#include<bits/stdc++.h>
using namespace std;
int main(){
deque<int> dq;
dq.push_front(10);
dq.push_front(20);
dq.push_back(30);
for(int i=0;i<dq.size();i++){
if(i%2==0){
cout << dq.front() << " ";
dq.pop_front();
}
else{
cout << dq.back() << " ";
dq.pop_back();
}
}
return 0;
}
```
### 輸出結果
```
30 20 10
```
# stack
`stack` 是 C++ 標準模板庫(STL)中的一種容器,是一個 LIFO(Last In, First Out) 結構,具有以下特性:
* 元素只能從頂端插入(push)和移除(pop)。
* 不允許隨機訪問元素,只能訪問頂端(top)。
## 宣告
```cpp=1
stack<int> s; // 宣告一個存放整數的 stack
```
## 插入資料
```cpp=1
s.push(value); // 將元素 value 從 stack 頂端插入
```
## 移除資料
```cpp=1
s.pop(); // 移除 stack 頂端的元素
```
## 存取資料
```cpp=1
s.top(); // 取得 stack 頂端的元素
```
## 其他操作
```cpp=1
s.size(); // 回傳整數,表示 stack 的大小
s.empty(); // 回傳布林值,若 stack 為空則返回 true,否則返回 false
```
## 圖解程式
```cpp
stack<int> s; // 創建一個儲存整數的 stack
s.push(10); // 將 10 插入 stack 頂端
```

```cpp
s.push(20); // 將 20 插入 stack 頂端
```

```cpp
s.push(30); // 將 30 插入 stack 頂端
```

```cpp
cout << s.top() << " "; // 輸出 stack 頂端的元素
s.pop(); // 移除 stack 頂端的元素
```
```
輸出:30
```

```cpp
cout << s.top() << " "; // 輸出 stack 頂端的元素
s.pop(); // 移除 stack 頂端的元素
```
```
輸出:20
```

```cpp
cout << s.top() << " "; // 輸出 stack 頂端的元素
s.pop(); // 移除 stack 頂端的元素
```
```
輸出:10
```
### 範例程式
```cpp=1
#include <bits/stdc++.h>
using namespace std;
int main() {
stack<int> s;
s.push(10);
s.push(20);
s.push(30);
while(!s.empty()){
cout << s.top() << " ";
s.pop();
}
return 0;
}
```
### 輸出結果
```
30 20 10
```
---
## 小試身手
* 題目:子柚在課堂上用原子筆隨意寫下括號,接著嘗試替括號配對,並找出「最大的括號有多長」。規則如下:
1 . 配對的左右括號必須是「距離最近的」
2 . 每個括號長度視為 1 (包含未配對括號)
* 輸入:第一行輸入一個正整數 $\text{n}$ ,接下來 $\text{n}$ 行每行輸入一個字串
* 輸出:針對每一筆資料輸出最長有效括號長度,用換行隔開
* 第一子題(40%):$n < 10$,字串長度不超過 $50$
第二子題(60%):無額外限制
### 範例輸入1
```
3
(())()())(
(((()())
)()))(()
```
### 範例輸出1
```
4
6
2
```
測資說明:如下表
| <font color="#f00">(</font> | <font color="#F7A004">(</font> | <font color="#F7A004">)</font> | <font color="#f00">)</font> | <font color="#AC19C9">(</font> | <font color="#AC19C9">)</font> | <font color="#1936C9">(</font> | <font color="#1936C9">)</font> | ) | ( |
| -- | -- | -- | -- | -- | -- | -- | -- | -- | -- |
|0|1|2|3|4|5|6|7|8|9|
s[0] 配對 s[3],長度為 4
s[1] 配對 s[2],長度為 2
s[4] 配對 s[5],長度為 2
s[6] 配對 s[7],長度為 2
其餘未配對,長度為 1
| ( | ( | <font color="#f00">(</font> | <font color="#F7A004">(</font> | <font color="#F7A004">)</font> | <font color="#1936C9">(</font> | <font color="#1936C9">)</font> | <font color="#f00">)</font> |
| -- | -- | -- | -- | -- | -- | -- | -- |
|0|1|2|3|4|5|6|7|
s[3] 配對 s[4],長度為 2
s[5] 配對 s[6],長度為 2
s[2] 配對 s[7],長度為 6
其餘未配對,長度為 1
| ) | <font color="#f00">(</font> | <font color="#f00">)</font> | ) | ) | ( | <font color="#1936C9">(</font> | <font color="#1936C9">)</font> |
| -- | -- | -- | -- | -- | -- | -- | -- |
|0|1|2|3|4|5|6|7|
s[1] 配對 s[2],長度為 2
s[6] 配對 s[7],長度為 2
其餘未配對,長度為 1
### 範例輸入2
```
1
)(()((()))()()(((()))))(((())(()()))
```
### 範例輸出2
```
22
```
### 參考解答
---
# permutation
當我們要生成排列組合的時候,往往要花費大量時間來列出所有可能,在程式裡可能會使用多重 `for` 迴圈或者遞迴來處理,但往往時間複雜度都會很高。好在STL標準模板庫中有提供一種自動推導字典排序的函式,讓我們來看看 `permutation` 的用法吧!
## 字典序
字典序是透過ASCII編碼來比較大小,可以觀察數字 0 ~ 9 ,大寫字母 A ~ Z ,小寫字母 a ~ z 所對應的編碼,詳見下圖:

圖片來源:https://www.sciencebuddies.org/science-fair-projects/references/ascii-table
### 以先後順序比較
按照每個元素的順序,從第一個元素開始逐個比較,直到確定大小。例如:
* `apple` < `banana`(因為 `a` 比 `b` 小)。
* `car` < `cat`(因為 `r` 比 `t` 小)。
### 數字字典序
把數字看作字符串來比較每一位,例如:
* `123` < `132`(因為 `2` 比 `3` 小)。
* `12` < `120`(因為第 3 位空缺,看作比非空的小)。
## 常用-生成所有排序
```cpp=1
#include <bits/stdc++.h>
using namespace std;
int main() {
int N;
cin >> N;
int a[N];
// 初始化陣列 a 為 1 到 N
for (int i = 0; i < N; i++) {
a[i] = i + 1;
}
// 生成所有字典序排列
do {
for (int i = 0; i < N; i++) {
cout << a[i] << " ";
}
cout << endl;
} while (next_permutation(a, a + N)); // 將陣列 a 排成下一個字典序
// 若字典序已經最大則會回傳 false
return 0;
}
```
* 若陣列非升序排列,可視情況決定要不要先 `sort`
### 輸入
```
4
```
### 輸出結果
```
1 2 3 4
1 2 4 3
1 3 2 4
1 3 4 2
1 4 2 3
1 4 3 2
2 1 3 4
2 1 4 3
2 3 1 4
2 3 4 1
2 4 1 3
2 4 3 1
3 1 2 4
3 1 4 2
3 2 1 4
3 2 4 1
3 4 1 2
3 4 2 1
4 1 2 3
4 1 3 2
4 2 1 3
4 2 3 1
4 3 1 2
4 3 2 1
```
---
# DP
動態規劃是用來降低複雜度最好的方式,但往往都需要一點巧思,將複雜的問題轉化成簡單的子問題,找到一個共同的規則套用在問題中。
---
## 0/1背包
背包問題指的是有一堆物品要放入背包,這些物品各自有不同的「重量」與「價值」,背包有重量限制,你的目標是找出在不超過重量限制的情況下,可以獲得的最大價值,舉例如下:
背包限重8
|編號|重量|價值|
|-|-|-|
|1|2|1|
|2|3|2|
|3|4|5|
|4|5|6|
如果使用窮舉法,可以找到以下可能:
```
1+2+3+4:重量14 價值14 (超重)
1+2+3 :重量9 價值8 (超重)
1+2+4 :重量10 價值9 (超重)
1+3+4 :重量11 價值12 (超重)
2+3+4 :重量12 價值13 (超重)
1+2 :重量5 價值3
1+3 :重量6 價值6
1+4 :重量7 價值7
2+3 :重量7 價值7
2+4 :重量8 價值8
3+4 :重量9 價值11 (超重)
1 :重量2 價值1
2 :重量3 價值2
3 :重量4 價值5
4 :重量5 價值6
```
答案為 8,但窮舉法的時間複雜度是 $O(2^n)$,當物品種類變多,效率會變很差。
### 演算法講解
窮舉法之所以不方便解決這個問題,是因為物品過多,導致組合數太多。既然如此,為何我們不考慮先討論兩個物品就好呢?找出兩個問題的解後,再放入第三個物品,一次增加一種。於是我們畫出一個表格,第一行代表重量上限。並且因為可以選擇都不放,所以我們加入一個價值、重量皆為 0 的物品,方便討論。
假設有 n 種物品,背包最大負重為 w ,初始化二維陣列 dp[n+1][n+1]。
宣告兩個一維陣列儲存價值和重量,m 物品價值為 v[m],重量為 w[m]。
考慮第 i 種物品,最大重量為 j 的背包,dp[i+ 1][j]的最佳解,我們必須比較【放 / 不放】取利益:
* 【不放】不考慮此物品時最佳解(dp[i][j])
* 【放】不考慮此物品,並且重量上限排除此物品的最佳解 + 本物品價值(dp[i][j – w[i]] + v[i])
在遍歷時要注意,j必須大於等於 v[i],否則會越界(跑到陣列外)。若j小於 v[i],由於無法將物品放入背包,故最佳解必為【不放】。
※ 紅色那格為 dp[0][0]
| 標號 | 價值 | 重量 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| 0 | 0 | 0 | <font color="#f00">**0**</font> | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 1 | 1 | 2 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 2 | 2 | 3 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 3 | 5 | 4 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 4 | 6 | 5 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
我們先對編號 1 的物體進行討論。由於j必須大於等於 v[i],所以從重量上限 2 開始討論。考慮【放 / 不放】:
* 【不放】dp[0][2] = <font color="#F7A004">**0**</font>
* 【放】dp[0][2 – w[1]] + v[1] = dp[0][2 - 2] + v[1] = <font color="#00A600">**0**</font> + <font color="#0080FF">**1**</font> = <font color="#f00">**1**</font>
顯然 <font color="#f00">**1**</font> > <font color="#F7A004">**0**</font>,所以dp[1][2]= <font color="#f00">**1**</font>
| 標號 | 價值 | 重量 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| 0 | 0 | 0 | <font color="#00A600">**0**</font> | 0 | <font color="#F7A004">**0**</font> | 0 | 0 | 0 | 0 | 0 | 0 |
| 1 | <font color="#0080FF">**1**</font> | 2 | 0 | 0 | <font color="#f00">**1**</font> | 0 | 0 | 0 | 0 | 0 | 0 |
| 2 | 2 | 3 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 3 | 5 | 4 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 4 | 6 | 5 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
顯然對於重量上限 3~8 最佳解都會是 1。
| 標號 | 價值 | 重量 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 1 | 1 | 2 | 0 | 0 | 1 | <font color="#f00">**1**</font> | <font color="#f00">**1**</font> | <font color="#f00">**1**</font> | <font color="#f00">**1**</font> | <font color="#f00">**1**</font> | <font color="#f00">**1**</font> |
| 2 | 2 | 3 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 3 | 5 | 4 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 4 | 6 | 5 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
接著加入編號 2 的物體進行討論。由於j必須大於等於 v[i],所以最佳解必為【不放】。
| 標號 | 價值 | 重量 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 1 | 1 | 2 | <font color="#F7A004">**0**</font> | <font color="#F7A004">**0**</font> | <font color="#F7A004">**1**</font> | 1 | 1 | 1 | 1 | 1 | 1 |
| 2 | 2 | 3 | <font color="#f00">**0**</font> | <font color="#f00">**0**</font> | <font color="#f00">**1**</font> | 0 | 0 | 0 | 0 | 0 | 0 |
| 3 | 5 | 4 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 4 | 6 | 5 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
重量上限 3 時,考慮【放 / 不放】:
* 【不放】dp[1][3] = <font color="#F7A004">**1**</font>
* 【放】dp[1][3 – w[2] + v[2] = dp[1][3 - 3] + v[2] = <font color="#00A600">**0**</font> + <font color="#0080FF">**2**</font> = <font color="#f00">**2**</font>
顯然 <font color="#f00">**2**</font> > <font color="#F7A004">**1**</font>,所以dp[1][2]= <font color="#f00">**2**</font>
| 標號 | 價值 | 重量 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 1 | 1 | 2 | <font color="#00A600">**0**</font> | 0 | 1 | <font color="#F7A004">**1**</font> | 1 | 1 | 1 | 1 | 1 |
| 2 | <font color="#0080FF">**2**</font> | 3 | 0 | 0 | 1 | <font color="#f00">**2**</font> | 0 | 0 | 0 | 0 | 0 |
| 3 | 5 | 4 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 4 | 6 | 5 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
接著重量上限 5 的時候,考慮【放 / 不放】:
* 【不放】dp[1][5] = <font color="#F7A004">**1**</font>
* 【放】dp[1][5 – w[2] + v[2] = dp[1][5 - 3] + v[2] = <font color="#00A600">**1**</font> + <font color="#0080FF">**2**</font> = <font color="#f00">**3**</font>
顯然 <font color="#f00">**3**</font> > <font color="#F7A004">**1**</font>,所以dp[1][2]= <font color="#f00">**3**</font>
| 標號 | 價值 | 重量 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 1 | 1 | 2 | 0 | 0 | <font color="#00A600">**1**</font> | 1 | 1 | <font color="#F7A004">**1**</font> | 1 | 1 | 1 |
| 2 | <font color="#0080FF">**2**</font> | 3 | 0 | 0 | 1 | 2 | 2 | <font color="#f00">**3**</font> | 0 | 0 | 0 |
| 3 | 5 | 4 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 4 | 6 | 5 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
依此類推,完成表格,最後我們可以看到,背包重量上限 8 的情況下,考慮所有物品放入情況,最大價值為 <font color="#f00">**8**</font> 。
| 標號 | 價值 | 重量 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 1 | 1 | 2 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
| 2 | 2 | 3 | 0 | 0 | 1 | 2 | 2 | 3 | 3 | 3 | 3 |
| 3 | 5 | 4 | 0 | 0 | 1 | 2 | 5 | 5 | 6 | 7 | 7 |
| 4 | 6 | 5 | 0 | 0 | 1 | 2 | 5 | 6 | 6 | 7 | <font color="#f00">**8**</font> |
---
### 小試身手
* 題目:神魔之塔是一款 2013 年所發行的轉珠遊戲,至今每月活躍玩家仍有破百萬。在遊戲中,每場戰鬥最多能出場六名英雄(不一定要上滿六名英雄),隊伍根據召喚師等級有隊伍空間上限,每隻英雄都有自己所佔的空間,以及各項屬性。現在子柚有 $\text{n}$ 名英雄,每名英雄所占隊伍空間為 $S_i$ ,綜合戰力為 $C_i$ ,帳號等級為 $\text{L}$ ,召喚師等級和隊伍空間上限的換算如下表,請替子柚找出最佳的隊伍配置。(照字典排序小到大輸出)
帳號起始召喚師等級 1 ,這 1 級也算進去升 1 等內(也就是5等的時候背包上限會+1)
起始背包空間上限為 85 。
| 等級區間 | 隊伍空間上限 |
| -------- | -------- |
| 0~99(含) | 每 5 等增加 1 |
| 100~199(含) | 每 2 等增加 1 |
| 200~399(含) | 每 5 等增加 1 |
| 400~1000(含) | 每 10 等增加 1 |
舉例:
9 等的時候背包上限為 86
84 等的時候背包上限為 101
263 等的時候背包上限為 167
810 等的時候背包上限為 236
* 輸入說明:第一行輸入兩個整數 $\text{n}$ 和 $\text{L}$ ,第二行開始每行輸入兩個整數 $S_i$ 和 $C_i$ ,其中 $i$ 為英雄編號$\left(1 \leq i \leq \text{n}\right)$
* 輸出說明:輸出最高戰力
#### 範例輸入1
```
7 84
18 97
11 65
12 66
6 41
13 71
30 154
19 118
```
#### 範例輸出1
```
547
```
:::spoiler **參考解答**
```cpp=1
#include<bits/stdc++.h>
using namespace std;
const int c[]={0,100,200,400,INT_MAX};
const int c2[]={5,2,5,10,-1};
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
int n,L;
cin>>n>>L;
int w=85;
for(int i=1;i<=4;i++){
if(L>c[i])w+=(c[i]-c[i-1])/c2[i-1];
else{
w+=(L-c[i-1])/c2[i-1];
break;
}
}
vector<vector<int>>dp(w+1,vector<int>(7,0));
vector<vector<int>>v(n,vector<int>(2));
for(int i=0;i<n;i++)cin>>v[i][0]>>v[i][1];
for(int i=0;i<n;i++){
for(int j=w;j>=0;j--){
for(int k=6;k>=1;k--){
if(j>=v[i][0])dp[j][k]=max(dp[j][k],dp[j-v[i][0]][k-1]+v[i][1]);
}
}
}
int ans=-1;
for(int i=0;i<=6;i++)ans=max(ans,dp[w][i]);
cout<<ans;
}
```
:::
---
## LCS(Longest Common Subsequence)
LCS(最長共同子序列)的定義為出現於每一個序列、而且是最長的子序列,舉例如下:
```
longest
love
```
不難發現,兩者的最長共同子序列為 loe,再看一個例子:
```
abcdefghijklmnzyxwvutsrqpo
opqrstuvwxyzabcdefghijklmn
```
這時候就頭昏眼花了吧,有沒有什麼方法可以讓我們有系統性的找出最長共同子序列呢?
---
### 演算法講解
回到最一開始的舉例,但我們這次把問題拆解:
```
longest
love
```
既然第一個字母 l 相同,那我其實只需要找到 ongest 和 ove 的最長共同子序列,再補上 l 就是答案了。
```
ongest
ove
```
既然第一個字母 o 相同,那我其實只需要找到 ngest 和 ve 的最長共同子序列,再補上 o 就是答案了。
```
ngest
ve
```
v 和 ngest 都沒有相同的字母,那其實找 e 和 negst 的最長共同子序列就好了。
```
ngest
e
```
e 和 ng 都沒有相同的字母,其實只要找 e 和 est 的最長共同子序列就好了。
```
est
e
```
既然第一個字母 e 相同,那我其實只需要找到 st 和 空序列的最長共同子序列,再補上 e 就是答案了,但是任何序列和空序列的最長共同子序列一定是空序列,因此不需再往下做。
回頭把最長共同子序列拼出來,依序可得 e o l,反向排列後即是答案 loe。
這次我初始化的一個二維陣列,並且在遍歷的過程中遵守兩個規則:
* 相同,從左上那格+1填入
* 不同,從上方和左方挑選最大的填入
### 起始條件
```cpp
int dp[m][n];
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
dp[i][j]=1;
}
}
```
### 狀態轉移方程
```cpp
if(string1[i]==string2[j]){
dp[i][j]=dp[i-1][j-1]+1;
}
else{
dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
}
```
### 解答
```cpp
cout << dp[m-1][n-1];
```
| | | l | o | n | g | e | s | t |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| **l** | 0 | | | | | | | |
| **o** | 0 | | | | | | | |
| **v** | 0 | | | | | | | |
| **e** | 0 | | | | | | | |
從左上開始,先比較 l 和 l 是否相同,發現相同,所以從左上那格+1填入此格。
| | | <font color="#f00">l</font> | o | n | g | e | s | t |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| | <font color="#1936C9">**0**</font> | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| <font color="#f00">**l**</font> | 0 | <font color="#AC19C9">**1**</font> | | | | | | |
| **o** | 0 | | | | | | | |
| **v** | 0 | | | | | | | |
| **e** | 0 | | | | | | | |
接著比較 o 和 l 是否相同,發現不同,所以從上方和左方挑選最大的填入此格。
| | | l | <font color="#f00">o</font> | n | g | e | s | t |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| | 0 | 0 | <font color="#1936C9">**0**</font> | 0 | 0 | 0 | 0 | 0 |
| <font color="#f00">**l**</font> | 0 | <font color="#1936C9">**1**</font> | <font color="#AC19C9">**1**</font> | | | | | |
| **o** | 0 | | | | | | | |
| **v** | 0 | | | | | | | |
| **e** | 0 | | | | | | | |
遍歷完後,會得到下方表格:
| | | l | o | n | g | e | s | t |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| **l** | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
| **o** | 0 | 1 | 2 | 2 | 2 | 2 | 2 | 2 |
| **v** | 0 | 1 | 2 | 2 | 2 | 2 | 2 | 2 |
| **e** | 0 | 1 | 2 | 2 | 2 | 3 | 3 | 3 |
此時若題目只詢問最長共同子序列長度,就只需要輸出最後一格就好了,但想知道這個序列是什麼也難不倒我們,只需要按照下方規則從最後一格遍歷回來:
* 優先往大的地方走(只能往左和往上)
* 發現下一格數字改變,記錄字母於陣列中,並且跳到左上方的格子
| | | l | o | n | g | <font color="#f00">e</font> | s | t |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| **l** | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
| **o** | 0 | 1 | 2 | 2 | 2 | 2 | 2 | 2 |
| **v** | 0 | 1 | 2 | 2 | <font color="#F7A004">2</font> | 2 | 2 | 2 |
| <font color="#f00">**e**</font> | 0 | 1 | 2 | 2 | <font color="#1936C9">**2**</font> | <font color="#AC19C9">**3**</font> | 3 | 3 |
陣列:{e}
優先往大的地方走(但是只能往左和往上)。
| | | l | o | n | g | e | s | t |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| **l** | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
| **o** | 0 | 1 | <font color="#F7A004">2</font> | 2 | 2 | 2 | 2 | 2 |
| **v** | 0 | 1 | <font color="#AC19C9">**2**</font> | 2 | 2 | 2 | 2 | 2 |
| **e** | 0 | 1 | 2 | 2 | 2 | 3 | 3 | 3 |
| | | l | <font color="#f00">o</font> | n | g | e | s | t |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| **l** | 0 | <font color="#F7A004">1</font> | 1 | 1 | 1 | 1 | 1 | 1 |
| <font color="#f00">**o**</font> | 0 | <font color="#1936C9">**1**</font> | <font color="#AC19C9">**2**</font> | 2 | 2 | 2 | 2 | 2 |
| **v** | 0 | 1 | 2 | 2 | 2 | 2 | 2 | 2 |
| **e** | 0 | 1 | 2 | 2 | 2 | 3 | 3 | 3 |
陣列:{e, o}
| | | <font color="#f00">l</font> | o | n | g | e | s | t |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| | <font color="#F7A004">0</font> | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| <font color="#f00">**l**</font> | <font color="#1936C9">**0**</font> | <font color="#AC19C9">**1**</font> | 1 | 1 | 1 | 1 | 1 | 1 |
| **o** | 0 | 1 | 2 | 2 | 2 | 2 | 2 | 2 |
| **v** | 0 | 1 | 2 | 2 | 2 | 2 | 2 | 2 |
| **e** | 0 | 1 | 2 | 2 | 2 | 3 | 3 | 3 |
陣列:{e, o, l}
最後反向輸出陣列即可得到最長共同子序列 loe 。
* 網路上找到的 [LCS操作練習](https://alchemist-al.com/algorithms/longest-common-subsequence)
---
### 小試身手
* 題目:子柚和ㄚ豪在上英文課時想打瞌睡,為了避免自己睡著,兩人決定比賽尋找課文文句的最長共同子序列(LCS),並且將找到的子序列輸出出來,其中空格與標點符號不計,大小寫視為相同。
* 輸入說明:輸入共有兩行含有空格的字串
* 輸出說明:第一行輸出一個整數 $\text{n}$ 代表最長共同子序列的長度,第二行輸出最長共同子序列的字串(以小寫字母輸出),若最長共同子序列為空字串,則輸出 $\text{Nothing}$。$\left( 0 \leq n \leq 100 \right)$
#### 範例輸入1
```
Athletes often experience muscle pain because of their workout routines.
It is advisable to stop exercising as soon as one is hurt to prevent more serious injuries.
```
#### 範例輸出1
```
30
tletotexercsinasoeirortrouines(或aletsoexerisinasoeirororouines)
```
---
:::spoiler **參考解答**
```cpp=1
#include<bits/stdc++.h>
using namespace std;
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
string a,b;
string s="",s2="";
getline(cin,a);
for(auto i:a)if(isalpha(i))s+=tolower(i);
getline(cin,b);
for(auto i:b)if(isalpha(i))s2+=tolower(i);
vector<vector<int>>v(s.size()+1,vector<int>(s2.size()+1,0));
for(int i=1;i<=s.size();i++){
for(int j=1;j<=s2.size();j++){
if(s[i-1]==s2[j-1])v[i][j]=v[i-1][j-1]+1;
else v[i][j]=max(v[i-1][j],v[i][j-1]);
}
}
cout<<v[s.size()][s2.size()]<<'\n';
if(v[s.size()][s2.size()]==0)cout<<"Nothing";
vector<char>v2;
int i=s.size(),j=s2.size();
while(v2.size()!=v[s.size()][s2.size()]){
if(v[i][j]==max(v[i-1][j],v[i][j-1])+1){
v2.push_back(s[i-1]);
i--;
j--;
}
else{
if(v[i-1][j]>v[i][j-1])i--;
else j--;
}
}
for(int i=v2.size()-1;i>=0;i--)cout<<v2[i];
}
```
:::
---
## LIS(Longest Increasing Subsequence)
LIS(最長遞增子序列)的定義為所有子序列中,遞增的、最長的子序列,舉例如下:
```
1 4 2 3
```
我們可以把所有子序列找出來:
```
1 4 2 3
1 4 2
1 4 3
1 2 3
1 4
1 2
1 3
1
4 2 3
4 2
4 3
4
2 3
2
3
```
其中不屬於遞增(由小到大)的我們將其移除,留下遞增子序列:
```
1 2 3
1 4
1 2
1 3
1
2 3
2
3
```
這之中最長的為 1 2 3,故最長遞增子序列為 1 2 3,長度為3。
但窮舉出每一種可能效率太低了,下面我們學習一種演算法來提高效率。
---
### 演算法講解
首先將n個數字先存入a[n]陣列中,並且初始化長度為1。
接著宣告一個陣列dp[n],陣列中的值代表「以此數字為結尾的最長遞增子序列長度」。
| 1 | 4 | 2 | 3 |
|---- | ---- | ---- | ---- |
| 1 | 1 | 1 | 1 |
* 若本項大於前一項,代表可以再放入這一項到子序列後方,則長度為 `max`(前項+1 , 本項)
* 若本項小於前項,代表無法放入這一項到子序列後方,則長度不變
### 起始條件
每個元素單獨成為 LIS 時,長度至少為 1。
```cpp
int dp[n];
for(int i=0;i<n;i++){
dp[i]=1;
}
```
### 狀態轉移方程
```cpp
if(a[i]>a[i-1]){
dp[i]=max(dp[i-1]+1,dp[i]);
}
else{
dp[i]=dp[i-1];
}
```
### 解答
```cpp
cout << dp[n-1];
```
按照這個邏輯,我們用雙重 `for` 迴圈來比較每一項之間的大小關係。
4 > 1,`max`(1 + 1 , 1)= 2。
| <font color="#1936C9">1</font> | <font color="#f00">4</font> | 2 | 3 |
|---- | ---- | ---- | ---- |
| <font color="#F7A004">**1**</font> | <font color="#AC19C9">**2**</font> | 1 | 1 |
2 > 1,`max`(1 + 1 , 1)= 2。
| <font color="#1936C9">1</font> | 4 | <font color="#f00">2</font> | 3 |
|---- | ---- | ---- | ---- |
| <font color="#F7A004">**1**</font> | 2 | <font color="#AC19C9">**2**</font> | 1 |
2 < 4,不變。
| 1 | <font color="#1936C9">4</font> | <font color="#f00">2</font> | 3 |
|---- | ---- | ---- | ---- |
| 1 | <font color="#F7A004">**2**</font> | <font color="#AC19C9">**2**</font> | 1 |
3 > 1,`max`(1 + 1 , 1)= 2。
| <font color="#1936C9">1</font> | 4 | 2 | <font color="#f00">3</font> |
|---- | ---- | ---- | ---- |
| <font color="#F7A004">**1**</font> | 2 | 2 | <font color="#AC19C9">**2**</font> |
3 < 4,不變。
| 1 | <font color="#1936C9">4</font> | 2 | <font color="#f00">3</font> |
|---- | ---- | ---- | ---- |
| 1 | <font color="#F7A004">**2**</font> | 2 | <font color="#AC19C9">**2**</font> |
3 > 2,`max`(2 + 1 , 2)= 3。
| 1 | 4 | <font color="#1936C9">2</font> | <font color="#f00">3</font> |
|---- | ---- | ---- | ---- |
| 1 | 2 | <font color="#F7A004">**2**</font> | <font color="#AC19C9">**3**</font> |
故最長遞增子序列長度為3,若要找出此子序列,我們可以遵循以下規則進行反向遍歷。
* 紀錄第一個此長度的數字
* 長度為 1 時結束
首次出現長度為 3,紀錄 3。
| 1 | 4 | 2 | <font color="#f00">3</font> |
|---- | ---- | ---- | ---- |
| 1 | 2 | 2 | <font color="#AC19C9">**3**</font> |
首次出現長度為 2,紀錄 2。
| 1 | 4 | <font color="#f00">2</font> | 3 |
|---- | ---- | ---- | ---- |
| 1 | 2 | <font color="#AC19C9">**2**</font> | 3 |
長度 2 並非首次出現,跳過不紀錄。
| 1 | <font color="#f00">4</font> | 2 | 3 |
|---- | ---- | ---- | ---- |
| 1 | <font color="#AC19C9">**2**</font> | 2 | 3 |
首次出現長度為 1,紀錄 1。
| <font color="#f00">1</font> | 4 | 2 | 3 |
|---- | ---- | ---- | ---- |
| <font color="#AC19C9">**1**</font> | 2 | 2 | 3 |
* 網路上找到的 [LIS操作練習](https://alchemist-al.com/algorithms/longest-increasing-subsequence)
---
### 補充(只求LIS長度)
若是題目只求LIS長度,我們可以用偷懶一點的方式:嘗試找到LIS每個位置中可以擺放的最小值。(複雜度:$nlogn$)
```cpp=
#include<bits/stdc++.h>
using namespace std;
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
int n;
cin >> n;
vector<int> a(n); // 原序列
for(int i=0;i<n;i++){
cin >> a[i];
}
vector<int> b; // LIS每個位置的最小值
for(int i=0;i<n;i++){
if(b.empty() or b.back()<a[i]){ // 空序列or現在這個數比LIS最後一位還大
b.push_back(a[i]); // 插入到LIS最後方
}
else{
*lower_bound(b.begin(),b.end(),a[i])=a[i];
// 使用二分搜找到第一個大於a[i]的數字,將其替換成更小的a[i]。
}
}
cout << b.size(); // 輸出LIS長度
return 0;
}
```
### 小試身手
* 題目:子柚和ㄚ豪在對樂透時,發現網站的數字分成開出順序和大小順序,而有些時候又非常恰巧兩者順序會相同,於是兩人就想找出開出數序中由小到大最長的排序方式。


* 輸入說明:第一行輸入一個整數 $\text{n}$ ,第二行輸入 $\text{n}$ 個用空格隔開的數字。
* 輸出說明:第一行輸出一個整數 $\text{m}$,表示最長遞增子序列的長度;第二行輸出最長遞增子序列(若有多個,輸出字典序最小的那一個)。 $\left(0 < m \leq 1000\right)$
* 第一子題(40%):$\text{n} = 7 , 0 < \text{m} \leq 7$
第二子題(60%):$0 < \text{n} \leq 10000 , 0 \leq \text{m} \leq 1000$
#### 範例輸入1
```
7
29 38 28 11 07 39 33
```
#### 範例輸出1
```
3
29 38 39
```
#### 範例輸入2
```
15
63 24 98 2 4 68 24 64 21 9 67 34 128 91 45
```
#### 範例輸出2
```
6
2 4 24 64 67 91
```
---
:::spoiler **參考解答**
```cpp=
#include<bits/stdc++.h>
using namespace std;
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
int n;cin>>n;
vector<int>v(n);
for(int i=0;i<n;i++)cin>>v[i];
vector<int>a(n,1);
int ans=1;
for(int i=0;i<n;i++){
for(int j=i+1;j<n;j++){
if(v[j]>v[i])a[j]=max(a[i]+1,a[j]);
}
ans=max(a[i],ans);
}
cout<<ans<<'\n';
vector<int>tmp;
for(int i=n-1;i>=0&&ans!=0;i--){
if(a[i]==ans){
tmp.push_back(v[i]);
ans--;
}
}
for(int i=tmp.size()-1;i>=0;i--)cout<<tmp[i]<<' ';
}
```
:::
---
# BFS&DFS
BFS(Breadth-First Search,廣度優先搜尋)是一種圖論演算法,常用於尋找最短路徑、遍歷圖中的所有節點或解決迷宮問題等。它的主要特點是逐層擴展,即先探索與當前節點距離為 1 的所有節點,再探索距離為 2、3... 直到遍歷完整個圖。
DFS(Depth-First Search,深度優先搜尋)是一種遍歷或搜尋圖的演算法,它的基本思想是「一條路走到底」,直到無法繼續後才回溯,並探索其他可能的路徑。DFS 可用來解決拓撲排序、圖遍歷、迷宮求解等問題。

## 演算法講解
以下我將示範如何使用BFS和DFS解決迷宮問題。
迷宮問題的基本要素包括:起點、終點、障礙物。
我們的目標是找到一條從起點到終點的可行路徑。來試看看走迷宮吧!
### DFS實作
```
地圖大小:5 5
起點:0 0
終點:4 4
0 0 1 0 0
0 1 0 1 0
0 0 0 0 0
1 1 1 0 1
0 0 0 0 0
```
我們把地圖畫成表格來看(紅色是起點,橘色是終點)
| <font color="#f00">0</font> | 0 | 1 | 0 | 0 |
| --- | --- | ---- | --- | --- |
| 0 | 1 | 0 | 1 | 0 |
| 0 | 0 | 0 | 0 | 0 |
| 1 | 1 | 1 | 0 | 1 |
| 0 | 0 | 0 | 0 | <font color="#F7A004">**0**</font> |
首先我們站在起點(青色是路徑)
| <font color="#00FFFF">0</font> | 0 | 1 | 0 | 0 |
| --- | --- | ---- | --- | --- |
| 0 | 1 | 0 | 1 | 0 |
| 0 | 0 | 0 | 0 | 0 |
| 1 | 1 | 1 | 0 | 1 |
| 0 | 0 | 0 | 0 | <font color="#F7A004">**0**</font> |
未到達終點
→分析可以走的方向:向右、向下
→選擇向右嘗試看看
| <font color="#00FFFF">0</font> | <font color="#00FFFF">0</font> | 1 | 0 | 0 |
| --- | --- | ---- | --- | --- |
| 0 | 1 | 0 | 1 | 0 |
| 0 | 0 | 0 | 0 | 0 |
| 1 | 1 | 1 | 0 | 1 |
| 0 | 0 | 0 | 0 | <font color="#F7A004">**0**</font> |
未到達終點
→分析可以走的方向:無
→走回上一步
| <font color="#00FFFF">0</font> | 0 | 1 | 0 | 0 |
| --- | --- | ---- | --- | --- |
| 0 | 1 | 0 | 1 | 0 |
| 0 | 0 | 0 | 0 | 0 |
| 1 | 1 | 1 | 0 | 1 |
| 0 | 0 | 0 | 0 | <font color="#F7A004">**0**</font> |
分析可以走的方向:向右(無法到達終點)、向下
→選擇向下嘗試看看
| <font color="#00FFFF">0</font> | 0 | 1 | 0 | 0 |
| --- | --- | ---- | --- | --- |
| <font color="#00FFFF">**0**</font> | 1 | 0 | 1 | 0 |
| 0 | 0 | 0 | 0 | 0 |
| 1 | 1 | 1 | 0 | 1 |
| 0 | 0 | 0 | 0 | <font color="#F7A004">**0**</font> |
未到達終點
→分析可以走的方向:向下
| <font color="#00FFFF">0</font> | 0 | 1 | 0 | 0 |
| --- | --- | ---- | --- | --- |
| <font color="#00FFFF">**0**</font> | 1 | 0 | 1 | 0 |
| <font color="#00FFFF">**0**</font> | 0 | 0 | 0 | 0 |
| 1 | 1 | 1 | 0 | 1 |
| 0 | 0 | 0 | 0 | <font color="#F7A004">**0**</font> |
未到達終點
→分析可以走的方向:向右
| <font color="#00FFFF">0</font> | 0 | 1 | 0 | 0 |
| --- | --- | ---- | --- | --- |
| <font color="#00FFFF">**0**</font> | 1 | 0 | 1 | 0 |
| <font color="#00FFFF">**0**</font> | <font color="#00FFFF">**0**</font> | 0 | 0 | 0 |
| 1 | 1 | 1 | 0 | 1 |
| 0 | 0 | 0 | 0 | <font color="#F7A004">**0**</font> |
未到達終點
→分析可以走的方向:向右
| <font color="#00FFFF">0</font> | 0 | 1 | 0 | 0 |
| --- | --- | ---- | --- | --- |
| <font color="#00FFFF">**0**</font> | 1 | 0 | 1 | 0 |
| <font color="#00FFFF">**0**</font> | <font color="#00FFFF">**0**</font> | <font color="#00FFFF">**0**</font> | 0 | 0 |
| 1 | 1 | 1 | 0 | 1 |
| 0 | 0 | 0 | 0 | <font color="#F7A004">**0**</font> |
未到達終點
→分析可以走的方向:向上、向右
→選擇向上嘗試看看
| <font color="#00FFFF">0</font> | 0 | 1 | 0 | 0 |
| --- | --- | ---- | --- | --- |
| <font color="#00FFFF">**0**</font> | 1 | <font color="#00FFFF">**0**</font> | 1 | 0 |
| <font color="#00FFFF">**0**</font> | <font color="#00FFFF">**0**</font> | <font color="#00FFFF">**0**</font> | 0 | 0 |
| 1 | 1 | 1 | 0 | 1 |
| 0 | 0 | 0 | 0 | <font color="#F7A004">**0**</font> |
未到達終點
→分析可以走的方向:無
→走回上一步
| <font color="#00FFFF">0</font> | 0 | 1 | 0 | 0 |
| --- | --- | ---- | --- | --- |
| <font color="#00FFFF">**0**</font> | 1 | <font color="#00FFFF">**0**</font> | 1 | 0 |
| <font color="#00FFFF">**0**</font> | <font color="#00FFFF">**0**</font> | <font color="#00FFFF">**0**</font> | 0 | 0 |
| 1 | 1 | 1 | 0 | 1 |
| 0 | 0 | 0 | 0 | <font color="#F7A004">**0**</font> |
分析可以走的方向:向上、向右
→選擇向右嘗試看看
| <font color="#00FFFF">0</font> | 0 | 1 | 0 | 0 |
| --- | --- | ---- | --- | --- |
| <font color="#00FFFF">**0**</font> | 1 | 0 | 1 | 0 |
| <font color="#00FFFF">**0**</font> | <font color="#00FFFF">**0**</font> | <font color="#00FFFF">**0**</font> | <font color="#00FFFF">**0**</font> | 0 |
| 1 | 1 | 1 | 0 | 1 |
| 0 | 0 | 0 | 0 | <font color="#F7A004">**0**</font> |
未到達終點
→分析可以走的方向:向下、向右
→選擇向下嘗試看看
| <font color="#00FFFF">0</font> | 0 | 1 | 0 | 0 |
| --- | --- | ---- | --- | --- |
| <font color="#00FFFF">**0**</font> | 1 | 0 | 1 | 0 |
| <font color="#00FFFF">**0**</font> | <font color="#00FFFF">**0**</font> | <font color="#00FFFF">**0**</font> | <font color="#00FFFF">**0**</font> | 0 |
| 1 | 1 | 1 | <font color="#00FFFF">**0**</font> | 1 |
| 0 | 0 | 0 | 0 | <font color="#F7A004">**0**</font> |
未到達終點
→分析可以走的方向:向下
| <font color="#00FFFF">0</font> | 0 | 1 | 0 | 0 |
| --- | --- | ---- | --- | --- |
| <font color="#00FFFF">**0**</font> | 1 | 0 | 1 | 0 |
| <font color="#00FFFF">**0**</font> | <font color="#00FFFF">**0**</font> | <font color="#00FFFF">**0**</font> | <font color="#00FFFF">**0**</font> | 0 |
| 1 | 1 | 1 | <font color="#00FFFF">**0**</font> | 1 |
| 0 | 0 | 0 | <font color="#00FFFF">**0**</font> | <font color="#F7A004">**0**</font> |
未到達終點
→分析可以走的方向:向右、向左
→選擇向右
| <font color="#00FFFF">0</font> | 0 | 1 | 0 | 0 |
| --- | --- | ---- | --- | --- |
| <font color="#00FFFF">**0**</font> | 1 | 0 | 1 | 0 |
| <font color="#00FFFF">**0**</font> | <font color="#00FFFF">**0**</font> | <font color="#00FFFF">**0**</font> | <font color="#00FFFF">**0**</font> | 0 |
| 1 | 1 | 1 | <font color="#00FFFF">**0**</font> | 1 |
| 0 | 0 | 0 | <font color="#00FFFF">**0**</font> | <font color="#00FFFF">**0**</font> |
到達終點
→回傳有解、步數等資訊
上述這個方法就是DFS的模型,我們可以用程式將這個邏輯寫出來,如下
### DFS解
```cpp=
#include <bits/stdc++.h>
using namespace std;
const int MAX_N = 100;
int maze[MAX_N][MAX_N]; // 迷宮地圖
bool visited[MAX_N][MAX_N]; // 記錄是否訪問過
int n, m;
int min_steps = INT_MAX; // 記錄最短步數
// 方向向量:右、下、左、上
int dx[4] = {1, 0, -1, 0};
int dy[4] = {0, 1, 0, -1};
void dfs(int x, int y, int steps, int ex, int ey) {
if (x == ex && y == ey) {
min_steps = min(min_steps, steps);
return;
}
visited[x][y] = true;
for (int i = 0; i < 4; i++) {
int nx = x + dx[i];
int ny = y + dy[i];
if (nx >= 0 && ny >= 0 && nx < n && ny < m && maze[nx][ny] == 0 && !visited[nx][ny]) {
dfs(nx, ny, steps + 1, ex, ey);
}
}
visited[x][y] = false; // 回溯
}
int main() {
cin >> n >> m;
int sx, sy, ex, ey;
cin >> sx >> sy >> ex >> ey;
for (int i = 0; i < n; i++)
for (int j = 0; j < m; j++)
cin >> maze[i][j];
memset(visited, false, sizeof(visited));
dfs(sx, sy, 0, ex, ey);
if (min_steps == INT_MAX) cout << "無法到達終點\n";
else cout << "最短路徑長度:" << min_steps << endl;
return 0;
}
```
---
先求有,再求好,剛才雖然成功走出迷宮了,但前面走了一些冤枉路,也不太確定這是不是最短路徑,這時候我們的目標從「走出迷宮」變成以「最短路徑走出迷宮」,具體該怎麼做呢,讓我們繼續看下去。
### BFS實作
```
地圖大小:5 5
起點:0 0
終點:4 4
0 0 1 0 0
0 1 0 1 0
0 0 0 0 0
1 1 1 0 1
0 0 0 0 0
```
我們把地圖畫成表格來看(紅色是起點,橘色是終點)
| <font color="#f00">0</font> | 0 | 1 | 0 | 0 |
| --- | --- | ---- | --- | --- |
| 0 | 1 | 0 | 1 | 0 |
| 0 | 0 | 0 | 0 | 0 |
| 1 | 1 | 1 | 0 | 1 |
| 0 | 0 | 0 | 0 | <font color="#F7A004">**0**</font> |
首先我們站在起點(青色是路徑),起點不需要走任何一步就能到達,所以標記為0,代表花0步可以走到這一格。
| <font color="#00FFFF">0</font> | 0 | 1 | 0 | 0 |
| --- | --- | ---- | --- | --- |
| 0 | 1 | 0 | 1 | 0 |
| 0 | 0 | 0 | 0 | 0 |
| 1 | 1 | 1 | 0 | 1 |
| 0 | 0 | 0 | 0 | <font color="#F7A004">**0**</font> |
這時候,假設我們自己只能走1步,請問能走到哪幾格,顯然是右方跟下方對吧,所以我們幫這兩格標記上數字1,代表花1步可以走到這一格。
| <font color="#FF3EFF">**0**</font> | <font color="#00FFFF">**1**</font> | 1 | 0 | 0 |
| --- | --- | ---- | --- | --- |
| <font color="#00FFFF">**1**</font> | 1 | 0 | 1 | 0 |
| 0 | 0 | 0 | 0 | 0 |
| 1 | 1 | 1 | 0 | 1 |
| 0 | 0 | 0 | 0 | <font color="#F7A004">**0**</font> |
以此類推,再往外走1步,標記上數字2,代表花兩步可以走到這一格。
| <font color="#FF3EFF">**0**</font> | <font color="#FF3EFF">**1**</font> | 1 | 0 | 0 |
| --- | --- | ---- | --- | --- |
| <font color="#FF3EFF">**1**</font> | 1 | 0 | 1 | 0 |
| <font color="#00FFFF">**2**</font> | 0 | 0 | 0 | 0 |
| 1 | 1 | 1 | 0 | 1 |
| 0 | 0 | 0 | 0 | <font color="#F7A004">**0**</font> |
下方省略講解,看圖
| <font color="#FF3EFF">**0**</font> | <font color="#FF3EFF">**1**</font> | 1 | 0 | 0 |
| --- | --- | ---- | --- | --- |
| <font color="#FF3EFF">**1**</font> | 1 | 0 | 1 | 0 |
| <font color="#FF3EFF">**2**</font> | <font color="#00FFFF">**3**</font> | 0 | 0 | 0 |
| 1 | 1 | 1 | 0 | 1 |
| 0 | 0 | 0 | 0 | <font color="#F7A004">**0**</font> |
| <font color="#FF3EFF">**0**</font> | <font color="#FF3EFF">**1**</font> | 1 | 0 | 0 |
| --- | --- | ---- | --- | --- |
| <font color="#FF3EFF">**1**</font> | 1 | 0 | 1 | 0 |
| <font color="#FF3EFF">**2**</font> | <font color="#FF3EFF">**3**</font> | <font color="#00FFFF">**4**</font> | 0 | 0 |
| 1 | 1 | 1 | 0 | 1 |
| 0 | 0 | 0 | 0 | <font color="#F7A004">**0**</font> |
| <font color="#FF3EFF">**0**</font> | <font color="#FF3EFF">**1**</font> | 1 | 0 | 0 |
| --- | --- | ---- | --- | --- |
| <font color="#FF3EFF">**1**</font> | 1 | <font color="#00FFFF">**5**</font> | 1 | 0 |
| <font color="#FF3EFF">**2**</font> | <font color="#FF3EFF">**3**</font> | <font color="#FF3EFF">**4**</font> | <font color="#00FFFF">**5**</font> | 0 |
| 1 | 1 | 1 | 0 | 1 |
| 0 | 0 | 0 | 0 | <font color="#F7A004">**0**</font> |
| <font color="#FF3EFF">**0**</font> | <font color="#FF3EFF">**1**</font> | 1 | 0 | 0 |
| --- | --- | ---- | --- | --- |
| <font color="#FF3EFF">**1**</font> | 1 | <font color="#FF3EFF">**5**</font> | 1 | 0 |
| <font color="#FF3EFF">**2**</font> | <font color="#FF3EFF">**3**</font> | <font color="#FF3EFF">**4**</font> | <font color="#FF3EFF">**5**</font> | <font color="#00FFFF">**6**</font> |
| 1 | 1 | 1 | <font color="#00FFFF">**6**</font> | 1 |
| 0 | 0 | 0 | 0 | <font color="#F7A004">**0**</font> |
| <font color="#FF3EFF">**0**</font> | <font color="#FF3EFF">**1**</font> | 1 | 0 | 0 |
| --- | --- | ---- | --- | --- |
| <font color="#FF3EFF">**1**</font> | 1 | <font color="#FF3EFF">**5**</font> | 1 | <font color="#00FFFF">**7**</font> |
| <font color="#FF3EFF">**2**</font> | <font color="#FF3EFF">**3**</font> | <font color="#FF3EFF">**4**</font> | <font color="#FF3EFF">**5**</font> | <font color="#FF3EFF">**6**</font> |
| 1 | 1 | 1 | <font color="#FF3EFF">**6**</font> | 1 |
| 0 | 0 | 0 | <font color="#00FFFF">**7**</font> | <font color="#F7A004">**0**</font> |
| <font color="#FF3EFF">**0**</font> | <font color="#FF3EFF">**1**</font> | 1 | 0 | <font color="#00FFFF">**8**</font> |
| --- | --- | ---- | --- | --- |
| <font color="#FF3EFF">**1**</font> | 1 | <font color="#FF3EFF">**5**</font> | 1 | <font color="#FF3EFF">**7**</font> |
| <font color="#FF3EFF">**2**</font> | <font color="#FF3EFF">**3**</font> | <font color="#FF3EFF">**4**</font> | <font color="#FF3EFF">**5**</font> | <font color="#FF3EFF">**6**</font> |
| 1 | 1 | 1 | <font color="#FF3EFF">**6**</font> | 1 |
| 0 | 0 | <font color="#00FFFF">**8**</font> | <font color="#FF3EFF">**7**</font> | <font color="#00FFFF">**8**</font> |
| <font color="#FF3EFF">**0**</font> | <font color="#FF3EFF">**1**</font> | 1 | <font color="#00FFFF">**9**</font> | <font color="#FF3EFF">**8**</font> |
| --- | --- | ---- | --- | --- |
| <font color="#FF3EFF">**1**</font> | 1 | <font color="#FF3EFF">**5**</font> | 1 | <font color="#FF3EFF">**7**</font> |
| <font color="#FF3EFF">**2**</font> | <font color="#FF3EFF">**3**</font> | <font color="#FF3EFF">**4**</font> | <font color="#FF3EFF">**5**</font> | <font color="#FF3EFF">**6**</font> |
| 1 | 1 | 1 | <font color="#FF3EFF">**6**</font> | 1 |
| 0 | <font color="#00FFFF">**9**</font> | <font color="#FF3EFF">**8**</font> | <font color="#FF3EFF">**7**</font> | <font color="#FF3EFF">**8**</font> |
| <font color="#FF3EFF">**0**</font> | <font color="#FF3EFF">**1**</font> | 1 | <font color="#FF3EFF">**9**</font> | <font color="#FF3EFF">**8**</font> |
| --- | --- | ---- | --- | --- |
| <font color="#FF3EFF">**1**</font> | 1 | <font color="#FF3EFF">**5**</font> | 1 | <font color="#FF3EFF">**7**</font> |
| <font color="#FF3EFF">**2**</font> | <font color="#FF3EFF">**3**</font> | <font color="#FF3EFF">**4**</font> | <font color="#FF3EFF">**5**</font> | <font color="#FF3EFF">**6**</font> |
| 1 | 1 | 1 | <font color="#FF3EFF">**6**</font> | 1 |
| <font color="#00FFFF">**10**</font> | <font color="#FF3EFF">**9**</font> | <font color="#FF3EFF">**8**</font> | <font color="#FF3EFF">**7**</font> | <font color="#FF3EFF">**8**</font> |
完成整個迷宮後,我們就可以讀取終點那格的數字,就能知道最短路徑是多少了。這個方法就是BFS的模型,我們可以用程式將這個邏輯寫出來,如下
### BFS解
```cpp=
#include <bits/stdc++.h>
using namespace std;
const int MAX_N = 100;
int maze[MAX_N][MAX_N]; // 迷宮地圖,0 表示可走,1 表示牆壁
int dist[MAX_N][MAX_N]; // 記錄從起點到該點的距離
int n, m; // 迷宮大小(行數與列數)
// 方向向量:右、下、左、上
int dx[4] = {1, 0, -1, 0};
int dy[4] = {0, 1, 0, -1};
struct Node {
int x, y, d;
};
int bfs(int sx, int sy, int ex, int ey) {
queue<Node> q;
q.push({sx, sy, 0});
dist[sx][sy] = 0;
while (!q.empty()) {
Node cur = q.front();
q.pop();
// 抵達終點
if (cur.x == ex && cur.y == ey) return cur.d;
// 探索四個方向
for (int i = 0; i < 4; i++) {
int nx = cur.x + dx[i];
int ny = cur.y + dy[i];
if (nx >= 0 && ny >= 0 && nx < n && ny < m && maze[nx][ny] == 0 && dist[nx][ny] == -1) {
dist[nx][ny] = cur.d + 1;
q.push({nx, ny, cur.d + 1});
}
}
}
return -1; // 無法到達
}
int main() {
cin >> n >> m;
int sx, sy, ex, ey;
cin >> sx >> sy >> ex >> ey;
for (int i = 0; i < n; i++)
for (int j = 0; j < m; j++) {
cin >> maze[i][j];
dist[i][j] = -1; // 初始化距離
}
int result = bfs(sx, sy, ex, ey);
if (result == -1) cout << "無法到達終點\n";
else cout << "最短路徑長度:" << result << endl;
return 0;
}
```
# 貪心演算法
貪心演算法(Greedy Algorithm) 是一種在每一個選擇中都做出當前最佳選擇的算法。透過每次選擇當下的局部最優解,最終達到全局最優解。
貪心演算法並非適用於所有問題,只有在「貪心選擇性質」和「最優子結構性質」滿足的情況下,才能保證全局最優。簡而言之,貪心演算法只能用在「局部」可以放大到「全局」的題目。
貪心演算法並沒有一個簡單的公式解法,主要依據題目的不同,將問題拆解成子問題,並對每一個子問題找到最佳解,從而得到問題的最佳解。
下方舉例幾種適用貪心演算法的題目及演算法,可以仔細觀察題目是如何被拆解成子問題的。
## Dijkstra
Dijkstra 演算法是一種「單源最短路徑演算法」,用於計算從起點(源點)到圖中所有其他節點的最短路徑長度。它主要適用於「邊長權重為非負數」的圖。
### 核心思路
初始化二維陣列 w 紀錄邊長權重
初始化一維陣列 d 紀錄每個節點的最短距離
初始化一維陣列 visited 紀錄該節點是否拜訪過(以該點為出發點)
初始化一維陣列 pre 紀錄該節點的前一個節點
→從未訪問的節點中,選擇距離最小的節點 u
→將 u 標記為已訪問
→對於 u 的所有未訪問的鄰居節點 v,若經由 u 到 v 的距離小於目前記錄的距離,則進行更新
$d[v]=min(d[v],d[u]+w(u,v))$
並將該鄰居節點 v 的前一個節點設為 u
### 演算法講解
題目如下,假設A為起點

初始化陣列
| | A | B | C | D | E | F |
| ---- | ---- | ---- | ---- | ---- | ---- | ---- |
| **A** | 0 | 4 | 5 | 0 | 0 | 0 |
| **B** | 4 | 0 | 11 | 9 | 7 | 0 |
| **C** | 5 | 11 | 0 | 0 | 3 | 0 |
| **D** | 0 | 9 | 0 | 0 | 13 | 2 |
| **E** | 0 | 7 | 3 | 13 | 0 | 6 |
| **F** | 0 | 0 | 0 | 2 | 6 | 0 |
| A | B | C | D | E | F |
| ---- | ---- | ---- | ---- | ---- | ---- |
| 0 | INT_MAX | INT_MAX | INT_MAX | INT_MAX | INT_MAX |
| false | false | false | false | false | false |
| - | - | - | - | - | - |
從未訪問的節點中,選擇距離最小的節點A
拜訪A鄰居節點B、C
* B:0+4=4 < INT_MAX → 更新
* C:0+5=5 < INT_MAX → 更新
| A | B | C | D | E | F |
| ---- | ---- | ---- | ---- | ---- | ---- |
| 0 | 4 | 5 | INT_MAX | INT_MAX | INT_MAX |
| true | false | false | false | false | false |
| - | A | A | - | - | - |
從未訪問的節點中,選擇距離最小的節點B
拜訪B鄰居節點D、E
* D:4+9=13 < INT_MAX → 更新
* E:4+7=11 < INT_MAX → 更新
| A | B | C | D | E | F |
| ---- | ---- | ---- | ---- | ---- | ---- |
| 0 | 4 | 5 | 13 | 11 | INT_MAX |
| true | true | false | false | false | false |
| - | A | A | B | B | - |
從未訪問的節點中,選擇距離最小的節點C
拜訪C鄰居節點E(B已經訪問過)
* E:5+3=8 < 11 → 更新
| A | B | C | D | E | F |
| ---- | ---- | ---- | ---- | ---- | ---- |
| 0 | 4 | 5 | 13 | 8 | INT_MAX |
| true | true | true | false | false | false |
| - | A | A | B | C | - |
從未訪問的節點中,選擇距離最小的節點E
拜訪E鄰居節點D、F(B、C已經訪問過)
* D:8+13=21 > 13 → 不更新
* F:8+6=14 < INT_MAX → 更新
| A | B | C | D | E | F |
| ---- | ---- | ---- | ---- | ---- | ---- |
| 0 | 4 | 5 | 13 | 8 | 14 |
| true | true | true | false | true | false |
| - | A | A | B | C | E |
從未訪問的節點中,選擇距離最小的節點D
拜訪D鄰居節點F(B、E已經訪問過)
* F:13+2=15 > 14 → 不更新
| A | B | C | D | E | F |
| ---- | ---- | ---- | ---- | ---- | ---- |
| 0 | 4 | 5 | 13 | 8 | 14 |
| true | true | true | true | true | false |
| - | A | A | B | C | E |
從未訪問的節點中,選擇距離最小的節點F
沒有可拜訪的節點
| A | B | C | D | E | F |
| ---- | ---- | ---- | ---- | ---- | ---- |
| 0 | 4 | 5 | 13 | 8 | 14 |
| true | true | true | true | true | true |
| - | A | A | B | C | E |
從A出發,最短路徑結果如下:
| 終點 | 最短距離 | 路徑 |
| ---- | ---- | ---- |
| A | 0 | A |
| B | 4 | A→B |
| C | 5 | A→C |
| D | 13 | A→B→D |
| E | 8 | A→C→E |
| F | 14 | A→C→E→F |
---
# Floyd-Warshall
Floyd-Warshall 演算法使用三層迴圈,固定中間點來計算任意兩點之間的最短距離或最小最大權重等。雖然此演算法簡單直觀,但時間複雜度較高,通常是 $O(n^3)$,因此需要根據具體問題的情況來選擇是否使用。
## 最短距離
```cpp
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
for(int k=0;k<n;k++){
int dis=a[j][i]+a[i][k]; // i 作為 j 走到 k 的中繼點
a[j][k]=min(dis,a[j][k]); // 維護最短距離
}
}
}
```
## 最小最大權重(Minimax Path)
```cpp
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
for(int k=0;k<n;k++){
int dis=max(a[j][i],a[i][k]); // i 作為 j 走到 k 的中繼點
a[j][k]=min(dis,a[j][k]); // 維護最小權重
}
}
}
```
## 最大最小權重(Maximin Path)
```cpp
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
for(int k=0;k<n;k++){
int dis=min(a[j][i],a[i][k]); // i 作為 j 走到 k 的中繼點
a[j][k]=max(dis,a[j][k]); // 維護最大權重
}
}
}
```
## 練習題
[c128. 00544 - Heavy Cargo](https://zerojudge.tw/ShowProblem?problemid=c128)
# 快速冪
取餘數是程式中常見的手法,但有些時候數字太大,連long long都存不下,該怎麼處理呢?答案很簡單,用數學的方式處理~
$(a×b)\text{ mod }m = ((a \text{ mod } m)×(b \text{ mod } m)) \text{ mod } m$
利用此數學性質,我們可以在每一步計算時取餘數,每次將指數減半,從而避免中間結果過大。整個過程只需要 $O(log (n))$的時間複雜度,非常高效。
## 二進制快速冪
```cpp
#include <iostream>
using namespace std;
long long binary_pow(long long base, long long exp, long long mod) {
long long result = 1;
base = base % mod; // 確保 base 小於 mod
while (exp > 0) {
if (exp % 2 == 1) { // 當 exp 是奇數時,乘上 base
result = (result * base) % mod;
}
base = (base * base) % mod; // base 每次平方
exp /= 2; // exp 右移一位
}
return result;
}
int main() {
long long k, n;
cin >> k >> n;
cout << binary_pow(10, k, n) << endl;
return 0;
}
```
## 非二進制快速冪
```cpp
#include <iostream>
using namespace std;
long long quick_pow(long long base, long long exp, long long mod) {
long long result = 1;
while (exp > 0) {
if (exp % 2 == 1) {
result = (result * base) % mod;
}
base = (base * base) % mod;
exp /= 2;
}
return result;
}
int main() {
long long k, n;
cin >> k >> n;
cout << quick_pow(10, k, n) << endl;
return 0;
}
```
## 練習題
[b467. NOIP2013 Day1.1.转圈游戏](https://zerojudge.tw/ShowProblem?problemid=b467)
# 並查集(DSU)
並查集(Disjoint Set Union, DSU)是一種用來處理不相交集合(disjoint sets)操作的資料結構,常見於圖論中的聯通性問題。
查詢(Find):查詢某一元素所屬的集合。
合併(Union):將兩個集合合併成一個集合。
## 舉例
在一款線上遊戲中,每位玩家一開始都是獨自行動,也就是說,每個人都擁有自己的小公會(自己是自己的會長)。隨著遊戲進行,玩家們可以互相組隊,形成新的公會,而不同的公會也可能進一步合併成更大的公會。
為了管理整個公會系統,我們需要支援兩種操作:
合併公會(Union):兩個公會合併成一個大公會。
查詢所屬公會(Find):快速查出某位玩家目前所屬的公會(以會長代表)。
當兩個公會合併後,系統會指定其中一位作為新的會長,代表整個新公會。而每個該公會的玩家都會歸順到這位新會長下。
## 主要操作
```cpp
// f(now): 查詢現在這位玩家所屬公會的代表(公會會長)
// 同時做「路徑壓縮」,讓玩家以後能更快找到自己的會長
int f(int now) {
if (p[now] == now) {
// 如果自己就是自己的會長(代表),直接回傳
return now;
} else {
// 否則,往上找到會長,並把自己直接連到會長(壓縮路徑)
return p[now] = f(p[now]);
}
}
// 初始化:一開始每位玩家都創自己的公會(自己是自己的會長)
for (int i = 0; i < n; i++) {
p[i] = i;
}
// 進行 m 次公會合併操作
for (int i = 0; i < m; i++) {
int a, b;
cin >> a >> b; // 讀入要合併的兩位玩家
a = f(a); // 找出 a 所屬的公會會長
b = f(b); // 找出 b 所屬的公會會長
if (a != b) {
// 如果他們不在同一個公會,就把 a 的公會併入 b 的公會
p[a] = b;
}
}
```
## 練習題
[a445. 新手訓練系列- 我的朋友很少](https://zerojudge.tw/ShowProblem?problemid=a445)
# 最小生成樹(MST)
在圖論中,最小生成樹(Minimum Spanning Tree, MST)是一棵包含所有節點且權重總和最小的無環子圖。換句話說,給定一個無向加權圖,我們希望找到一個連接所有節點的子圖,使得:
* 沒有環
* 剛好連接所有節點
* 權重總和最小
## Kruskal 演算法
Kruskal 演算法是一種貪心法(Greedy Algorithm),其步驟如下:
1. 將所有邊按照權重排序(從小到大)。
2. 使用並查集(Union-Find)來維護集合狀態。
3. 依次選擇最小的邊,如果不會形成環則加入 MST。
4. 重複步驟 3,直到加入 (n-1) 條邊為止(假設圖中有 n 個節點)。
時間複雜度主要來自邊的排序,為 $O(m \text{ log } m)$,其中 m 是邊的數量。
```cpp=1
#include<bits/stdc++.h>
using namespace std;
vector<int> p;
// 並查集
int f(int now){
if(p[now]==now) return now;
else{
p[now]=f(p[now]);
return p[now];
}
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
int n,m;
typedef pair<int,pair<int,int>> node; // (A到B邊的權重,A點,B點)
while(cin >> n >> m){
int need=n-1; // n個節點的樹應該會有n-1條邊
vector<node> v;
p.resize(n);
for(int i=0;i<m;i++){
int a,b,c;
cin >> a >> b >> c;
v.push_back({c,{a,b}});
v.push_back({c,{b,a}});
}
sort(v.begin(),v.end()); // 從邊長權重最小開始考慮
// 並查集預設,每個節點都是單獨的集合
for(int i=0;i<n;i++){
p[i]=i;
}
long long sum=0;
for(auto h : v){
int x=f(h.second.first);
int y=f(h.second.second);
// 不可繞成環
if(x==y){
continue;
}
else{
p[x]=y;
need--;
sum+=h.first;
}
}
if(need==0){
cout << sum << endl;
}
else{
cout << "-1" << endl; // 無法生成MST,依題意輸出
}
}
return 0;
}
```
## Prim 演算法
Prim 演算法與 Kruskal 不同,它是基於逐步擴展樹的方式來構造 MST。演算法的步驟如下:
1. 從任意節點開始(通常選擇 0 號節點)。
2. 使用最小堆(Priority Queue)記錄鄰近的邊,並維護一個「已選擇的節點集合」。
3. 每次選擇與當前 MST 相連且權重最小的邊,將新的節點加入 MST。
4. 重複步驟 3,直到所有節點都被加入 MST。
```cpp=
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
int n, m;
while (cin >> n >> m) {
/* 建立鄰接表:adj[u] 內存放 (v, w),
表示 u ↔ v 之間有一條權重為 w 的無向邊 */
vector<vector<pair<int, int>>> adj(n);
for (int i = 0; i < m; ++i) {
int a, b, c;
cin >> a >> b >> c;
adj[a].push_back({b, c});
adj[b].push_back({a, c});
}
/* Prim 相關資料結構 */
vector<bool> inMST(n, false); // 節點是否已納入 MST
vector<int> best(n, 1e9); // 目前連進每個節點的最小邊權
priority_queue<pair<int,int>,vector<pair<int,int>>,greater<pair<int, int>>> pq; // (邊權, 節點) 的最小堆
/* 從 0 號節點出發 */
best[0] = 0;
pq.push({0, 0});
int total = 0; // MST 權重總和
int picked = 0; // 已選進 MST 的節點數
/* 主迴圈:每次拉出與 MST 相連且最小的邊 */
while (!pq.empty() && picked < n) {
auto [w, u] = pq.top(); pq.pop();
if (inMST[u]) continue; // 重複點,跳過
inMST[u] = true; // 把 u 納入 MST
total += w; // 加上這條邊的權重
picked++; // 已選節點數量+1
/* 更新鄰接節點的最佳邊權 */
for (auto [v, wt] : adj[u]) {
if (!inMST[v] && wt < best[v]) {
best[v] = wt;
pq.push({best[v], v});
}
}
}
/* 判斷是否成功連通所有節點 */
if (picked == n) {
cout << total << '\n'; // 成功:輸出最小權重和
} else {
cout << -1 << '\n'; // 失敗:圖不連通,無法形成 MST
}
}
return 0;
}
```
## 練習題
[a129. 最小生成樹](https://zerojudge.tw/ShowProblem?problemid=a129)
# 拓樸排序
在tree中,我們可以輕易找到點的層級順序,但在有向圖中,我們似乎無法給出一個準確的排序來滿足由高層級往低層級的拜訪,拓樸排序問題因此而生。
## 原始問題
考慮一個有向圖,上方有n個點m條邊,點依序編號1~n,希望找到一種排序p1,p2,p3,...,pn,滿足對於任意一條邊x→y(x=pi,y=pj)都有i<j呢?

## 思路
若是一個點存在任何的入度,那它一定不是序列的第一個數。
反之,若是一個點沒有任何入度,則它可以成為序列的第一個數。
找到序列的第一個數後,就可以把該點的出度都拔掉,產生下一個獨立的子問題,遞迴求解即可得到拓樸排序。
## 遞迴演算法
1. 找到入度為0的點p
2. 把 p 和它所有的出度都拔掉,形成一張新的有向圖
3. 回到1.求解新的有向圖,並把序列接在 p 後面
## BFS演算法
1. 把所有入度為0的點都推入queue
2. 從queue中取出點p
3. 把p的出度都拔掉,維護相關點的入度值(若點q的入度有p,則此時p移除,入度-1)
4. 若某點在3.的執行中入度變成0,推入queue
5. 若queue不為空,繼續2.
6. 算法結束後,若得到一組解,可證得此圖為有向無環圖(DAG);反之則此圖必存在環
## 拓樸排序-BFS版本
```cpp=1
#include <bits/stdc++.h>
using namespace std;
vector<int> adj[1000];
int in[1000];
vector<int> topo;
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
int n, m;
cin >> n >> m;
memset(in, 0, sizeof(in));
for (int i = 0; i < m; i++) {
int u, v;
cin >> u >> v;
adj[u].push_back(v);
in[v]++;
}
queue<int> q;
for (int i = 0; i < n; i++) {
if (in[i] == 0) {
q.push(i);
}
}
while (!q.empty()) {
int now = q.front();
q.pop();
topo.push_back(now);
for (int nxt : adj[now]) {
in[nxt]--;
if (in[nxt] == 0) {
q.push(nxt);
}
}
}
if (topo.size() == n) {
for (int x : topo) cout << x << " ";
cout << "\n";
} else {
cout << "Cycle detected\n";
}
return 0;
}
```
## 練習題
[b333. NOIP2013 4.车站分级](https://zerojudge.tw/ShowProblem?problemid=b333)
## DAG longest path
```cpp=
#include <iostream>
#include <vector>
#include <queue>
#include <cstring>
using namespace std;
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
int m; // 測試資料組數
cin >> m;
while (m--) {
int n; // 節點數
cin >> n;
vector<int> t(n + 1); // t[i] 表示完成第 i 個項目所需的天數
vector<vector<int>> out(n + 1); // out[i] 表示第 i 個項目的下游項目編號
vector<int> in(n + 1, 0); // in[i] 表示第 i 個項目的入邊數(上游項目數目)
vector<int> s(n + 1, 0); // s[i] 表示完成第 i 個項目時,累計花費的時間
for (int i = 1; i <= n; ++i) {
int a, b;
cin >> a >> b;
t[i] = a;
for (int j = 0; j < b; ++j) {
int c;
cin >> c;
out[i].push_back(c);
in[c]++;
}
}
queue<int> q;
for (int i = 1; i <= n; ++i) {
if (in[i] == 0) q.push(i);
}
int ans = 0;
while (!q.empty()) {
int now = q.front();
q.pop();
s[now] += t[now];
ans = max(ans, s[now]);
for (int i : out[now]) {
s[i] = max(s[i], s[now]);
in[i]--;
if (in[i] == 0) q.push(i);
}
}
cout << ans << "\n";
}
return 0;
}
```
## 練習題
[a454. TOI2010 第二題:專案時程](https://zerojudge.tw/ShowProblem?problemid=a454)
# 尤拉路徑(Eulerian Path)
在圖論中,尤拉路徑(Eulerian Path) 是指一條不重複經過任何邊的路徑,這條路徑可以穿越所有邊一次且只一次。如果這條路徑的起點和終點相同,我們稱它為 尤拉迴路(Eulerian Circuit) 或 尤拉環(Eulerian Cycle)。
## 存在條件
### 無向圖
尤拉路徑存在的條件:
* 圖是連通的(除了孤立點以外)
* 有 0 或 2 個奇數度數的節點(度數 = 與該點相連的邊的條數)
尤拉迴路存在的條件:
* 圖是連通的
* 所有節點的度數都是偶數
### 有向圖
尤拉路徑存在的條件:
* 圖是連通的(忽略方向)
* 至多有一個點滿足 出度 = 入度 + 1(起點)
* 至多有一個點滿足 入度 = 出度 + 1(終點)
* 其他所有點的入度等於出度
尤拉迴路存在的條件:
* 圖是連通的
* 每個點的入度 = 出度
## Hierholzer 演算法
1. 判斷是否存在尤拉路徑/迴路:
2. 從合法起點出發:
若有奇數度點,就從其中之一出發
若全為偶數度,則任一點皆可當起點
3. 沿著還沒走過的邊一直往下走,直到走到底:
4. 回朔插入路徑:
當走到底(沒有可走邊)時,就把當前節點加入最終路徑中(小路徑插入主路徑)
5. 最後檢查是否所有邊都走過:
```cpp=1
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 100005;
vector<multiset<int>> adj; // 每個節點的鄰邊集合
vector<int> path; // 儲存最後走出來的尤拉路徑
// Step 4:遞迴 DFS,邊走邊把路徑插入
void dfs(int u) {
while (!adj[u].empty()) {
int v = *adj[u].begin(); // 取出最小鄰點 v
adj[u].erase(adj[u].begin()); // 刪除 u → v 的邊
adj[v].erase(adj[v].find(u)); // 刪除 v → u 的對應邊(無向圖)
dfs(v); // 繼續走下去
}
path.push_back(u); // 回朔階段,把這個點加入答案路徑
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
int n, m; // n = 節點數, m = 邊數
cin >> n >> m;
adj.assign(n, multiset<int>()); // 初始化每個節點的鄰邊集合
// 讀入圖的邊(無向圖)
for (int i = 0; i < m; i++) {
int u, v;
cin >> u >> v;
adj[u].insert(v);
adj[v].insert(u);
}
// Step 1:檢查圖中奇數度的點數量
int odd = 0, start = 0;
for (int i = 0; i < n; i++) {
if (adj[i].size() % 2 == 1) {
odd++;
start = i; // 找一個奇數度點當作起點
}
}
// Step 1:尤拉路徑僅可能出現在 0 或 2 個奇數度點的圖中
if (odd != 0 && odd != 2) {
cout << "No Eulerian Path\n";
return 0;
}
// Step 2:從起點開始 DFS(奇數度點優先,否則任意點)
dfs(start);
// Step 5:檢查是否所有邊都被走過(路徑長度應為 m + 1)
if (path.size() != m + 1) {
cout << "No Eulerian Path (Disconnected)\n";
} else {
reverse(path.begin(), path.end()); // 反轉結果得到正向路徑
cout << "Eulerian Path:\n";
for (int u : path) {
cout << u << " ";
}
cout << "\n";
}
return 0;
}
```
## 練習題
[1084 . 一筆畫問題](https://tioj.ck.tp.edu.tw/problems/1084)
# LCA
紀錄DFS完整拜訪過程,包含深度跟點的名稱

接著直接查詢

```cpp=
#include<bits/stdc++.h>
using namespace std;
vector<int> depth;
vector<int> nodes;
vector<vector<int>> a;
vector<bool> visited;
int n,q;
void dfs(int now,int d,int les){
visited[now]=true;
depth.push_back(d);
nodes.push_back(now);
for(auto h : a[now]){
if(!visited[h]){
dfs(h,d+1,now);
}
}
if(d-1>=0){
depth.push_back(d-1);
nodes.push_back(les);
}
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cin >> n >> q;
a.resize(n);
visited.resize(n);
for(int i=0;i<n;i++){
int x;
while(cin >> x){
if(x==0){
break;
}
x--;
a[i].push_back(x);
}
}
dfs(0,0,0);
for(int i=0;i<q;i++){
int f,g;
cin >> f >> g;
f--;
g--;
int index1,index2;
for(int j=nodes.size()-1;j>=0;j--){
if(nodes[j]==f){
index1=j;
break;
}
}
for(int j=nodes.size()-1;j>=0;j--){
if(nodes[j]==g){
index2=j;
break;
}
}
int minn=INT_MAX;
int index3;
if(index1>index2){
swap(index1,index2);
}
for(int j=index1;j<=index2;j++){
if(depth[j]<minn){
minn=depth[j];
index3=j;
}
}
cout << nodes[index3]+1 << " ";
cout << abs(depth[index1]-depth[index3])+abs(depth[index2]-depth[index3]) << endl;
}
return 0;
}
```
## 線上資源
[Lowest Common Ancestor (LCA) Problem | Eulerian path method](https://www.youtube.com/watch?v=sD1IoalFomA&t=95s)
## 練習題
[d767. 血緣關係](https://zerojudge.tw/ShowProblem?problemid=d767)