# APCS準備用講義 主講者:國立北港高級中學 二年忠班 1號 吳成昊 預期課程結束後的成果:APCS達到實作3級分以上 建議練習時間:每週5-8小時 :::info 要注意的是聽完每節課之後都應該把提到的題目自己在做一遍,並自己尋找類似的題目來做,多加練習才能熟悉程式並在APCS中得到好成績 ::: ## 0. 競程基礎知識 ### 競程是什麼? 競程,或叫做演算法比賽(Algorithm Competition)是一個資訊領域中十分大型與多樣的比賽,除了考驗參賽者的程式能力,更加注重思考以及邏輯等等的能力。競程內容大致為:給參賽者多個或多組題目,並要求參賽者以程式,在特定條件限制下使用更加合適的演算法解出題目。 常見的競程比賽(以高中來說)有: ==IOI==、==APCS==、==少年圖靈計畫==、==全國數理暨資訊學科能力競賽==等等... 其中雖然APCS稱為檢定,但因其考試形式與競程相同,故列入上述列表。 ### 相關詞語 **執行結果:** :::success AC: > Accepted的簡寫,你最想要看到的結果 ::: :::warning WA: > Wrong Answer的縮寫,你一定有哪裡寫錯了(只有在少之又少的狀況下可能為測資怪怪的) ::: :::danger TLE: > Time Limit Exceeded 超時執行,每個題目都有他自己的時間限制,超過這個時間就會把執行中斷,並告訴你這個訊息 ::: :::danger MLE: > Memory Limit Exceeded 超出記憶體限制量,很少發生的狀況,除非你幹了一些不該幹的事,請改用空間複雜度較低的方法 ::: :::danger CE: > Compile Error的意思,你有加好分號嗎?還是有哪裡的括號沒有閉合?總之,編譯錯誤! ::: :::danger RE: > Runtime Error的狀態,也就是程式執行時有地方出問題導致無法正常結束,例如無限迴圈啦、你做了除以0的除法啦之類的,給我回去好好檢查自己的程式碼! ::: ### 時空複雜度: **時間複雜度:** 用於表示一個==演算法的效能好壞==,以Big-O Notion表示,要注意的是它並不是輸入資料多少的所需時間函數,而是==隨著資料量增加,計算時間的增加曲線函數關係==。什麼意思呢?舉例來說,這裡有一個變數$n$代表資料的數量,而有兩個函數分別為$y=n$與$y=n^2$,在資料量低的時候,兩者的差異並不明顯,但$y=n^2$的==成長速率==要明顯比$y=n$還要快,這個成長速率的概念便是時間複雜度的簡化版。 **常見時間複雜度有以下幾種:** $O(1)、O(\log_{2}n)、O(n\log_{2}n)、O(n)、O(n^2)$ 再往上的$O(n!)$等等通常是一個非常不好的方法,在此不多贅述 ## 1. C++基本語法 ### 標頭檔: 各位在學習C++過程中很常看到以下程式碼吧 ```cpp= #include<iostream> ``` 這串程式碼其實叫做標頭檔,就是告訴電腦我這個程式需要哪些額外套件,就像吃炸雞要配可樂一樣叫電腦把要的東西拉進來,競程上通常只用一個萬用標頭檔: ```cpp= #include<bits/stdc++.h> ``` 這串標頭檔包含了競程上所有常用的功能,十分好用啊! --- ### 輸入與輸出: **標準輸入:cin >> (要用來儲存資料的變數)** cin讀取規則會==斷在任何不可見的字符==,如以下例子 輸入 ```cpp= 1 2 3 4 5 ``` 程式碼 ```cpp= cin >> a >> b >> c >> d >> e; cout << a << b << c << d << e; ``` 輸出 ```cpp= 12345 ``` 這樣子會依序讀入資料到a、b、c、d、e,可以一直延伸。 **標準輸出:cout << (要輸出的東西)** ### 資料流(>>或<<)可以==一直延伸==,但應保持==一行做一件事的原則== **換行字符: \n** 使用endl亦有相同作用,但endl同時會將緩存flush掉 又因為flush是==十分耗時==的動作可能會讓你差點TLE的程式真的TLE 所以競程實作上請多加利用"\n"這個換行字符,少用endl ##### 以輸出10000次HelloWorld為例 ```cpp= #include<iostream> using namespace std; int i = 0; int main() { while(i < 10000){ cout << "HelloWorld" << endl; i++; } } ``` 以上程式碼耗時平均60ms ```cpp= #include<iostream> using namespace std; int i = 0; int main() { while(i < 10000){ cout << "HelloWorld" << "\n"; i++; } } ``` 以上程式碼平均耗時50ms :::warning 不要以為只有10ms很少,後面時間複雜度(後面會講)更高的程式可能1ms就會是TLE與沒有TLE的差距,務必注意! ::: **簡單優化:** 一個程式中除了演算法耗時外,另一個耗時的就是==IO處理==! IO處理是什麼,簡單來說就是==所有的輸入輸出==都跟他有關!也就是說他是無法避免的時間損耗。 所以!我們要主動做一些IO優化,以下是兩個最基本的優化,把他們放在main函數裡的最前面即可! ```cpp= cin.tie(0); ios_base::sync_with_stdio(false); ``` 這兩個優化的功能可以見這裡->[Wiwiho的競程筆記](https://cp.wiwiho.me/io-optimize/) 在此僅說加了優化後不可以跟c的輸入輸出(scanf、printf)混用,其餘不多做贅述。 **整行輸入: getline(cin,(要用來儲存資料的變數))** 取得整行換行字符前的東西,要注意的是,若你在getline前先做了一次cin有可能會導致以下結果: ```cpp= #include<iostream> using namespace std; int i; string s; int main() { cin >> i; getline(cin,s); cout << i << "\n"; cout << s << "\n"; } ``` 輸入: ```cpp= 1 Hello World! ``` 輸出: ```cpp= 1 ``` ### 各位有沒有發現輸出的s不見了! :::warning 輸入時 1 後面其實有一個換行字符,而cin輸入時會停在那裡,因此導致getline一進來就直接吃到換行而停止輸入! ::: 解法也很簡單,只要在cin後面加上以下的程式碼就可以了 ```cpp= cin.ignore(1000000,"\n"); ``` 第一個參數可以填任意值,代表可忽略字元的上限(建議超大) 第二個則代表這個字串以前以及他本身會被忽略。 --- ### 資料型態 在C++中有各式各樣的資料型態,以下程式碼舉例: ```cpp= #include <iostream> using namespace std; int main(){ int a; //32位整數資料,範圍:-2,147,483,648 to 2,147,483,647 bool b;//布林值,只能儲存"是"或"否"兩種數值 char c;//字元,一個"半形"的字 float f;//浮點數,總之就是可以保存小數點 double d;//雙精浮點數,總之就是可以保留比較多位的小數點 long long l;//64位長整數,真的超大,範圍:-9,223,372,036,854,775,808 至 9,223,372,036,854,775,807 } ``` 以上幾種有沒有看的頭昏眼花,以下做~~人話版~~簡化版解釋: 1. int,整數,顧名思義是存==整數==進去的,如果你硬要把一個小數點塞進去的話...小數點後的數字就會不見。==(無條件捨去)== 2. bool,布林值,可以存==true==或==false==兩種數值,意思就是"對"或"不對",或者"是"或"不是"之類的概念。 3. char,字元,我們的字是由一堆字元所組成的,像是APPLE這個單字,是由'A' 'P' 'P' 'L' 'E'這幾個字元所組成的,如果用英文來比喻就是字母的意思。(標點符號也算字元!) 4. float,浮點數,看到一個點字不難想像他就是可以用來存==帶小數點的數字==用的,但因為有double這個資料類型,在競程實務上很少用他。(都跑去用double了) 5. double,雙精浮點數,看到雙精,不知道各位有沒有想到“精準度“這個詞,這個東西也是存==小數點==用的,但是可以存比float更加後面好幾位的小數點。**舉例來說,如果你只能以量筒最小刻度為單位去裝水,double就像把量筒從1ml一個刻度變成0.1ml一個刻度的概念。** 6. long long,==長整數==,這東西簡單來說就是超大,真的很大。 **要注意的事情:** - 如果你往任何資料型別塞入一個==超過他儲存範圍的數字==,那他就會==爆掉變成奇怪的東西==,舉例來說:我在int中存入1,000,000,000,000,他這時候已經超過範圍了,就會發生神奇的事情導致結果壞掉,在競程上要特別注意,==int存不下去就用long long== - long long的運算遠比int還要慢 - 在C++中要表示long long要==在字尾加一個L== - 在C++中要表示float要在==字尾加一個f== 快速練習:[TIOJ 1001. Hello World](https://tioj.ck.tp.edu.tw/problems/1001) --- ### 資料結構 我真的不習慣中文名稱,所以用英文XD **Stack:** 這東西可以想像成一堆書,你不能從底下抽書出來,一定要先拿出上面的書本,而且==最後放上去的書本會先被拿起來==,Stack就是這樣的概念 **Queue:** 想像成一個隊伍,==只能從隊尾進去,隊首出來==,Queue就是這種東西 **Array:** 想像成一個箱子,而且這個箱子是有==分格子==的,==一格只能放一個東西== --- ### 算術運算 可能有同學覺得為甚麼要特別講算術運算,不就是簡單的加減乘除嗎? **的確如此**,但是!有些神奇的運算規則是各位需要特別注意的! **算術運算子:** | 符號 | 意涵 | 注意事項 | | -------- | -------- | -------- | | `a + b` | 加(plus) |單純的加法| | `a - b` | 減(minus) |單純的減法| | `a * b` | 乘以(times) |單純的乘法| | `a / b` | 除以(divide) |除法,要注意如果整數除以整數便會無條件捨去後面的小數點,如果要有小數點請用float或double去除!| | `a % b` | 取模(取餘數) |這個運算子是我們比較少見到的東西,他的用途就是會計算a除b的餘數,在競程上十分常用到這個概念,務必熟悉!| 快速練習:[TIOJ 1002. A+B Problem](https://tioj.ck.tp.edu.tw/problems/1002) --- ### 邏輯運算 **邏輯運算子:** |符號|意涵|注意事項| |--------|--------|--------| |`!`|not|會把true變成false反之亦然| |`==`|equal|要注意是==不是=跟數學不一樣| |`!=`|not equal|若左邊不等於右邊則成立,等於便不成立| |`&&`|and|必須要兩個或多個條件都成立才會是true否則為false| | `\|\|` |or|只要其中有一個條件成立便會輸出true,全部不成立才會輸出false| 所以你可以透過一連串的組合來達到神奇的效果,以下有個題目可以快速練一下(實務上鮮少有以下情形,這題目只是用來檢測你熟不熟邏輯運算) ```cpp= #include<iostream> using namespace std; int main() { int a = 10, b = 10, c = 5, d = 2; bool result = true; result = !(0 != (a == b && (c*d) == b && b == c)); cout << result; } ``` 請問以上程式碼執行後的result應為何者? (A) result == 1 (B) result == 0 \(C\) Compile Error :::spoiler 詳解 答案為(A) 解析: 首先括號內先做,a == b為true,c*d == b為true,b == c為false,又因為這三個是以&&連接起來,只要有一個不成立就會輸出false,因此括號內的結果是false。 再來看0 != (...)就會是0 != false,0就是false的意思,false != false顯然不成立,所以輸出false。 最後,外面有一個!,所以將裡面的結果翻轉,輸出true結束。 ::: --- ### 位元運算 位元運算就是利用二進制的0跟1去做運算,有很多神奇的用途,屬於基本語法,實務上非常常見於競程的題目中,但是在理解上很容易會卡關,可以多加練習補強。 **位元運算子** | 名稱 | 符號(舉例) |說明 | -------- | -------- | -------- | | AND | `a&b` |所有位元都是1時便輸出1,其他狀況會輸出0| | NOT | `~a` |將1輸出成0,0輸出成1| | OR | `a\|b` |只要位元中有一個是1便輸出1,兩個都是0才會輸出0| | XOR | `a^b^c^d` |若有奇數個1則輸出0,若有偶數個1則輸出1| | 左移 | `a << n` |將所有位元向左移$n$位,並在右邊補一個0| | 右移 | `a >> n` |將所有位元向右移$n$位,並在最右邊補一個原本的最高位數字| ### 光聽這些是不是很模糊呢? 我們來舉實際的例子! 首先,我們要將一個數字轉換為二進位要怎麼轉呢? 用以下數字為例: $17 = 1\times2^4 + 0\times2^3 + 0\times2^2 + 0\times2^1 + 1\times2^0$ 因此可記作==1 0 0 0 1== $21 = 1\times2^4 + 0\times2^3 + 1\times2^2 + 0\times2^1 + 1\times2^0$因此可記作==1 0 1 0 1== $80 = 1\times2^6 + 0\times2^5 + 1\times2^4 + 0\times2^3 + 0\times2^2+0\times2^1 + 0\times2^0$因此可記作==1 0 1 0 0 0 0== 各位會發現,只要是==偶數==,二進位的==最後一格就會是0==,==奇數則為1==,是==很重要==的特性 以上是用手計算的過程,但實際上要怎麼用程式轉換呢? 以下是將501轉換為二進位的程式碼,各位可以自己看看原理是什麼! ```cpp= #include<iostream> using namespace std; int main() { int i = 501; string binary; while(i != 0) { binary += (i % 2 == 0)? "0":"1"; i = i/2; } cout << binary; } ``` :::spoiler 原理解說 首先,while迴圈會在==i = 0的時候終止==,因為迴圈中用的是==整數除法會無條件捨去==,因此到了最後一定會剩下0 我將二進位的結果存成一個字串,並且在第9行的時候我去算當下的數字對二取餘數是否等於0,如果是,就代表當前最高位$k$是有數字的,記作$1\times2^k$,沒有就代表當前最高位是0,記作$0\times2^k$ 最後將原本的i除以2,進到下一位的數字(即$2^{k-1}$的計算),如此循環往復便可以逐一將每個位的結果計算出來。 ::: **XOR異或運算:** 這個概念是十分難懂的東西,我們在此說明,我們知道每個數字都可以用二進位來表示,而異或運算是對於數字間每一位分別進行運算,什麼意思呢? 如果我有兩個數字,分別為 21 跟 17,我們已經知道這兩個數字可以分別用二進位表示成10001跟10101,用表格記錄一下得到: |位數|5|4|3|2|1| |-|-|-|-|-|-| |17|1|0|0|0|1| |20|1|0|1|0|1| 如果我們對這兩個數字進行XOR會發生什麼呢? > 1. 從第五位開始,有==偶數==個1,進行XOR之後得出1 > 2. 第四位==0對0 XOR還是0== > 3. 第三位有==奇數==個1輸出0 > 4. 第二位==0 XOR 0還是0== > 5. 第一位有==偶數==個1輸出1 最後得到10001=17 各位有沒有發現我把特定幾串字畫起來,這些其實就是XOR的運算規則! 在==同一位中==,全部數字排列下來有==奇數個1則輸出0==,有==偶數個1則輸出1==,==0跟0還是0== 為什麼說全部數字呢?因為XOR可以==一直延伸==,只要把所有數字都用表格攤開就可以運算了 **如果有位數不足一律==往最高位數補0==直到對齊!** 這裡有一題給各位快速練習 $\rightarrow$ [ZJ c461. apcs 邏輯運算子 (Logic Operators)](https://zerojudge.tw/ShowProblem?problemid=c461) :::spoiler 解析 :::