Skip to content
77 changes: 77 additions & 0 deletions weeks/week-18/solutions/1114405018/AI_LOG.md
Original file line number Diff line number Diff line change
@@ -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,二分較快
61 changes: 61 additions & 0 deletions weeks/week-18/solutions/1114405018/TEST_LOG_TASK4.md
Original file line number Diff line number Diff line change
@@ -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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
64 changes: 64 additions & 0 deletions weeks/week-18/solutions/1114405018/plot_radar.py
Original file line number Diff line number Diff line change
@@ -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')
38 changes: 38 additions & 0 deletions weeks/week-18/solutions/1114405018/task1_data_clean.py
Original file line number Diff line number Diff line change
@@ -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()
26 changes: 26 additions & 0 deletions weeks/week-18/solutions/1114405018/task2_caesar_cipher.py
Original file line number Diff line number Diff line change
@@ -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()
30 changes: 30 additions & 0 deletions weeks/week-18/solutions/1114405018/task3_digit_root.py
Original file line number Diff line number Diff line change
@@ -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()
69 changes: 69 additions & 0 deletions weeks/week-18/solutions/1114405018/task4_binary_search.py
Original file line number Diff line number Diff line change
@@ -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()
25 changes: 25 additions & 0 deletions weeks/week-18/solutions/1114405018/tests/test_task1.py
Original file line number Diff line number Diff line change
@@ -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()
Loading