Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions weeks/week-18/solutions/1114405022-0622/AI_LOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# AI_LOG — CPE 模擬實戰期末上機考

## 第一題:資料清理 (Data Cleaning)

### 我問 AI 什麼
1. 告知 AI 要開始第一題「資料清理」,並跟 AI 討論拆解 $\ge 3$ 個 test cases。
2. 要求 AI 建立測試檔案、執行並確認紅燈、完成 `test:` commit。
3. 要求 AI 寫實作代碼、執行確認綠燈、完成 `feat:` commit。
4. 請 AI 用我的學號參數 $D = 4$ 計算 Sample Input 的兩組預期輸出。

### AI 反問我什麼 / 我怎麼回答
* **AI 反問**:「函式簽名與回傳型別」打算如何定義?無數值符合時回傳空 list 還是 `"NONE"` 字串?
* **我回答**:定義為 `def clean_data(data: list, d: int) -> list:`,無符合者回傳 `"NONE"`。
* **AI 反問**:回傳 `"NONE"` 字串與簽名中的 `-> list` 衝突,建議將計算與輸出分離,或使用 `list | str` 聯集型別,想選哪種?
* **我回答**:堅持函式直接回傳 `"NONE"` 字串。
* **AI 反問**:`d = 0` 的例外、空 list 邊界、全被篩掉的 edge case 怎麼處理?
* **我回答**:`d = 0` 拋 `ValueError`,其餘皆回傳 `"NONE"`。
* **AI 反問**:(提供測試檔後) 這 5 個 test cases 是否足夠,是否需要加其他特殊案例?
* **我回答**:修改我的決定,把原本的 `d = 0` 拋 `ValueError`,改為 `d = 0` 直接回傳 `"NONE"`,並要求更新測試。

### AI 給了什麼
1. 建立了含有 5 個測試案例的 `test_clean_data.py`。
2. 配合我修改的要求,更新 `test_d_zero_returns_none` 測試。
3. 實作了 `clean_data.py`(使用 set 進行 O(N) 複雜度保序去重,隨後進行篩選與排序)。

### 我改了什麼
1. **修改了例外處理設計**:原本決定 `d = 0` 拋 `ValueError`,但後來考量程式的魯棒性,決定改成當 `d = 0` 時與找不到資料一樣「回傳 `"NONE"`」。我主動抓出原本測試設計的衝突,並指引 AI 修改 `test_d_zero_raises_valueerror` 為 `test_d_zero_returns_none`。
2. **型別優化**:主動確認傳入 `list` 而回傳 `list | str` 的動態型別混合處理,確保測試跟實作的斷言完全一致。

---

## 第二題:凱撒密碼 (Caesar Cipher)

### 我問 AI 什麼
1. 告知 AI 開始第二題,並要求拆解 $\ge 3$ 個 test cases。
2. 要求 AI 依照我的學號位移量 `SHIFT = 3` 進行 TDD 流程(寫測試 $\to$ 紅燈 $\to$ commit $\to$ 實作 $\to$ 綠燈 $\to$ commit)。

### AI 反問我什麼 / 我怎麼回答
* **AI 反問**:函式簽名與回傳型別為何?
* **我回答**:回傳將 `text` 依照 `shift` 位移後的加密字串。參數包括 `text` (str) 與 `shift` (int)。
* **AI 反問**:`shift` 非整數的例外、空字串與純數字符號的邊界、以及 `SHIFT = 3` 的 `A`、`z`、`"Hello, NPU!"` 該如何加密?
* **我回答**:
1. 問 AI 關於 `shift` 非整數的處理建議,隨後聽取 AI 的建議選擇「嚴謹做法,明確拋出 `TypeError`」。
2. 非英文字元(數字、符號、空白)保持不變。
3. `A` 移 3 格變 `D`,小寫 `z` 循環變 `c`。在 AI 協助下,釐清 `"Hello, NPU!"` 會被加密成 `"Khoor, QSX!"`。
* **AI 反問**:進一步舉例 `"abc XYZ"` 在 `SHIFT = 3` 該加密成什麼?
* **我回答**:理解了字母表末端循環的原理,得出 `"def ABC"` 的正確預期。

### AI 給了什麼
1. 建立了 `test_caesar.py`,包含 4 個測試(基本、末端循環、非字母與空字串、TypeError 測試)。
2. 實作了 `caesar.py`(利用 `ord()` 和 `chr()` 計算大小寫字母位移,並透過 `% 26` 完美實現循環與負數處理)。

