# C++基礎語法與演算法自主學習 # C++ 課程學習計畫 | 週次 | 日期 | 節次 | 課程 | 自學內容 | |------|------|------|----------------|------------------------------------------------------------| | 1 | 8/31 | 5 | 基本架構 | 學習基本架構,編寫 `Hello World` 程式,確保編譯並執行成功 | | 1 | 8/31 | 6 | 變數、輸出輸入 | 學習 C++ 的輸出、輸入,以及變數的宣告與使用。編寫程式,要求使用者輸入數字並顯示它 | | 2 | 9/7 | 5 | 算術運算子 | 理解算術運算子的使用,並利用其進行計算 | | 2 | 9/7 | 6 | if 判斷式 | 學習 `if` 判斷式的條件控制,並編寫程式判斷輸入數字的正負或零 | | 3 | 9/14 | 5 | for 迴圈、while 迴圈 | 學習如何使用迴圈來重複執行函式與運算 | | 3 | 9/14 | 6 | 陣列 | 學習陣列的宣告,並利用迴圈計算陣列數字的總和 | | 4 | 9/28 | 5 | 巢狀迴圈 | 理解巢狀迴圈的思考方式,並學習如何分解複雜的巢狀迴圈 | | 4 | 9/28 | 6 | 巢狀迴圈綜合練習 | 綜合練習巢狀迴圈的應用 | | 5 | 10/5 | 5 | 二維陣列 | 理解二維陣列的使用方法,以及適用的資料存儲類型 | | 5 | 10/5 | 6 | 二維陣列綜合練習 | 綜合練習二維陣列的應用 | | 6 | 10/19 | 5 | 準備 CodeWar 比賽與 APCS | 利用 ZeroJudge 練習歷屆 APCS 題目 | | 6 | 10/19 | 6 | 準備 CodeWar 比賽與 APCS | 利用 Kattis 練習題目,並練習 CodeWar 歷屆題目 | ## 學習動機 **因為對程式語言有興趣,並且在未來想就讀資工相關科系,故選擇最多學習資源與最容易入門的C++做自主學習內容** ## 預期成果 **希望學完基礎語法後能輕鬆的撰寫程式,理解基礎演算法,在往後幫助我參加APCS檢測與相關競技程式比賽,也可以提早入門程式語言** # 競程介紹 >**這是額外補充** ## 競程用語 * **競程:程式競賽** * **OJ : Oline Judge(用來執行程式判斷程式碼是否正確)** --- * **<font color=#008000>AC : Accepted 正確答案</font>** * **<font color=#FF0000>WA : Wrong Answer 錯誤答案</font>** * **<font color=#FFD700>CE : Compile Error 編譯錯誤</font>** * **<font color=#0000FF>TLE : Time Limit Exceed 超時</font>** ## 競程介紹 ### 簡介 * **拿程式來打比賽** * **用程式實作出演算法** * **主要分為 IOI 賽制與 ICPC 賽制** ### IOI 賽制 * **高中競程主流賽制** * **依照答對某個子任務拿到對應的分數** * **不會有任何懲罰** ### ICPC 賽制 * **大學競程主流賽制** * **會有罰時** * **若兩隊答對數量相同,則比較誰的罰時短** ### 共通點 * **都可以使用C C++** --- # 基礎程式 ```cpp= #include <iostream> using namespace std; int main(){ //輸出Hello, world cout << "Hello, world\n"; return 0; } ``` ## **標頭檔** ```cpp= #include <iostream> ``` * **利用 #include 引入`<iostream>`程式標頭檔** ## 命名空間 ```cpp= uisng namespace std; ``` * **使用namespace命名空間來儲存std(standard)** * **通常用來讓cout cin等程式碼運作順利** ## 函式int main() * **int是指回傳的值是int類型的值** * **main()用來存放主要的函式** ## 註解 * **用//來註解文字** * **或是用** ```cpp= /* hello world */ ``` * **來註解一段文字** ## 輸出Hello, world ```cpp= //輸出Hello, world cout << "Hello, world\n"; return 0; ``` * **cout是用來輸出的函式** * **<<是運算子符號** * **想輸出的文字需要用 "" 或 '' 來圍住** * **\n或endl都可以用來換行(在競程中通常使用\n來減少程式碼執行速度)** * **;用來結尾,一行分號結尾的程式就是一個陳述,陳述是程式執行的單位, C++ 程式是一行陳述執行完畢,才接著下一行陳述繼續執行。** # 變數 ## 變數基本概念 * **在程式中需要用變數來儲存運算結果** * **變數有許多基本型態** ## 宣告 * **在C++程式中建立變數** ```cpp= int a ; //一般宣告 int a,b,c,d; //多個宣告 int a = 0; //宣告變數的值 ``` * **變數在宣告後才可以使用** ## 變數宣告規則 * **不能以數字為開頭** * **不能用符號和空格(底線除外)** * **不能用保留字(int include)** * **建議使用英文命名** ## 常用資料型態 ### **int 整數(Integer)** * **用來儲存整數值,可以區分為 short、int、long 與 long long(C++ 11),可容納的大小各不相同,int 至少會跟 short 一樣大,long 至少會跟 int 一樣大,long long 至少 64 個位元(通常用在需要龐大運算的變數)。** * **在初學最常使用 int** ```cpp= int n = 0; ``` ### **float 浮點數** * **也被稱為小數,用來儲存小數值,可以區分為 float、double 與 long double,越後面的型態使用的記憶體空間越大,精度也就越高。** * **在初學最常使用double** ```cpp= double = 3.14; ``` ### **char 字元** * **char 的 sizeof(char) 結果要是 1,基本上用來儲存字元資料,但沒有規定什麼是字元資料,也可用來儲存較小範圍的整數。** ```cpp= char = 'A' ``` ### **bool 布林值** * **用來儲存正確或錯誤(True / Flase)** ```cpp= bool answer = true; ``` ### **string 字串** * **用來儲存多個字元** ```cpp= string name = "Yude" ``` ## 輸出輸入 * **在C語言中的輸出輸入通常使用printf和scanf** * **C語言中的函式在C++ 都能使用,但在C++ 中新增了一套新的、更容易使用的輸入輸出庫** ## 標準輸出流 cout * **cout是ostream類的預定義對象。它與標準輸出設備連接,通常是螢幕** * **cout與流插入運算符(<<)結合使用,以在控制台上顯示輸出** #### 範例:輸出Hello, world ```cpp= #include <iostream> using namespace std; int main(){ cout << "Hello, world\n"; return 0; } ``` ### Output ```cpp= Hello, world ``` ## 標準輸入流 cin * **cin是istream類的預定義對象。它與標準輸入設備連接,標準輸入設備通常是鍵盤。** * **cin與流提取運算符(>>)結合使用,從控制台讀取輸入。** ### 範例:Hello "Name" ```cpp= #include <iostream> using namespace std; int main(){ string name; cin >> name; cout << "Hello " << name << '\n'; return 0; } ``` ### Input ```cpp= Yude ``` ### Output ```cpp= Hello Yude ``` ## 算術運算子 * **用來節省大量的運算** * **介紹五種常用運算子** ### 加法 + ```cpp= int a = 2; a = a + 3; //可縮寫成 a += 3 ``` ### 減法 - ```cpp= int a = 3; a = a - 2; //可縮寫成 a -= 2 ``` ### 乘法 * * **C++ 跟四則運算一樣需要先乘除後加減** * **括號裡的數字先算** * **<font color=#FF0000>不能除以0</font>** ```cpp= int a = 3; a = (a+2)*5; ``` ### 除法 / ```cpp= int a = 3; a = (a+6)/3; ``` ### 取餘數 % * **這個符號叫做 MOD** * **又名"模運算"** * **<font color=#FF0000>不能跟0取餘數</font>** ```cpp= int a = 3; a = (a+7)%5; ``` ### 補充 ```cpp= int i = 3; i ++; //=4 i --; //=2 ``` ### 題目練習 ### [**我是計算機**](https://codeforces.com/gym/471838/problem/B) ```cpp= #include <bits/stdc++.h> using namespace std; int main() { int a, b; cin >> a >> b; //a+b, a−b, a×b, ⌊a/b⌋, amodb, a^2+b^2 cout << a + b << ' ' << a - b << ' ' << a * b << ' ' << a / b << ' ' << a % b << ' '<< a*a + b*b << ' '; return 0; } ``` #### Input ```cpp= 1 1 ``` #### Ouput ```cpp= 2 0 1 1 0 2 ``` --- # If 判斷式 * **在程式中常常會遇到多種狀況** * **像是判斷值的大小 判斷是否及格** * **不同的狀況常有不同的處理方式** * **此時就需要用到if來判斷狀況** ## if-else 條件式 ```cpp= if( 條件 ) { 如果條件成立時做什麼... } // 多增加判斷條件可以用else if else { 否則做什麼... } ``` ## 範例 判斷成績是否及格 ```cpp= #include <bits/stdc++.h> using namespace std; int main(){ int grade; cin >> grade; if(grade >= 60) cout << "Pass"; else cout << "Fail"; } ``` ## Input ```cpp= 67 ``` ## Ouput ```cpp= Pass ``` ## if與算術運算子題目練習 ### [**兩光法師占卜術**](https://zerojudge.tw/ShowProblem?problemid=a003) ```cpp= #include <windows.h> #include <bits/stdc++.h> using namespace std; int main(){ SetConsoleOutputCP(CP_UTF8); //因輸出中文會出現亂碼,所以使用此指令,不建議在程式碼中使用中文 int m, d; cin >> m >> d; int s = (m * 2 + d) % 3; if(s == 0) cout << "普通"; else if(s == 1) cout << "吉"; else cout << "大吉"; return 0; } ``` --- # 迴圈 * **在類似的事情需要反覆進行多次的時候** ## while 迴圈 * **用於反覆執行一段程式碼,直到不再需要執行為止** ```cpp= int n; cin >> n; while(n!=0){ cout << n <<'\n' ; n--; } ``` * **小括號中的條件只要滿足,就會完整執行一輪程式碼** * **每執行完一輪,會再次檢查條件,決定是否執行下一輪** * **當執行條件不成立了,表示不需要再繼續執行,此時 while 就結束了** * **<font color=#0000FF>執行條件只會在每一輪開始前檢查一次,其它時間不會進行檢查</font>** #### **上述程式碼的意思是:** #### **輸入n,當n不等於0時,重複執行輸出n然後n-1** ## 題目練習 ### [Stuck In A Time Loop](https://open.kattis.com/problems/timeloop) ```cpp= #include <bits/stdc++.h> using namespace std; int main (){ int N; cin >> N; int i=0; while(i!=N){ i++; cout << i <<" Abracadabra\n"; } return 0; } ``` ## for 迴圈 * **為另一種迴圈的語法,與while沒有太大差別** * **有些書籍將 for 定義為執行特定次數時使用** * **但應該定義為for是較有彈性且較方便的** #### 完成迴圈,通常有四個要素 * **執行前的準備** * **執行的條件** * **反覆執行的事情** * **收尾或下一輪的前置準備** #### 我們用 while 迴圈來表示 ```cpp= int i=1; //執行前的準備 while(i<=n){ //執行的條件 cout << i << '\n'; //反覆執行的事情 i++; //收尾或下一輪的前置準備 } ``` #### 如果用 for 來表示 ```cpp= for (int i=1;i<=n,i++){ cout << i << '\n'; } ``` ## for 迴圈題目練習 ### [**Cinema Crowds 2**](https://open.kattis.com/problems/cinema2) ```cpp= #include <bits/stdc++.h> using namespace std; #define FOR(i,a,b) for(int i=a;i<b;i++) int main(){ int N,M; cin >> N >> M; int X[M]; int sum=0; FOR(i,0,M){ cin >> X[i]; sum += X[i]; //總共的遊客人數。 if (sum == N) { cout << M-(i+1) << '\n'; break; } else if (sum > N) { cout << M-i << '\n'; break; } } } ``` --- # Array 陣列 * **當我們需要儲存大量資料時,果用多種變數來儲存,勢必會變得非常不方便** ```cpp= int n, num1, num2 num3 /*........*/; cin >> n; cin >> num1; cin >> num2; cin >> num3; //....... ``` * **由於變數的名字無法由變數決定,無法整理成迴圈** #### 如果num後的數字可以用變數指定,就變成迴圈 ```cpp= for (int i=0;i<n;i++){ cin >> Num[i]; } ``` * **這就是陣列最大的優點,<font color=#FF0000>可用運算式指定編號</font>** ### 陣列宣告 * **X[n]是長度為n的陣列** * **宣告陣列必須先宣告陣列長度** ```cpp= int N[5] = {0,7,14,2,3} ``` ### 陣列指派 ```cpp= N[2] = 9; ``` ### 陣列輸入 ```cpp= cin >>N[5]; ``` ### 陣列輸出 ```cpp= cout << N[5]; ``` ### 範例 印出陣列所有內容 ```CPP= #include<iostream> using namespace std; int main(){ int N[5]={2,4,5,6,1}; int i=0; while(i<5){ cout << N[i] << ' '; i++; } return 0; } ``` ### Output ```cpp= 2 4 5 6 1 ``` ### 陣列結合迴圈題目練習 ### [**Reverse**](https://open.kattis.com/submissions/11753318) ```cpp= #include <bits/stdc++.h> using namespace std; #define FOR(i,a,b) for(int i=a;i<b;i++) #define rFOR(i,a,b) for(int i=b-1;i>=a;i--) //倒過來做迴圈 int main(){ int n; cin >> n; int X[n]; FOR(i,0,n){ cin >> X[i]; } rFOR(j,0,n) cout << X[j] << ' '; } ``` ### Input ```cpp= 5 1 2 3 4 5 ``` ### Ouput ```cpp= 5 4 3 2 1 ``` --- # 巢狀迴圈 * **有些問題不容易用一層迴圈來解決,需要用到更多層** * **這時我們就要理解巢狀迴圈的用法,以便於解決問題** ## 內外層不相干 * **當我們要輸出一個同邊長(或不同邊長)的矩形,該如何分析問題呢** ### 矩形繪製 #### 輸入一個整數 n,以 # 輸出一個邊長為 n 的矩形。 #### $1 \le n \le 9$ * **我們先把題目拆分成: 輸出n行,每行為n個#** * **整理成迴圈** **換行的迴圈** ```cpp= for(int i=0;i<n;i++) { //輸出n個# cout << '\n'; } ``` **輸出n的迴圈** ```cpp= for(int j=0;j<n;j++) //使用j是便於閱讀,避免搞混 { cout << "#"; } ``` **結合在一起就會變成** ```cpp= for(int i=0;i<n;i++) { for(int j=0;j<n;j++) { cout << "#"; } cout << '\n'; } ``` **於是原本複雜的「輸出 n 行,每行輸出 n 個 #」被我們拆成:** * **輸出 n 行,每行怎麼辦不管它** * **輸出一行 n 個 #,到底要幾行不管它** **兩個獨立的問題,個別處理後再合併即可。** ## 內外層有關聯 ### 三角形繪製 #### 輸入一個整數 n,以 # 輸出一個邊長為 n 的直角三角形 #### $1 \le n \le 9$ * **我們將題目拆分成:輸出n行,依序為等差數列輸出1~n個#** **因為題目有點複雜,我們先將要輸出幾個#做出來** ```cpp= for(int i=1;i<n;i++) { cout << i << " #"; } ``` **假如n為5,Ouput應該長這樣** ```cpp= 1 # 2 # 3 # 4 # 5 # ``` * **所以我們得知,每一次的i都需要+1,所以不能用i<n** * **所以我們先做出外層再做內層** **外層跟剛剛的矩形一樣** ```cpp= for (int i=0;i<n;i++) { //輸出#的程式碼 cout << '\n'; } ``` * **假如要輸出1 2 3 4 5 ...個#,是不是可以利用j<i來輸出** * **這時,i就要=1,並i<=n,因為一開始是輸出i個#,而輸出n行** **更改後的外層** ```cpp= for (int i=1;i<=n;i++) { //輸出#的程式碼 cout << '\n'; } ``` **那麼內就能改成** ```cpp= for(int j=0;j<i,j++) { cout << "#"; } ``` **結合在一起就是** ```cpp= for (int i=1;i<=n;i++) { for(int j=0;j<i,j++) { cout << "#"; } cout << '\n'; } ``` ## 題目練習 ### [**Zamka**](https://open.kattis.com/problems/zamka) ```cpp= #include <bits/stdc++.h> using namespace std; #define fast ios::sync_with_stdio; cin.tie(0); cout.tie(0) int main(void){ fast; int L,D,X; cin >> L >> D >> X; int N = -1; int M = -1; int sum; int num; for(int i=L;i<=D;++i) { sum = 0; num = i; while(num > 0) { sum += num % 10; num /= 10; } if (sum == X) { N = i; break; } } for(int i=D;i>=L;--i) { sum = 0; num = i; while(num > 0) { sum += num % 10; num /= 10; } if (sum == X) { M = i; break; } } cout << N << '\n'; cout << M << '\n'; } ``` --- # 二維陣列 * **如果我們要儲存2D的資料,像是迷宮,不能使用一維陣列儲存** * **這時我們就可以使用二維陣列** ``` o o o o o o o o o x x x o o o x x o x x o o o x o x x o o x x x o o o o o o x x x o o o o o o x o ``` ### 列優先(row-major) * **列優先是一個符合電腦輸出順序的方式** * **會先寫滿一列,在開始寫下一列** * **若將原先 7x7 的二維迷宮依此順序儲存,則會變成:** | | 0 | 1 | 2 | 3 | 4 | 5 | 6 | | --- | --- | --- | --- | --- | --- | --- | --- | | 0 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | | 1 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | | 2 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | | 3 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | | 4 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | | 5 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | | 6 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | * **讓每列的長度固定,就能用乘法和除法做換算** * **像是座標(3,2)的上面共有 3 列,每列長度 7,可知前面共有 $3$ x $7$ = $21$ 格** * **左邊共有 2 行,共計 2 格,因此位置會是$3$ x $7$ + $2$** * **任意座標 $(i,j)$ 可知位置是 $i$ x $n$ + $j$ ($n$ 為列的長度)** ### 二維陣列的宣告與使用 #### 宣告 * **宣告一個由 N 列,每列 M 行,共計 N X M 個 int 變數的陣列:** ```cpp= int x[N][M]; ``` * **先描述有幾列,再描述每列有幾行** * **存取時也是先描述要哪一列,再描述要該列的第幾個元素** ### 多維陣列 n-dimension array * **按照相同的邏輯,複數個相同大小的二維陣列, 可以聚集成一個三維陣列;複數個相同大小的三維陣列, 則可以聚集成一個四維陣列,…** :::success n 維陣列是「每個元素皆為 n-1 維陣列」的一維陣列 ::: ## 題目練習 ### [**This Ain't Your Grandpa's Checkerboard**](https://open.kattis.com/problems/thisaintyourgrandpascheckerboard) ```cpp= #include <bits/stdc++.h> using namespace std; #define fast ios::sync_with_stdio; cin.tie(0); cout.tie(0) #define FOR(i,n) for(int i = 0; i < n; ++i) bool correct_grid(vector<string>& x, int n){ FOR(i,n){ int r_B=0, r_W=0, c_B=0, c_W=0; FOR(j,n){ //檢查同列的黑白數 if (x[i][j] == 'B') r_B++; else r_W++; //檢查同行的黑白數 if (x[j][i] == 'B') c_B++; else c_W++; // 檢查是否有連續三個或更多同色 if (j < n - 2) { if (x[i][j] == x[i][j + 1] && x[i][j] == x[i][j + 2]) return false; if (x[j][i] == x[j + 1][i] && x[j][i] == x[j + 2][i]) return false; } } //檢查黑白數是否相同 if (r_B != r_W || c_B != c_W) return false; } return true; } int main(){ fast; int n; cin>>n; vector<string> x(n); FOR(i,n) cin >> x[i]; if(correct_grid(x ,n)) cout << "1\n"; else cout << "0\n"; return 0; } ``` ### 俄羅斯方塊 給一個俄羅斯方塊在放置方塊後、尚未處理消去的盤面, 你必須輸出消去幾排,以及消去後的樣子。 盤面寬度固定為 10,高度由輸入決定。 以 `.` 代表空位,以 `#` 代表非空。 當一橫列 10 格全部為 `#` 時,則此列會被消去, 所有上面的橫列全部往下掉 1 格,最上面空出來的列以 `.` 補滿。 :::info #### 範例輸入 ``` 5 ...#...### ..#....##. ########## #####.#### ########## ``` #### 範例輸出 ``` remove 2 rows. .......... .......... ...#...### ..#....##. #####.#### ``` #### 範例輸入 ``` 5 ..##...### ..#....##. ####..###. #####.###. #########. ``` #### 範例輸出 ``` remove 0 rows. ..##...### ..#....##. ####..###. #####.###. #########. ``` #### 範例輸入 ``` 5 .###...##. ########## ########## ########.# ########## ``` #### 範例輸出 ``` remove 3 rows. .......... .......... .......... .###...##. ########.# ``` ::: ```cpp= #include <bits/stdc++.h> using namespace std; #define fast ios::sync_with_stdio; cin.tie(0); cout.tie(0) #define FOR(i,a,b) for(int i = a; i<b; i++) int main(){ fast; int n; cin >> n; string X[n]; bool same[n]; int remove = 0; FOR(i, 0, n) cin >> X[i]; FOR(i,0,n){ bool check = true; FOR(j,0,10){ if(X[i][j] == '.') { check = false; break; } } if(check){ remove++; same[i] = 1; } else same[i] = 0; } cout << "remove " << remove << " rows.\n"; FOR(i,0,remove) cout << "..........\n"; FOR(i,0,n){ if(same[i] == 0) cout << X[i] << '\n'; } return 0; } ``` # Complexity 複雜度分析 用來分析一個演算法所需的計算量或記憶體空間 1. 時間複雜度:執行一份程式所需的計算量 2. 空間複雜度:執行一份程式所需的記憶體空間 ## 時間複雜度 - $O$ (Big-O):最壞複雜度,上限(Upper bound) - $Ω$ (Omega):最佳複雜度,下限(Lower bound) - $θ$ (Theta):平均複雜度 通常關注Big-O,因為我們較關心最糟情況程式執行多少次 ### 各種常見的時間複雜度 由小到大 | 名稱 | 時間複雜度 | | -------------- | ------------- | | 常數時間 | $O(1)$ | | 對數時間 | $O(\log n)$ | | 線性時間 | $O(n)$ | | 線性乘對數時間 | $O(n \log n)$ | | 平方時間 | $O(n^2)$ | | 指數時間 | $O(2^n)$ | | 階乘時間 | $O(n!)$ | ![image](https://hackmd.io/_uploads/rJpdsEDnT.png =60%x) 當輸入個數很少的時候 步驟數差異不大 但當輸入個數**逐漸增加**時每個複雜度量級的差距會越來越大 所以我們只想知道在資料量非常大的時候 最糟情況如何 最重要的是**成長速度** ### 時間複雜度計算原則 以計算量(基本操作次數)而定 會受資料量以及演算法影響 只會紀錄**最高次方項**並**忽略所有的係數** 例如一個程式需要執行 $3n^2 + 4n + 5$ 個基本操作 我們會稱它的時間複雜度為 $O(n^2)$ ### 舉例分析時間複雜度 - 基本操作 $O(1)$ 常數時間 用公式解求 1 ~ n 加總的程式 不受資料量影響 ```cpp= int n; cin >> n; n = n * (n-1) / 2 ``` - 迴圈 $O(n)$ 線性時間 會受輸入的資料量影響 ```cpp= int n; cin >> n; for(int i = 1; i <= n; i++){ sum += i; } ``` - 特定計算 $O(\log n)$ 對數時間 執行 $x$ 次 $i = i * 2$ 之後 $i \ge n$ 退出迴圈 得到$2^x \ge n$,運用對數律可知 $x = \log_2 n$, ```cpp= int i = 1; while(i < n) { i = i * 2; } ``` - 雙重迴圈與二維陣列 $O(n^2) 平方時間$ ```cpp= int n; cin >> n; for(int i = 0; i < n; i++){ for(int j = 0; j < n; j++){ t += a[i] * a[j]; } } ``` 一般情況下 電腦每秒大約可以執行 $10^8$ 次基本操作 如果將題目限制以你寫的程式碼方法分析時間複雜度 如果比 $10^8$ 高 可能會 <span style="color:yellow">超時 (TLE)</span> ### 透過輸入範圍估算時間複雜度 | 輸入範圍 | 時間複雜度 | | --- | --- | | $n \leq 10^{18}$ | $O(1) or O(\log n)$ | | $n \leq 10^9$ | $O(\sqrt n)$ | | $n\leq 10^6$ | $O(n) \ or \ O(n \log n)$ | | $n \le 5000$ | $O(n^2)$ | | $n \le 500$ | $O(n^3)$ | | $n \le 20$ | $O(2^n)$ | | $n \le 10$ | $O(n!)$ | # 數學 ## 二進位制運算 $Binary$ $addition$ * 利用 and xor 和右移來實作 ```cpp= signed main(void) { int x,y; cin >> x >> y; while(x) { int a = x&y, b = x^y; a<<=1; x=a,y=b; //cout << x << " " << y << '\n'; } cout << y; } ``` ## 快速冪 $Fastpow$ * 將指數部分以二分的方式處理 例: 3^ 22=(3^ 2)^11 =6^ 11=(6^ 2)^5 *6 =36^ 5 *6=(36^ 2)^2 *36 *6 =1296^2 *36 *6=1679616 *36 *6 ```cpp= const int mod = 1e9+7; long long int fastpow(long long int x,long long int y) { //cout << x << " " << y << "\n"; if(y == 0)return 1; if(y%2)return fastpow(x*x%mod,y/2)*x%mod; return fastpow(x*x%mod,y/2); } ``` * [CSES 1095](https://cses.fi/problemset/task/1095) ```cpp= #include <bits/stdc++.h> using namespace std; #define IO ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); #define int long long const int mod = 1e9+7; int fp(int x,int y) { if(y == 0) return 1; if(y%2) return fp(x*x%mod,y/2)*x%mod; return fp(x*x%mod,y/2); } signed main(void) { IO read(n); while(n--) { int a,b; cin >> a >> b; cout << fp(a,b) << endl; } } ``` ## 模倒數/模逆元/模反元素 $Inversion$ $mod$ * 費馬小定理:a、p互質,a^p=a (mod p) ```cpp= //i^(mod - 2) % mod //inv[i]= (mod - (mod / i) * inv[mod % i]) % mod(不推薦使用) int inv(int x) { return fastpow(x,mod-2); } ``` ## 質數篩 * 複雜度為 $O(n$ $log$ $log$ $n)$ ```cpp= bool notprime[10005]; int main() { vector<int> prime; for(int i=2;i<=10000;i++) { if(!notprime[i]) { prime.push_back(i); for(int j=2;i*j<=10000;j++)notprime[i*j]=1; cout<<i<<' '; } } } ``` # 遞迴 比迴圈更高級的作法 ## 尋找相似子結構 找出原問題的答案與相似子問題的關係 ## 同性質子問題 ### 例題: 求$f(n) = 1 \times 2 \times 3 .... \times n$ $f(5) =$ <span style="color:lime">$1 \times 2 \times 3 \times$ $4$</span> $\times 5$ $f(4) =$ <span style="color:lime">$1 \times 2 \times 3 \times$ $4$</span> 由上,可發現 f(5)中的部份計算與 f(4) 完全相同 故**f(5)的結果,可由f(4)算出** $f(5) = f(4) \times 5$ ### 一般化 $f( 5 ) = f( 4 ) \times 5$ $f( 4 ) = f( 3 ) \times 4$ 可推得 $f( i ) = f( i-1 ) \times i$ ## 實作方法 - Top-down : 由原問題需求出發 - Bottom-up : 從已知最小問題出發 ### Bottom-up $f(1) = f(0) \times 1 = 1 \times 1 = 1$ $f(2) = f(1) \times 2 = 1 \times 2 = 2$ $f(3) = f(2) \times 3 = 2 \times 3 = 6$ $f(4) = f(3) \times 4 = 6 \times 4 = 24$ $f(5) = f(4) \times 5 = 24 \times 5 = 120$ ```cpp= f[0] = 1; for(int i=1;i<=n;i++){ f[i] = f[i-1] * i; } ``` ### Top-down $f(5)$ 需要 $f(4)$,先求 $f(4)$ $f(4)$ 需要 $f(3)$,先求 $f(3)$ $f(3)$ 需要 $f(2)$,先求 $f(2)$ $f(2)$ 需要 $f(1)$,先求 $f(1)$ $f(1)$ 需要 $f(0)$,先求 $f(0)$ $f(0)$ 已知,回推 ```cpp= int f(int i){ if (i == 0) return 1; return f(i-1) * i; } ``` ## 例題 ### 費氏數列 求存在多少種以 1 和 2 構成的序列 使序列總和恰為 n 例如 n = 5 答案為 8 1. $11111$ 2. $1112$ 3. $1121$ 4. $1211$ 5. $2111$ 6. $122$ 7. $212$ 8. $221$ 序列最後一個元素依題意必為 1 或者 2 設總和為 5 時 窮舉所有可能的最後元素如下 $$ \left\{ \begin{aligned} A_1 + A_2 + ... + A_K +1 = 5\\ A_1 + A_2 + ... + A_K +2 = 5\\ \end{aligned} \right. $$ 整理完可得 $$ \left\{ \begin{aligned} A_1 + A_2 + ... + A_K = 4\\ A_1 + A_2 + ... + A_K = 3\\ \end{aligned} \right. $$ 可知任意一組序列總和為 4 時, 末尾補上 1 即可湊出一組總和為 5 同理任意一組序列總和為 3 時, 末尾補上 2 即可湊出一組總和為 5 設序列總和為 4 共 x 組 序列總和為 3 共 y 組 則序列總和為 5 共 $x * 1 + y * 1$ 組 ### 問題轉換 原問題:求有多少序列總和為 5 轉變為 - 求有多少序列總和為 4 - 求有多少序列總和為 3 將上述答案加總,即為所求 設 f( n ) = 有多少序列總和為 n 由上述觀察,可列式表達關係為 $f(n) = f(n-1) + f(n-2)$ ### 記錄答案避免重算 子問題會重疊,$n=5$只有六項卻呼叫15次 所以把算過的答案存下來減少計算量 ```cpp= int f(int i){ if(used[i]) return rec[i]; used[i] = true; if(i <= 1) return rec[i] = 1; return rec[i] = f(i-1) + f(i-2); } ``` ## 快速冪 $Fastpow$ 更有效率的計算 $a^b$ ### 將指數部分以二分的方式處理 ```cpp= const int mod = 1e9+7; int fastpow(int x,int y) { if(y == 0)return 1; if(y%2)return fp(x*x%mod,y/2)*x%mod; return fp(x*x%mod,y/2); } ``` ### 折半 $f(a,6)=a×a×a×a×a×a$ $=(a×a×a)×(a×a×a)$ $=a^3×a^3$ $=f(a,3)×f(a,3)$ 奇數無法折剛好 那就別折,套$f(a,b)=f(a,b−1)×a$ 若 b 為奇數,則 b-1 為偶數,可折半 可得關係式 $f(a,b)$ = \begin{cases} f(a, \frac{b}{2})^2, & \text{if } b \text%2 != 0 \\ f(a,b-1) \times a, & \text{else} \end{cases} ```cpp= int f(int a, int b){ if(b == 0) return 1; if(b%2 != 0) return f(a,b-1) * a; int t = f(a, b/2); return t * t; } ``` ## 題目 ### [Kattis batmanacci](https://open.kattis.com/problems/batmanacci) ```cpp= #include <bits/stdc++.h> using namespace std; #define ll long long const ll mx = 1e18; ll len[100100]; char f(int n,ll k){ if(n==1) return 'N'; if(n==2) return 'A'; if(k <= len[n-2]) return f(n-2,k); return f(n-1,k-len[n-2]); } signed main(void) { int n,k; cin >> n >> k; len[1] = 1; len[2] = 1; for(int i=3;i<n;i++) len[i] = min(mx,len[i-1]+len[i-2]); char ans = f(n,k); cout << ans << endl; } ``` ### [AtCoder typical90_bq](https://atcoder.jp/contests/typical90/tasks/typical90_bq) ```cpp= #include <bits/stdc++.h> using namespace std; #define IO ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); #define ll long long #define int long long const ll md = 1e9+7; //n<=1e18 使用fastpow int fp(int x,int y){ if(y == 0) return 1; if(y%2) return fp(x*x%md,y/2)*x%md; return fp(x*x%md,y/2); } //第一格k種,第二格k-1種,剩下都是k-2種 //故得出 k * k-1 * (k-2)^(n-2) signed main(void) { IO int n,k; cin >> n >> k; int ans=0; if(n==1) ans = k; else if(n==2) ans = (k * (k-1)) % md; else ans = (((k * fp(k-2,n-2)) % md) * (k-1)) % md; cout << ans << endl; } ``` # 圖論 ## 廣度優先搜索 $BFS$ 先選定一個頂點開始走訪,逐一走過此頂點相鄰未被走過的頂點,若相鄰頂點都被走過,再從走訪過的頂點中擇一為新記錄點,逐一走過新記錄點相鄰未被走過的頂點,以此類推。 廣度優先可以利用佇列 $Queue$ 的方式來處理。 ![image](https://hackmd.io/_uploads/rkVoaRe5T.png) * Uav 439 ```cpp= #include<bits/stdc++.h> using namespace std; // 使用 pair 存儲座標 queue<pair<int, int>> q; // 存儲每個格子到目標點的最短距離 int dis[8][8]; // 定義馬的八種可能移動方向 int dx[8] = {1, 2, 2, 1, -1, -2, -2, -1}; int dy[8] = {2, 1, -1, -2, -2, -1, 1, 2}; int main() { int ax, ay, bx, by; char c[4]; // 循環讀取輸入,每次讀取四個字符 while (cin >> c[0] >> c[1] >> c[2] >> c[3]) { // 初始化最短距離數組 for (int i = 0; i < 8; i++) for (int j = 0; j < 8; j++) dis[i][j] = 1e9; // 讀取起始點和終點的座標 ax = c[0] - 'a', ay = c[1] - '1', bx = c[2] - 'a', by = c[3] - '1'; // 設置起始點的最短距離為0 dis[ax][ay] = 0; // 將起始點加入隊列 q.push({ax, ay}); // 使用廣度優先搜索計算最短距離 while (!q.empty()) { auto p = q.front(); q.pop(); // 遍歷馬的八個可能移動方向 for (int i = 0; i < 8; i++) { if (p.first + dx[i] >= 0 && p.first + dx[i] < 8 && p.second + dy[i] >= 0 && p.second + dy[i] < 8 && dis[p.first + dx[i]][p.second + dy[i]] > dis[p.first][p.second] + 1) { // 更新下一個格子的最短距離,並將該格子加入隊列 q.push({p.first + dx[i], p.second + dy[i]}); dis[p.first + dx[i]][p.second + dy[i]] = dis[p.first][p.second] + 1; } } } // 輸出最終結果 cout << "To get from " << c[0] << c[1] << " to " << c[2] << c[3] << " takes " << dis[bx][by] << " knight moves.\n"; } } ``` * Uav 532 ```cpp= #include<bits/stdc++.h> using namespace std; // 使用 tuple 存儲三維座標 queue<tuple<int, int, int>> q; // 存儲每個格子到終點的最短距離 int dis[30][30][30]; // 定義六個可能的移動方向 int dx[6] = {1, -1, 0, 0, 0, 0}; int dy[6] = {0, 0, 1, -1, 0, 0}; int dz[6] = {0, 0, 0, 0, 1, -1}; // 存儲迷宮內容 char m[30][30][30]; // 存儲終點座標 tuple<int, int, int> e; int main() { cin.tie(0)->sync_with_stdio(0); int a, b, c; // 循環讀取輸入,直到 a 為 0 為止 while (cin >> a >> b >> c && a) { // 初始化最短距離數組 for (int i = 0; i < a; i++) for (int j = 0; j < b; j++) for (int k = 0; k < c; k++) { cin >> m[i][j][k]; dis[i][j][k] = 1e9; // 將起點加入隊列 if (m[i][j][k] == 'S') { dis[i][j][k] = 0; q.push({i, j, k}); } else if (m[i][j][k] == 'E') { // 記錄終點座標 e = {i, j, k}; } } // 使用廣度優先搜索計算最短距離 while (!q.empty()) { auto [x, y, z] = q.front(); q.pop(); for (int i = 0; i < 6; i++) { if (x + dx[i] >= 0 && x + dx[i] < a && y + dy[i] >= 0 && y + dy[i] < b && z + dz[i] >= 0 && z + dz[i] < c && m[x + dx[i]][y + dy[i]][z + dz[i]] != '#' && dis[x + dx[i]][y + dy[i]][z + dz[i]] > dis[x][y][z] + 1) { // 更新下一個格子的最短距離,並將該格子加入隊列 dis[x + dx[i]][y + dy[i]][z + dz[i]] = dis[x][y][z] + 1; q.push({x + dx[i], y + dy[i], z + dz[i]}); } } } // 判斷是否成功逃脫,並輸出結果 if ([](){auto [x, y, z] = e; return dis[x][y][z] == 1e9;}()) { cout << "Trapped!\n"; } else { cout << "Escaped in " << [](){auto [x, y, z] = e; return dis[x][y][z];}() << " minute(s).\n"; } } } ``` ## 深度優先搜索 $DFS$ 先選定一個頂點開始走訪,接著從此頂點相鄰未被走過的頂點中,擇一走訪標示為記錄點,以此類推,不斷從新記錄點的相鄰未被走過頂點中尋找。 若新紀錄點的相鄰頂點都被走過,則退回前一個紀錄點,繼續從未被走過頂點中尋找。 深度優先可以利用堆疊 $Stack$ 的方式來處理。 ![image](https://hackmd.io/_uploads/HkGEyybq6.png) ## 回朔 $Backtracking$ * 窮舉每一種可能 ```cpp= int a[10]; bool used[10]; //回朔 void bt(int x){ if(x==4) { for(int i=0;i<4;i++)cout<<a[i]<<" \n"[i==3]; return; } //123組成4位數 for(int i=1;i<=3;i++) { a[x]=i; bt(x+1); } //12345組成四位數(無重複) for(int i=1;i<=5;i++) { if(used[i])continue; a[x]=i,used[i]=1; bt(x+1); used[i]=0; } } int main(){ bt(0); } ``` * Uva 441 ```cpp= #include<bits/stdc++.h> using namespace std; int n, a[15], ans[6]; bool use[15]; // 回溯函數,列舉所有可能的6個元素的組合 void bt(int x, int y) { // 當已經找到6個元素時,輸出組合並返回 if (x == 6) { for (int i = 0; i < 6; i++) cout << ans[i] << " \n"[i == 5]; return; } // 從當前位置 y 開始遍歷數組 for (int i = y; i < n; i++) { // 如果該位置的元素還未被使用 if (!use[i]) { // 標記該元素為已使用,並將其添加到當前組合中 use[i] = 1; ans[x] = a[i]; // 遞歸進入下一層,注意遞歸時更新 y 的值 bt(x + 1, i + 1); // 回溯:取消標記,以便嘗試其他組合 use[i] = 0; } } } int main() { // 循環讀取輸入 cin >> n; while (1) { // 讀取數組元素 for (int i = 0; i < n; i++) cin >> a[i]; // 呼叫回溯函數,從位置0開始列舉組合 bt(0, 0); // 讀取下一組輸入的數組大小 cin >> n; // 如果 n 不為零,輸出換行,否則跳出循環 if (n) cout << '\n'; else break; } } ``` # APCS ## $10/22$場次 **這次是我第一次參加$APCS$,這時候程度還是相當不夠 實作的時候太過緊張,又把題目想太難 所以只拿了第一題的子分數和其他題的子分數共$45$分,差點就能$2$級分** ### 成績: 觀念題2級分 實作題1級分 ![image](https://hackmd.io/_uploads/S1LPctTn1g.png) ### 第$1$題 機械鼠 [**題目連結**](https://zerojudge.tw/ShowProblem?problemid=m370) * **題目說明** 有$n$個位置上有食物,另外有一隻老鼠一開始位於位置$x$。 老鼠在開始覓食前要選擇今天要往左邊或往右移動去尋找食物,經過食物時可以停下來吃食物,吃完後可以選擇繼續往相同方向移動,或者是結束今天的覓食。 請問老鼠最多能吃到多少個食物,以及最後停下來吃食物的位置。 * **解題思路** 設一個陣列$f$,來存食物的位置 再設兩個變數$l、r$存左邊比較多還是右邊 利用迴圈來跑每一個食物,看有沒有比$x$大 最後比較$l、r$ $l$大輸出 `f[0]` $r$大輸出`f[n-1]` * **程式碼** ```cpp= #include <bits/stdc++.h> using namespace std; signed main(void) { int x,n; while(cin >> x >> n) { int f[n]; for(int i=0;i<n;i++) cin >> f[i]; sort(f,f+n); int l=0,r=0; for(int i=0;i<n;i++) { if(f[i]<x) l++; else r++; } if(l > r) cout << l << " " << f[0] << "\n"; else cout << r << " " << f[n-1] << "\n"; } } ``` ## $1/7$場次 **這次是第二次參加,也比較不會緊張了,目前分數還沒出來 觀念出很多函式題和遞迴題,預計$2、3$級分 實作題寫得蠻順利的,第一題卡了一下就寫出來了 第二題是建表加二維陣列題,其實應該能用函式寫得更簡單 但當時怕出錯就直接用判斷式硬爆,後來發現判斷經過字元的種類數寫錯了** ### 成績: 觀念題3級分 實作題3級分 ![image](https://hackmd.io/_uploads/BkhTqYThyg.png) ### 第$1$題 遊戲選角 [**題目連結**](https://zerojudge.tw/ShowProblem?problemid=m931) * **題目說明** 有$n$個角色,每個角色有攻擊力和防禦力。 角色的能力值是攻擊力和防禦力的平方和 輸出能力值第二大的攻擊力和防禦力數值。 保證每個角色的能力值相異。 * **解題思路** 先用$p$存每個角色的能力值 排序完後 再一一對比各個角色的平方和是否為`p[n-2]`(第二大) 符合的話便輸出 * **程式碼** ```cpp= #include <bits/stdc++.h> using namespace std; signed main(void) { int n; cin >> n; int a,d,A[50],D[50],p[50]; for(int i=0;i<n;i++) { cin >> a >> d; A[i] = a; D[i] = d; p[i] = a*a + d*d; } sort(p,p+n); for(int i=0;i<n;i++) { if(A[i]*A[i] + D[i]*D[i] == p[n-2]) cout << A[i] << " " << D[i] << "\n"; } } ``` ### 第$2$題 蜜蜂觀察 [**題目連結**](https://zerojudge.tw/ShowProblem?problemid=m932) * **題目說明** 蜜蜂 Bob 在一個大小是 $m$x$n$ 的蜂巢 (見範例說明圖示),並且每個蜂巢格上會有一個代表字母(大小或小寫英文字母)。 ![image](https://hackmd.io/_uploads/HJIaUfddT.png) Bob 一開始在蜂巢左下角,行走方向定義如圖:0 是往右上; 1 是往右邊; 2 是往右下; 3 是往左下; 4 是往左邊; 5 是往左上。 輸入每步移動的方向,請輸出經過的路徑每格的代表字母,以及經過字元的種類數(大小寫相異),若經過時碰到牆壁該行動會停在原地。 * **解題思路** 先變成正常的陣列 ![image](https://hackmd.io/_uploads/HyqaoX_ua.png) 再來建一個表來表示$0$~$5$的$x、y$變化(用陣列來看非數學座標化) | | ▵$x$ | ▵$y$ | | --- | ---- | ---- | | $0$ | 0 | -1 | | $1$ | 1 | 0 | | $2$ | 1 | 1 | | $3$ | 0 | 1 | | $4$ | -1 | 0 | | $5$ | -1 | -1 | 然後因為有牆壁,只要撞到就會停下來 所以我們寫一個`in_range()`函式來判斷做變化後是否會超出邊界(撞牆) ```cpp= bool in_range(int pre_x, int pre_y) { return ((pre_x < n && pre_x >= 0) && (pre_y < m && pre_y >= 0)); } ``` 然後因為最後需要判斷經過字元的種類數 所以我們用set來包不一樣的字元 `set<char>s` 每一次移動的時候動態增加字元 `s.insert(b[pre_y][pre_x]);` 最後種類數直接 `s.size()` * **程式碼** ```cpp= #include <bits/stdc++.h> using namespace std; char b[50][50]; int step_x[6] = {0,1,1,0,-1,-1}; int step_y[6] = {-1,0,1,1,0,-1}; string ans; set<char> s; int pre_x,pre_y,m,n; bool in_range(int pre_x, int pre_y) { return ((pre_x < n && pre_x >= 0) && (pre_y < m && pre_y >= 0)); } signed main(void) { ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); int k; cin >> m >> n >> k; for(int i=0;i<m;i++) { for(int j=0;j<n;j++) { cin >> b[i][j]; } } pre_x = 0,pre_y = m-1; int dir; for(int i=0;i<k;i++) { cin >> dir; if(!(in_range(pre_x + step_x[dir],pre_y + step_y[dir]))) { ans += b[pre_y][pre_x]; s.insert(b[pre_y][pre_x]); } else { pre_y += step_y[dir]; pre_x += step_x[dir]; ans += b[pre_y][pre_x]; s.insert(b[pre_y][pre_x]); } } cout << ans << "\n" << s.size()<< "\n"; } ```