夏日強化一日目

如何修煉,如何變強

著作權聲明

本講義屬於接棒程式設計學院所擁有,如需取得重製權以及公開傳輸權需要透過接棒程式設計學院取得著作人同意;
如果需要修改本講義著作,則需要取得改作權;
另外,如果有需要以光碟或紙本等實體的方式傳播,則需要取得接棒程式設計學院散佈權。

——接棒程式設計學院

作者:sa072686(sa072686@gmail.com)

前提

學習沒有捷徑

練就對了,如不動手,神仙難救

管它什麼名師了不起就讓你少走些冤枉路

如何單練

師父總不可能 24 小時 365 天當你的背後靈

你可能將面對以下幾個問題

  • 我該寫什麼樣的程式練習?
  • 我找到的題目有多難?如何下手?
  • 我寫對了嗎?效率夠嗎?

你需要 Online Judge

Online Judge(OJ)

  • 提供為數眾多的題目
  • 上傳程式碼後自動評測
    • 幫你判斷有沒有 bug 跑得夠不夠快
    • 也有 rank 提供執行效能的參考
  • 具備統計資料或參考難易度
  • 可能有題解供參考學習
    • 通常也會有論壇可以提問和交流
  • 像有著幾千關的益智遊戲!

評測原理

  • 理論上程式無法判別其它程式能否正常執行結束
    • 更遑論執行結果是否正確
  • 代案:餵入大量「測試資料」,觀察結果來判斷
    • 無法 100% 保證正確,但 95% 以上可靠

評測流程

  • 嘗試編譯,如果不行 → Compile Error(CE)
  • 嘗試執行並餵入測試資料
    • 如果使用記憶體過多 → Memory Limit Exceeded(MLE)
    • 如果異常終止(程式當掉) → Runtime Error(RE)
    • 如果輸出量異常的大 → Output Limit Exceeded(OLE)
    • 如果時間內無法執行完 → Time Limit Exceeded(TLE)
  • 如果正常執行結束
    • 結果不對 → Wrong Answer(WA)
    • 格式不對 → Presentation Error(PE)
      • 大部份 OJ 沒有這個,直接判定結果不對
    • 結果正確 → Accepted(AC)
      • ​​​通過測試不一定等於正確,通常稱 Correct

尋找題目

  • 除非有作客觀統計,否則主觀難度作不得準
    • 沒有客觀統計時,只能從 AC 人數判斷
  • 可參考別人列好的題單
  • 以能夠解出 6 至 8 成的難易度最為合適
    • 難度太低無聊難以成長,太高容易挫敗無法維持
    • 每人標準不同,自行衡量挑戰性和成就感
    • 太難時放低標準,太簡單時提高標準
    • 難題可以記著日後再戰,戰勝時對成長會有實感

英文問題

  • 如果你打算走本科系,大學就是原文書+英文投影片
    • 有些還是英文授課
  • 新出的技術文件或論文,絕大半是英文的
    • 如果有翻譯,八成是老舊而不堪用的
    • 一定要等人過一手翻譯完的話,會晚別人幾個時代
  • 遇到問題,找到的論壇發問和回答八成也是英文
  • 找整理過的教學文章時中文是最糟糕的選擇
    • 資料量少、大半講得不明不白等問題
    • 各種不講原理不說明不解釋直接上 code
    • 其他語言少有類似問題,第二外語可彌補(如日文)

英文是逃不掉的

  • 搭配 Google Dictionary 只查單字
    • 趁現在適應,以後基本不虧
  • 通常用詞不難,結構固定
    • 較難懂的故事背景倒是可以不看懂
  • 閱讀理解能力比外語能力影響更大
    • 中文題敘也可以寫到超級無敵難懂

留下 code

  • 最好備份一份在雲端,如 Github 等
  • 作為你的足跡和努力過的證明
    • 偶而可以回顧感受一下成長
    • 也許可作為備審或履歷

不要 AC 就丟

  • 檢討怎樣寫能迴避出過的 bug
    • 多想想,慢慢會找到較不易出 bug 的寫法
  • 思考如何整理得更精簡更漂亮
    • 有意識的養成整理的習慣,比較會有進步
  • 多嘗試不同的做法
    • 真正上戰場(考試、競賽)最好用熟悉的方法
    • 平時練習就應該多熟悉不同的做法或想法

