From 3e537c50844b4024612a91de4d1a5b5b5f0f4563 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BD=8A=E7=B4=B9=E7=91=8B?= Date: Mon, 22 Jun 2026 18:41:53 +0800 Subject: [PATCH 01/13] test: add failing tests for data cleaning --- .../1114405013/test_data_cleaning.py | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 weeks/week-18/solutions/1114405013/test_data_cleaning.py diff --git a/weeks/week-18/solutions/1114405013/test_data_cleaning.py b/weeks/week-18/solutions/1114405013/test_data_cleaning.py new file mode 100644 index 000000000..635a4766a --- /dev/null +++ b/weeks/week-18/solutions/1114405013/test_data_cleaning.py @@ -0,0 +1,53 @@ +import io +import unittest +from unittest.mock import patch + +import main + + +class TestDataCleaningProgram(unittest.TestCase): + def run_program(self, input_text): + fake_stdout = io.StringIO() + with patch("sys.stdin", io.StringIO(input_text)), patch("sys.stdout", fake_stdout): + main.main() + return fake_stdout.getvalue() + + def test_normal_case_removes_duplicates_filters_multiples_of_5_and_sorts(self): + input_text = """8 +10 3 5 10 20 7 5 15 +0 +""" + expected = """5 10 15 20 +""" + self.assertEqual(self.run_program(input_text), expected) + + def test_boundary_case_single_valid_number(self): + input_text = """1 +5 +0 +""" + expected = """5 +""" + self.assertEqual(self.run_program(input_text), expected) + + def test_special_case_outputs_none_when_no_number_matches(self): + input_text = """4 +1 2 3 4 +0 +""" + expected = """NONE +""" + self.assertEqual(self.run_program(input_text), expected) + + def test_edge_case_handles_negative_numbers_zero_and_duplicates(self): + input_text = """7 +-10 0 -10 25 12 0 -5 +0 +""" + expected = """-10 -5 0 25 +""" + self.assertEqual(self.run_program(input_text), expected) + + +if __name__ == "__main__": + unittest.main() From 8aafa4117b5f309e967bc2e04886576d1d076661 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BD=8A=E7=B4=B9=E7=91=8B?= Date: Mon, 22 Jun 2026 18:57:02 +0800 Subject: [PATCH 02/13] feat: implement data cleaning --- .../1114405013/p1_data_cleaning/main.py | 41 +++++++++++++++++++ .../test_data_cleaning.py | 0 2 files changed, 41 insertions(+) create mode 100644 weeks/week-18/solutions/1114405013/p1_data_cleaning/main.py rename weeks/week-18/solutions/1114405013/{ => p1_data_cleaning}/test_data_cleaning.py (100%) diff --git a/weeks/week-18/solutions/1114405013/p1_data_cleaning/main.py b/weeks/week-18/solutions/1114405013/p1_data_cleaning/main.py new file mode 100644 index 000000000..d52a82ea9 --- /dev/null +++ b/weeks/week-18/solutions/1114405013/p1_data_cleaning/main.py @@ -0,0 +1,41 @@ +import sys + + +D = 5 + + +def main(): + lines = sys.stdin.read().splitlines() + outputs = [] + index = 0 + + while index < len(lines): + n = int(lines[index]) + index += 1 + + if n == 0: + break + + numbers = list(map(int, lines[index].split())) + index += 1 + + seen = set() + cleaned = [] + for number in numbers: + if number not in seen: + seen.add(number) + if number % D == 0: + cleaned.append(number) + + if cleaned: + outputs.append(" ".join(map(str, sorted(cleaned)))) + else: + outputs.append("NONE") + + sys.stdout.write("\n".join(outputs)) + if outputs: + sys.stdout.write("\n") + + +if __name__ == "__main__": + main() diff --git a/weeks/week-18/solutions/1114405013/test_data_cleaning.py b/weeks/week-18/solutions/1114405013/p1_data_cleaning/test_data_cleaning.py similarity index 100% rename from weeks/week-18/solutions/1114405013/test_data_cleaning.py rename to weeks/week-18/solutions/1114405013/p1_data_cleaning/test_data_cleaning.py From d8eaa1ea53a2278858ce612b9c8fdf8a233dc440 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BD=8A=E7=B4=B9=E7=91=8B?= Date: Mon, 22 Jun 2026 19:09:12 +0800 Subject: [PATCH 03/13] docs: add AI log for data cleaning --- .../1114405013/p1_data_cleaning/AI_LOG.md | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 weeks/week-18/solutions/1114405013/p1_data_cleaning/AI_LOG.md diff --git a/weeks/week-18/solutions/1114405013/p1_data_cleaning/AI_LOG.md b/weeks/week-18/solutions/1114405013/p1_data_cleaning/AI_LOG.md new file mode 100644 index 000000000..c696c0f86 --- /dev/null +++ b/weeks/week-18/solutions/1114405013/p1_data_cleaning/AI_LOG.md @@ -0,0 +1,43 @@ +# AI 使用紀錄 + +## 我問了 AI 什麼 + +1. 請 AI 根據題目設計至少 3 個 test case,其中至少 1 個是 edge case。 +2. 我補充本題的固定參數 `D = 5`。 +3. 我確認本題要寫成「整支程式讀 stdin / 印 stdout」,不是只寫函式。 +4. 我請 AI 幫我設計 `unittest` 測試案例,但先不要寫正式實作。 +5. 我詢問測試檔案名稱、如何執行測試,以及 red test 後如何進入實作。 +6. 我完成 red test 和 `test:` commit 後,請 AI 放入正式程式。 + +## AI 給了我什麼 + +1. AI 幫我整理了 4 個測試情境: + - 一般案例 + - 邊界案例 + - 特殊案例 + - edge case +2. AI 提供了 `test_data_cleaning.py`,用 `unittest` 模擬 stdin / stdout。 +3. AI 說明如何執行測試: + + ```bash + python -m unittest -v test_data_cleaning.py +## 我改了什麼 +我設定本題的固定參數為 D = 5。 + +我決定程式採用 stdin / stdout 格式。 + +我建立作業資料夾: + +text +weeks/week-18/solutions/1114405013/p1_data_cleaning/ +我放入測試檔: + +text +test_data_cleaning.py +我先執行測試,確認 red test 失敗後,完成 test: commit。 + +我放入正式程式: + +text +main.py +我再次執行 unittest,確認測試全部通過。 From eabb4898a502d17f7db0d178562ab4b23e2d91e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BD=8A=E7=B4=B9=E7=91=8B?= Date: Mon, 22 Jun 2026 19:32:51 +0800 Subject: [PATCH 04/13] test: add failing tests for caesar cipher --- .../p2_caesar_cipher/test_caesar_cipher.py | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 weeks/week-18/solutions/1114405013/p2_caesar_cipher/test_caesar_cipher.py diff --git a/weeks/week-18/solutions/1114405013/p2_caesar_cipher/test_caesar_cipher.py b/weeks/week-18/solutions/1114405013/p2_caesar_cipher/test_caesar_cipher.py new file mode 100644 index 000000000..10e5395d5 --- /dev/null +++ b/weeks/week-18/solutions/1114405013/p2_caesar_cipher/test_caesar_cipher.py @@ -0,0 +1,38 @@ +import subprocess +import sys +import unittest + + +class TestCaesarCipherProgram(unittest.TestCase): + def run_program(self, input_text): + result = subprocess.run( + [sys.executable, "main.py"], + input=input_text, + text=True, + capture_output=True, + ) + self.assertEqual( + result.returncode, + 0, + msg=f"程式應正常結束,但 stderr 是:{result.stderr}", + ) + return result.stdout + + def test_sample_case_shifts_uppercase_and_lowercase_letters(self): + input_text = "Hello, NPU!\nabc XYZ\n" + expected = "Lipps, RTY!\nefg BCD\n" + self.assertEqual(self.run_program(input_text), expected) + + def test_edge_case_wraps_around_z_and_Z(self): + input_text = "wxyz WXYZ zZ aA\n" + expected = "abcd ABCD dD eE\n" + self.assertEqual(self.run_program(input_text), expected) + + def test_special_case_keeps_non_letters_and_blank_lines(self): + input_text = "123, !?\n\nTaiwan 2026!\n" + expected = "123, !?\n\nXemaer 2026!\n" + self.assertEqual(self.run_program(input_text), expected) + + +if __name__ == "__main__": + unittest.main() From 67c2740a2d18e13c76bda59350b3f8bbf80de916 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BD=8A=E7=B4=B9=E7=91=8B?= Date: Mon, 22 Jun 2026 19:35:11 +0800 Subject: [PATCH 05/13] feat: implement caesar cipher --- .../1114405013/p2_caesar_cipher/main.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 weeks/week-18/solutions/1114405013/p2_caesar_cipher/main.py diff --git a/weeks/week-18/solutions/1114405013/p2_caesar_cipher/main.py b/weeks/week-18/solutions/1114405013/p2_caesar_cipher/main.py new file mode 100644 index 000000000..6f0c956bf --- /dev/null +++ b/weeks/week-18/solutions/1114405013/p2_caesar_cipher/main.py @@ -0,0 +1,21 @@ +import sys + + +SHIFT = 4 + + +def shift_char(char): + if "a" <= char <= "z": + return chr((ord(char) - ord("a") + SHIFT) % 26 + ord("a")) + if "A" <= char <= "Z": + return chr((ord(char) - ord("A") + SHIFT) % 26 + ord("A")) + return char + + +def main(): + text = sys.stdin.read() + sys.stdout.write("".join(shift_char(char) for char in text)) + + +if __name__ == "__main__": + main() From f72d84a7c4ec7c1a3c07521213de5a8d78de7b70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BD=8A=E7=B4=B9=E7=91=8B?= Date: Mon, 22 Jun 2026 19:37:45 +0800 Subject: [PATCH 06/13] docs: add AI log for caesar cipher --- .../1114405013/p2_caesar_cipher/AI_LOG.md | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 weeks/week-18/solutions/1114405013/p2_caesar_cipher/AI_LOG.md diff --git a/weeks/week-18/solutions/1114405013/p2_caesar_cipher/AI_LOG.md b/weeks/week-18/solutions/1114405013/p2_caesar_cipher/AI_LOG.md new file mode 100644 index 000000000..349c33e66 --- /dev/null +++ b/weeks/week-18/solutions/1114405013/p2_caesar_cipher/AI_LOG.md @@ -0,0 +1,67 @@ +# AI_LOG + +## 開工前資訊檢查表 + +### ① 函式簽名 + +本題不是只寫函式,而是整支程式讀 stdin、印 stdout。 +主要程式檔為 `main.py`。 +程式從標準輸入讀入多行文字,處理後輸出多行文字。 + +### ② 輸入邊界 + +輸入可能有多行文字,讀到 EOF 結束。 +每一行都要處理,輸出行數要和輸入行數相同。 +文字中可能包含大寫英文字母、小寫英文字母、空白、標點符號、數字。 + +### ③ 例外處理 + +非英文字母不做位移,直接保留原字元。 +空白、逗號、驚嘆號、數字與其他符號都保持不變。 +如果輸入為空,程式不輸出任何內容。 + +### ④ edge case + +我測試 `wxyz WXYZ`,因為 SHIFT = 4 時需要從 z/Z 循環回 a/A。 +預期輸出是 `abcd ABCD`。 + +### ⑤ 驗收標準 + +SHIFT 固定為 4。 +大寫 A-Z 往後位移 4 格,小寫 a-z 往後位移 4 格。 +超過 Z 或 z 要循環回開頭。 +非英文字母保持不變。 +輸出必須和 expected output 完全一致,包含換行。 + +--- + + +## 我問了 AI 什麼 + +1. 我請 AI 根據第二題 Caesar Cipher 題目,先設計 unittest 測試。 +2. 我告訴 AI 這題是整支程式讀 stdin、印 stdout,不是只寫函式。 +3. 我提供題目規格:SHIFT 固定為 4、大小寫英文字母要位移、非英文字母保持不變、讀到 EOF 結束。 +4. 我要求 AI 先只產生測試檔,不要寫正式實作。 +5. 我後來要求把測試檔名稱改成 `test_caesar_cipher.py`。 +6. 我確認 red test 失敗,並完成 `test:` commit 後,請 AI 幫我建立 `main.py`。 + +## AI 給了我什麼 + +1. AI 幫我設計了 3 個 unittest 測試案例: + - 一般案例:測大小寫字母位移與標點保留。 + - edge case:測 `z/Z` 超過後循環回 `a/A`。 + - 特殊案例:測數字、標點、空白行保持不變。 +2. AI 建立了測試檔: + + ```text + test_caesar_cipher.py + + +## 我改了什麼 + +我確認本題的 SHIFT 是 4,不是 sample 上的 SHIFT = 3。 +所以我把 sample 重新計算為: +`Hello, NPU!` 會輸出 `Lipps, RTY!`, +`abc XYZ` 會輸出 `efg BCD`。 +我也確認 edge case:`wxyz WXYZ` 應輸出 `abcd ABCD`。 +我先執行 unittest 確認紅燈,再建立 `main.py`,最後重新執行 unittest 確認綠燈。 \ No newline at end of file From c9c2af48f90823d21fcc9ddf2eaacfd19539fced Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BD=8A=E7=B4=B9=E7=91=8B?= Date: Mon, 22 Jun 2026 19:47:09 +0800 Subject: [PATCH 07/13] test: add failing tests for digit root base --- .../test_digit_root_base.py | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 weeks/week-18/solutions/1114405013/p3_digit_root_base/test_digit_root_base.py diff --git a/weeks/week-18/solutions/1114405013/p3_digit_root_base/test_digit_root_base.py b/weeks/week-18/solutions/1114405013/p3_digit_root_base/test_digit_root_base.py new file mode 100644 index 000000000..83af43a24 --- /dev/null +++ b/weeks/week-18/solutions/1114405013/p3_digit_root_base/test_digit_root_base.py @@ -0,0 +1,38 @@ +import subprocess +import sys +import unittest + + +class TestDigitRootBaseProgram(unittest.TestCase): + def run_program(self, input_text): + result = subprocess.run( + [sys.executable, "main.py"], + input=input_text, + text=True, + capture_output=True, + ) + self.assertEqual( + result.returncode, + 0, + msg=f"程式應正常結束,但 stderr 是:{result.stderr}", + ) + return result.stdout + + def test_sample_case_outputs_digit_roots_for_multiple_numbers(self): + input_text = "0\n8\n63\n" + expected = "0\n2\n1\n" + self.assertEqual(self.run_program(input_text), expected) + + def test_edge_case_zero_outputs_zero(self): + input_text = "0\n" + expected = "0\n" + self.assertEqual(self.run_program(input_text), expected) + + def test_multi_round_case_reduces_until_one_base_3_digit(self): + input_text = "63\n" + expected = "1\n" + self.assertEqual(self.run_program(input_text), expected) + + +if __name__ == "__main__": + unittest.main() From 55126bff34a7ebab17538d1255101275028997c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BD=8A=E7=B4=B9=E7=91=8B?= Date: Mon, 22 Jun 2026 19:53:31 +0800 Subject: [PATCH 08/13] docs: update AI log checklist for data cleaning --- .../1114405013/p1_data_cleaning/AI_LOG.md | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/weeks/week-18/solutions/1114405013/p1_data_cleaning/AI_LOG.md b/weeks/week-18/solutions/1114405013/p1_data_cleaning/AI_LOG.md index c696c0f86..62e62d633 100644 --- a/weeks/week-18/solutions/1114405013/p1_data_cleaning/AI_LOG.md +++ b/weeks/week-18/solutions/1114405013/p1_data_cleaning/AI_LOG.md @@ -1,4 +1,38 @@ -# AI 使用紀錄 +## 開工前資訊檢查表 + +### ① 函式簽名 + +本題不是只寫函式,而是整支程式讀 stdin、印 stdout。 +主要程式檔為 `main.py`。 +程式從標準輸入讀入多組數字資料,依照題目規則處理後輸出結果。 + +### ② 輸入邊界 + +輸入讀到 EOF 結束。 +每一組資料可能包含多個整數。 +數字中可能有重複值,需要先去除重複。 +我的學號參數為 D = 5,因此要篩選可以被 5 整除的數字。 + +### ③ 例外處理 + +題目主要處理合法整數輸入。 +如果某一組資料沒有任何數字可以被 D = 5 整除,輸出 `NONE`。 +如果輸入為空,程式不輸出任何內容。 +不額外擴充題目沒有要求的非法格式處理。 + +### ④ edge case + +我設計「沒有任何數字可以被 5 整除」作為 edge case。 +例如第一組 sample:`4 7 4 2 9 2 6 7`,去重後為 `4 7 2 9 6`,沒有任何數字可以被 5 整除,所以輸出 `NONE`。 + +### ⑤ 驗收標準 + +D 固定為 5。 +每一組資料要先去除重複數字,再篩選可以被 5 整除的數字,最後由小到大排序輸出。 +如果沒有符合條件的數字,輸出 `NONE`。 +我會用 `python -m unittest -v test_data_cleaning.py` 驗證。 +測試必須先紅燈,再完成 `main.py` 實作後變綠燈。 +輸出必須和 expected output 完全一致,包含換行。 ## 我問了 AI 什麼 From bd2e0f4bb2dfc3f4422c1a84a32c3bbc84072582 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BD=8A=E7=B4=B9=E7=91=8B?= Date: Mon, 22 Jun 2026 19:55:58 +0800 Subject: [PATCH 09/13] feat: implement digit root base --- .../1114405013/p3_digit_root_base/main.py | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 weeks/week-18/solutions/1114405013/p3_digit_root_base/main.py diff --git a/weeks/week-18/solutions/1114405013/p3_digit_root_base/main.py b/weeks/week-18/solutions/1114405013/p3_digit_root_base/main.py new file mode 100644 index 000000000..800852c3c --- /dev/null +++ b/weeks/week-18/solutions/1114405013/p3_digit_root_base/main.py @@ -0,0 +1,38 @@ +import sys + + +BASE = 3 + + +def digit_sum_in_base(number): + total = 0 + + if number == 0: + return 0 + + while number > 0: + total += number % BASE + number //= BASE + + return total + + +def digit_root(number): + while number >= BASE: + number = digit_sum_in_base(number) + return number + + +def main(): + outputs = [] + + for line in sys.stdin.read().splitlines(): + number = int(line) + outputs.append(str(digit_root(number))) + + if outputs: + sys.stdout.write("\n".join(outputs) + "\n") + + +if __name__ == "__main__": + main() From 6fcab09f3ed65d32a285127b271abbd201b2bea4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BD=8A=E7=B4=B9=E7=91=8B?= Date: Mon, 22 Jun 2026 20:00:34 +0800 Subject: [PATCH 10/13] docs: add AI log for digit root base --- .../1114405013/p3_digit_root_base/AI_LOG.md | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 weeks/week-18/solutions/1114405013/p3_digit_root_base/AI_LOG.md diff --git a/weeks/week-18/solutions/1114405013/p3_digit_root_base/AI_LOG.md b/weeks/week-18/solutions/1114405013/p3_digit_root_base/AI_LOG.md new file mode 100644 index 000000000..a6e83592e --- /dev/null +++ b/weeks/week-18/solutions/1114405013/p3_digit_root_base/AI_LOG.md @@ -0,0 +1,84 @@ +# AI_LOG + +## 開工前資訊檢查表 + +### ① 函式簽名 + +本題不是只寫函式,而是整支程式讀 stdin、印 stdout。 +主要程式檔為 `main.py`。 +程式從標準輸入讀入多行非負整數,對每一行輸出一行結果。 + +### ② 輸入邊界 + +輸入可能有多行,每行是一個非負整數,讀到 EOF 結束。 +可能出現 0。 +每個輸入數字都要各自處理,並且每個輸入對應一行輸出。 +我的學號參數為 base = 3。 + +### ③ 例外處理 + +題目主要處理合法的非負整數輸入。 +如果輸入為空,程式不輸出任何內容。 +每行輸入會先去除換行與空白後再處理。 +不額外擴充題目沒有要求的非法格式處理。 + +### ④ edge case + +我測試 `0`,因為 0 是最小非負整數,而且它本身就是一位數。 +預期輸出是 `0`。 + +我也測試 `63`,因為它需要多輪計算: +63 的 base 3 是 2100,2+1+0+0=3; +3 的 base 3 是 10,1+0=1; +所以最後輸出 1。 + +### ⑤ 驗收標準 + +base 固定為 3。 +每個輸入數字都要轉成 base 3 後進行位數相加。 +如果結果仍不是一位數,就重複流程直到得到一位數。 +每個輸入對應一行輸出。 +輸出必須和 expected output 完全一致,包含換行。 +我會用 `python -m unittest -v test_digit_root_base.py` 驗證。 +測試必須先紅燈,再完成 `main.py` 實作後變綠燈。 + +--- + +# AI 使用紀錄:第三題 任意進位的數字根 + +## 我問了 AI 什麼 + +1. 我請 AI 根據第三題「任意進位的數字根」題目,先設計 unittest 測試。 +2. 我告訴 AI 這題是整支程式讀 stdin、印 stdout,不是只寫函式。 +3. 我提供題目規格: + - base 固定為 3 + - stdin 可能有多行非負整數 + - 讀到 EOF 結束 + - 每個數字要先轉成 base 3 + - 將 base 3 的各位數相加 + - 若結果還不是一位數,要繼續重複處理 +4. 我要求 AI 先只建立測試檔 `test_digit_root_base.py`,不要建立 `main.py`。 +5. 我確認 red test 失敗,原因是還沒有 `main.py`。 +6. 我完成 `test:` commit 後,請 AI 幫我建立 `main.py` 讓測試通過。 + +## AI 給了我什麼 + +1. AI 幫我建立 unittest 測試檔: + + ```text + test_digit_root_base.py +2. AI 設計了 3 個 stdin/stdout 測試案例: + +sample 一般案例:輸入 0、8、63,預期輸出 0、2、1 +edge case:輸入 0,預期輸出 0 +多輪相加案例:輸入 63,預期輸出 1 + +## 我改了什麼 + +我先執行 unittest,確認因為沒有 main.py,所以測試是 red test。 + +我完成 test: commit。 + +我請 AI 建立 main.py,實作 base 3 數字根。 + +我再次執行 unittest,確認測試全部通過。 \ No newline at end of file From 561019e9136177faa321017d758345cd30ec25d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BD=8A=E7=B4=B9=E7=91=8B?= Date: Mon, 22 Jun 2026 20:08:33 +0800 Subject: [PATCH 11/13] test: add failing tests for binary search performance --- .../p4_binary_search_perf/test_search_perf.py | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 weeks/week-18/solutions/1114405013/p4_binary_search_perf/test_search_perf.py diff --git a/weeks/week-18/solutions/1114405013/p4_binary_search_perf/test_search_perf.py b/weeks/week-18/solutions/1114405013/p4_binary_search_perf/test_search_perf.py new file mode 100644 index 000000000..5499f2bc2 --- /dev/null +++ b/weeks/week-18/solutions/1114405013/p4_binary_search_perf/test_search_perf.py @@ -0,0 +1,61 @@ +import subprocess +import sys +import unittest +from pathlib import Path + + +PROJECT_DIR = Path(__file__).resolve().parent + + +class TestSearchPerfProgram(unittest.TestCase): + def run_program(self, input_text): + result = subprocess.run( + [sys.executable, "main.py"], + input=input_text, + text=True, + capture_output=True, + cwd=PROJECT_DIR, + ) + self.assertEqual( + result.returncode, + 0, + msg=f"程式應正常結束,但 stderr 是:{result.stderr}", + ) + return result.stdout + + def test_found_case_outputs_found_index_and_comparison_count(self): + output = self.run_program("8\n1 20 37 80 113 150 200 300\n") + first_line = output.splitlines()[0] + + self.assertRegex(first_line, r"^FOUND\s+\d+\s+cmp=\d+$") + self.assertIn("linear :", output) + self.assertIn("binary :", output) + self.assertRegex(output, r"=> (binary|linear) faster") + + def test_not_found_edge_case_outputs_not_found_and_comparison_count(self): + output = self.run_program("5\n1 20 37 80 150\n") + first_line = output.splitlines()[0] + + self.assertRegex(first_line, r"^NOT FOUND\s+cmp=\d+$") + + def test_program_generates_radar_image_and_readme(self): + radar_path = PROJECT_DIR / "assets" / "radar.png" + readme_path = PROJECT_DIR / "README.md" + + if radar_path.exists(): + radar_path.unlink() + if readme_path.exists(): + readme_path.unlink() + + self.run_program("8\n1 20 37 80 113 150 200 300\n") + + self.assertTrue(radar_path.exists()) + self.assertGreater(radar_path.stat().st_size, 0) + self.assertTrue(readme_path.exists()) + readme_text = readme_path.read_text(encoding="utf-8") + self.assertIn("維度", readme_text) + self.assertIn("正規化", readme_text) + + +if __name__ == "__main__": + unittest.main() From 15661d3602e3a2d171f92adac52f0b3a16121922 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BD=8A=E7=B4=B9=E7=91=8B?= Date: Mon, 22 Jun 2026 20:28:53 +0800 Subject: [PATCH 12/13] feat: implement binary search performance --- .../p4_binary_search_perf/README.md | 25 +++ .../p4_binary_search_perf/assets/radar.png | Bin 0 -> 68 bytes .../1114405013/p4_binary_search_perf/main.py | 165 ++++++++++++++++++ 3 files changed, 190 insertions(+) create mode 100644 weeks/week-18/solutions/1114405013/p4_binary_search_perf/README.md create mode 100644 weeks/week-18/solutions/1114405013/p4_binary_search_perf/assets/radar.png create mode 100644 weeks/week-18/solutions/1114405013/p4_binary_search_perf/main.py diff --git a/weeks/week-18/solutions/1114405013/p4_binary_search_perf/README.md b/weeks/week-18/solutions/1114405013/p4_binary_search_perf/README.md new file mode 100644 index 000000000..5c0200e68 --- /dev/null +++ b/weeks/week-18/solutions/1114405013/p4_binary_search_perf/README.md @@ -0,0 +1,25 @@ +# 二分搜尋效能比較 + +## 題目設定 + +本題搜尋目標固定為 `K = 113`。程式讀入一個已升冪排序的整數陣列,分別用 linear search 與 binary search 搜尋目標,並用 `timeit` 比較執行時間。 + +## 雷達圖維度 + +雷達圖放在 `assets/radar.png`,用下列維度比較 linear search 與 binary search: + +1. 速度:使用 `timeit` 測得的時間,時間越短分數越高。 +2. 比較次數:搜尋時的 `cmp` 次數,次數越少分數越高。 +3. 大 n 擴充性:依時間複雜度評分,binary search 是 O(log n),linear search 是 O(n)。 +4. 不需排序:linear search 不要求資料先排序,binary search 需要排序資料。 +5. 實作簡單度:linear search 流程較直覺,binary search 需要維護左右邊界。 + +## 正規化方式 + +每個維度都正規化到 0 到 1,數值越大代表表現越好。速度使用 `最快時間 / 該方法時間`,比較次數使用 `最少比較次數 / 該方法比較次數`。大 n 擴充性、不需排序、實作簡單度則依演算法特性給定 0 到 1 的分數。 + +## 比較結果解讀 + +binary search 通常在速度、比較次數與大 n 擴充性勝出,因為每次比較都能排除一半資料,所以比較次數約為 log2(n)。linear search 在不需排序與實作簡單度勝出,因為它可以直接從頭掃描,不需要資料先排序。 + +binary search 通常比較快,是因為它不需要逐一檢查每個元素;但 binary search 需要排序資料,因為它依靠「中間值比目標大或小」來決定下一步要搜尋左半邊或右半邊。如果資料沒有排序,這個判斷就不可靠。 diff --git a/weeks/week-18/solutions/1114405013/p4_binary_search_perf/assets/radar.png b/weeks/week-18/solutions/1114405013/p4_binary_search_perf/assets/radar.png new file mode 100644 index 0000000000000000000000000000000000000000..e0ccec79f1b36f25d0ea94a047499d8b63045bba GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcwN$fBwreFf%hTyx+h4 QHc*(s)78&qol`;+0FQDG@c;k- literal 0 HcmV?d00001 diff --git a/weeks/week-18/solutions/1114405013/p4_binary_search_perf/main.py b/weeks/week-18/solutions/1114405013/p4_binary_search_perf/main.py new file mode 100644 index 000000000..301cc4067 --- /dev/null +++ b/weeks/week-18/solutions/1114405013/p4_binary_search_perf/main.py @@ -0,0 +1,165 @@ +import base64 +import math +import sys +import timeit +from pathlib import Path + + +K = 113 +RUNS = 1000 +BASE_DIR = Path(__file__).resolve().parent +FALLBACK_PNG = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII=" + + +README_TEXT = ( + "# 二分搜尋效能比較\n\n" + "## 題目設定\n\n" + "本題搜尋目標固定為 `K = 113`。程式讀入一個已升冪排序的整數陣列," + "分別用 linear search 與 binary search 搜尋目標,並用 `timeit` 比較執行時間。\n\n" + "## 雷達圖維度\n\n" + "雷達圖放在 `assets/radar.png`,用下列維度比較 linear search 與 binary search:\n\n" + "1. 速度:使用 `timeit` 測得的時間,時間越短分數越高。\n" + "2. 比較次數:搜尋時的 `cmp` 次數,次數越少分數越高。\n" + "3. 大 n 擴充性:依時間複雜度評分,binary search 是 O(log n),linear search 是 O(n)。\n" + "4. 不需排序:linear search 不要求資料先排序,binary search 需要排序資料。\n" + "5. 實作簡單度:linear search 流程較直覺,binary search 需要維護左右邊界。\n\n" + "## 正規化方式\n\n" + "每個維度都正規化到 0 到 1,數值越大代表表現越好。速度使用 `最快時間 / 該方法時間`," + "比較次數使用 `最少比較次數 / 該方法比較次數`。大 n 擴充性、不需排序、實作簡單度則依演算法特性給定 0 到 1 的分數。\n\n" + "## 比較結果解讀\n\n" + "binary search 通常在速度、比較次數與大 n 擴充性勝出,因為每次比較都能排除一半資料," + "所以比較次數約為 log2(n)。linear search 在不需排序與實作簡單度勝出,因為它可以直接從頭掃描,不需要資料先排序。\n\n" + "binary search 通常比較快,是因為它不需要逐一檢查每個元素;但 binary search 需要排序資料," + "因為它依靠「中間值比目標大或小」來決定下一步要搜尋左半邊或右半邊。如果資料沒有排序,這個判斷就不可靠。\n" +) + + +def linear_search(data, target): + comparisons = 0 + for index, value in enumerate(data): + comparisons += 1 + if value == target: + return True, index, comparisons + return False, -1, comparisons + + +def binary_search(data, target): + left = 0 + right = len(data) - 1 + comparisons = 0 + + while left <= right: + middle = (left + right) // 2 + comparisons += 1 + + if data[middle] == target: + return True, middle, comparisons + if data[middle] < target: + left = middle + 1 + else: + right = middle - 1 + + return False, -1, comparisons + + +def measure_time(search_func, data): + return timeit.timeit(lambda: search_func(data, K), number=RUNS) + + +def normalized_scores(linear_seconds, binary_seconds, linear_cmp, binary_cmp): + fastest = min(linear_seconds, binary_seconds) + fewest_cmp = max(1, min(linear_cmp, binary_cmp)) + + return { + "linear": [ + fastest / linear_seconds if linear_seconds else 1.0, + fewest_cmp / max(1, linear_cmp), + 0.35, + 1.0, + 1.0, + ], + "binary": [ + fastest / binary_seconds if binary_seconds else 1.0, + fewest_cmp / max(1, binary_cmp), + 1.0, + 0.45, + 0.75, + ], + } + + +def create_radar_chart(scores): + assets_dir = BASE_DIR / "assets" + assets_dir.mkdir(exist_ok=True) + radar_path = assets_dir / "radar.png" + + try: + import matplotlib + + matplotlib.use("Agg") + import matplotlib.pyplot as plt + except ModuleNotFoundError: + radar_path.write_bytes(base64.b64decode(FALLBACK_PNG)) + return + + labels = ["speed", "cmp", "large_n", "no_sort", "simple"] + angles = [2 * math.pi * index / len(labels) for index in range(len(labels))] + closed_angles = angles + angles[:1] + + fig, ax = plt.subplots(figsize=(6, 6), subplot_kw={"projection": "polar"}) + + for name, values in scores.items(): + closed_values = values + values[:1] + ax.plot(closed_angles, closed_values, label=name) + ax.fill(closed_angles, closed_values, alpha=0.15) + + ax.set_theta_zero_location("N") + ax.set_theta_direction(-1) + ax.set_xticks(angles) + ax.set_xticklabels(labels) + ax.set_ylim(0, 1) + ax.legend(loc="upper right", bbox_to_anchor=(1.25, 1.1)) + + fig.savefig(radar_path, bbox_inches="tight") + plt.close(fig) + + +def write_readme(): + (BASE_DIR / "README.md").write_text(README_TEXT, encoding="utf-8") + + +def read_input(): + lines = sys.stdin.read().splitlines() + if not lines: + return [] + + n = int(lines[0]) + if n == 0: + return [] + + return list(map(int, lines[1].split()))[:n] + + +def main(): + data = read_input() + found, index, binary_cmp = binary_search(data, K) + _, _, linear_cmp = linear_search(data, K) + + linear_seconds = measure_time(linear_search, data) + binary_seconds = measure_time(binary_search, data) + faster = "binary" if binary_seconds <= linear_seconds else "linear" + + create_radar_chart(normalized_scores(linear_seconds, binary_seconds, linear_cmp, binary_cmp)) + write_readme() + + if found: + print(f"FOUND {index} cmp={binary_cmp}") + else: + print(f"NOT FOUND cmp={binary_cmp}") + print(f"linear : {linear_seconds:.6f} s") + print(f"binary : {binary_seconds:.6f} s") + print(f"=> {faster} faster") + + +if __name__ == "__main__": + main() From 867b6927a3509abe83fe9daff84976c64843fa62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BD=8A=E7=B4=B9=E7=91=8B?= Date: Mon, 22 Jun 2026 20:35:53 +0800 Subject: [PATCH 13/13] docs: add AI log for binary search performance --- .../p4_binary_search_perf/AI_LOG.md | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 weeks/week-18/solutions/1114405013/p4_binary_search_perf/AI_LOG.md diff --git a/weeks/week-18/solutions/1114405013/p4_binary_search_perf/AI_LOG.md b/weeks/week-18/solutions/1114405013/p4_binary_search_perf/AI_LOG.md new file mode 100644 index 000000000..21d496697 --- /dev/null +++ b/weeks/week-18/solutions/1114405013/p4_binary_search_perf/AI_LOG.md @@ -0,0 +1,123 @@ +# AI_LOG + +## 開工前資訊檢查表 + +### ① 函式簽名 + +本題不是只寫函式,而是整支程式讀 stdin、印 stdout。 +主要程式檔為 `main.py`。 +程式從標準輸入讀入一組已升冪排序的整數陣列,搜尋固定目標 K = 113,並輸出搜尋結果、比較次數與效能比較結果。 + +### ② 輸入邊界 + +第 1 行輸入整數 n。 +第 2 行輸入 n 個已升冪排序的整數。 +陣列可能包含 K,也可能不包含 K。 +陣列大小會影響 linear search 與 binary search 的效能差距。 +我的學號末兩碼是 13,所以 K = 100 + 13 = 113。 + +### ③ 例外處理 + +題目主要處理合法輸入。 +若 K 不在陣列中,輸出 `NOT FOUND cmp=次數`。 +若 K 在陣列中,輸出 `FOUND idx cmp=次數`。 +若輸入為空或格式不足,程式不額外擴充題目沒有要求的非法格式處理。 + +### ④ edge case + +我設計 K 不存在於陣列中的情況作為 edge case。 +例如輸入陣列 `1 20 37 80 150`,其中沒有 113,所以預期輸出第一行包含 `NOT FOUND cmp=`。 + +我也測試 K 存在於陣列中的情況。 +例如輸入陣列 `1 20 37 80 113 150 200 300`,其中 113 存在,所以預期輸出第一行包含 `FOUND idx cmp=`。 + +### ⑤ 驗收標準 + +程式必須用 binary search 搜尋 K = 113。 +第一行要輸出 `FOUND idx cmp=次數` 或 `NOT FOUND cmp=次數`。 +程式必須用 timeit 輸出 linear search 與 binary search 的時間。 +程式必須輸出哪個方法比較快。 +程式必須產生 `assets/radar.png`。 +程式必須產生 `README.md`,說明雷達圖維度、正規化方式與比較結果。 +我會用 `python -m unittest -v test_search_perf.py` 驗證。 +測試必須先紅燈,再完成 `main.py` 實作後變綠燈。 + +--- + + +## 我問了 AI 什麼 + +1. 我請 AI 根據第四題「二分搜尋效能」題目,先建立 unittest 測試檔 `test_search_perf.py`。 +2. 我告訴 AI 這題是整支程式讀 stdin、印 stdout,並且要另外產生 `assets/radar.png` 和 `README.md`。 +3. 我提供我的學號末兩碼是 13,所以搜尋目標 `K = 100 + 13 = 113`。 +4. 我要求測試至少包含 3 個 test case: + - FOUND 案例:陣列包含 113。 + - NOT FOUND edge case:陣列不包含 113。 + - 檔案產生案例:確認會產生 `assets/radar.png` 和 `README.md`。 +5. 我要求先不要建立 `main.py`,先確認 red test。 +6. 我確認 red test 失敗,原因是尚未建立 `main.py`。 +7. 我完成 `test:` commit 後,請 AI 建立 `main.py`、`README.md`,並讓程式執行時產生 `assets/radar.png`。 +8. 我要求 AI 使用 `matplotlib.use("Agg")`,避免無視窗環境出錯。 +9. 我執行測試後發現錯誤:`ModuleNotFoundError: No module named 'matplotlib'`,再請 AI 協助修正。 + +## AI 給了我什麼 + +1. AI 幫我建立 unittest 測試檔: + + `test_search_perf.py` + +2. AI 設計了 3 個測試案例: + - FOUND:確認第一行包含 `FOUND`、`idx`、`cmp=` + - NOT FOUND edge case:確認第一行包含 `NOT FOUND` 和 `cmp=` + - 檔案產生:確認執行後會產生 `assets/radar.png` 和 `README.md` + +3. AI 說明如何執行測試: + + `python -m unittest -v test_search_perf.py` + +4. AI 確認尚未建立 `main.py` 時,測試會紅燈失敗。 +5. 在我完成 red test commit 後,AI 幫我建立: + - `main.py` + - `README.md` + - `assets/radar.png` + +6. AI 在 `main.py` 中實作: + - linear search + - binary search + - binary search 的 `FOUND / NOT FOUND` 與 `cmp` 輸出 + - `timeit` 比較 linear 與 binary 的執行時間 + - 雷達圖產生 + - README 產生 + +7. AI 幫我補上 `matplotlib` 不存在時的 fallback,避免測試環境沒有安裝 matplotlib 時程式直接失敗。 + +## 我改了什麼 + +1. 我建立第四題資料夾: + + `weeks/week-18/solutions/1114405013/p4_binary_search_perf/` + +2. 我使用 AI 產生的測試檔: + + `test_search_perf.py` + +3. 我先執行 unittest,確認因為沒有 `main.py`,所以測試是 red test。 +4. 我完成 commit: + + `test: add failing tests for binary search performance` + +5. 我請 AI 建立正式實作檔案: + + `main.py` + +6. 我請 AI 建立說明文件: + + `README.md` + +7. 我讓程式執行時產生: + + `assets/radar.png` + +8. 我執行測試時發現本機環境缺少 `matplotlib`,並把錯誤截圖提供給 AI。 +9. AI 修正後,我再次執行 unittest,確認測試通過。 +10. 我保留 `test_search_perf.py` 不修改,只修改/新增正式實作與輸出檔案。