### 我改了什麼
1. **決策例外處理策略**:在 AI 給予多種分析後(Implicit casting vs TypeError),我主動選擇了最符合軟體工程 Fail-Fast 原則的 `TypeError` 當作例外阻擋條件。
2. **補充驗證案例**:在討論階段,我主動提出對 `"abc XYZ"` 這種跨越字母表末端的連續大寫/小寫組合進行確認,補足了 AI 原始構想中對多重邊界的信心。

---

## 第三題:任意進位的數字根 (Digital Root in Any Base)

### 我問 AI 什麼
1. 告知 AI 開始第三題,並要求拆解 $\ge 3$ 個 test cases。
2. 請求 AI 提供函式簽名的建議。
3. 要求 AI 依照 TDD 流程(寫測試 $\to$ 確認紅燈 $\to$ commit $\to$ 實作 $\to$ 綠燈 $\to$ commit)。

### AI 反問我什麼 / 我怎麼回答
* **AI 反問**:針對 $x=0$ 的邊界、`base` 不合法 (如 $\le 1$) 的例外行為、以及驗收標準(如何計算過程)的看法。
* **我回答**:$x=0$ 時輸出 $0$;`base=0` 時也輸出 $0$。
* **AI 反問**:實作方式傾向方案 A(模擬法:反覆轉換進位並相加)還是方案 B(數學法:利用 $\pmod{base-1}$ 性質)?
* **我回答**:選擇方案 A(模擬法)。
* **AI 反問**:關於極大數值(如 $10^{100}$)的處理,認為是否會導致效能問題?
* **我回答**:認為跑得動,只是需要較多時間。

### AI 給了什麼
1. 建議了簽名 `def digital_root(x: int, base: int) -> int:`。
2. 建立了 `test_digital_root.py`,包含 5 個測試(基本 base=16、大數循環、x=0、base=0、不同基底)。
3. 實作了 `digital_root.py`(利用 while 迴圈與模除運算 `% base` 實現模擬法數位根計算)。

### 我改了什麼
1. **自定義基底異常行為**:在 AI 建議拋出 `ValueError` 處理非法基底時,我決定將 `base=0` 的行為定義為直接回傳 $0$,使函式在異常輸入下仍能保持穩定回傳。
2. **確認模擬法可行性**:針對極大數值的運算效能進行討論,確認模擬法在數位根的對數級縮減特性下足以應對大數輸入,決定不採取過於複雜的數學優化,以維持程式碼的可讀性與符合題目「模擬過程」的意圖。

---

## 第四題:二分搜尋效能 (Binary Search Performance)

### 我問 AI 什麼
1. 告知 AI 開始第四題,並要求拆解 $\ge 3$ 個 test cases。
2. 要求 AI 依照 TDD 流程(寫測試 $\to$ 確認紅燈 $\to$ commit $\to$ 實作 $\to$ 綠燈 $\to$ commit)。

### AI 反問我什麼 / 我怎麼回答
* **AI 反問**:函式簽名與回傳型別建議(Tuple 回傳 `(index, cmp)` vs 屬性記錄)。
* **我回答**:選擇建議 A,回傳 `(index, cmp_count)` 之 tuple。
* **AI 反問**:空 list (n=0) 時的預期結果。
* **我回答**:回傳 `(-1, 0)`。
* **AI 反問**:`binary_search` 收到未排序資料的處理方式。
* **我回答**:選擇「做法 B」,即在函式內部檢查排序,若未排序則拋出 `ValueError`。
* **AI 反問**:如何驗證比較次數 (cmp) 的正確性。
* **我回答**:同意線性搜尋驗證 `cmp == index + 1`,二分搜尋驗證符合 $\log_2 n$ 量級。

### AI 給了什麼
1. 建立了 `test_search_performance.py`,包含 6 個測試(線性找/沒找、二分找/沒找、未排序拋錯、空 list 邊界)。
2. 實作了 `search_performance.py`(包含 `linear_search` 與 `binary_search`,並在二分搜尋前加入排序檢查)。

### 我改了什麼
1. **明確化回傳介面**:在 AI 建議多種方式後,決定採用 Tuple 回傳,以確保每個呼叫都能獲取獨立的比較次數,避免在多執行緒或複雜呼叫中產生狀態污染。
2. **強化安全性檢查**:堅持在 `binary_search` 內部加入排序檢查機制(raise ValueError),而非僅依賴外部輸入正確,確保實作符合嚴謹的工程標準。
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 19 additions & 0 deletions weeks/week-18/solutions/1114405022-0622/caesar.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
def encrypt_caesar(text: str, shift: int) -> str:
if not isinstance(shift, int):
raise TypeError("shift must be an integer")

