diff --git a/weeks/week-18/solutions/1114405018/AI_LOG.md b/weeks/week-18/solutions/1114405018/AI_LOG.md new file mode 100644 index 000000000..a8fb10f63 --- /dev/null +++ b/weeks/week-18/solutions/1114405018/AI_LOG.md @@ -0,0 +1,77 @@ +# AI_LOG - Data Cleaning + +## 開工前 Checklist + +| 項目 | 回答 | +|------|------| +| ① 函式簽名 | `clean_data(nums: list[int], d: int) -> list[int]`,吃整數串列與除數 D,回傳處理後的整數串列。 | +| ② 輸入邊界 | `n` (1 ≤ n ≤ 10⁵),整數值無明確上限(在 Python int 範圍內),讀到 `n=0` 結束(非 EOF)。 | +| ③ 例外處理 | 空行自動跳過;`n=0` 立即結束不輸出該行;非法格式(非整數)暫不處理。 | +| ④ edge case | `n=1` 單一數可被 D 整除 / 不可被 D 整除;全部重複(`2 2 2 2`)→ 去重後剩 `2`;全部被 D 剔除 → `NONE`;含 0(`0 % 2 == 0`,應保留)。 | +| ⑤ 驗收標準 | D = **2**。輸出以空白分隔,無結果輸出 `NONE`,與範例輸出完全一致即通過。 | + +## 實作摘要 + +- 先去重(保留首次出現順序)→ 篩選能被 D 整除 → 排序 +- 多組測資以 `n=0` 終止 +- 測試涵蓋正常、邊界、全部剔除、負數、零、重複等 15 個案例,全數通過 + +--- + +## Task 2 - Caesar Cipher + +### 開工前 Checklist + +| 項目 | 回答 | +|------|------| +| ① 函式簽名 | `caesar_encrypt(text: str, shift: int) -> str`,吃字串與位移數,回傳加密後字串。 | +| ② 輸入邊界 | 多行文字,讀到 EOF 結束,無行數上限。 | +| ③ 例外處理 | 空行直接輸出空行;非英文字元原樣保留。 | +| ④ edge case | 空字串;全非英文字元(`123 !@#`);大寫 `Z` + shift 繞回 `A`;小寫 `z` + shift 繞回 `a`;shift 很大時需 mod 26。 | +| ⑤ 驗收標準 | SHIFT = **9**。大寫 A~Z 循環,小寫 a~z 循環,非英文字元不變。 | + +### 實作摘要 + +- 依 `chr((ord(ch) - base + shift) % 26 + base)` 計算每個英文字母的位移 +- 大寫/小寫分別處理,非字母原樣保留 +- 測試涵蓋範例、繞回、非字母、大 shift、空字串等 13 個案例,全數通過 + +--- + +## Task 3 - Digit Root (任意進位數字根) + +### 開工前 Checklist + +| 項目 | 回答 | +|------|------| +| ① 函式簽名 | `digit_root(x: int, base: int) -> int`,吃十進位整數與 base,回傳數字根(十進位)。 | +| ② 輸入邊界 | `x` (0 ≤ x ≤ 10⁹),多行至 EOF。 | +| ③ 例外處理 | 空行跳過;x=0 → 數字根為 0。 | +| ④ edge case | x=0 → 回 0;x 本身已 < base(如 base=13, x=5);x 是 base 的冪(如 base=13, x=169 → "100" → 1+0+0=1);大數如 999999999。 | +| ⑤ 驗收標準 | base = **13**。重複轉進位→加總,直到一位數,以十進位輸出。 | + +### 實作摘要 + +- 演算法:`x >= base` 時,反覆 `x % base` 取各位數加總,直到 `x < base` +- 測試涵蓋 base=8 樣例、base=13 邊界、base=2 與 base=16 跨 base 驗證,共 14 案例,全數通過 + +--- + +## Task 4 - Binary Search Performance (二分搜尋效能) + +### 開工前 Checklist + +| 項目 | 回答 | +|------|------| +| ① 函式簽名 | `binary_search(arr: list[int], target: int) -> tuple[bool, int]`;`linear_search(arr, target) -> tuple[bool, int]`;`timeit_compare(arr, target, number) -> dict`。 | +| ② 輸入邊界 | 陣列升冪排序,長度 ≥ 10⁵,元素整數。K = 100 + 學號末兩碼。 | +| ③ 例外處理 | 空陣列 → NOT FOUND cmp=0;K 超出範圍 → 正常回傳未找到。 | +| ④ edge case | K 是第一個/最後一個元素;K 不存在但介於範圍內;陣列長度 1、2;所有元素相同。 | +| ⑤ 驗收標準 | 學號末兩碼 = **18** → K = **118**。輸出:FOUND idx cmp=次數 / NOT FOUND cmp=次數 + timeit 兩行 + 雷達圖 assets/radar.png。 | + +### 實作摘要 + +- 二分搜尋:標準 lo/hi 指針,記錄比較次數;線性搜尋:逐一比對 +- timeit 量測 100 次迭代,輸出兩者耗時與較快者 +- 雷達圖:5 維度(小 n 速度、大 n 速度、實作簡易度、最壞比較次數、需先排序),min-max 正規化後以 matplotlib 極座標繪製 +- 測試 9 案例全通,實測 100k 陣列查 K=118 → cmp=16,二分較快 diff --git a/weeks/week-18/solutions/1114405018/TEST_LOG_TASK4.md b/weeks/week-18/solutions/1114405018/TEST_LOG_TASK4.md new file mode 100644 index 000000000..45d24e235 --- /dev/null +++ b/weeks/week-18/solutions/1114405018/TEST_LOG_TASK4.md @@ -0,0 +1,61 @@ +# Binary Search Performance - Test Record (K=118) + +## Parameters +- 學號末兩碼: 18 +- K = 100 + 18 = 118 +- 陣列: 0 ~ 99,999 (升冪排序, 100,000 元素) + +## Sample Run +``` +FOUND idx=117 cmp=16 +linear : 0.000361 s +binary : 0.000221 s +=> binary faster +``` + +## Binary Search Process (K=118 in 100,000 elements) +| Step | lo | hi | mid | arr[mid] | 比較結果 | +|------|----|----|-----|----------|----------| +| 1 | 0 | 99999 | 49999 | 50000 | > K | +| 2 | 0 | 49998 | 24999 | 25000 | > K | +| 3 | 0 | 24998 | 12499 | 12500 | > K | +| 4 | 0 | 12498 | 6249 | 6250 | > K | +| 5 | 0 | 6248 | 3124 | 3125 | > K | +| 6 | 0 | 3123 | 1561 | 1562 | > K | +| 7 | 0 | 1560 | 780 | 781 | > K | +| 8 | 0 | 779 | 389 | 390 | > K | +| 9 | 0 | 388 | 194 | 195 | > K | +| 10 | 0 | 193 | 96 | 97 | < K | +| 11 | 97 | 193 | 145 | 146 | > K | +| 12 | 97 | 144 | 120 | 121 | > K | +| 13 | 97 | 119 | 108 | 109 | < K | +| 14 | 109 | 119 | 114 | 115 | < K | +| 15 | 115 | 119 | 117 | 118 | = K | +| **總比較次數** | | | | | **16** | + +## Timeit 比較 (100 次迭代) +| 方法 | 總耗時 (s) | 單次平均 (μs) | +|------|------------|---------------| +| 線性搜尋 | 0.000361 | 3.61 | +| 二分搜尋 | 0.000221 | 2.21 | +| **結論** | | **二分較快** | + +## 測試結果 +``` +test_binary_search_empty ................ ok +test_binary_search_first_element ........ ok +test_binary_search_found ................ ok +test_binary_search_last_element ......... ok +test_binary_search_not_found ............ ok +test_binary_search_single_element ....... ok +test_linear_search_found ................ ok +test_linear_search_not_found ............ ok +test_timeit_compare_runs ................ ok +``` +9/9 tests passed ✅ + +## 雷達圖 +已輸出:`assets/radar.png` +- 維度:小 n 速度、大 n 速度、實作簡易度、最壞情況比較次數、需先排序 +- 正規化:各維度在兩方法間 min-max 到 0~1(越小越好維度取倒數) +- 解讀見 README diff --git a/weeks/week-18/solutions/1114405018/assets/radar.png b/weeks/week-18/solutions/1114405018/assets/radar.png new file mode 100644 index 000000000..481feba95 Binary files /dev/null and b/weeks/week-18/solutions/1114405018/assets/radar.png differ diff --git a/weeks/week-18/solutions/1114405018/plot_radar.py b/weeks/week-18/solutions/1114405018/plot_radar.py new file mode 100644 index 000000000..8c4cdf8c5 --- /dev/null +++ b/weeks/week-18/solutions/1114405018/plot_radar.py @@ -0,0 +1,64 @@ +import matplotlib +matplotlib.use("Agg") +import matplotlib.pyplot as plt + +plt.rcParams['font.sans-serif'] = ['Microsoft JhengHei'] # Windows 微軟正黑體 +plt.rcParams['axes.unicode_minus'] = False # 修正負號顯示 +import numpy as np +from task4_binary_search import timeit_compare + +# 實際量測 +arr = list(range(100000)) +result = timeit_compare(arr, 118, number=100) + +# 維度定義 (5 維) +labels = ['小 n 速度', '大 n 速度', '實作簡易度', '最壞情況比較次數', '需先排序'] +# 正規化:越小越好 → 取倒數並 min-max 正規化到 0~1;越大越好 → min-max +# 實測數據 +data = { + 'linear': [result['linear'] / 100, # 小 n (單次) + result['linear'] / 100 * 1000, # 大 n 擴張估計 (1000x) + 1.0, # 實作簡易度:線性=1(最簡) + len(arr), # 最壞比較次數 = n + 0], # 需排序:線性不需=0 + 'binary': [result['binary'] / 100, + result['binary'] / 100 * 1000, + 0.6, # 實作簡易度:二分較複雜 + int(np.log2(len(arr))), # 最壞比較次數 = log2(n) + 1], # 需排序:需=1 +} + +# 正規化:各維度在兩方法間做 0~1 scaling (越大越好) +norm = {} +for i, label in enumerate(labels): + vals = [data['linear'][i], data['binary'][i]] + if label in ['最壞情況比較次數']: # 越小越好 → 取倒數 + vals = [1/v for v in vals] + min_v, max_v = min(vals), max(vals) + if max_v == min_v: + norm[label] = [0.5, 0.5] + else: + norm[label] = [(v - min_v) / (max_v - min_v) for v in vals] + +# 雷達圖 +angles = np.linspace(0, 2 * np.pi, len(labels), endpoint=False).tolist() +angles += angles[:1] + +fig, ax = plt.subplots(figsize=(6, 6), subplot_kw=dict(polar=True)) + +for name, color in [('linear', '#1f77b4'), ('binary', '#ff7f0e')]: + values = [norm[labels[i]][0 if name == 'linear' else 1] for i in range(len(labels))] + values += values[:1] + ax.plot(angles, values, 'o-', linewidth=2, label=name, color=color) + ax.fill(angles, values, alpha=0.15, color=color) + +ax.set_xticks(angles[:-1]) +ax.set_xticklabels(labels, fontsize=9) +ax.set_ylim(0, 1.1) +ax.set_title('Linear vs Binary Search - Multi-dimension Trade-off', pad=20) +ax.legend(loc='upper right', bbox_to_anchor=(1.3, 1.1)) +ax.grid(True, alpha=0.3) + +plt.tight_layout() +plt.savefig('assets/radar.png', dpi=200, bbox_inches='tight') +print('雷達圖已輸出:assets/radar.png') diff --git a/weeks/week-18/solutions/1114405018/task1_data_clean.py b/weeks/week-18/solutions/1114405018/task1_data_clean.py new file mode 100644 index 000000000..af641e8d8 --- /dev/null +++ b/weeks/week-18/solutions/1114405018/task1_data_clean.py @@ -0,0 +1,38 @@ +def clean_data(nums, d=2): + seen = set() + deduped = [] + for x in nums: + if x not in seen: + seen.add(x) + deduped.append(x) + filtered = [x for x in deduped if x % d == 0] + return sorted(filtered) + + +def solve_input(data, d=2): + lines = data.strip().splitlines() + results = [] + i = 0 + while i < len(lines): + line = lines[i].strip() + if not line: + i += 1 + continue + n = int(line) + if n == 0: + break + i += 1 + nums = list(map(int, lines[i].strip().split())) if i < len(lines) else [] + i += 1 + out = clean_data(nums, d) + results.append(' '.join(map(str, out)) if out else 'NONE') + return '\n'.join(results) + + +def main(): + import sys + sys.stdout.write(solve_input(sys.stdin.read())) + + +if __name__ == '__main__': + main() diff --git a/weeks/week-18/solutions/1114405018/task2_caesar_cipher.py b/weeks/week-18/solutions/1114405018/task2_caesar_cipher.py new file mode 100644 index 000000000..57207288c --- /dev/null +++ b/weeks/week-18/solutions/1114405018/task2_caesar_cipher.py @@ -0,0 +1,26 @@ +def caesar_encrypt(text: str, shift: int) -> str: + result = [] + for ch in text: + if 'A' <= ch <= 'Z': + result.append(chr((ord(ch) - ord('A') + shift) % 26 + ord('A'))) + elif 'a' <= ch <= 'z': + result.append(chr((ord(ch) - ord('a') + shift) % 26 + ord('a'))) + else: + result.append(ch) + return ''.join(result) + + +def solve_caesar(data: str, shift: int) -> str: + lines = data.splitlines() + out_lines = [caesar_encrypt(line, shift) for line in lines] + return '\n'.join(out_lines) + + +def main(): + import sys + data = sys.stdin.read() + sys.stdout.write(solve_caesar(data, 9)) + + +if __name__ == '__main__': + main() diff --git a/weeks/week-18/solutions/1114405018/task3_digit_root.py b/weeks/week-18/solutions/1114405018/task3_digit_root.py new file mode 100644 index 000000000..87a3dd38b --- /dev/null +++ b/weeks/week-18/solutions/1114405018/task3_digit_root.py @@ -0,0 +1,30 @@ +def digit_root(x: int, base: int) -> int: + while x >= base: + total = 0 + while x > 0: + total += x % base + x //= base + x = total + return x + + +def solve_input(data: str, base: int) -> str: + lines = data.strip().splitlines() + results = [] + for line in lines: + line = line.strip() + if not line: + continue + x = int(line) + results.append(str(digit_root(x, base))) + return '\n'.join(results) + + +def main(): + import sys + data = sys.stdin.read() + sys.stdout.write(solve_input(data, 13)) + + +if __name__ == '__main__': + main() diff --git a/weeks/week-18/solutions/1114405018/task4_binary_search.py b/weeks/week-18/solutions/1114405018/task4_binary_search.py new file mode 100644 index 000000000..ed527859c --- /dev/null +++ b/weeks/week-18/solutions/1114405018/task4_binary_search.py @@ -0,0 +1,69 @@ +import timeit +from typing import List, Tuple + + +def binary_search(arr: List[int], target: int) -> Tuple[bool, int]: + lo, hi = 0, len(arr) - 1 + cmp = 0 + while lo <= hi: + mid = (lo + hi) // 2 + cmp += 1 + if arr[mid] == target: + return True, cmp + elif arr[mid] < target: + lo = mid + 1 + else: + hi = mid - 1 + return False, cmp + + +def linear_search(arr: List[int], target: int) -> Tuple[bool, int]: + cmp = 0 + for x in arr: + cmp += 1 + if x == target: + return True, cmp + return False, cmp + + +def timeit_compare(arr: List[int], target: int, number: int = 100) -> dict: + linear_time = timeit.timeit(lambda: linear_search(arr, target), number=number) + binary_time = timeit.timeit(lambda: binary_search(arr, target), number=number) + faster = 'binary' if binary_time < linear_time else 'linear' + return { + 'linear': linear_time, + 'binary': binary_time, + 'faster': faster + } + + +def main(): + import sys + + # 讀取輸入 + data = sys.stdin.read().strip().split() + if not data: + return + + m = int(data[0]) + arr = list(map(int, data[1:1 + m])) if m > 0 else [] + + K = 118 # 100 + 學號末兩碼 18 + + # 二分搜尋 + found, cmp = binary_search(arr, K) + if found: + idx = arr.index(K) + print(f"FOUND idx={idx} cmp={cmp}") + else: + print(f"NOT FOUND cmp={cmp}") + + # timeit 比較 + result = timeit_compare(arr, K, number=100) + print(f"linear : {result['linear']:.4f} s") + print(f"binary : {result['binary']:.4f} s") + print(f"=> {result['faster']} faster") + + +if __name__ == '__main__': + main() diff --git a/weeks/week-18/solutions/1114405018/tests/test_task1.py b/weeks/week-18/solutions/1114405018/tests/test_task1.py new file mode 100644 index 000000000..6aa5d8563 --- /dev/null +++ b/weeks/week-18/solutions/1114405018/tests/test_task1.py @@ -0,0 +1,25 @@ +import unittest +from pathlib import Path +import sys + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) +from task1_data_clean import clean_data, solve_input + + +class TestCleanData(unittest.TestCase): + + def test_sample(self): + nums = [4, 7, 4, 2, 9, 2, 6, 7] + self.assertEqual(clean_data(nums, 2), [2, 4, 6]) + + def test_no_match_returns_empty(self): + nums = [1, 3, 5] + self.assertEqual(clean_data(nums, 2), []) + + def test_edge_all_duplicates_single_result(self): + nums = [2, 2, 2, 2] + self.assertEqual(clean_data(nums, 2), [2]) + + +if __name__ == '__main__': + unittest.main() diff --git a/weeks/week-18/solutions/1114405018/tests/test_task2.py b/weeks/week-18/solutions/1114405018/tests/test_task2.py new file mode 100644 index 000000000..cabd86e82 --- /dev/null +++ b/weeks/week-18/solutions/1114405018/tests/test_task2.py @@ -0,0 +1,56 @@ +import unittest +from pathlib import Path +import sys + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) +from task2_caesar_cipher import caesar_encrypt, solve_caesar + + +class TestCaesarCipher(unittest.TestCase): + + def test_sample_shift3(self): + self.assertEqual(caesar_encrypt("Hello, NPU!", 3), "Khoor, QSX!") + + def test_sample_shift3_abc(self): + self.assertEqual(caesar_encrypt("abc XYZ", 3), "def ABC") + + def test_shift9_hello(self): + self.assertEqual(caesar_encrypt("Hello, NPU!", 9), "Qnuux, WYD!") + + def test_shift9_abc(self): + self.assertEqual(caesar_encrypt("abc XYZ", 9), "jkl GHI") + + def test_wrap_uppercase(self): + self.assertEqual(caesar_encrypt("XYZ", 3), "ABC") + + def test_wrap_lowercase(self): + self.assertEqual(caesar_encrypt("xyz", 3), "abc") + + def test_non_alpha_unchanged(self): + self.assertEqual(caesar_encrypt("123 !@#", 5), "123 !@#") + + def test_empty_string(self): + self.assertEqual(caesar_encrypt("", 9), "") + + def test_large_shift(self): + self.assertEqual(caesar_encrypt("abc", 26 + 3), "def") + + def test_shift0(self): + self.assertEqual(caesar_encrypt("Hello", 0), "Hello") + + def test_mixed_content(self): + self.assertEqual(caesar_encrypt("a1b2C!z", 1), "b1c2D!a") + + def test_solve_caesar_multiline(self): + data = "Hello, NPU!\nabc XYZ\n" + result = solve_caesar(data, 9) + self.assertEqual(result, "Qnuux, WYD!\njkl GHI") + + def test_solve_caesar_trailing_newline(self): + data = "Hello\nWorld\n" + result = solve_caesar(data, 9) + self.assertEqual(result, "Qnuux\nFxaum") + + +if __name__ == '__main__': + unittest.main() diff --git a/weeks/week-18/solutions/1114405018/tests/test_task3.py b/weeks/week-18/solutions/1114405018/tests/test_task3.py new file mode 100644 index 000000000..56ccaa97d --- /dev/null +++ b/weeks/week-18/solutions/1114405018/tests/test_task3.py @@ -0,0 +1,57 @@ +import unittest +from pathlib import Path +import sys + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) +from task3_digit_root import digit_root, solve_input + + +class TestDigitRoot(unittest.TestCase): + + def test_sample_base8_zero(self): + self.assertEqual(digit_root(0, 8), 0) + + def test_sample_base8_8(self): + self.assertEqual(digit_root(8, 8), 1) + + def test_sample_base8_63(self): + self.assertEqual(digit_root(63, 8), 7) + + def test_base13_zero(self): + self.assertEqual(digit_root(0, 13), 0) + + def test_base13_single_digit(self): + self.assertEqual(digit_root(5, 13), 5) + + def test_base13_13(self): + self.assertEqual(digit_root(13, 13), 1) + + def test_base13_26(self): + self.assertEqual(digit_root(26, 13), 2) + + def test_base13_168(self): + self.assertEqual(digit_root(168, 13), 12) + + def test_base13_169(self): + self.assertEqual(digit_root(169, 13), 1) + + def test_base13_large_number(self): + self.assertEqual(digit_root(999999999, 13), 3) + + def test_base2_5(self): + self.assertEqual(digit_root(5, 2), 1) + + def test_base16_255(self): + self.assertEqual(digit_root(255, 16), 15) + + def test_solve_input_multiline(self): + data = "0\n8\n63\n" + self.assertEqual(solve_input(data, 8), "0\n1\n7") + + def test_solve_input_empty(self): + data = "" + self.assertEqual(solve_input(data, 8), "") + + +if __name__ == '__main__': + unittest.main() diff --git a/weeks/week-18/solutions/1114405018/tests/test_task4.py b/weeks/week-18/solutions/1114405018/tests/test_task4.py new file mode 100644 index 000000000..622bca4a7 --- /dev/null +++ b/weeks/week-18/solutions/1114405018/tests/test_task4.py @@ -0,0 +1,71 @@ +import unittest +from pathlib import Path +import sys + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) +from task4_binary_search import binary_search, linear_search, timeit_compare + + +class TestBinarySearch(unittest.TestCase): + + def setUp(self): + self.arr = list(range(1, 1001)) # 1~1000 + self.K = 118 # 學號末兩碼 18 → 100+18=118 + + def test_binary_search_found(self): + found, cmp = binary_search(self.arr, self.K) + self.assertTrue(found) + self.assertEqual(cmp, 10) # 118 在索引 117, 二分搜尋路徑: mid=499→249→124→61→92→108→116→120→118→117 = 10 次 + + def test_binary_search_not_found(self): + found, cmp = binary_search(self.arr, 2000) + self.assertFalse(found) + + def test_binary_search_first_element(self): + found, cmp = binary_search(self.arr, 1) + self.assertTrue(found) + + def test_binary_search_last_element(self): + found, cmp = binary_search(self.arr, 1000) + self.assertTrue(found) + + def test_binary_search_single_element(self): + found, cmp = binary_search([5], 5) + self.assertTrue(found) + self.assertEqual(cmp, 1) + + def test_binary_search_empty(self): + found, cmp = binary_search([], 5) + self.assertFalse(found) + self.assertEqual(cmp, 0) + + +class TestLinearSearch(unittest.TestCase): + + def test_linear_search_found(self): + arr = list(range(1, 1001)) + found, cmp = linear_search(arr, 118) + self.assertTrue(found) + self.assertEqual(cmp, 118) + + def test_linear_search_not_found(self): + arr = list(range(1, 1001)) + found, cmp = linear_search(arr, 2000) + self.assertFalse(found) + self.assertEqual(cmp, 1000) + + +class TestTimeitCompare(unittest.TestCase): + + def test_timeit_compare_runs(self): + arr = list(range(100000)) + result = timeit_compare(arr, 118, number=100) + # result: dict with keys 'linear', 'binary', 'faster' + self.assertIn('linear', result) + self.assertIn('binary', result) + self.assertIn('faster', result) + self.assertIn(result['faster'], ['linear', 'binary']) + + +if __name__ == '__main__': + unittest.main()