OJ 簡介

推薦度以新手入門難易來評估

連結在 OJ 名字上

  • 國外著名 OJ,題目質量優秀,系統安定快速
  • 英文
  • 有參考難度,可依難度排序
    • 依統計資料定期重新計算,較客觀可靠
  • 入門題數量多,找難度適中的題目方便
  • 推薦度:★★★★★

UVa

  • 歷史悠久的古老 OJ,題目豐富
  • 英文(一部份老題目有中譯)
  • 系統老舊年久失修,老題目測資脆弱
  • 題目難度相關參考資料不多,適合有一定實力後
  • 推薦度:★★★☆
  • 日本大多數題目集散地,不時有線上賽可參與
  • 日文(絕大部份都有英文)
  • 題目有依積分來分級,參考度高
    • 罕見地也有在辦較入門等級的線上賽
  • 系統穩定快速,但介面不友善,需仰賴外部網站
  • 推薦度★★★★☆

一些不適合初學者的 OJ

  • TOJ - 多考古題或精選題,難易落差大
  • TIOJ - 多考古題或難題,平均難度極高
  • ZJ - 中文,但題目品質落差十分巨大,很多爛題
    • 有基本辨識和判別力之前不推
  • CodeForces - 和 AtCoder 性質相似,但幾乎無入門題
  • Aizu OJ - 多考古題,有搭配書籍的各種純練習題
    • 純練習題:沒有變化,不考你活用,純練特定技巧
  • …等等族繁不及備載

OJ 前置知識

  • 自動評測和人工評測,細節上會有所差異
  • 近代題目已較少要求格式,專注於解題本身
    • 但你還是可能遇見過時的古舊題目

絕對要上傳得到 Accepted

  • 不要自己測過就算了,嚴謹程度差很多
  • 一般而言自己的盲點比較難抓,請務必接受評測

絕對不要把對錯甩鍋給 OJ

  • 程式的正確性應該自行驗證,這是基本禮貌與責任
  • OJ 只是幫你做二次確認,不是讓你全面依賴的
  • 你應該確認到有信心是對的再上傳

我測試都對,傳上去不對

  • 99.99999.....9% 以上是你寫錯了,只是你沒能測出來

所有測資都對,傳上去不對

  • 題目裡的只是「範☆例」不是「所有」
  • 最低限度的「上傳前至少這個總該要對吧」而已
    • 連範例都沒過,就別傳上去浪費彼此時間了

IO 優化

  • cin / cout 因為一些歷史因素,預設設定下效率不佳
    • 可能光輸出、輸入就已經 TLE
  • 在 main() 最前面加入以下兩行能大幅改善此問題
int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);
    // your code here
}
  • 注意:此時不得和 C 的輸出、輸入混用
    • 例如 scanf()、printf()、getchar() 等
  • 手動輸入時,輸出不會即時反映,測試時建議註解
  • 原理可搜尋關鍵字「C++ IO優化」或閱讀這些函數的 reference

多重輸入至 EOF(End Of File)

  • 利用 cin / getline 僅在 EOF 時回傳值為 0 的特性
while (cin >> a >> b)
{
	// ...
}
while (getline(cin, s))
{
	// ...
}

輸入很多東西時,先確保你讀得到前幾個,再讀後續即可

手動輸入 EOF 測試會不會停

  • 在單獨的一行輸入 CTRL+Z 再按 ENTER
    • 畫面上會顯示 ^Z
  • 手動宣稱「未來不會再有任何輸入」的意思
  • 注意:如果你不是 windows 作業系統,可能不適用
    • Linux 系統會是 CTRL+D
  • 注意:看不了的話,左上切 MirrorUVa 分頁
  • 務必上傳並獲得 Accepted 才算完成

動手做做看

換行 End Of Line

  • 如字面上,換行是「一行的結束」而不是「一行的開始」
  • 你應該主動在任何一行都明確標記此行已結束
    • 包括最後一行,除非題目有特別註明

使用 "\n" 來換行

  • 不要使用 endl,有可能導致 TLE
    • 它會自帶 flush 效能會變糟
    • 原理可搜尋關鍵字「Buffered I/O」「IO flush」