result = []
for char in text:
if 'A' <= char <= 'Z':
# 大寫字母位移
new_char = chr((ord(char) - ord('A') + shift) % 26 + ord('A'))
result.append(new_char)
elif 'a' <= char <= 'z':
# 小寫字母位移
new_char = chr((ord(char) - ord('a') + shift) % 26 + ord('a'))
result.append(new_char)
else:
# 非字母字元保持不變
result.append(char)

return "".join(result)
20 changes: 20 additions & 0 deletions weeks/week-18/solutions/1114405022-0622/clean_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
def clean_data(data: list, d: int) -> list | str:
if d == 0:
return "NONE"

# 1. 去重且保序 (Preserve first occurrence)
seen = set()
deduped = []
for x in data:
if x not in seen:
deduped.append(x)
seen.add(x)

# 2. 篩選 (Divisible by d)
filtered = [x for x in deduped if x % d == 0]

# 3. 排序 (Ascending)
filtered.sort()

# 回傳結果
return filtered if filtered else "NONE"
15 changes: 15 additions & 0 deletions weeks/week-18/solutions/1114405022-0622/digital_root.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
def digital_root(x: int, base: int) -> int:
if base == 0:
return 0
if x == 0:
return 0

while x >= base:
digits_sum = 0
temp_x = x
while temp_x > 0:
digits_sum += temp_x % base
temp_x //= base
x = digits_sum

return x
58 changes: 58 additions & 0 deletions weeks/week-18/solutions/1114405022-0622/plot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import matplotlib
import matplotlib.pyplot as plt
import numpy as np
import json
import os

# 必須加上此行,否則在沒有 GUI 的環境會報錯
matplotlib.use("Agg")
# 設定中文字體 (Windows 系統常用微軟正黑體)
plt.rcParams['font.sans-serif'] = ['Microsoft JhengHei']
plt.rcParams['axes.unicode_minus'] = False

def draw_radar_chart():
# 1. 定義維度 (中文)
categories = ['執行時間', '比較次數', '空間開銷', '準備成本']
N = len(categories)

# 2. 準備數據 (基於 la-search 表現)
# 數值越小越好,因此我們使用 1/value 進行正規化,使面積越大代表效能越好
# 這裡使用典型權衡值
data = {
'線性搜尋': [1, 1, 5, 5], # Time (慢), Cmp (高), Space (低), Setup (低)
'二分搜尋': [5, 5, 5, 1], # Time (快), Cmp (低), Space (低), Setup (高)
'集合搜尋': [5, 5, 1, 2], # Time (最快), Cmp (低), Space (高), Setup (中)
}

# 正規化 (0-1 區間,越大越好)
# 這裡簡單對應:1=最差, 5=最好

angles = np.linspace(0, 2 * np.pi, N, endpoint=False).tolist()
angles += angles[:1] # 閉合圓環

fig, ax = plt.subplots(figsize=(8, 8), subplot_kw=dict(polar=True))

for label, values in data.items():
vals = values + values[:1]
ax.plot(angles, vals, linewidth=2, label=label)
ax.fill(angles, vals, alpha=0.25)

ax.set_theta_offset(np.pi / 2)
ax.set_theta_direction(-1)
ax.set_thetagrids(np.degrees(angles[:-1]), categories)

# 1. 設定徑向範圍,讓最高分 (5) 離邊緣有一段距離,避免標籤重疊
ax.set_ylim(0, 6)

# 2. 增加標籤與圓環的間距
ax.tick_params(axis='x', pad=25)

plt.title("搜尋演算法多維權衡 (面積越大效能越好)", size=18, y=1.15)
plt.legend(loc='upper right', bbox_to_anchor=(1.5, 1.1))

plt.savefig("C:/Users/user/Desktop/2026 python/weeks/week-18/solutions/1114405022/Q4/assets/radar.png", bbox_inches='tight')
plt.close()

if __name__ == "__main__":
draw_radar_chart()

29 changes: 29 additions & 0 deletions weeks/week-18/solutions/1114405022-0622/search_performance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
def linear_search(data: list, target) -> tuple[int, int]:
cmp_count = 0
for i, val in enumerate(data):
cmp_count += 1
if val == target:
return (i, cmp_count)
return (-1, cmp_count)


