<style>
.reveal .slides {
text-align: left;
font-size:35px
}
</style>
# pretrain 4
Introduction to Competitive Programming
12/22
----
* 常用函式
* 容器
* IO / 編譯優化
---
## 常用函式
----
* sort
* nth_element
* lower_bound / upper_bound
* unique *
* next / prev_permutation *
* 字串/數字之間轉換 *
----
### sort
* 在標頭檔 algorithm 裡面
* 複雜度為 $O(NlgN)$ 參數是頭尾的 iterator ( first , last , compare )
* 此函式會將此區間的資料排序
* 使用方式為 ex : sort ( arr , arr+n , cmp ) 可以丟 vector , array , struct , deque等支援隨機存取的資料結構
* 於第三個函數傳入自訂比較函數compare
----
### 範例
```cpp=
int arr[1e5+5]={};
bool cmp(int x,int y){
return x>y;
}
int main(){
for(int i=1;i<=100;i++) arr[i]=i;
sort(arr+1,arr+100+1,cmp);
}
```
陣列結果 : 100,99,98,97,96,95.....
----
### 例題:
[[洛谷P1012 拚數](https://www.luogu.com.cn/problem/P1012)]
題意:給你N個數字把他拚成最大的數字
```cpp=
3
13 312 343
會拼成 34331213
2
76 7
會拼成 776
```
----
### 實作:
```cpp=
bool cmp(string a,string b){
if(a+b > b+a)
return 1;
return 0;
}
```
----
### nth_element
* 在標頭檔 algorithm 裡面
* 複雜度為平均 $O(n)$ 參數是頭尾的 iterator ( first , target ( k ) , last )
* 此函式會將此區間第 k - first 小的數放在 k 位置,且 [first , k] 皆比 k 小,( k , last )皆比k大(但這兩個區間內是無序的)
* 使用方式為 ex : nth_element(arr , arr+k , arr+n , cmp) 可以丟 vector , array , struct , deque 等支援隨機存取的資料結構
* 於第四個函數傳入自訂比較函數 compare
----
### 範例
```cpp=
int arr[1e5+5]={};
bool cmp(int x,int y){
return x>y; //更改為降序排列
}
int main(){
for(int i=1;i<=100;i++) arr[i]=i;
nth_element(arr+1,arr+50+1,arr+100+1,cmp);
}
```
結果 : 100~51 (無序排列) , 50 , 49~1 (無序排列)
----
### 例題:
[[CF 412B](https://codeforces.com/problemset/problem/412/B)]
題意:給你 N 個數字,以及 K,求出第 K 大的數字是多少
```cpp=
6 4
100 20 40 20 50 50
答案為40
```
----
### 實作:
```cpp=
int main(){
int n,k,a[100005];
cin>>n>>k;
for(int i=1;i<=n;i++) cin>>a[i];
nth_element(a+1,a+k,a+n+1,greater<int>());
cout<<a[k-1];
return 0;
}
```
----
### lower_bound / upper_bound
* 在標頭檔 algorithm 裡面
* lower_bound ( first , last , value , cmp) upper_bound ( first , last , value , cmp)
* lower : 尋找第一個不小於 value 的迭代器,若不存在則回傳 end
* upper : 尋找第一個大於 value 的迭代器,若不存在則回傳 end
* 以上兩個函數裡面所尋找的陣列必須要有經過排序(有單調性)才可以正常使用
* 時間複雜度 O(lg n) 兩者皆是
----
### 範例
```cpp=
using namespace std;
int a[100005];
int main(){
int n,x;
cin>>n;
for(int i=0;i<n;++i)cin>>a[i];
sort(a,a+n);
cin>>x;
cout<<lower_bound(a,a+n,x)-a<<'\n';
cout<<upper_bound(a,a+n,x)-a<<'\n';
//通過返回的地址減去起始地址begin,得到索引值
return 0;
}
```
----
### 例題:
[[CF 493C](https://codeforces.com/contest/493/problem/C)]
題意:兩人比賽投籃,與正常不同的是,三分線與籃筐的距離 d 是不確定的。
首先輸入第一個人投了 n 個球,n 個球每個球投的時候他與籃筐的距離;
然後輸入第二個人投了 m 個球,m 個球每個球投的時候他與籃筐的距離。
```cpp=
3
1 2 3
2
5 6
答案為9:6
5
6 7 8 9 10
5
1 2 3 4 5
答案為15:10
```
----
### 實作:
```cpp=
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
int n,m;
int a,b;
vector<int> tf,ts;
void judge(vector<int> &tx){
for(int i=0;i<n;i++){
int val1 = upper_bound(tf.begin(),tf.end(),tx[i])-tf.begin();
val1 = val1*2 + (n-val1)*3;
int val2 = upper_bound(ts.begin(),ts.end(),tx[i]) - ts.begin();
val2 = val2*2 + (m - val2)*3;
if(val1 - val2 == (a - b)){
if(val1 >a){
a = val1;
b = val2;
}
}
else if(val1 - val2 > a-b ){
a = val1;
b = val2;
}
}
}
int main(){
cin>>n;
tf.resize(n);
for(int i=0;i<n;i++){
cin>>tf[i];
}
cin>>m;
ts.resize(m);
for(int i=0;i<m;i++){
cin>>ts[i];
}
sort(tf.begin(),tf.end());
sort(ts.begin(),ts.end());
if(n>=m) a = n*3 ;b = m*3 ;
else a = n*2 ; b = m*2 ;
judge(tf);
judge(ts);
printf("%d:%d",a,b);
return 0;
}
```
----
### unique
* 在 <algorithm> 中
* 參數是兩個 iterator(first,last),將此區間連續相同的元素刪到只留一個,回傳刪完後最後一個沒被刪的元素的下一項的 iterator (可以想像成新的 last),但注意原陣列大小不會改變
* 複雜度 O(n)
* 常用於離散化,把一個陣列中的資料從任意值域映射到 0∼C−1,其中 C 是相異數字數量,並保持原本的大小關係
<!-- 相異數字第k小 -->
----
### 範例
```cpp
int a[9] = {1, 3, 3, 3, 4, 5, 6, 6, 1};
int n = unique(a, a + 9) - a;
cout << n << '\n'; // 離散化後還剩多少元素
for(int i = 0; i < n; i++) {
cout << a[i] << ' ';
}
```
#### 輸出
```
6
1 3 4 5 6 1
```
----
離散化程式碼
離散化三步驟: sort, unique, lower_bound
```cpp=
//vector<int> a(n) = original array,tot;
for(int i = 0; i < n; i++) tot.push_back(a[i]);
sort(tot.begin(), tot.end());
tot.resize(unique(tot.begin(), tot.end()) - tot.begin());
for(int i = 0; i < n; i++)
a[i] = lower_bound(tot.begin(), tot.end(), a[i]) - tot.begin();
```
----
### 例題:
題意:你有一個很大的陣列,一開始陣列全部為 0,給 n 個區間 l, r ,要你在 l ~ r 的範圍加值 1,最後給你 q 次詢問,問你某個位置 p 的值是多少
- $0 \le l, r, p \le 10^{18}\ \ \ \ n,q \le 10^6$
----
### 想法
觀察 1:是全部操作都做完之後再查詢,因此可以用上上禮拜教的差分去求解
觀察 2:空間不夠開 $10^{18}$ 的陣列,因此需要用離散化,離散化後空間只需要 2 * n
----
### 範例 code:
```cpp=
int n, q;
cin >> n >> q;
vector<pair<long long,long long> > a;
vector<long long> tot;
for(int i = 0; i < n; i++) {
int x, y;
cin >> x >> y;
a.push_back({x, y + 1});
tot.push_back(x);
tot.push_back(y + 1);
}
sort(tot.begin(), tot.end());
tot.resize(unique(tot.begin(), tot.end()) - tot.begin());
for(int i = 0; i < n; i++){
a[i].first = lower_bound(tot.begin(), tot.end(), a[i].first) - tot.begin();
a[i].second = lower_bound(tot.begin(), tot.end(), a[i].second) - tot.begin();
}
vector<int> cnt(tot.size() + 1);
for(int i = 0; i < n; i++) {
cnt[a[i].first]++;
cnt[a[i].second]--;
}
while(q--) {
int p;
cin >> p;
int pos = lower_bound(tot.begin(), tot.end(), p) - tot.begin();
cout << tot[pos] << '\n';
}
```
----
### next / prev_permutation
* 在 <algorithm> 中
* 參數是兩個 iterator(first,last),將此區間改變成下/前個排列,若不存在則回傳 false,反之回傳 true
* 均攤複雜度 $Ω(1)$,最差 $O(n)$
* next_permutation 產出的下一個排列都會比原本的字典序還大,而 prev 則相反
* 如果一開始的排列是字典序最小的,就可以用 next_permutation 產生出全部的排列。
----
### 範例
```cpp=
int a[3] = {1, 2, 3};
do{
for(int i = 0; i < 3; i++) cout << a[i] << ' ';
cout << '\n';
} while(next_permutation(a, a + 3));
```
#### 輸出:
```
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1
```
----
### 字串/數字之間轉換
以下函式都是定義在 <string>
字串轉數字: stoi、stod、stoll、...(很多形態都有)
轉數字之後如果超過上界,就會回傳上界
數字轉字串: to_string (注意這要 c++11 以後才能用)
----
範例
```cpp
1000 -> to_string(1000) -> "1000"
11.11 -> to_string(11.11) -> "11.11"
"1000" -> stoi("1000") -> 1000
```
----
---
## 容器
----
* Iterator *
* pair/tuple *
* vector
* stack
* queue
* priority_queue
* set *
* map *
* unordered_set / unordered_map *
* multiset / multimap *
* bitset *
----
### 前情提要:模板
STL中的容器使用模板(template)來讓它能接受任意型態的資料,因此在宣告變數時,必須要告訴編譯器該容器是要裝什麼型態的資料。
c++的模板以 <> 來傳遞參數,例如 vector<int>、set<string,greater<string>>
----
### Iterator 迭代器
迭代器是用來遍歷容器的東西,由於有些容器不像陣列能用下標 (index) 遍歷,迭代器便能用來遍歷這類結構。
----
c++的迭代器皆被實作的與指標相似,用遞增運算子 (\++it / it\++) 來移動到下一項,且用反指標運算子(*it)來取得當前項的值。
多數還支援遞減運算子(- -it / it- -) 移動到前一項(稱作雙向迭代器)
有些支援 +/- 運算子(it+k / it-k) 一次移動多項(稱作隨機存取(random access))
----
對於一個容器,分別有代表頭尾的迭代器,其中頭代表首項,而尾代表最後一項的下一項,因此遍歷方式通常是for(it=begin;it!=end;it++)
對於陣列 (C-style array),其迭代器就是指標,且頭尾分別是arr/arr+n,對於STL內的容器,其迭代器是C::iterator,且頭尾分別是arr.begin() / arr.end()
此外STL內還有實作反向迭代器用來從最後一項開始往前遍歷,其形態是 C::reverse_iterator 頭尾分別是 arr.rbegin() / arr.rend()
----
### pair/tuple
分別在 <utility> 和 <tuple> 中
偷懶用資料結構,分別是可以用來宣告裝 2 個東西/多個東西的變數
tuple 取用值略麻煩一點建議自己寫 struct
----
### 使用範例:pair
* 宣告
```cpp
pair<int,int>
pair<double,int>
```
* 取值
```cpp=
pair<int,int> a
a.first (可以取到第一個值)
a.second (可以取到第二個值)
```
----
### 使用範例:tuple
* 宣告
```cpp
tuple<int, int, int>
tuple<double, int, long double, ...(還可以塞很多東西)>
```
* 取值
```cpp=
tuple<int, int, int> a
get<0>(a) (可以取到第一個值)
get<2>(a) (可以取到第三個值)
```
----
### vector
* 在標頭檔 vector 裡面
* vector 裡面可以塞 int , string , long long 啥都可以塞
* 是動態陣列。有時候我們無法預估陣列所需要宣告的長度,又必須宣告陣列,這時會選用 vector
* vector 的常用函式 : push_back , pop_back , size , empty , begin , end .
----
* 須注意的是若等到 vector 空間不夠裝,vector 會自動安排記憶體,但這麼做常數比較大,所以通常在使用前會先預設一些記憶體位置給 vector
* 我們會使用 reserve() 。e.g. `vector.reserve(128)`
* 或是直接在宣告時用建構元 (constructor) 指定大小。 e.g. `vector<int> vec(n)`
----
實際演練 : 常用函式
```cpp=
#include <iostream>
#include <vector> // vector 的標題檔
using namespace std;
int main() {
vector<int> Mega;
//宣告一個 vector 名稱叫 Mega 用來裝 int 元素
//裡面的容器是空的
for (int i=1; i<=5; i++) Mega.push_back(i);
for (int i=5; i>=1; i--) Mega.push_back(i);
cout << Mega.size() << "\n";
for (int i=1; i<=4; i++) Mega.pop_back();// 將 vector 尾端元素刪掉
cout << Mega.size() << "\n";
cout << Mega.empty() << "\n";
// empty() 函式為查詢 vector 裡面是否有元素
// 回傳值為 bool 1 裡面沒有元素 0 則是有
Mega.clear();
cout << Mega.empty() << "\n";
}
```
----
實際演練 : 遍歷 陣列遍歷
```cpp=
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<char> roy;
for (int i=0; i<26; i++) {
char c = i + 'A';
roy.push_back(c);
}
for (int i=0; i<roy.size(); i++) {
cout << roy[i] << " ";
}
cout << roy[5] << "\n";
cout << roy[0] << "\n";
// 查詢單一個元素可用陣列方式查詢
}
```
----
實際演練 : 遍歷 iterator 遍歷
```cpp=
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> vt;
vector<int>::iterator iter;
for (int i=95; i<=100; i++) {
vt.push_back(i);
}
iter = vt.begin();
for(iter = vt.begin(); iter != vt.end(); iter++) {
cout << *iter << "\n";
// 一定要 「 * 」 號
}
}
```
----
### stack
* 在標頭檔 stack 裡面
* stack裡面可以塞 int , string , long long 啥都可以塞
* 堆疊是一種先進後出的結構,像是疊盤子一樣,先放下去的會最慢被拿出來。
* stack的常用函式 : emplace(push) , size , empty , top , pop .
* 不支援隨機存取:O(N) 查找速度慢:O(N) 在集合中增刪元素很快:O(1)
* 常用在 DFS
----
實際演練:
```cpp=
#include <iostream>
#include <stack> // stack 的標題檔
using namespace std;
int main() {
stack<int> Stk;
//宣告一個 stack 名稱叫 stk 用來裝int元素
//一開始裡面的容器是空的
for (int i=4; i<=10; i++) {
Stk.push(i);
}
cout << Stk.top() << "\n";
// 輸出 stack 最上層的元素,所以會輸出 10
Stk.pop();
// 刪除 stack 最上層的元素
cout << Stk.top() << "\n";
// 輸出 stack 最上層的元素,所以會輸出 9
cout << "裡面有多少元素" <<Stk.size() << "\n";
// 查詢 stk 裡面有幾個元素,裡面元素有 4 5 6 7 8 9共六個,所以會輸出 6
cout << Stk.empty() << "\n";
// empty() 函式為查詢 stack 裡面是否有元素
// 回傳值為bool 1 裡面沒有元素 0 則是
//清除 stack的元素
while(Stk.size() != 0) { // 或是 while(Stk.empty == 0)
Stk.pop();
}
cout << "裡面有多少元素" << Stk.size() << "\n";
cout << Stk.empty() << "\n";
}
```
----
# 超\*經典例題
stack之括弧匹配
想法:
給予一個字串並包含了括號 '[', ']', '(', ')' 和 '{', '}',請驗證該字串中的括號是否合法配對。
也就是 "([])", "{()}", "[]" 為合法配對,但是 "(]", 或 "([)]" 就是不合法配對。
規律:
* 括號數量一定是偶數
* 有左邊系列括號,就一定有右邊系列括號
* 越慢出現的左邊括號,他的右邊括號就越早出現(順序相反)。
----
### queue
* 在標頭檔 queue 裡面
* queue裡面可以塞 int , string , long long 啥都可以塞
* 佇列是一種先進先出的結構,像是水管一樣的單向道,最早進去的會最先通出來。
* queue的常用函式 : push , size , empty , front , pop .
* 不支援隨機存取:O(N) 查找速度慢:O(N) 在集合中增刪元素很快:O(1)
* 常用在BFS
----
實際演練:
```cpp=
#include <iostream>
#include <queue> // queue 的標題檔
using namespace std;
int main() {
queue<int> que;
//宣告一個 queue 名稱叫 que 用來裝int元素
//一開始裡面的容器是空的
for (int i=4; i<=10; i++) {
que.push(i);
}
cout << que.front() << "\n";
// 輸出 queue 最上層的元素,所以會輸出 4
que.pop();
// 刪除 queue 最上層的元素
cout << que.front() << "\n";
// 輸出 queue 最上層的元素,所以會輸出 5
cout << "裡面有多少元素" <<queue.size() << "\n";
// 查詢 queue 裡面有幾個元素,裡面元素有 5 6 7 8 9 10共六個,所以會輸出 6
cout << que.empty() << "\n";
// empty() 函式為查詢 queue 裡面是否有元素
// 回傳值為bool 1 裡面沒有元素 0 則是
//清除 queue的元素
while(que.size() != 0) { // 或是 while(que.empty == 0)
que.pop();
}
cout << "裡面有多少元素" << que.size() << "\n";
cout << que.empty() << "\n";
}
```
----
### 經典例題環節:
使用兩個 stack 去實作出 queue 的功能
作法:
始終維護 s1 作爲存儲空間,以 s2 作爲臨時緩衝區。
入隊時,將元素壓入 s1。
出隊時,將s1的元素逐個“倒入”(彈出並壓入)s2,將 s2 的頂元素彈出作爲出隊元素,之後再將 s2 剩下的元素逐個“倒回” s1。

----
### priority_queue
* 在標頭檔 queue 裡面
* priority_queue 裡面可以塞 int , string , long long , 實作原理是 max_heap 在運作
* 為一序列容器 是一個帶優先級的 queue
* priority_queue 的常用函式 : push , size , empty , top , pop .
* 可以很快很方便的加入元素或取出當前最優先的元素
* 常用在各大題目
----
### priority_queue 複雜度分析
* push() 新增元素,複雜度 O(log n)
* size() 取得目前元素數量,複雜度 O(1)
* top() 取得目前最高優先(數值最大)的元素(不會刪掉),複雜度 O(1)
* pop() 刪掉目前最高優先(數值最大)的元素,複雜度 O(log n)
----
實際演練:
```cpp=
#include <queue>
priority_queue<int> pq;
priority_queue<int, vector<int>, greater<int> > pq1; //從小到大 變成min_heap
pq.push(5);
pq.push(8);
pq.push(4);
// 元素可重複,會個別保留
pq.push(5);
cout << pq.size() << "\n";
cout << pq.top() << "\n";
pq.pop();
cout << pq.size() << "\n";
cout << pq.top() << "\n";
cout << pq.empty() << "\n";
```
----
### 題目演練
對每一個輸入,請輸出到現在為止已輸入的數的中位數。
```
輸入: 輸出:
1 1
3 2
4 3
60 3
70 4
50 27
2 4
```
----
### 實際演練
```cpp=
void solve(){
long long x;
priority_queue<long long> small;
priority_queue<long long, vector<long long>, greater<long long>> big;
while(cin >> x){
big.push(x);
while(big.size() > small.size()){
small.push(big.top());
big.pop();
}
while(!big.empty() && !small.empty() && big.top() < small.top()){
ll a = big.top(), b = small.top();
big.pop(), small.pop();
big.push(b), small.push(a);
}
if(small.size() > big.size())
cout << small.top() << endl;
else
cout << (small.top()+big.top())/2 << endl;
}
```
----
### set 集合
* 在標頭檔 <set> 裡面
* 可以插入值,及查詢值有沒有在集合裡面
* set 是有序的,對 set 遍歷時,會以定義的大小順序遍歷,預設是由小到大
* 大多數操作皆為 $O(lgN)$
* set 的常用函式:insert, erase, count, size, empty, begin, end, lower_bound, upper_bound
----
### 使用範例:
```cpp
set<int> s; // []
s.insert(1); // st = [1]
s.insert(5); // st = [1, 5]
s.insert(3); // st = [1, 3, 5]
s.insert(2); // st = [1, 2, 3, 5]
s.insert(1); // st = [1, 2, 3, 5]
```
----
### 使用範例:
```cpp
set<pair<int,int> > s; //[]
s.insert({1, 2}); // [{1, 2}]
s.insert({2, 1}); // [{1, 2}, {2, 1}]
s.insert({2, 1}); // [{1, 2}, {2, 1}]
s.insert({3, 5}); // [{1, 2}, {2, 1}, {3, 5}]
s.count({2, 1}); //return 1;
```
----
### map
* 在標頭檔 <map> 裡面
* 映射,基本上就是多了個對應值的set,單位型態為 pair<key,value>
* 用法跟 set 很像,但多了 operator[] 可以取值或存值。
* 大多操作皆為 $O(lg\ n)$
* map 的常用函式:insert, erase, count, size, empty, begin, end, lower_bound, upper_bound.
----
### 使用範例:
```cpp
map<int, int> mp;
mp[3] = 1; //[3] = 1,
mp[2] = 2; //[2] = 2, [3] = 1;
mp[3] = 5; //[2] = 2, [3] = 5;
```
<!-- 加個 string 當 key 的粒子-->
----
### 使用範例:
```cpp
map<string,int> mp; // map 還可以用 string 當 key
mp["qwer"] = 1;
mp["abc"] = 2;
```
----
### unordered_set / unordered_map
* 基本上就是無序的 set / map
* 由於是由 hash 實作,複雜度最好是 $O(1)$,但也可能到 $O(N)$
* 當不需要資料有序時可以使用
* 使用時候要小心,會被刻意生出來的測資卡,在有 hack 的比賽不建議使用
* 函式與 set / map 類似,只是少了 lower_bound, upper_bound.
----
### multiset / multimap
* key 可以重複的 set / map
* 由於 key 會重複,因此少了 [ ]operator
* 大多操作皆為 $O(lg\ n)$
* 函式與 set / map 類似
----
### 使用範例:
```cpp
multiset<int> s; //[]
s.insert(1); // [1]
s.insert(2); // [1, 2]
s.insert(3); // [1, 2, 3]
s.insert(1); // [1, 1, 2, 3]
s.count(1); //return 2;
```
----
### 使用範例:
```cpp
multimap<int, string> mp; //[]
mp.insert({1, "c8763"});
mp.insert({1, "38763"});
mp.insert({2, "1234"});
mp.insert({4, "mpmp"});
mp.count(1); // return 2
```
----
### bitset
* 可看作是 bool 陣列,但長度必須是(編譯時期)常數 (constexpr)
* 支援位元運算,且成員函數多為位元相關函數
* 用在位元運算常數非常小
* 沒有要做位元運算別亂用(用 vector<bool> 就好)
* 函式:count、size、test、any、none、all、set、reset、flip。
----
----
### 使用範例:
```cpp
bitset<10> a;
bitset<10> b(10);
bitset<10> c("10010");
```
---
## IO / 常數優化
----
* IO 優化
* 常數優化
----
### 什麼是優化?
保持語義不變的情況下,對程式運行速度、程式可執行文件大小作出改進
----
in/out優化
這個函數是一個“是否相容 stdio”的開關,C++ 為了相容 C,保證程式在使用了 printf 和 std::cout 的時候不發生混亂,將輸出綁到了一起。同步的輸出是安全的。
這其實是 C++ 為了相容而採取的保守措施,也是使 cin/cout 速度較慢的主要原因。我們可以在進行 IO 操作之前將 stdio 解除綁定
但是在這樣做之後要注意不能同時使用 std::cin 和 scanf,也不能同時使用 std::cout 和 printf,但是可以同時使用 std::cin 和 printf,也可以同時使用 scanf 和 std::cout。
```cpp=
ios_base::sync_with_stdio(0),cin.tie(0),cout.tie(0);
```
----
## 強常數優化
但不是萬能的
```cpp=
#pragma GCC optimize("O3,unroll-loops")
#pragma target optimize("avx2,bmi,bmi2,lzcnt,popcnt")
```
網上針對 pragma GCC optimize 的文章有非常多,隨便搜尋都有一大堆,每個人都會有自己的習慣跟信仰,並不代表這邊就是最好的優化,也只是個人的常用而已。
----
----
## Question Time and Practice
今天的課程也可以上 [cpprefference](https://cplusplus.com/reference/) 去看容器或者是函式的詳細內容,由於篇幅關係,可能還有一些函式沒有說到。
{"metaMigratedAt":"2023-06-17T16:46:10.382Z","metaMigratedFrom":"YAML","title":"pretrain 4","breaks":true,"contributors":"[{\"id\":\"19f09ccf-6b99-452f-971f-955cfc1657f3\",\"add\":236,\"del\":35},{\"id\":\"5df14ba0-4dd2-4172-b309-8b5af9ede7a1\",\"add\":7234,\"del\":844},{\"id\":\"e8edfe24-eef8-448f-beff-2798a40d21cd\",\"add\":96,\"del\":16},{\"id\":\"e4a3af8b-860c-46a0-96f1-558667fdcf41\",\"add\":13013,\"del\":3277}]"}