不要存在任何多餘的 IO

  • 評測你 code 的是電腦,請丟掉所有人類的常識
  • 嚴格遵守任何題目要求的格式,一字不差
    • 多一個少一個空白,數字放不放同一行都嚴格執行
    • 注意大小寫或有沒有加 s
    • 一律建議複製貼上,逐字比對
  • 不要輸出任何好心提示如 Please input a integer 之類
  • 數字以空白分隔就不要在最後輸出多餘空白
    • 除非題目有註明,否則一律是 WA
    • 「3_5」和「3_5_」是不一樣的,連字數都不一樣

數字間空白

  • 你可以獨立輸出第一個東西,第二個以後一律補空白
    • 你永遠知道誰是第一個,但不見得知道誰是最後一個
  • 用一個 bool 變數記錄目前是否是第一個也行

數字間空白的 2 種實作例

cout << ary[0];
for (i=1; i<n; i++)
{
	cout << ' ' << ary[i];
}
cout << '\n';
bool first = true;
for (i=0; i<n; i++)
{
    if (!first)
    {
        cout << ' ';
    }
    first = false;
    cout << ary[i];
}
cout << '\n';

測資間空白行

  • 同數字間空白,測資表示不能每組都印
    • 一樣你一定知道誰是第一個,但不見得知道誰最後
  • 和輸入混雜時不容易觀察,可在 main 最前面加入以下 code 來輸出到檔案,避免和輸入混雜
freopen("d:/output.txt", "w", stdout);
  • 路徑可自行更換
  • 如果路徑使用反斜線,要用 2 個反斜線(跳脫字元)
  • 上傳前一定要拿掉

動手做做看

千萬不要放以下兩種東西

  • 因為一些歷史因素,過時的教材可能會這麼教
  • 千萬不要用,現在已無此必要
while(1);
system("pause");

int 的 overflow

  • int 只到 2^31 - 1,即 21474836471e9 上下
    • 過程會超過時,請改用 long long 處理
  • long long 只到 2^63 - 1,約 1e18
    • 如果超過,要不是大數,就是不該用整數型別處理

動手做做看

千萬不要用 float

  • 請愛用 double 避免被誤差害死

IO 的格式化

並非本次重點,遇到以下時參考這份投影片

  • 輸出長度 x 靠右對齊,不足補空白
  • 輸出長度 x 靠右對齊,不足補 0
  • 四捨五入至小數點下第 k 位
  • 輸入/輸出 8、16 進位整數
    • 其它進位需要自行處理

問題轉換與流程規劃

  • 理解程式能做的事,把問題轉換過去
  • 逐步規劃階段性目標,分段測試確保結果無誤
  • 不要總想一口氣完成所有的事

程式能做的事不外乎以下

  • 更改一個變數的值
  • 按照條件是否成立,決定執行哪段程式碼
  • 條件成立時,反覆執行同一段程式碼

條件每組比較兩個東西,用 AND 或 OR 串聯每一組

  • 結果 0 代表不成立
  • 結果不是 0 代表成立

把題目要求複述一遍

  • 通常就可以整理完它想問什麼、求什麼
  • 拆成約 2 到 5 個步驟,每個步驟各自轉成條件或者計算式
    • 每步獨立後可個別進行測試,結果錯誤只會是
      • 資料來源就已經錯了
      • 這個單純的小問題錯了
    • 不拆成太多步,容易亂掉;每步太大時,再繼續拆
  • 個別解決後,串起來就變成一個流程

只拆 2~5 (O)

  • 問題 A
  • 問題 B
  • 問題 C
  • 問題 D
  • 問題 E
  • 問題 F
  • 問題 G
  • 問題 H
  • 問題 I
  • 問題 J
  • 問題 K

一次全拆 (X)

  • 問題 A
  • 問題 B
    • 問題 E
    • 問題 F
  • 問題 C
    • 問題 G
    • 問題 H
      • 問題 J
      • 問題 K
    • 問題 I
  • 問題 D

記不完

容易亂

光拆就不好拆

原題只看 ABCD

解 B 只看 EF

解 C 只看 GHI

解 H 只看 JK

永遠不看太多

怕忘就先寫好註解再寫 code

每步變成更簡單的小問題

  • 每個步驟只解決一個問題
  • 每個問題決定資料來源以什麼格式放在哪些變數,決定計算結果以什麼格式放進哪些變數
    • 這樣獨立思考、解決時,才串得起來
    • 計算結果通常就是下個問題的資料來源
  • 最初的資料來源是 cin
  • 最後的計算結果丟給 cout

