การคิดเชิงคำนวณ (Computational Thinking)
การคิดเชิงคำนวณคือทักษะรากฐานของวิชาวิทยาการคอมพิวเตอร์ทั้งหมด เพราะมันคือศิลปะของการ “เปลี่ยนปัญหาที่ยังคลุมเครือให้กลายเป็นข้อกำหนดที่ชัดเจนพอจะลงมือทำได้” และที่น่าสนใจคือ ทักษะเดียวกันนี้คือสิ่งที่ทำให้คุณกลายเป็นคนที่สั่งงาน AI ได้เก่งขึ้นอย่างทันที — เพราะ AI ก็ต้องการข้อกำหนดที่ชัดเจนไม่ต่างจากคอมพิวเตอร์
แนวคิดหลัก
หัวข้อที่มีชื่อว่า “แนวคิดหลัก”การคิดเชิงคำนวณ (Computational Thinking) คือกระบวนการคิดในการ กำหนดปัญหาและวางแนวทางแก้ไขในรูปแบบที่ “ตัวประมวลผล” — ไม่ว่าจะเป็นคน คอมพิวเตอร์ หรือ AI — สามารถลงมือทำตามได้
สิ่งสำคัญที่สุด: การคิดเชิงคำนวณ ไม่ใช่การเขียนโค้ด การเขียนโค้ดเป็นเพียงเครื่องมือปลายทางอย่างหนึ่ง แต่การคิดเชิงคำนวณคือวิธี “คิด” ที่เกิดขึ้นก่อนจะแตะคีย์บอร์ดด้วยซ้ำ
คุณใช้การคิดเชิงคำนวณอยู่แล้วในชีวิตประจำวันโดยไม่รู้ตัว เช่น เวลาวางแผนการเดินทาง จัดลำดับการทำงานบ้าน หรือหาของในตู้เสื้อผ้าที่รก สิ่งที่วิชานี้ทำคือทำให้กระบวนการคิดเหล่านั้น ชัดเจน เป็นระบบ และนำกลับมาใช้ซ้ำได้
หัวใจของมันคือการแยกแยะให้ได้ว่า:
- อะไรคือ ข้อมูลนำเข้า (input) ที่เรามี
- อะไรคือ ผลลัพธ์ (output) ที่เราต้องการ
- มี ขั้นตอน อะไรบ้างที่พาเราจาก input ไป output ได้อย่างไม่กำกวม
สี่เสาหลัก
หัวข้อที่มีชื่อว่า “สี่เสาหลัก”การคิดเชิงคำนวณประกอบด้วยทักษะย่อยสี่อย่างที่ทำงานร่วมกัน เราจะใช้ตัวอย่างเดียวกันตลอดทั้งสี่เสา คือ “จัดชั้นหนังสือที่วางระเกะระกะให้เป็นระเบียบ”
1. การแตกปัญหา (Decomposition)
หัวข้อที่มีชื่อว่า “1. การแตกปัญหา (Decomposition)”นิยาม: การแยกปัญหาใหญ่ที่ดูยุ่งเหยิงออกเป็นปัญหาย่อย ๆ ที่เล็กพอจะจัดการได้ทีละชิ้น
ปัญหา “จัดชั้นหนังสือให้เป็นระเบียบ” ฟังดูใหญ่และคลุมเครือ แตกออกมาได้เป็น:
- หยิบหนังสือทั้งหมดออกมากองรวมกัน
- ตัดสินใจว่าจะจัดเรียงตามอะไร (ผู้แต่ง? ประเภท? สี?)
- แบ่งหนังสือออกเป็นกลุ่มตามเกณฑ์นั้น
- เรียงลำดับภายในแต่ละกลุ่ม
- นำกลับขึ้นชั้นตามลำดับที่จัดไว้
เมื่อแตกปัญหาแล้ว แต่ละข้อย่อยจะ “เล็กพอ” จนคุณรู้ทันทีว่าต้องทำอะไร นี่คือสัญญาณว่าแตกปัญหาได้ดีพอแล้ว
ตัดสินโค้ด AI ด้วยเสานี้: เมื่อ AI ส่งวิธีแก้กลับมา ให้ถามว่ามันแตกปัญหาออกเป็นขั้นตอนย่อยที่ถูกต้องครบถ้วนหรือไม่ — มันเผลอข้ามเคสพิเศษไปเงียบ ๆ หรือเปล่า (input ว่าง? มีค่าซ้ำ? มีค่าเสมอกัน?) เพราะขั้นตอนย่อยนั้นไม่เคยถูกตั้งชื่อไว้ตั้งแต่แรก
2. การรู้จำแบบรูป (Pattern Recognition)
หัวข้อที่มีชื่อว่า “2. การรู้จำแบบรูป (Pattern Recognition)”นิยาม: การมองหาความคล้ายคลึงหรือรูปแบบที่ซ้ำกัน เพื่อนำวิธีแก้แบบเดียวกันมาใช้ซ้ำ
ในการจัดหนังสือ คุณจะสังเกตว่า “การเรียงลำดับภายในกลุ่ม” เป็นรูปแบบเดียวกันทุกกลุ่ม ไม่ว่าจะเป็นกลุ่มนิยายหรือกลุ่มตำราเรียน — วิธีเรียงตามตัวอักษร ก-ฮ ใช้ได้เหมือนกันหมด
นั่นแปลว่าคุณไม่ต้องคิดวิธีใหม่สำหรับทุกกลุ่ม คุณคิดวิธี “เรียงตามตัวอักษร” ครั้งเดียว แล้วนำไปใช้ซ้ำกับทุกกลุ่ม
การเห็นว่า “อันนี้มันเหมือนกับอันที่เคยทำมาแล้ว” คือหัวใจของการเขียนฟังก์ชันที่ใช้ซ้ำได้ในการเขียนโปรแกรม
ตัดสินโค้ด AI ด้วยเสานี้: ถามว่าโค้ดของ AI นำรูปแบบที่รู้จักดีและผ่านการพิสูจน์แล้วมาใช้ (เช่น binary search แบบมาตรฐาน หรือการค้นด้วย hash map แบบมาตรฐาน) หรือมันประดิษฐ์อะไรใหม่ขึ้นมาเอง — ความ “ใหม่” คือสัญญาณเตือน ไม่ใช่คำชม ในโค้ดที่มีรูปแบบซึ่งรู้กันอยู่แล้วว่าใช้ได้ดี
3. การคิดเชิงนามธรรม (Abstraction)
หัวข้อที่มีชื่อว่า “3. การคิดเชิงนามธรรม (Abstraction)”นิยาม: การคัดกรองรายละเอียดที่ไม่เกี่ยวข้องออกไป เก็บไว้เฉพาะสิ่งที่จำเป็นต่อการแก้ปัญหา
ในการจัดหนังสือ ถ้าเกณฑ์คือ “เรียงตามชื่อผู้แต่ง” สิ่งที่สำคัญคือ ชื่อผู้แต่ง เท่านั้น ส่วนสีปก น้ำหนัก กลิ่นกระดาษ ปีที่ซื้อ — ล้วนเป็นรายละเอียดที่ ตัดทิ้งได้ เพราะไม่เกี่ยวกับการเรียง
นามธรรมทำให้เราลดหนังสือแต่ละเล่มให้เหลือเพียง “ชื่อผู้แต่ง” หนึ่งค่า แล้วทำงานกับค่านั้นแทนที่จะวุ่นกับทุกคุณสมบัติของหนังสือ
นามธรรมคือการตอบคำถามว่า “อะไรคือสิ่งที่สำคัญต่อปัญหานี้จริง ๆ” ทุกครั้งที่คุณสร้างตัวแปรหรือออกแบบโครงสร้างข้อมูล คุณกำลังตัดสินใจเรื่องนามธรรม
ตัดสินโค้ด AI ด้วยเสานี้: ตรวจว่าโครงสร้างข้อมูลที่ AI เลือกเก็บอะไรไว้และตัดอะไรทิ้ง มันทิ้งฟิลด์ที่คุณจะต้องใช้ทีหลังไปหรือเปล่า (เช่น ตัด timestamp ที่จะใช้ตัดสินกรณีเสมอกันทิ้งไป) หรือมันเก็บส่วนเกินที่ทำให้โค้ดอ่านยากขึ้นโดยไม่จำเป็น
4. การออกแบบขั้นตอนวิธี (Algorithm Design)
หัวข้อที่มีชื่อว่า “4. การออกแบบขั้นตอนวิธี (Algorithm Design)”นิยาม: การเรียงลำดับขั้นตอนที่ชัดเจน ทำซ้ำได้ และไม่กำกวม เพื่อแก้ปัญหาตั้งแต่ต้นจนจบ
นำผลลัพธ์จากสามเสาก่อนหน้ามาเรียงเป็นขั้นตอนที่ทำตามได้จริง:
1. นำหนังสือทุกเล่มออกมากองไว้2. สำหรับหนังสือแต่ละเล่ม: อ่านชื่อผู้แต่ง (abstraction)3. แบ่งหนังสือเป็นกลุ่มตามตัวอักษรแรกของผู้แต่ง (decomposition)4. สำหรับแต่ละกลุ่ม: เรียงตามตัวอักษร ก→ฮ (pattern ที่ใช้ซ้ำ)5. นำกลุ่มขึ้นชั้นเรียงจาก ก ไป ฮขั้นตอนวิธีที่ดีต้องบอกได้ว่า “ทำอะไร ตามลำดับไหน และจบเมื่อไหร่” โดยไม่ต้องเดา ถ้าคุณยื่นขั้นตอนนี้ให้คนอื่น (หรือ AI) ทำตาม ผลลัพธ์ควรออกมาเหมือนกัน
การออกแบบขั้นตอนวิธีคือจุดที่การคิดทั้งหมดตกผลึกเป็น “สูตร” ที่ทำซ้ำได้ และเป็นจุดที่เราเริ่มสนใจเรื่องประสิทธิภาพ เช่น การเรียงหนังสือแบบเทียบทีละคู่ใช้เวลาราว
O(n²)แต่ถ้าใช้วิธีที่ฉลาดกว่าอาจลดเหลือO(n log n)
ตัดสินโค้ด AI ด้วยเสานี้: ลองไล่ขั้นตอนของ AI ด้วยมือบนตัวอย่างเล็ก ๆ มันจบการทำงานได้ในทุกกรณีหรือไม่ Big-O ของมันคืออะไร — และมันซ่อน
O(n²)อยู่ในฟังก์ชันย่อยที่คุณยังไม่ได้ดูหรือเปล่า
จากสูตรสู่การรัน: ดูขั้นตอนวิธีทำงานทีละก้าว
หัวข้อที่มีชื่อว่า “จากสูตรสู่การรัน: ดูขั้นตอนวิธีทำงานทีละก้าว”การคิดเชิงคำนวณจะพิสูจน์คุณค่าจริง ๆ ก็ตอนที่คุณเปลี่ยนแผนให้กลายเป็นขั้นตอนวิธีที่ชัดเจนพอจะให้เครื่อง — หรือเพื่อนร่วมชั้น — รันตามได้โดยไม่ต้องถามคุณกลับแม้แต่คำถามเดียว หนังสือ CLRS เปิดเล่มด้วยนิยามที่ใกล้เคียงกันมาก: ขั้นตอนวิธี (algorithm) คือ กระบวนการคำนวณที่ถูกกำหนดไว้อย่างชัดเจน ซึ่งรับค่าใด ๆ (หรือชุดของค่า) เป็นข้อมูลนำเข้า แล้วผลิตค่าใด ๆ (หรือชุดของค่า) เป็นผลลัพธ์ (Cormen et al., บทที่ 1) สังเกตว่านิยามนี้ตรงกับสี่เสาหลักเป๊ะ ๆ: การแตกปัญหาให้ขั้นตอนมา นามธรรมให้ค่าที่ต้องทำงานด้วยมา การรู้จำแบบรูปให้ขั้นตอนที่ใช้ซ้ำได้มา และการออกแบบขั้นตอนวิธีคือการเรียงทุกอย่างให้ชัดเจนไม่กำกวม
มาดูการรันจริงทีละการเปรียบเทียบ: เรียงลิสต์ [5, 3, 8, 1] ด้วย bubble sort — เปรียบเทียบตัวข้างเคียงซ้ำ ๆ แล้วสลับถ้าผิดลำดับ
def bubble_sort(nums: list[int]) -> list[int]: n = len(nums) for i in range(n - 1): swapped = False for j in range(n - 1 - i): if nums[j] > nums[j + 1]: nums[j], nums[j + 1] = nums[j + 1], nums[j] swapped = True if not swapped: break # เรียงเสร็จแล้ว ไม่ต้องวนต่อ return numsไล่ด้วยมือบน [5, 3, 8, 1]:
| รอบ | เปรียบเทียบ | สลับ? | ลิสต์หลังจากนี้ |
|---|---|---|---|
| 1 | 5, 3 | สลับ | [3, 5, 8, 1] |
| 1 | 5, 8 | ไม่สลับ | [3, 5, 8, 1] |
| 1 | 8, 1 | สลับ | [3, 5, 1, 8] |
| 2 | 3, 5 | ไม่สลับ | [3, 5, 1, 8] |
| 2 | 5, 1 | สลับ | [3, 1, 5, 8] |
| 2 | 5, 8 | ไม่สลับ | [3, 1, 5, 8] |
| 3 | 3, 1 | สลับ | [1, 3, 5, 8] |
| 3 | 3, 5 | ไม่สลับ | [1, 3, 5, 8] |
เรียงเสร็จภายใน 3 รอบ สังเกตสี่เสาหลักที่ทำงานอยู่เบื้องหลัง: การแตกปัญหา แบ่ง “เรียงลิสต์” ออกเป็นรอบที่ทำซ้ำ ๆ; การรู้จำแบบรูป นำขั้นตอน “เปรียบเทียบแล้วสลับ” แบบเดียวกันมาใช้ซ้ำทุกครั้ง; นามธรรม ทำให้ขั้นตอนวิธีไม่สนใจเลยว่าตัวเลขเหล่านั้นแทนอะไร — ราคา คะแนน หรืออายุ — สนใจแค่ลำดับเปรียบเทียบกันเท่านั้น; การออกแบบขั้นตอนวิธี คือการซ้อนลูปอย่างแม่นยำจนรับประกันได้ว่าจะจบการทำงาน
ลองเล่นวิดเจ็ตด้านบนแล้วหยุดดูทีละก้าว — มันกำลังรันตามตารางไล่ขั้นตอนด้านบนเป๊ะ ๆ
ทำไมประสิทธิภาพของสูตรจึงสำคัญ
หัวข้อที่มีชื่อว่า “ทำไมประสิทธิภาพของสูตรจึงสำคัญ”โครงสร้างลูปซ้อนของ bubble sort แปลว่าในกรณีที่แย่ที่สุด มันต้องเปรียบเทียบประมาณ n² ครั้งสำหรับลิสต์ที่มี n ตัว นั่นไม่มีปัญหาอะไรถ้ามีหนังสือ 4 เล่มหรือตัวเลข 4 ตัว แต่มันจะเริ่มมีปัญหาเร็วมาก:
| n | O(n) | O(n log n) | O(n²) |
|---|---|---|---|
| 10 | 10 | ~33 | 100 |
| 1,000 | 1,000 | ~9,966 | 1,000,000 |
| 1,000,000 | 1,000,000 | ~19,931,569 | 1,000,000,000,000 |
ที่ข้อมูลหนึ่งล้านตัว สูตรแบบ O(n²) ต้องใช้การคำนวณประมาณ หนึ่งล้านล้าน (trillion) ครั้ง — กินเวลาหลายนาทีถึงหลายชั่วโมงบนฮาร์ดแวร์จริง — ในขณะที่สูตรแบบ O(n log n) (เช่น merge sort หรือฟังก์ชัน sorted() ในตัวของ Python ที่ใช้ Timsort) จะเสร็จภายในเสี้ยววินาที
นี่คืออีกครึ่งหนึ่งของการออกแบบขั้นตอนวิธี: แค่สูตรถูกต้องยังไม่พอ — มันต้องถูกต้อง และ ถูกพอที่จะใช้ได้จริงกับ
nขนาดที่คุณมีจริง ๆ สูตรO(n²)ที่ถูกต้องแต่ไม่มีวันรันจบในโปรดักชัน พูดในทางปฏิบัติก็คือ “ผิด” นั่นเอง
จากข้อกำหนดสู่พรอมต์
หัวข้อที่มีชื่อว่า “จากข้อกำหนดสู่พรอมต์”ทักษะหลักของการคิดเชิงคำนวณคือการเปลี่ยน คำขอที่คลุมเครือ ให้เป็น ข้อกำหนดที่ชัดเจน และสิ่งที่น่าทึ่งคือ ข้อกำหนดที่ชัดเจนนั้นเองคือพรอมต์ที่ดีสำหรับ AI
ลองเทียบกัน:
| คำขอแบบคลุมเครือ | ข้อกำหนด/พรอมต์ที่ชัดเจน |
|---|---|
| “ช่วยจัดข้อมูลนักเรียนให้หน่อย” | “เรียงรายชื่อนักเรียนจากไฟล์ CSV ตามคะแนนสอบจากมากไปน้อย ถ้าคะแนนเท่ากันให้เรียงตามนามสกุล ก→ฮ และส่งออกเป็น CSV ที่มีคอลัมน์ ชื่อ-คะแนน-อันดับ” |
| “เขียนโค้ดคิดเลขให้หน่อย” | “เขียนฟังก์ชัน Python ชื่อ average(nums: list[float]) -> float ที่คืนค่าเฉลี่ย ถ้า list ว่างให้คืน 0.0” |
| “สรุปบทความนี้ให้หน่อย” | “สรุปบทความนี้เป็น 3 หัวข้อ หัวข้อละไม่เกิน 2 ประโยค เน้นข้อสรุปและตัวเลขสำคัญ เขียนเป็นภาษาไทย” |
สังเกตว่าทั้งคนและ AI ตอบคำขอฝั่งขวาได้ดีกว่าเสมอ เพราะมันระบุครบทั้ง input (ข้อมูลอะไร) output (ต้องการอะไร รูปแบบไหน) และ เงื่อนไขขอบเขต (กรณีเสมอ กรณี list ว่าง) — ซึ่งก็คือผลลัพธ์ตรง ๆ ของการคิดเชิงคำนวณนั่นเอง
คำขอที่คลุมเครือบังคับให้ผู้ฟัง “เดา” ว่าคุณต้องการอะไร และการเดาคือต้นตอของผลลัพธ์ที่ผิดเป้า การเขียนข้อกำหนดที่ดีคือการเอาภาระการเดาออกไปจนหมด
ปัญหาในโลกจริง
หัวข้อที่มีชื่อว่า “ปัญหาในโลกจริง”มาลองแก้ปัญหาที่คลุมเครือจริง ๆ แบบครบวงจร: “ช่วยจัดตารางอ่านหนังสือสอบให้หน่อย”
คำขอนี้คลุมเครือมาก — สอบวิชาอะไร? มีเวลาเท่าไหร่? อ่านได้วันละกี่ชั่วโมง? เราจะใช้สี่เสาหลักไล่ทีละขั้น
การแตกปัญหา (Decomposition): แตกออกเป็นคำถามย่อยที่ต้องตอบให้ได้ก่อน
- มีวิชาอะไรต้องสอบบ้าง และสอบวันไหน
- แต่ละวิชายากแค่ไหน / ต้องใช้เวลากี่ชั่วโมง
- ในแต่ละวันมีเวลาว่างกี่ชั่วโมง
- เหลือเวลาทั้งหมดกี่วันก่อนสอบวันแรก
การคิดเชิงนามธรรม (Abstraction): ตัดสิ่งที่ไม่เกี่ยว เก็บเฉพาะข้อมูลที่จำเป็น เราลดแต่ละวิชาให้เหลือเพียงสามค่า
subjects = [ {"name": "แคลคูลัส", "exam_day": 5, "hours_needed": 8}, {"name": "ฟิสิกส์", "exam_day": 7, "hours_needed": 6}, {"name": "เขียนโปรแกรม", "exam_day": 3, "hours_needed": 4},]hours_per_day = 3 # เวลาว่างต่อวัน(สีปกหนังสือ อารมณ์ตอนอ่าน หรือร้านกาแฟที่ชอบไปนั่ง — ตัดทิ้งหมด)
การรู้จำแบบรูป (Pattern Recognition): สังเกตว่านี่คือปัญหาเดียวกับ “จัดสรรทรัพยากรที่มีจำกัด (เวลา) ให้กับงานที่มีกำหนดส่ง (วันสอบ)” — เป็นรูปแบบเดียวกับการจัดคิวงานหรือการจัดส่งของ และกฎที่ใช้ได้ดีคือ “งานที่ deadline ใกล้ที่สุด ทำก่อน”
การออกแบบขั้นตอนวิธี (Algorithm Design): ประกอบทุกอย่างเป็นขั้นตอนที่ทำตามได้
1. เรียงวิชาตามวันสอบจากใกล้ไปไกล2. สำหรับแต่ละวันที่เหลือก่อนสอบ: ก. หาวิชาที่ deadline ใกล้สุดและยังอ่านไม่ครบชั่วโมงที่ต้องการ ข. จัดสรรเวลาว่างของวันนั้นให้วิชานั้น ค. ลดชั่วโมงที่เหลือของวิชานั้นลง3. ถ้ามีวิชาที่อ่านไม่ทันก่อนสอบ → แจ้งเตือนให้เพิ่มเวลา/ตัดเนื้อหาการเรียงวิชาตามวันสอบล่วงหน้าใช้เวลา O(n log n) ส่วนลูปจัดสรรเวลาแต่ละวันใช้เวลา O(days) — เร็วพอจะรันได้ทันทีแม้มีวิชาเป็นเทอมเต็ม
ตอนนี้ปัญหาที่เคยตอบไม่ได้ กลายเป็นข้อกำหนดที่ชัดเจนพอจะเขียนโปรแกรม หรือสั่ง AI ให้เขียนให้ได้ทันที
แบบฝึกหัด
หัวข้อที่มีชื่อว่า “แบบฝึกหัด”ลองฝึกแตกปัญหาและทำนามธรรมด้วยตนเองก่อนดูเฉลย
ข้อ 1. จงแตกปัญหา “ทำไข่เจียวให้น้อง” ออกเป็นขั้นตอนย่อยที่ชัดเจนอย่างน้อย 6 ขั้น โดยแต่ละขั้นต้องไม่กำกวม
เฉลย
1. ตอกไข่ 2 ฟองลงชาม2. เติมเกลือ 1 หยิบมือ แล้วตีให้เข้ากันจนเป็นเนื้อเดียว3. ตั้งกระทะด้วยไฟกลาง ใส่น้ำมัน 2 ช้อนโต๊ะ รอจนน้ำมันร้อน4. เทไข่ลงกระทะ5. รอจนขอบไข่เริ่มสุกเป็นสีทอง (ประมาณ 1 นาที) แล้วพลิกอีกด้าน6. รออีก 30 วินาที แล้วตักขึ้นใส่จานจุดสำคัญ: ทุกขั้นมีปริมาณและสัญญาณ “จบเมื่อไหร่” ชัดเจน ไม่ใช่แค่ “ทอดไข่”
ข้อ 2. คุณต้องหาเพื่อนชื่อ “พิม” ในกลุ่มไลน์ที่มีสมาชิก 500 คน จงทำนามธรรม: ข้อมูลใดของสมาชิกที่ “จำเป็น” ต่อการค้นหา และข้อมูลใดที่ “ตัดทิ้งได้”
เฉลย
จำเป็น: ชื่อที่แสดง (display name) เท่านั้น เพราะเกณฑ์ค้นหาคือชื่อ
ตัดทิ้งได้: รูปโปรไฟล์ สถานะออนไลน์ วันที่เข้ากลุ่ม จำนวนข้อความที่เคยส่ง สีธีม ฯลฯ
บทเรียน: นามธรรมที่ดีทำให้ปัญหาค้นหาในกลุ่ม 500 คน เหลือแค่ “เทียบสตริงชื่อ” เท่านั้น
ข้อ 3. “เรียงไพ่ 5 ใบในมือจากเล็กไปใหญ่” — ในรูปแบบนี้ซ่อน pattern อะไรที่นำไปใช้ซ้ำกับการเรียงของอื่น ๆ ได้
เฉลย
Pattern คือ “การเปรียบเทียบทีละคู่แล้วสลับที่ถ้าผิดลำดับ” (เหมือนวิธีที่คนเรียงไพ่จริง ๆ คือหยิบทีละใบไปแทรกในตำแหน่งที่ถูก — นี่คือ Insertion Sort)
Pattern นี้ใช้ซ้ำได้กับการเรียงตัวเลข เรียงชื่อ เรียงคะแนน — เปลี่ยนแค่ “เกณฑ์เปรียบเทียบ” แต่โครงสร้างเหมือนเดิม นี่คือสาเหตุที่ภาษาโปรแกรมมีฟังก์ชัน sort() ที่รับ “เกณฑ์เปรียบเทียบ” เข้ามาได้
ข้อ 4. จงเปลี่ยนคำขอคลุมเครือ “ช่วยตั้งชื่อไฟล์รูปให้เป็นระเบียบหน่อย” ให้เป็นข้อกำหนดที่ชัดเจนพอจะนำไปทำหรือสั่ง AI ได้
เฉลย
ตัวอย่างข้อกำหนดที่ชัดเจน:
“เปลี่ยนชื่อไฟล์รูปทุกไฟล์ในโฟลเดอร์นี้ให้อยู่ในรูปแบบ
YYYY-MM-DD_ลำดับ3หลัก.jpgโดยใช้วันที่ถ่ายจาก metadata ของไฟล์ และเรียงลำดับตามเวลาถ่ายจากเก่าไปใหม่ เช่น2026-06-30_001.jpg”
ครบ input (ไฟล์ในโฟลเดอร์ + metadata), output (รูปแบบชื่อ), และเงื่อนไข (เรียงตามเวลา)
ข้อ 5. ดูวิดเจ็ต <algo-sort> ด้านบนที่มีแท่งข้อมูล ถ้า input เป็นข้อมูลที่เรียงกลับด้าน (reverse-sorted กรณีแย่สุด) และมี 8 ตัว bubble sort จะเปรียบเทียบทั้งหมดกี่ครั้ง และคิดเป็น Big-O อะไร
เฉลย
รอบ 1 เปรียบเทียบ 7 คู่ รอบ 2 เปรียบเทียบ 6 คู่ รอบ 3 เปรียบเทียบ 5 คู่ … ไปจนรอบที่ 7 เปรียบเทียบ 1 คู่: 7+6+5+4+3+2+1 = 28 ครั้ง สำหรับ n = 8
โดยทั่วไปผลรวมนี้คือ n(n-1)/2 ซึ่งโตเป็นสัดส่วนกับ n² — ดังนั้นกรณีแย่สุดของ bubble sort คือ O(n²)
ข้อ 6. โค้ด “bubble sort” ที่ AI เขียนให้ชิ้นนี้มีบั๊ก จงหาบั๊กและอธิบายว่าทำไมมันถึงพัง
def bubble_sort_buggy(nums: list[int]) -> list[int]: n = len(nums) for i in range(n): for j in range(n): if nums[j] > nums[j + 1]: nums[j], nums[j + 1] = nums[j + 1], nums[j] return numsเฉลย
ลูปในวนค่า j ไปจนถึง n - 1 ทำให้รอบสุดท้ายมันเข้าถึง nums[j + 1] ซึ่งก็คือ nums[n] — ดัชนีเกินขอบเขตของลิสต์ ทำให้เกิด IndexError
วิธีแก้: ขอบเขตของลูปในต้องหยุดก่อนขอบเขตส่วนที่ยังไม่เรียงหนึ่งตำแหน่งเสมอ คือ for j in range(n - 1 - i): (เหมือนโค้ดฉบับถูกต้องด้านบน) นี่คือบั๊กจากการแตกปัญหา/ออกแบบขั้นตอนวิธีตรง ๆ — เงื่อนไขหยุดของลูปไม่เคยถูกกำหนดไว้อย่างแม่นยำ
ข้อ 7. ฟอร์มสมัครสมาชิกของคุณต้องเช็กว่า username ซ้ำหรือไม่ เทียบกับ username ที่มีอยู่แล้ว 1,000,000 ชื่อ ทุกครั้งที่มีคนพิมพ์ ควรใช้โครงสร้างข้อมูลอะไรรองรับการเช็กนี้ และทำไม
เฉลย
hash set (ใน Python คือ set) การค้นหาใช้เวลาเฉลี่ย O(1) ดังนั้นการเช็กกับ username หนึ่งล้านชื่อจึงเร็วแทบจะทันที ถ้าใช้ list ธรรมดาจะต้องใช้เวลา O(n) — สแกนได้มากถึงหนึ่งล้านรายการทุกครั้งที่มีคนพิมพ์ ซึ่งเป็นตัวอย่างของทางเลือกไร้เดียงสาที่ “ใช้ได้” ตอนเดโมกับ username 10 ชื่อ แต่พังไม่เป็นท่าในโปรดักชัน
ข้อ 8. จงไล่ขั้นตอนของสูตรจัดตารางแบบ “deadline ใกล้สุดทำก่อน” จากหัวข้อปัญหาในโลกจริงด้วยมือ บน input นี้ แล้วทำนายว่าแต่ละวิชาถูกอ่านกี่ชั่วโมงในแต่ละวัน
subjects = [ {"name": "เคมี", "last_day": 1, "hours_needed": 3}, {"name": "ประวัติศาสตร์", "last_day": 3, "hours_needed": 4},]hours_per_day = 3เฉลย
| วัน | วิชาที่อ่าน | ชั่วโมงที่ใช้ | ที่เหลือหลังจากนี้ |
|---|---|---|---|
| 1 | เคมี (deadline ใกล้สุด) | 3 | เคมี: 0, ประวัติศาสตร์: 4 |
| 2 | ประวัติศาสตร์ (เหลือวิชาเดียว) | 3 | ประวัติศาสตร์: 1 |
| 3 | ประวัติศาสตร์ | 1 (ว่าง 2 ชั่วโมง) | ประวัติศาสตร์: 0 |
เคมีอ่านจบพอดีในวันสุดท้ายที่ใช้ได้ ส่วนประวัติศาสตร์อ่านจบก่อนกำหนดหนึ่งวันโดยเหลือเวลาว่าง 2 ชั่วโมงในวันที่ 3 นี่คือกฎ greedy “deadline ใกล้สุดทำก่อน” ทำงานจริง — ใช้เวลาของวันนี้กับวิชาที่ยังไม่เสร็จและใกล้หมดเวลาที่สุดเสมอ
วิจารณ์โค้ดจาก AI
หัวข้อที่มีชื่อว่า “วิจารณ์โค้ดจาก AI”รอบที่ 1: ขอบเขตที่คลุมเครือ
หัวข้อที่มีชื่อว่า “รอบที่ 1: ขอบเขตที่คลุมเครือ”สมมติคุณส่งพรอมต์คลุมเครือนี้ให้ AI:
“เขียนฟังก์ชันหาตัวเลขที่ใหญ่ที่สุด”
AI อาจตอบกลับมาแบบนี้:
def largest(a, b): return a if a > b else bโค้ดนี้ “ทำงานได้” แต่ ผิดขอบเขต — คุณอาจตั้งใจจะหาค่ามากที่สุดใน รายการตัวเลขจำนวนเท่าไรก็ได้ แต่ AI เดาว่าคุณหมายถึงแค่ “ตัวเลขสองตัว” เพราะพรอมต์ไม่ได้ระบุ ปัญหาไม่ได้อยู่ที่ AI โง่ แต่อยู่ที่ข้อกำหนดเปิดช่องให้เดา
ทางแก้คือ กระชับข้อกำหนด ด้วยสี่เสาหลัก: ระบุ input (list ของตัวเลข), output (ค่ามากสุดหนึ่งค่า), และเงื่อนไขขอบเขต (ถ้า list ว่างจะทำอย่างไร)
เฉลย
พรอมต์ที่กระชับแล้ว:
“เขียนฟังก์ชัน Python ชื่อ
largest(nums: list[float]) -> floatที่คืนค่าที่มากที่สุดใน list ถ้า list ว่างให้ยกValueErrorห้ามใช้ฟังก์ชันmax()ในตัวของภาษา”
ผลลัพธ์ที่ถูกขอบเขต:
def largest(nums: list[float]) -> float: if not nums: raise ValueError("รายการต้องมีอย่างน้อยหนึ่งตัว") biggest = nums[0] for n in nums[1:]: if n > biggest: biggest = n return biggestบทเรียน: พรอมต์ที่ดีคือข้อกำหนดที่ดี และข้อกำหนดที่ดีมาจากการคิดเชิงคำนวณ ทุกครั้งที่ AI ตอบผิดเป้า ให้ถามตัวเองก่อนว่า “ฉันเปิดช่องให้มันเดาตรงไหน”
รอบที่ 2: “ทำงานได้” ยังไม่พอ
หัวข้อที่มีชื่อว่า “รอบที่ 2: “ทำงานได้” ยังไม่พอ”สมมติคุณสั่ง AI ว่า:
“เรียงลิสต์รหัสธุรกรรมหนึ่งล้านรายการนี้ให้หน่อย”
AI อาจตอบแบบนี้:
def sort_transactions(ids): n = len(ids) for i in range(n): for j in range(n - 1): if ids[j] > ids[j + 1]: ids[j], ids[j + 1] = ids[j + 1], ids[j] return idsโค้ดนี้ “ถูก” — สุดท้ายมันจะได้ลิสต์ที่เรียงแล้วแน่นอน แต่ลองดูตารางจากหัวข้อประสิทธิภาพด้านบน: ที่ n = 1,000,000 bubble sort แบบ O(n²) ต้องใช้การเปรียบเทียบระดับ หนึ่งล้านล้าน ครั้ง บนฮาร์ดแวร์จริงนั่นคือหลักนาทีถึงหลักชั่วโมง และถ้าอยู่ใน request handler ที่มี timeout มันจะไม่มีวันตอบกลับเลย AI เพิ่งประมวลผลให้ “ได้คำตอบที่ถูก” แต่ไม่เคยถามว่า “ด้วยต้นทุนเท่าไหร่” — เพราะพรอมต์ไม่เคยถาม
เฉลย
พรอมต์ที่กระชับแล้ว:
“เรียงลิสต์รหัสธุรกรรม 1,000,000 รายการนี้ ใช้ขั้นตอนวิธีที่เป็น
O(n log n)หรือเร็วกว่า ห้ามเขียน quadratic sort เอง ให้ใช้ฟังก์ชันเรียงในตัวของภาษา”
ผลลัพธ์ที่ถูกขอบเขต:
def sort_transactions(ids: list[int]) -> list[int]: return sorted(ids)ฟังก์ชัน sorted() ในตัวของ Python ใช้อัลกอริทึม Timsort ซึ่งกรณีแย่สุดคือ O(n log n) และเขียนด้วยภาษา C ภายใน — เร็วกว่าลูปที่เขียนเองทั้งในแง่ขั้นตอนวิธีและค่าคงที่จริง
บทเรียน: การตัดสินโค้ดจาก AI ไม่ใช่แค่ “ได้ค่าที่ถูกต้องกับเทสต์ของฉันไหม” แต่ต้องถามว่า “Big-O ของมันคืออะไร และ Big-O นั้นจะรอดไหมเมื่อเจอ n จริง” คำตอบที่ถูกต้องแต่เป็น quadratic สำหรับปัญหาระดับล้านแถว คือบั๊กที่แค่ยังไม่มีใครโดนผลกระทบเท่านั้นเอง
🎮 เกมเดฟ: แตกปัญหา AI ศัตรูด้วย sense → decide → act
หัวข้อที่มีชื่อว่า “🎮 เกมเดฟ: แตกปัญหา AI ศัตรูด้วย sense → decide → act”ศัตรูทุกตัวในเกม — ตั้งแต่ Goblin ตัวจิ๋วไปจนถึงบอสตัวสุดท้าย — วนลูปการคิดเชิงคำนวณแบบเดียวกันทุกเฟรม คือ รับรู้สภาพแวดล้อม (sense) → ตัดสินใจ (decide) → ลงมือทำ (act) ถ้ายัดทั้งสามขั้นตอนไว้ในฟังก์ชันเดียว จะได้โค้ดที่ทดสอบไม่ได้ ต่อยอดไม่ได้ และใช้ซ้ำกับศัตรูตัวใหม่ไม่ได้เลย นี่คือเหตุผลที่ AI ในเกมจริงเกือบทุกสตูดิโอถูกแตกออกเป็นไพพ์ไลน์ sense → decide → act ที่เรากำลังจะสร้างต่อไปนี้
ตัวอย่าง: AI แบบยุ่งเหยิงเทียบกับไพพ์ไลน์ที่แตกปัญหาแล้ว
def enemy_update_naive(enemy: dict, player: dict) -> dict: dx, dy = enemy["x"] - player["x"], enemy["y"] - player["y"] dist = (dx * dx + dy * dy) ** 0.5 toward = 1 if enemy["x"] < player["x"] else -1 if dist < 5: if enemy["hp"] < 20: enemy["x"] -= toward # หนี: สวนทางผู้เล่น else: enemy["x"] += toward # ไล่ล่า: เข้าหาผู้เล่น else: enemy["x"] += enemy["patrol_dir"] # เดินลาดตระเวน if enemy["x"] in (enemy["patrol_min"], enemy["patrol_max"]): enemy["patrol_dir"] *= -1 return enemyฟังก์ชันเดียวทำทุกอย่างปนกัน: คำนวณระยะห่าง (sense) ตัดสินใจว่าจะหนี ไล่ล่า หรือลาดตระเวน (decide) แล้วขยับตำแหน่งจริง (act) ถ้าจะเพิ่มศัตรูชนิดใหม่ที่มีกฎตัดสินใจต่างออกไป (เช่น Archer ที่ยิงธนูแทนที่จะเดินเข้าหา) ต้องไปแก้ if/else ก้อนเดียวกันนี้ซ้ำ ๆ และไม่มีทางเทสต์ “มันตัดสินใจถูกไหม” แยกจาก “มันขยับถูกไหม” เพราะสองเรื่องนี้ปนกันอยู่ในฟังก์ชันเดียว
ลองแตกปัญหาใหม่ตามสี่เสาหลัก:
def sense(enemy: dict, player: dict) -> dict: dx, dy = enemy["x"] - player["x"], enemy["y"] - player["y"] return { # นามธรรม: เก็บเฉพาะสิ่งที่ decide() ต้องใช้ "dist": (dx * dx + dy * dy) ** 0.5, "low_hp": enemy["hp"] < 20, "toward": 1 if enemy["x"] < player["x"] else -1, }
def decide(percept: dict) -> str: if percept["dist"] < 5: return "flee" if percept["low_hp"] else "chase" return "patrol"
def act(enemy: dict, action: str, percept: dict) -> dict: if action == "flee": enemy["x"] -= percept["toward"] elif action == "chase": enemy["x"] += percept["toward"] else: enemy["x"] += enemy["patrol_dir"] if enemy["x"] in (enemy["patrol_min"], enemy["patrol_max"]): enemy["patrol_dir"] *= -1 return enemy
def enemy_update(enemy: dict, player: dict) -> dict: percept = sense(enemy, player) # 1. รับรู้ action = decide(percept) # 2. ตัดสินใจ return act(enemy, action, percept) # 3. ลงมือทำสังเกตสี่เสาหลักทำงานพร้อมกัน: การแตกปัญหา แยกฟังก์ชันเดียวออกเป็นสามฟังก์ชันที่รับผิดชอบคนละหน้าที่; นามธรรม ทำให้ percept เก็บแค่ dist, low_hp, toward — ตัด hp จริง ตำแหน่ง x,y จริง sprite แอนิเมชันทิ้งไปทั้งหมดเพราะ decide() ไม่จำเป็นต้องรู้; การรู้จำแบบรูป ทำให้ศัตรูชนิดอื่นอย่าง Archer หรือ Ogre ใช้ sense() กับ act() ชุดเดียวกันได้ทันที เปลี่ยนแค่กฎใน decide(); การออกแบบขั้นตอนวิธี คือลำดับตายตัว sense → decide → act ที่ enemy_update() เรียกทุกเฟรมโดยไม่มีวันสลับลำดับ
รูป: ไพพ์ไลน์ sense → decide → act ที่วนซ้ำทุกเฟรม
ทีนี้ลองขยายปัญหา: ถ้ามีทั้ง Hero, Archer, Golem, Ogre ต้องลงมือในเกมเดียวกัน (เช่นเกมแนวเทิร์นเบส) จะรู้ได้อย่างไรว่า “ตาใคร” ก่อน นี่คือนามธรรมอีกครั้ง — ตัดทุกอย่างของหน่วยนั้นทิ้งจนเหลือแค่ค่าเดียวคือ ความเร็ว (speed) แล้วปัญหาที่เหลือก็กลายเป็นการออกแบบขั้นตอนวิธีล้วน ๆ: ใครมีเวลาลงมือ (next) น้อยที่สุด คนนั้นได้ตาก่อน แล้วเลื่อนเวลาของเขาออกไปอีก 100 / speed — นี่คือคิวลำดับความสำคัญ (priority queue) แปลงร่างมาเป็นกลไกเกม ลองกดปุ่มด้านล่างดูว่าใครได้ตาก่อนใคร
แบบฝึกหัดเกม
เกม 1
บอสตัวหนึ่งมี 3 เฟส: ก้าวร้าว (aggressive) → ตั้งรับ (defensive) → บ้าคลั่ง (enrage) โดยสลับเฟสตาม % เลือดที่เหลือ (100-66%, 65-33%, ต่ำกว่า 33%) จงแตกปัญหานี้ให้เข้ากับไพพ์ไลน์ sense → decide → act ด้านบน: sense() ต้องรายงานอะไรเพิ่มที่มันไม่เคยรายงานมาก่อน และ decide() ต้องตัดสินใจกี่ชั้น
เฉลย
sense() ต้องเพิ่ม hp_percent เข้าไปใน percept นอกเหนือจาก dist และ low_hp เดิม decide() ต้องตัดสินใจสองชั้น: ชั้นแรกเลือก “เฟส” จาก hp_percent (aggressive/defensive/enrage) ชั้นที่สองเลือก “action” ภายในเฟสนั้นจาก percept อื่น ๆ (เช่น aggressive→chase, defensive→flee หรือบล็อก, enrage→ท่าพิเศษ) ส่วน act() ไม่ต้องแก้เลยเพราะมันแค่รับ action ที่ตัดสินใจมาแล้วไปทำให้เกิดขึ้นจริง — นี่คือประโยชน์ของการแตกปัญหา: ความซับซ้อนของเฟสเพิ่มเข้าไปที่ decide() เพียงจุดเดียว ไม่กระทบ sense()/act() เลย
เกม 2
dict สถานะศัตรูจริงในเกมมีฟิลด์เยอะกว่านี้มาก: x, y, hp, max_hp, sprite_id, animation_frame, loot_table, sound_on_hit, patrol_dir, patrol_min, patrol_max จากไพพ์ไลน์ sense→decide→act ด้านบน ฟิลด์ไหนที่ percept “จำเป็นต้องเก็บ” และฟิลด์ไหน “ตัดทิ้งได้”
เฉลย
จำเป็นสำหรับสร้าง percept: x, y (คำนวณ dist และ toward) กับ hp (คำนวณ low_hp) เท่านั้น ตัดทิ้งได้จากมุมมอง decide(): max_hp, sprite_id, animation_frame, loot_table, sound_on_hit — ล้วนเป็นเรื่องการแสดงผลหรือรางวัล ไม่เกี่ยวกับการตัดสินใจหนีหรือไล่ล่าเลย ส่วน patrol_dir, patrol_min, patrol_max จำเป็นสำหรับ act() แต่ไม่จำเป็นสำหรับ decide() — สังเกตว่านามธรรมไม่ใช่ค่าเดียวสำหรับทั้งไพพ์ไลน์ แต่ละฟังก์ชันมี “สิ่งที่จำเป็น” ของตัวเอง
เกม 3
สตูดิโอหนึ่งมีคลาส GoblinAI, ArcherAI, OgreAI แยกกันสามคลาส แต่ละคลาสมีเมท็อด update() ของตัวเองที่คำนวณระยะห่างใหม่ทุกครั้งและมี if dist < 5: ... else: ... เกือบเหมือนกันทุกตัวอักษร ต่างกันแค่ตัวเลขกับท่าโจมตี จงบอกว่า pattern อะไร “หายไป” จากโค้ดชุดนี้ และควรแก้อย่างไร
เฉลย
Pattern ที่หายไปคือไพพ์ไลน์ sense → decide → act ที่ใช้ซ้ำได้ ทั้งสามคลาสคัดลอกโค้ดคำนวณระยะห่าง (sense) และโค้ดขยับตำแหน่ง (act) เกือบทุกตัวอักษร ทั้งที่ควรใช้ sense() และ act() ชุดเดียวกันร่วมกันทั้งหมด ต่างกันแค่กฎใน decide() (เช่นส่งฟังก์ชัน decide ที่ต่างกันเข้าไปให้แต่ละคลาส หรือ override เฉพาะ decide()) การแก้คือแยก decide() ออกมาเป็นจุดเดียวที่อนุญาตให้ต่างกันได้ ส่วน sense()/act() เขียนครั้งเดียวใช้ร่วมกันทั้งสามคลาส ลดโค้ดซ้ำและลดจุดที่บั๊กจะหลุดรอด
เกม 4
จากวิดเจ็ต <algo-turn-queue> ด้านบน — Hero ความเร็ว 12, Archer ความเร็ว 16, Golem ความเร็ว 6, Ogre ความเร็ว 9 — เวลาที่แต่ละตัวจะลงมือครั้งแรกคือ 100/speed และหลังลงมือแต่ละครั้งเวลานั้นจะถูกบวกเพิ่มด้วย 100/speed อีกครั้ง จงไล่ด้วยมือว่าถ้ากดปุ่ม “เทิร์นถัดไป” สามครั้งติดกัน ใครลงมือเป็นลำดับที่ 1, 2, 3
เฉลย
เวลาลงมือเริ่มต้น (จาก 100/speed): Hero=8.33, Archer=6.25, Golem=16.67, Ogre=11.11
ครั้งที่ 1: ค่าน้อยสุดคือ Archer (6.25) → ลงมือ แล้วเลื่อนเป็น 6.25+6.25=12.5 ครั้งที่ 2: ค่าน้อยสุดตอนนี้คือ Hero (8.33) → ลงมือ แล้วเลื่อนเป็น 8.33+8.33=16.67 ครั้งที่ 3: ค่าน้อยสุดตอนนี้คือ Ogre (11.11) → ลงมือ แล้วเลื่อนเป็น 11.11+11.11=22.22
ลำดับคือ Archer → Hero → Ogre สังเกตว่า Golem (ช้าสุด) ยังไม่ได้ลงมือเลยแม้ผ่านไป 3 เทิร์น — นี่คือพฤติกรรมของคิวลำดับความสำคัญที่ดึงค่าน้อยสุดออกมาเสมอ หน่วยที่เร็วกว่าจะได้ลงมือถี่กว่าเป็นธรรมชาติของสูตร โดยไม่ต้องเขียนกฎพิเศษเพิ่มเลย
ปัญหาท้าทาย
ปัญหาท้าทาย 1 — แตกทั้ง game loop
ไม่ใช่แค่ AI ศัตรู แต่ game loop ทั้งเกม (input → update → collision → render) ก็แตกปัญหาได้แบบเดียวกัน จงร่างว่าแต่ละสเตจควรเป็นฟังก์ชันที่รับอะไรและคืนอะไร และอธิบายว่าทำไม “เส้นแบ่งนามธรรม” (abstraction barrier) ระหว่างสเตจถึงสำคัญ เช่น ทำไม render ไม่ควรแก้ state ของเกม
แนวทาง
ร่างแบบง่าย: actions = read_input() (input ไม่ควรรู้เรื่อง physics เลย) → state = update(state, actions, dt) (รวม AI ทุกตัวที่เป็น sense→decide→act ของตัวเอง คืน state ใหม่ ไม่วาดอะไรทั้งสิ้น) → state = resolve_collisions(state) (รับเฉพาะตำแหน่ง/ฮิตบ็อกซ์ ไม่รู้เรื่อง sprite) → render(state) (อ่านอย่างเดียว ห้ามแก้ state) เหตุผลที่ต้องมีเส้นแบ่งนามธรรม: ถ้า render แก้ state ได้ พฤติกรรมของเกมจะขึ้นกับว่าจอกำลังวาดตอนไหน ทำให้ผลลัพธ์ไม่ deterministic (รันซ้ำแล้วได้ผลไม่เหมือนเดิม) และเทสต์ก็เขียนไม่ได้เพราะต้องมีจอจริงถึงจะเห็นผล การบังคับให้แต่ละสเตจ “รู้แค่ที่จำเป็น” คือสิ่งที่ทำให้ทดสอบ update() ได้โดยไม่ต้องเปิดหน้าต่างเกมเลยด้วยซ้ำ
ปัญหาท้าทาย 2 — AI ยามในเกมแนวลอบเร้น
ออกแบบ AI ของยาม (guard) สำหรับเกมแนวลอบเร้น (stealth) ที่ sense() ต้องอ่านทั้ง “ระดับเสียง” และ “ระดับแสง” รอบตัวผู้เล่น ไม่ใช่แค่ระยะห่าง และมียามสามแบบ (ยามลาดตระเวนธรรมดา, ยามเฝ้าระวังสูง, ยามหัวหน้า) ที่ต้องใช้ sense()/act() ชุดเดียวกันแต่ decide() ต่างกัน จงร่างโครงสร้าง percept และบอกว่าเสาหลักไหนทำให้ออกแบบแบบนี้ใช้ซ้ำได้ข้ามยามสามแบบ
แนวทาง
ร่าง percept: {"noise_level": float, "light_level": float, "player_visible": bool, "dist_to_last_seen": float} — เก็บเฉพาะสัญญาณที่ decide() ทุกแบบต้องใช้ ไม่เก็บว่าค่าพวกนี้คำนวณมาจาก raycast กี่เส้นหรือ noise เกิดจากเสียงฝีเท้าหรือเสียงแก้วแตก (รายละเอียดของเซนเซอร์ซ่อนอยู่ใน sense() ทั้งหมด) เสาหลักที่ทำให้ยามสามแบบใช้ sense()/act() ร่วมกันได้คือ การรู้จำแบบรูป (ทั้งสามแบบมีโครงสร้าง “ตรวจจับ→ตัดสิน→ขยับ” เหมือนกันทุกตัว) ทำงานร่วมกับ นามธรรม (percept ซ่อนรายละเอียดของเซนเซอร์จาก decide() จนแม้ยามหัวหน้าที่มีเซนเซอร์ไวกว่าก็ยังส่ง percept รูปแบบเดียวกันให้ decide() ได้) ทำให้ decide() ของแต่ละแบบต่างกันแค่ threshold/กฎ โดยไม่ต้องเขียน sense()/act() ใหม่เลยสักตัว
เจาะลึกเพิ่มเติม
หัวข้อที่มีชื่อว่า “เจาะลึกเพิ่มเติม”- Jeannette M. Wing, “Computational Thinking” (2006) — บทความสั้นต้นกำเนิดที่ทำให้คำนี้แพร่หลาย อ่านได้ที่ Communications of the ACM, Vol. 49, No. 3
- Cormen, Leiserson, Rivest & Stein, Introduction to Algorithms (4th ed.), บทที่ 1 — นิยามที่เป็นทางการของคำว่า “algorithm” ซึ่งเป็นฐานของกรอบ “สูตรอาหาร” ที่บทเรียนนี้ใช้ อ่านบทนี้เพื่อดูเวอร์ชันที่เข้มงวดของ “ขั้นตอนวิธีคือกระบวนการที่ถูกกำหนดไว้ชัดเจน”
- Skiena, The Algorithm Design Manual (3rd ed.) — สร้างสัญชาตญาณ “เจอโจทย์แบบนี้ใช้เทคนิคไหน” ได้ดีที่สุดเมื่อผ่านพื้นฐานมาแล้ว เต็มไปด้วยเรื่องเล่าว่าขั้นตอนวิธีไร้เดียงสาพังตอนเจอข้อมูลขนาดใหญ่จริงอย่างไร
- Bhargava, Grokking Algorithms (2nd ed.) — อธิบายด้วยภาพประกอบเข้าใจง่าย ครอบคลุมการเรียงลำดับแบบต่าง ๆ (bubble, selection, insertion) และแนวคิด Big-O ที่ใช้ในบทเรียนนี้พอดี — เป็นเพื่อนคู่ใจที่ดีของวิดเจ็ตด้านบน
- Harvard CS50 — Week 0 — วิดีโอบรรยายเปิดคอร์สที่อธิบายการคิดเชิงคำนวณและอัลกอริทึมด้วยตัวอย่างสมุดโทรศัพท์ (cs50.harvard.edu)
- MIT 6.0001 — Introduction to Computer Science and Programming in Python — สัปดาห์แรกพูดถึงการคิดเชิงคำนวณก่อนเข้าสู่การเขียนโปรแกรม (MIT OpenCourseWare)
- Stanford CS106A — Programming Methodology — เน้นการแตกปัญหาและการออกแบบขั้นตอนวิธีเป็นฐานก่อนเขียนโค้ด (Stanford Online)