def binary_search(data: list, target) -> tuple[int, int]:
# 檢查是否已排序
if any(data[i] > data[i + 1] for i in range(len(data) - 1)):
raise ValueError("Data must be sorted for binary search")

low = 0
high = len(data) - 1
cmp_count = 0

while low <= high:
mid = (low + high) // 2
cmp_count += 1
if data[mid] == target:
return (mid, cmp_count)
elif data[mid] < target:
low = mid + 1
else:
high = mid - 1

return (-1, cmp_count)
27 changes: 27 additions & 0 deletions weeks/week-18/solutions/1114405022-0622/test_caesar.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import unittest
from caesar import encrypt_caesar


class TestCaesarCipher(unittest.TestCase):

def test_basic_encryption(self):
# 測試一般英文字母加密(大小寫混合、包含標點)
self.assertEqual(encrypt_caesar("Hello, NPU!", 3), "Khoor, QSX!")

def test_wrap_around(self):
# 測試大寫與小寫字母表末端的循環位移
self.assertEqual(encrypt_caesar("abc XYZ", 3), "def ABC")

def test_non_alphabet_and_empty(self):
# 測試非英文字母與空字串保持不變
self.assertEqual(encrypt_caesar("123 #!", 3), "123 #!")
self.assertEqual(encrypt_caesar("", 3), "")

def test_invalid_shift_raises_typeerror(self):
# 測試非整數位移量拋出 TypeError
with self.assertRaises(TypeError):
encrypt_caesar("abc", "invalid")


if __name__ == "__main__":
unittest.main()
30 changes: 30 additions & 0 deletions weeks/week-18/solutions/1114405022-0622/test_clean_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import unittest
from clean_data import clean_data


class TestCleanData(unittest.TestCase):

def test_basic_functionality(self):
# 測試基本流程:去重 -> 篩選(D=4) -> 排序
self.assertEqual(clean_data([4, 7, 4, 8, 12, 2, 16, 7], 4), [4, 8, 12, 16])

def test_all_filtered_out(self):
# 測試所有數字都被篩掉
self.assertEqual(clean_data([1, 2, 3], 4), "NONE")

def test_empty_list(self):
# 測試空 list
self.assertEqual(clean_data([], 4), "NONE")

def test_d_zero_returns_none(self):
# 測試 d=0 時回傳 "NONE"
self.assertEqual(clean_data([1, 2, 3], 0), "NONE")

def test_preserves_first_occurrence_then_sorts(self):
# 測試去重保序後再排序
# [10, 2, 10, 4] -> 去重 [10, 2, 4] -> 篩選(D=2) [10, 2, 4] -> 排序 [2, 4, 10]
self.assertEqual(clean_data([10, 2, 10, 4], 2), [2, 4, 10])


if __name__ == "__main__":
unittest.main()
29 changes: 29 additions & 0 deletions weeks/week-18/solutions/1114405022-0622/test_digital_root.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import unittest
from digital_root import digital_root


class TestDigitalRoot(unittest.TestCase):

def test_basic_base16(self):
# 測試 base=16: 31 (10進位) -> 1F (16進位) -> 1+15=16 -> 10 (16進位) -> 1+0=1
self.assertEqual(digital_root(31, 16), 1)

def test_base16_large(self):
# 測試 base=16: 255 (10進位) -> FF (16進位) -> 15+15=30 -> 1E (16進位) -> 1+14=15
self.assertEqual(digital_root(255, 16), 15)

def test_zero_input(self):
# 測試 x=0
self.assertEqual(digital_root(0, 16), 0)

def test_base_zero(self):
# 測試 base=0 時回傳 0
self.assertEqual(digital_root(100, 0), 0)

def test_different_base(self):
# 測試其他基底: x=12, base=8 -> 14 (8進位) -> 1+4=5
self.assertEqual(digital_root(12, 8), 5)


if __name__ == "__main__":
unittest.main()
21 changes: 21 additions & 0 deletions weeks/week-18/solutions/1114405022-0622/test_plot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import unittest
import os
from plot import draw_radar_chart


class TestPlot(unittest.TestCase):

def test_radar_png_generated(self):
# 執行繪圖函式
draw_radar_chart()

# 驗證檔案是否存在
file_path = "assets/radar.png"
self.assertTrue(os.path.exists(file_path), f"PNG file {file_path} was not generated")

# 驗證檔案大小不為 0
self.assertGreater(os.path.getsize(file_path), 0, f"PNG file {file_path} is empty")


if __name__ == "__main__":
unittest.main()
Loading