個別擊破難易下降

  • 解一個 100 行的大問題困難很多
  • 解十個 10 行的小問題簡單很多
  • 每個小問題能個別測試,出 bug 好找
  • 裡面有你以前做過的小問題時,更不容易錯

分段測試、個別除錯

  • 每步完成就先 cout 結果來測試,不要等到全部寫完
    • 每步應有明確目標和結果放在哪裡,應可手算驗證
  • 每次錯只會錯一個小步驟,debug 容易
    • 要不是自己錯,就是前面的人錯
  • 一次寫完才測的話,很難從最終結果特定出問題點在哪
    • 改了一個地方還是錯,你也沒法判斷是
      • 對的被改成錯的
      • 錯的被改成對的,但還有其他錯
      • 別的地方因此變成錯的

小結

  1. 複述題目,轉成算式和條件式
  2. 分解步驟,寫成註解
    • 資料來源在哪、格式是什麼
    • 解決什麼問題
    • 結果放到哪、格式是什麼
  3. 個別完成,個別測試,反覆修改至結果正確
  4. 上傳看結果,反覆修改至 AC 為止
    • 除非是 TLE,可能需要換成更有效率的做法

實際例子:Kattis leftbeehind

  • https://open.kattis.com/problems/leftbeehind
  • 輸入甜的數量 x 和酸的數量 y 判斷屬於以下哪種:
    • 酸比甜多:Left beehind.
      • 轉成 y > x
    • 甜比酸多:To the convention.
      • 轉成 x > y
    • 一樣多:Undecided.
      • 轉成 x == y
    • 剛好 13 罐:Never speak again.
      • 轉成 x+y == 13

實際例子:Kattis leftbeehind

  • https://open.kattis.com/problems/leftbeehind
  • 整理可看出是針對不同情形做出不同回答
  • 「對不同的情形,做不同的事」應該馬上想到 if
  • 尋找優先條件或隱藏條件
    • 根據 31 律,只有大於、小於、等於三種情形
      • 可是題目總共有四種
    • x+y == 13 長得最特別
      • 此條件成立時必也滿足 x>y 或 x<y 或 x==y
      • 八成是優先於其它的條件
    • 因上述關係,其它三個應存在隱藏條件
      • 必須同時滿足 x+y != 13

想到 if 之後

  • 一件事可能做,可能不做:單一 if 沒有 else
  • 二選一:if ... else
  • 多選一:if ... else if ... (更多 else if) ... else
    • 須注意放前面的條件優先
    • 放後面的條件有隱藏前提:前面必須都不成立

使用隱藏條件(較容易錯)

if (y > x && x+y != 13)
{
	cout << "Left beehind.\n";
}
else if (x > y && x+y != 13)
{
	cout << "To the convention.\n";
}
else if (x == y && x+y != 13)
{
	cout << "Undecided.\n";
}
else
{
	cout << "Never speak again.\n";
}

找到優先條件

if (x + y == 13)
{
	cout << "Never speak again.\n";
}
else if (x < y)
{
	cout << "Left beehind.\n";
}
else if (x > y)
{
	cout << "To the convention.\n";
}
else
{
	cout << "Undecided.\n";
}

沒有信心時分開解決,無須一口氣全寫完

實際例子:Kattis tarifa

實際例子:Kattis tarifa

  • https://open.kattis.com/problems/tarifa
  • 假設一個月有 20 流量,用掉了 14,剩下多少?
    • 先從簡單問題開始處理
  • 假設三個月,每月 20 流量
    • 第一個月用掉 12
    • 第二個月用掉 14
    • 第三個月用掉 9
    • 第四個月的可用流量是多少?
  • 先假設可手算等級的資料來找規則

實際例子:Kattis tarifa

想到迴圈之後

  • 把迴圈拆成內外分開思考
    • 內:每個月都要做的事
    • 外:窮舉每一個月
  • 列舉並展開每個月要做的事
  • 把每月相同、相異部份找出來,將相異部份替換成變數
  • 讓變數隨月份變化,就可整理成迴圈
  • 最後一個月邏輯不同,可不必硬放在同個迴圈中

實際例子:Kattis tarifa

