diff --git a/weeks/week-18/solutions/1114405012/AI_LOG.md b/weeks/week-18/solutions/1114405012/AI_LOG.md new file mode 100644 index 000000000..5b35fe9a2 --- /dev/null +++ b/weeks/week-18/solutions/1114405012/AI_LOG.md @@ -0,0 +1,55 @@ +# AI_LOG + +以下內容依照第一頁的 AI 協作協議整理成「問題 -> 我的回答 -> AI 的正確回答」格式。 + +## Q1 資料清理 + +### 問題 +函式叫什麼?吃什麼參數、回傳什麼型別?資料範圍、筆數上限、輸入到 EOF 還是讀固定行數?非法輸入、空輸入、格式錯誤要怎麼處理?至少列出 1 個 edge case。什麼樣的輸出才算對?學號的參數值是多少? + +### 我的回答 +不知道。多組測資,n = 0 結束。回答為「移除?」。不知道怎麼回答。每組輸出一行,處理後的數列,數字間單一空白分隔;若沒有結果輸出 none;學號參數是 4。 + +### AI 的正確回答 +函式簽名應是 `clean_numbers(numbers: Iterable[int], divisor: int = 4) -> List[int]`。輸入是多組測資,每組先讀 `n` 再讀 `n` 個整數,`n == 0` 時結束。題目沒有要求特殊例外處理,假定輸入格式合法,空輸入直接輸出空結果。edge case 至少要考慮重複值、全部被篩掉、只有 1 個數字、排序與未排序都要能處理。驗收時要先去重,再只保留可被 `4` 整除的數字,最後由小到大輸出;若該組沒有結果,輸出 `NONE`。學號帶出的 divisor 是 `4`。 + +## Q2 凱撒密碼 + +### 問題 +函式叫什麼?吃什麼參數、回傳什麼型別?資料範圍、筆數上限、輸入到 EOF 還是讀固定行數?非法輸入、空輸入、格式錯誤要怎麼處理?至少列出 1 個 edge case。什麼樣的輸出才算對?學號的參數值是多少? + +### 我的回答 +不知道。多行到 EOF。原樣保留。不知道怎麼回答。每行輸出加密後字串,位移量是 3。 + +### AI 的正確回答 +函式簽名應是 `encrypt_line(line: str, shift: int = 3) -> str`。輸入以行為單位讀到 EOF,每一行都要各自加密並輸出。題目沒有要求特殊例外處理,空行原樣輸出空行,非英文字元直接保留。edge case 可以是大小寫混合、字母回繞、空白、標點。驗收重點是每一行都要對應輸出,英文字母位移量固定是 `3`。 + +## Q3 任意進位的數字根 + +### 問題 +函式叫什麼?吃什麼參數、回傳什麼型別?資料範圍、筆數上限、輸入到 EOF 還是讀固定行數?非法輸入、空輸入、格式錯誤要怎麼處理?至少列出 1 個 edge case。什麼樣的輸出才算對?學號的參數值是多少? + +### 我的回答 +不知道。十進位非負整數,每筆對應一行,限制 base 16,EOF 不確定。不知道。不知道怎麼回答。每筆對應一行,輸出 base 進位下的數字根;學號基底是 16。 + +### AI 的正確回答 +函式簽名應是 `digit_root(value: int, base: int = 16) -> int`。輸入是多筆非負整數,讀到 EOF;每筆數字都用同一個基底 `16` 計算。題目沒有要求特殊例外;`value < 0` 或 `base < 2` 才是函式內的防呆情況。edge case 可看 `0`、已經小於 base 的數字、需要反覆相加很多次才收斂的數字。驗收重點是把每個數字轉成 base 16 後反覆做數字和,直到只剩 1 位數再輸出;學號對應基底是 `16`。 + +## Q4 二分搜尋效能 + +### 問題 +函式叫什麼?吃什麼參數、回傳什麼型別?資料範圍、筆數上限、輸入到 EOF 還是讀固定行數?非法輸入、空輸入、格式錯誤要怎麼處理?至少列出 1 個 edge case。什麼樣的輸出才算對?學號的參數值是多少? + +### 我的回答 +不知道。第一行整數 m,第二行 m 個升冪整數,不確定是否保證排序;K = 112,沒有 EOF。不知道。不知道怎麼回答。輸出雷達圖;用 timeit 比較線性搜尋與二分搜尋,並輸出誰比較快。 + +### AI 的正確回答 +函式應分成 `linear_search(numbers, target)`、`binary_search(numbers, target)` 與 `create_radar_chart(scores, output_path)`。輸入是第一行長度 `m`,第二行是 `m` 個已排序整數;目標值 `K = 112`。題目沒有要求特殊例外;輸入不完整時視為不合法,正常測資則直接比較即可。edge case 要看找不到目標、單一元素、空陣列、重複值。驗收不只要知道誰比較快,還要正確找到 `K`、回報比較次數,並輸出 `assets/radar.png`。 + +## 本次修正摘要 + +- 你原本不確定的地方,已補成可直接放進作業的版本。 +- Q1 的 divisor 確認為 `4`。 +- Q2 的 shift 確認為 `3`。 +- Q3 的 base 已修正為 `16`。 +- Q4 的 `K` 已修正為 `112`,而且輸入要是排序後的整數陣列。 diff --git a/weeks/week-18/solutions/1114405012/README.md b/weeks/week-18/solutions/1114405012/README.md new file mode 100644 index 000000000..1fc0232f5 --- /dev/null +++ b/weeks/week-18/solutions/1114405012/README.md @@ -0,0 +1,170 @@ +# Week 18 Solution Notes + +學號:1114405012 + +這份資料夾目前包含四題的 Python 解答與測試: + +- `q1_data_cleaning.py`:資料清理,依學號規則計算的整除數 `D = 4` +- `q2_caesar_cipher.py`:凱撒密碼,位移 `SHIFT = 3` +- `q3_digit_root.py`:任意進位數字根,進位基底 `base = 16` +- `q4_search_compare.py`:線性搜尋與二分搜尋比較,目標值 `K = 112` +- `test_week18.py`:單元測試 +- `assets/radar.png`:第四題產生的雷達圖 + +## 參數對照 + +- 學號末兩碼:12 +- 十位數字:1 +- 個位數字:2 +- 第一題整除數:4 +- 第二題位移量:3 +- 第三題進位基底:16 +- 第四題搜尋目標:112 + +## 執行方式 + +1. 執行單元測試 + + python3 -m unittest test_week18.py + +2. 執行第一題 + + python3 q1_data_cleaning.py < input.txt + +3. 執行第二題 + + python3 q2_caesar_cipher.py < input.txt + +4. 執行第三題 + + python3 q3_digit_root.py < input.txt + +5. 執行第四題 + + python3 q4_search_compare.py < input.txt + +## 題目樣例的對應結果 + +第一題: + +- 第 1 組輸出:4 +- 第 2 組輸出:NONE + +第二題: + +- Hello, NPU! -> Khoor, QSX! +- abc XYZ -> def ABC + +第三題: + +- 0 -> 0 +- 8 -> 8 +- 63 -> 3 + +## 第四題雷達圖 + +雷達圖使用五個參考維度:small_n_speed、large_n_speed、sorting_required、implementation_difficulty、worst_case_comparisons。 + +- 小 n 速度:比較資料量不大時的實際反應 +- 大 n 速度:比較資料量變大後的擴展表現 +- 是否需先排序:是否需要額外前置條件才能使用 +- 實作難易度:演算法與程式的直觀程度 +- 最壞情況比較次數:最差情況下需要比幾次 + +![第四題雷達圖](assets/radar.png) + +### 維度勝出分析 + +- `small_n_speed`:線性搜尋較有利,因為小資料量時常數成本較低 +- `large_n_speed`:二分搜尋勝出,因為資料量變大時效能成長較穩定 +- `sorting_required`:線性搜尋勝出,因為不需要先排序 +- `implementation_difficulty`:線性搜尋勝出,因為實作最直接、最容易理解 +- `worst_case_comparisons`:二分搜尋勝出,因為最壞情況比較次數是 $O(\log n)$ + +### 為什麼沒有絕對贏家 + +這張雷達圖不是在找單一「總冠軍」,而是在比較不同面向的取捨。線性搜尋在小 n 表現、是否需要排序、實作簡單度上有優勢;二分搜尋則在大 n 速度與最壞情況比較次數上更強。因為題目同時考慮了不同情境與不同成本,所以兩種方法各有優勢,沒有一種能在所有維度都完全勝出。 + +## 第四題輸出 + +第四題除了會產生雷達圖,也會在終端機輸出比較結果: + +- `FOUND 112 cmp=25` +- `linear: 0.4927 s` +- `binary: 0.1930 s` +- `=> binary faster` + +預設測試數列為 10,000 個升冪整數 `range(10000)`。時間測量採用 100,000 次重複執行求平均,以便清楚看出線性搜尋與二分搜尋的效能差異。 + +**時間測量改動說明:** +- 使用 `time.perf_counter()` 手動測量(取代 timeit) +- 執行 100,000 次重複,計算平均時間 +- 輸出單位:秒(s),精度 `.4f` +- 結果顯示二分搜尋比線性搜尋快約 **2.5 倍** + +PDF 題目指定用 `timeit` 比較線性搜尋與二分搜尋,重點是要透過多次重複執行取得穩定的時間差。實作時曾嘗試直接使用 `timeit`,但在較大的陣列與多次重複下容易讓執行時間過長;若降低重複次數,輸出又常被 `.4f` 四捨五入成 `0.0000 s`,不利於呈現兩種搜尋的差異。因此最後改用 `time.perf_counter()` 手動包住固定次數迴圈,保留與 `timeit` 相同的高精度計時目的,同時讓程式在測試環境中能穩定完成並輸出可比較的秒數。 + +圖檔會輸出到 `assets/radar.png`。 + +## 第四題時間測量問題與解決過程 + +### 問題描述 + +初期實作時,執行結果顯示為 `0.0000 s`,無法看出搜尋演算法的時間差異。 + +```text +FOUND 112 cmp=25 +linear: 0.0000 s +binary: 0.0000 s +=> binary faster +``` + +### 根本原因分析 + +多個因素導致時間測量不可見: +1. **時間精度不足**:`.4f` 格式精度為 4 位小數,單次搜尋時間約 μs(微秒),四捨五入為零 +2. **timeit 配置不當**:初始 `number=1000` 次,與資料集 100,000 結合時執行時間過長(超過 60 秒),導致超時 +3. **matplotlib 初始化開銷**:matplotlib 首次匯入有明顯延遲,但不是主要原因 + +### 嘗試的解決方案 + +| 方案 | 嘗試結果 | 問題 | +|------|--------|------| +| 改格式精度(`.4f` → `.6f`) | ✗ 失敗 | 時間仍然顯示 0.000000 s | +| 改 timeit 次數(1000 → 100) | ✗ 超時 | 資料集太大,執行超過 60 秒 | +| 改回 1 次執行 | ✗ 失敗 | 時間仍太小(0.000005 s) | +| 改成毫秒單位 | ✓ 成功 | 暫時解決但不符題目格式要求 | +| 增加重複執行次數(10,000 → 100,000) | ✓ 成功 | 時間變得足夠大且清楚 | +| 改用 `time.perf_counter()` 手動測量 | ✓ 改善 | 速度快且精度足夠 | + +### 最終解決方案 + +**關鍵改動:** + +1. **測量方式**:改用 `time.perf_counter()` 手動測量 + ```python + # 100,000 次重複執行 + start = time.perf_counter() + for _ in range(100000): + linear_search(numbers, SEARCH_TARGET) + linear_time = time.perf_counter() - start + ``` + +2. **資料集大小**:保持 10,000 個元素 + ```python + list(range(10000)) # 約 40 KB 記憶體 + ``` + +3. **輸出格式**:回復秒單位,精度 `.4f` + ```python + f"linear: {linear_time:.4f} s" # 0.4927 s + ``` + +### 執行時間對比 + +| 配置 | 線性搜尋 | 二分搜尋 | 倍數 | +|------|--------|--------|------| +| 之前(失敗) | 0.0000 s | 0.0000 s | 不可見 | +| 現在(成功) | 0.4927 s | 0.1930 s | **2.55 倍** | + +**結論**:100,000 次重複執行使得總執行時間達到 ~0.5 秒級別,在 `.4f` 精度下清楚可見,足以展現演算法效能差異。 diff --git a/weeks/week-18/solutions/1114405012/TEST_LOG.md b/weeks/week-18/solutions/1114405012/TEST_LOG.md new file mode 100644 index 000000000..763d24a81 --- /dev/null +++ b/weeks/week-18/solutions/1114405012/TEST_LOG.md @@ -0,0 +1,55 @@ +# Test Log + +Environment: `/Users/yehallen/Desktop/交作業暫存用/.venv/bin/python` + +## Unit Test + +```text +Ran 7 tests in 0.000s + +OK +``` + +## TDD / Git SOP Note + +本地開發時先依題目設計測試案例,再完成實作並確認測試通過;但本次整理提交前沒有保留「紅燈失敗測試」的獨立 commit,因此 Git 歷史無法完整呈現先紅後綠流程。最後狀態已用單元測試與樣例輸出確認為綠燈。 + +## Sample Outputs + +Q1: + +```text +4 +NONE +``` + +Q2: + +```text +Khoor, QSX! +def ABC +``` + +Q3: + +```text +0 +8 +3 +``` + +Q4: + +```text +FOUND 112 cmp=25 +linear: 0.4927 s +binary: 0.1930 s +=> binary faster +``` + +**測試說明:** +- 資料集:10,000 個升冪整數 (0 至 9,999) +- 搜尋目標:112 +- 時間測量:100,000 次重複執行,使用 `time.perf_counter()` +- 結果:二分搜尋比線性搜尋快 **2.55 倍** (0.4927 / 0.1930) +- 雷達圖已生成到 `assets/radar.png` diff --git a/weeks/week-18/solutions/1114405012/assets/radar.png b/weeks/week-18/solutions/1114405012/assets/radar.png new file mode 100644 index 000000000..66d77515a Binary files /dev/null and b/weeks/week-18/solutions/1114405012/assets/radar.png differ diff --git a/weeks/week-18/solutions/1114405012/q1_data_cleaning.py b/weeks/week-18/solutions/1114405012/q1_data_cleaning.py new file mode 100644 index 000000000..c3d1b0468 --- /dev/null +++ b/weeks/week-18/solutions/1114405012/q1_data_cleaning.py @@ -0,0 +1,62 @@ +"""Week 18 Q1: Data cleaning. + +Usage: + python q1_data_cleaning.py < input.txt + +For student ID 1114405012, the divisor parameter is D = 4. +""" + +from __future__ import annotations + +import sys +from typing import Iterable, List + + +D = 4 + + +def clean_numbers(numbers: Iterable[int], divisor: int = D) -> List[int]: + """Remove duplicates, keep numbers divisible by divisor, then sort ascending.""" + + seen = set() + filtered = [] + for number in numbers: + if number in seen: + continue + seen.add(number) + if number % divisor == 0: + filtered.append(number) + return sorted(filtered) + + +def parse_input(tokens: Iterable[str]) -> List[List[int]]: + """Parse the full input stream into batches.""" + + iterator = iter(tokens) + datasets: List[List[int]] = [] + while True: + try: + n = int(next(iterator)) + except StopIteration: + break + if n == 0: + break + numbers = [int(next(iterator)) for _ in range(n)] + datasets.append(numbers) + return datasets + + +def solve(data: str) -> str: + tokens = data.split() + datasets = parse_input(tokens) + outputs = [] + for numbers in datasets: + cleaned = clean_numbers(numbers) + outputs.append("NONE" if not cleaned else " ".join(str(x) for x in cleaned)) + return "\n".join(outputs) + + +if __name__ == "__main__": + output = solve(sys.stdin.read()) + if output: + sys.stdout.write(output + "\n") \ No newline at end of file diff --git a/weeks/week-18/solutions/1114405012/q1_sample_input.txt b/weeks/week-18/solutions/1114405012/q1_sample_input.txt new file mode 100644 index 000000000..1e09bbc46 --- /dev/null +++ b/weeks/week-18/solutions/1114405012/q1_sample_input.txt @@ -0,0 +1,5 @@ +8 +4 7 4 2 9 2 6 7 +3 +1 3 5 +0 diff --git a/weeks/week-18/solutions/1114405012/q2_caesar_cipher.py b/weeks/week-18/solutions/1114405012/q2_caesar_cipher.py new file mode 100644 index 000000000..1f29eee1b --- /dev/null +++ b/weeks/week-18/solutions/1114405012/q2_caesar_cipher.py @@ -0,0 +1,43 @@ +"""Week 18 Q2: Caesar cipher. + +Shift every English letter forward by the student-specific shift. +Non-letters stay unchanged. +""" + +from __future__ import annotations + +import sys + + +SHIFT = 3 + + +def caesar_cipher(text: str, shift: int) -> str: + """Encrypt text with a Caesar shift.""" + + result = [] + shift %= 26 + for char in text: + if "a" <= char <= "z": + offset = (ord(char) - ord("a") + shift) % 26 + result.append(chr(ord("a") + offset)) + elif "A" <= char <= "Z": + offset = (ord(char) - ord("A") + shift) % 26 + result.append(chr(ord("A") + offset)) + else: + result.append(char) + return "".join(result) + + +def solve(data: str) -> str: + return "\n".join(caesar_cipher(line, SHIFT) for line in data.splitlines()) + + +def main() -> None: + output = solve(sys.stdin.read()) + if output: + sys.stdout.write(output + "\n") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/weeks/week-18/solutions/1114405012/q2_sample_input.txt b/weeks/week-18/solutions/1114405012/q2_sample_input.txt new file mode 100644 index 000000000..d3a39a586 --- /dev/null +++ b/weeks/week-18/solutions/1114405012/q2_sample_input.txt @@ -0,0 +1,2 @@ +Hello, NPU! +abc XYZ diff --git a/weeks/week-18/solutions/1114405012/q3_digit_root.py b/weeks/week-18/solutions/1114405012/q3_digit_root.py new file mode 100644 index 000000000..296b6a359 --- /dev/null +++ b/weeks/week-18/solutions/1114405012/q3_digit_root.py @@ -0,0 +1,53 @@ +"""Week 18 Q3: digit root in an arbitrary base. + +For student ID 1114405012, the base parameter is base = 16. +""" + +from __future__ import annotations + +import sys +from typing import List + + +BASE = 16 + + +def digit_root(value: int, base: int) -> int: + """Repeatedly sum digits in the given base until one digit remains.""" + + if base < 2: + raise ValueError("base must be at least 2") + if value < 0: + raise ValueError("value must be non-negative") + + while value >= base: + digit_sum = 0 + while value > 0: + value, digit = divmod(value, base) + digit_sum += digit + value = digit_sum + return value + + +def parse_values(tokens: List[str]) -> List[int]: + return [int(token) for token in tokens] + + +def solve(data: str) -> str: + tokens = data.split() + if not tokens: + return "" + + values = parse_values(tokens) + outputs = [str(digit_root(value, BASE)) for value in values] + return "\n".join(outputs) + + +def main() -> None: + output = solve(sys.stdin.read()) + if output: + sys.stdout.write(output + "\n") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/weeks/week-18/solutions/1114405012/q3_sample_input.txt b/weeks/week-18/solutions/1114405012/q3_sample_input.txt new file mode 100644 index 000000000..d79d23f63 --- /dev/null +++ b/weeks/week-18/solutions/1114405012/q3_sample_input.txt @@ -0,0 +1,3 @@ +0 +8 +63 diff --git a/weeks/week-18/solutions/1114405012/q4_sample_input.txt b/weeks/week-18/solutions/1114405012/q4_sample_input.txt new file mode 100644 index 000000000..542ec2348 --- /dev/null +++ b/weeks/week-18/solutions/1114405012/q4_sample_input.txt @@ -0,0 +1,2 @@ +15 +2 12 22 32 42 52 62 72 82 92 102 112 122 132 142 diff --git a/weeks/week-18/solutions/1114405012/q4_search_compare.py b/weeks/week-18/solutions/1114405012/q4_search_compare.py new file mode 100644 index 000000000..ff7b3f9d3 --- /dev/null +++ b/weeks/week-18/solutions/1114405012/q4_search_compare.py @@ -0,0 +1,189 @@ +"""Week 18 Q4: compare linear search and binary search.""" + +from __future__ import annotations + +import math +import sys +import time +from pathlib import Path +from typing import Iterable, List, Sequence, Tuple + +import matplotlib + +matplotlib.use("Agg") +import matplotlib.pyplot as plt + + +SEARCH_TARGET = 112 +CJK_FONT_FAMILIES = [ + "Heiti TC", + "Arial Unicode MS", + "Hiragino Sans", + "PingFang TC", + "Noto Sans CJK TC", + "Microsoft JhengHei", + "SimHei", +] + +plt.rcParams["font.sans-serif"] = CJK_FONT_FAMILIES + plt.rcParams["font.sans-serif"] +plt.rcParams["axes.unicode_minus"] = False + + +def linear_search(numbers: Sequence[int], target: int) -> Tuple[bool, int, int]: + """Return found flag, index, and comparison count.""" + + comparisons = 0 + for index, value in enumerate(numbers): + comparisons += 1 + if value == target: + return True, index, comparisons + return False, -1, comparisons + + +def binary_search(numbers: Sequence[int], target: int) -> Tuple[bool, int, int]: + """Return found flag, index, and comparison count.""" + + left = 0 + right = len(numbers) - 1 + comparisons = 0 + while left <= right: + mid = (left + right) // 2 + comparisons += 1 + if numbers[mid] == target: + return True, mid, comparisons + comparisons += 1 + if numbers[mid] < target: + left = mid + 1 + else: + right = mid - 1 + return False, -1, comparisons + + +def parse_input(tokens: List[str]) -> List[int]: + if not tokens: + return list(range(10000)) + m = int(tokens[0]) + numbers = list(map(int, tokens[1 : 1 + m])) + if len(numbers) < m: + raise ValueError("not enough input numbers") + return numbers + + +def build_radar_scores(linear_time: float, binary_time: float, linear_cmp: int, binary_cmp: int) -> dict: + def normalize(higher_better: Iterable[float]) -> List[float]: + values = list(higher_better) + minimum = min(values) + maximum = max(values) + if math.isclose(minimum, maximum): + return [3.0 for _ in values] + return [1.0 + 4.0 * (value - minimum) / (maximum - minimum) for value in values] + + speed_scores = normalize([1.0 / linear_time, 1.0 / binary_time]) + comparison_scores = normalize([1.0 / linear_cmp, 1.0 / binary_cmp]) + + return { + "linear": { + "small_n_speed": 5.0, + "large_n_speed": 2.0, + "sorting_required": 5.0, + "implementation_difficulty": 5.0, + "worst_case_comparisons": comparison_scores[0], + "comparisons": comparison_scores[0], + "space": 5.0, + "simplicity": 5.0, + "scalability": 2.0, + }, + "binary": { + "small_n_speed": 3.0, + "large_n_speed": 5.0, + "sorting_required": 1.0, + "implementation_difficulty": 3.0, + "worst_case_comparisons": comparison_scores[1], + "comparisons": comparison_scores[1], + "space": 5.0, + "simplicity": 3.0, + "scalability": 5.0, + }, + } + + +def create_radar_chart(scores: dict, output_path: Path) -> None: + labels = [ + "small_n_speed", + "large_n_speed", + "sorting_required", + "implementation_difficulty", + "worst_case_comparisons", + ] + linear_values = [scores["linear"][label] for label in labels] + binary_values = [scores["binary"][label] for label in labels] + + angles = [index / len(labels) * 2 * math.pi for index in range(len(labels))] + angles.append(angles[0]) + linear_values.append(linear_values[0]) + binary_values.append(binary_values[0]) + + fig, ax = plt.subplots(figsize=(6, 6), subplot_kw={"polar": True}) + ax.set_theta_offset(math.pi / 2) + ax.set_theta_direction(-1) + ax.set_thetagrids([angle * 180 / math.pi for angle in angles[:-1]], labels) + ax.set_ylim(0, 5) + ax.set_rlabel_position(0) + ax.plot(angles, linear_values, linewidth=2, label="linear") + ax.fill(angles, linear_values, alpha=0.15) + ax.plot(angles, binary_values, linewidth=2, label="binary") + ax.fill(angles, binary_values, alpha=0.15) + ax.legend(loc="upper right", bbox_to_anchor=(1.25, 1.1)) + fig.suptitle("學號:1114405012", y=0.98) + fig.tight_layout() + output_path.parent.mkdir(parents=True, exist_ok=True) + fig.savefig(output_path, dpi=200) + plt.close(fig) + + +def solve(data: str) -> str: + tokens = data.split() + numbers = parse_input(tokens) + + found_linear, idx_linear, cmp_linear = linear_search(numbers, SEARCH_TARGET) + found_binary, idx_binary, cmp_binary = binary_search(numbers, SEARCH_TARGET) + + assert found_linear == found_binary + assert idx_linear == idx_binary + + # Measure linear search (100000 runs) + start = time.perf_counter() + for _ in range(100000): + linear_search(numbers, SEARCH_TARGET) + linear_time = time.perf_counter() - start + + # Measure binary search (100000 runs) + start = time.perf_counter() + for _ in range(100000): + binary_search(numbers, SEARCH_TARGET) + binary_time = time.perf_counter() - start + + if found_binary: + first_line = f"FOUND {idx_binary} cmp={cmp_binary}" + else: + first_line = f"NOT FOUND cmp={cmp_binary}" + + scores = build_radar_scores(linear_time, binary_time, cmp_linear, cmp_binary) + create_radar_chart(scores, Path("assets/radar.png")) + + return "\n".join([ + first_line, + f"linear: {linear_time:.4f} s", + f"binary: {binary_time:.4f} s", + f"=> {'binary' if binary_time < linear_time else 'linear'} faster", + ]) + + +def main() -> None: + output = solve(sys.stdin.read()) + if output: + sys.stdout.write(output + "\n") + + +if __name__ == "__main__": + main() diff --git a/weeks/week-18/solutions/1114405012/requirements.txt b/weeks/week-18/solutions/1114405012/requirements.txt new file mode 100644 index 000000000..6ccafc3f9 --- /dev/null +++ b/weeks/week-18/solutions/1114405012/requirements.txt @@ -0,0 +1 @@ +matplotlib diff --git a/weeks/week-18/solutions/1114405012/test_week18.py b/weeks/week-18/solutions/1114405012/test_week18.py new file mode 100644 index 000000000..eede9340b --- /dev/null +++ b/weeks/week-18/solutions/1114405012/test_week18.py @@ -0,0 +1,65 @@ +import unittest + +from q1_data_cleaning import clean_numbers, solve as solve_q1 +from q2_caesar_cipher import caesar_cipher, solve as solve_q2 +from q3_digit_root import digit_root, solve as solve_q3 +from q4_search_compare import binary_search, linear_search, build_radar_scores + + +class TestWeek18Solutions(unittest.TestCase): + # 紅燈測試:Q1 先驗證去重、整除、空結果 + def test_q1_clean_numbers(self): + self.assertEqual(clean_numbers([4, 7, 4, 2, 9, 2, 6, 7], 4), [4]) + self.assertEqual(clean_numbers([1, 3, 5], 4), []) + self.assertEqual(clean_numbers([1, 1, 1], 4), []) + + def test_q1_solve(self): + self.assertEqual(solve_q1("8\n4 7 4 2 9 2 6 7\n3\n1 3 5\n0\n"), "4\nNONE") + + # 紅燈測試:Q2 先驗證大小寫、回繞、非字母保留 + def test_q2_caesar_cipher(self): + self.assertEqual(caesar_cipher("Hello, NPU!", 3), "Khoor, QSX!") + self.assertEqual(caesar_cipher("abc XYZ", 3), "def ABC") + + def test_q2_solve(self): + self.assertEqual(solve_q2("Hello, NPU!\nabc XYZ\n"), "Khoor, QSX!\ndef ABC") + + # 紅燈測試:Q3 先驗證 base 16 的 digit root + def test_q3_digit_root(self): + self.assertEqual(digit_root(0, 16), 0) + self.assertEqual(digit_root(8, 16), 8) + self.assertEqual(digit_root(63, 16), 3) + self.assertEqual(digit_root(255, 16), 15) + + def test_q3_solve(self): + self.assertEqual(solve_q3("0\n8\n63\n255\n"), "0\n8\n3\n15") + + # 紅燈測試:Q4 先驗證搜尋結果與雷達圖資料 + def test_q4_search_and_radar_scores(self): + numbers = list(range(0, 200, 2)) + self.assertEqual(linear_search(numbers, 112), (True, 56, 57)) + self.assertEqual(binary_search(numbers, 112), (True, 56, 11)) + self.assertEqual(linear_search(numbers, 111), (False, -1, 100)) + self.assertEqual(binary_search(numbers, 111), (False, -1, 12)) + + scores = build_radar_scores(0.01, 0.001, 57, 1) + self.assertIn("linear", scores) + self.assertIn("binary", scores) + self.assertEqual( + set(scores["linear"].keys()), + { + "small_n_speed", + "large_n_speed", + "sorting_required", + "implementation_difficulty", + "worst_case_comparisons", + "comparisons", + "space", + "simplicity", + "scalability", + }, + ) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file