// 一開始沒流量
remain = 0;
// 第一個月累積
remain += gain - used[0];
// 第二個月累積
remain += gain - used[1];
// 第三個月累積
remain += gain - used[2];
// 第四個月的份,未使用
remain += gain;

cout << remain << "\n";

開始找反覆的部份,留下相同、替換相異

實際例子:Kattis tarifa

// 沒有反覆
remain = 0;
// 有反覆,留下相同、替換相異,包成迴圈
for (i=0; i<3; i++)
{
	remain += gain - used[i];
}
// 沒有反覆
remain += gain;
// 沒有反覆
cout << remain << "\n";

讓 i 依序跑過原本的 0 ~ 2 即維持邏輯不變

之後將 3 替換成輸入給的月份數即可

逐步完成即可

  • 可先只處理單一個月
  • 擴充成只處理固定 3 個月
  • 擴充成可處理任意月份數
  • 不直接一口氣完成最終版也無所謂
    • 遊戲先推出來賣再慢慢改版也沒人罵,怕什麼

實際例子:Kattis thanos

實際例子:Kattis thanos

  • https://open.kattis.com/problems/thanos
  • 不一定要找到一行解決的公式,慢慢算也行
  • 和人類不同,電腦非常擅長單調的反覆計算
    • 窮舉萬歲,暴力無罪
  • 慢慢算一年後、兩年後、…到食物不夠吃為止
    • 發現是反覆計算,用迴圈處理
    • 小心乘完之後可能 int 會放不下

實際例子:Kattis thanos

// 反覆至食物不足為止
for (i=0; p<=f; i++)
{
	// 注意人數乘以 r 和年份 i 增加,這兩件事應該成對出現
	p *= r;
}

實際例子:AtCoder abc042_a

實際例子:AtCoder abc042_a

  • https://atcoder.jp/contests/abc042/tasks/abc042_a
  • 「是否」和條件有關,想到 if
  • 想法一:重排變成 5、7、5 反過來它也是 5、7、5 重排
    • 窮舉 5、7、5 的所有重排,必須是其中之一
      • 5、5、7
      • 5、7、5
      • 7、5、5
  • 想法二:重排表示東西沒變 → 5 和 7 的數量同 5、7、5
    • 一定是 5 有兩個,且 7 有一個

實際例子:AtCoder abc042_a

// 以變數儲存結果,可方便將判斷和輸出拆成不同步驟
ok = false;
if (a == 5 && b == 5 && c == 7)
{
	ok = true;
}
else if (a == 5 && b == 7 && c == 5)
{
	ok = true;
}
else if (a == 7 && b == 5 && c == 5)
{
	ok = true;
}
// 依上一步的結果決定輸出什麼,可將輸出邏輯集中
// 若非 if else 形式,可能 YES 和 NO 一起輸出,或一起不輸出
if (ok)
{
	cout << "YES\n";
}
else
{
	cout << "NO\n";
}

實際例子:AtCoder abc042_a

// 數量從 0 開始數
five = 0;
seven = 0;
if (a == 5)
{
	five++;
}
else if (a == 7)
{
	seven++;
}
if (b == 5)
{
	five++;
}
else if (b == 7)
{
	seven++;
}
if (c == 5)
{
	five++;
}
else if (c == 7)
{
	seven++;
}
// 看結果數量是否正確
if (five == 2 && seven == 1)
{
	cout << "YES\n";
}
else
{
	cout << "NO\n";
}

實際例子:AtCoder abc042_a

// 數量從 0 開始數
five = 0;
seven = 0;
for (i=0; i<3; i++)
{
	if (num[i] == 5)
	{
		five++;
	}
	else if (num[i] == 7)
	{
		seven++;
	}
}
// 看結果數量是否正確
if (five == 2 && seven == 1)
{
	cout << "YES\n";
}
else
{
	cout << "NO\n";
}

定義好步驟格式時,只改做法不影響其他步驟

動手做做看

常見「型」的模板化

  • 例如「對 0 到 n-1 每個數字做 ... 處理」
  • 蠻大部份題目都會用上,抽出來作為獨立模板
  • 以後遇到時直接套,節省思考時間、不易出錯
    • 像國小背九九乘法表,節省乘法計算時間且不易錯
  • 一旦你想到如何改進它,所有用上它的一起改進

討論題

討論題

討論題

動手做做看

暑期強化一日目

By sa072686

暑期強化一日目

  • 190