February 19, 2024
|
12 mins read

“การเปลี่ยนมุมมอง” กับอัลกอริทึม Sieve of Eratosthenes

ในหลายโอกาส การแก้ปัญหา require การเปลี่ยนมุมมอง แต่การเปลี่ยนมุมมองคืออะไร ? ทำความเข้าใจไปด้วยกันผ่านปัญหาการหาค่าจำนวณเฉพาะ (prime number)

“การเปลี่ยนมุมมอง” กับอัลกอริทึม Sieve of Eratosthenes

หลาย ๆ ครั้งเราน่าจะเคยได้ยินหลายเรื่องราวเกี่ยวกับการแก้ปัญหาและการเปลี่ยนมุมมอง ที่การปรับมุมมองใหม่ ๆ การที่มองปัญหาในอีกด้าน อาจนำไปสู่วิธีแก้ปัญหาที่ดีกว่าเดิม ยกตัวอย่างเช่น quote ของ Henry Ford ที่หลายคนคุ้นเคย

“If I had asked people what they wanted, they would have said faster horses.” — Henry Ford

การพยายาม optimize solution เดิมให้ดีขึ้น (faster horses) ไม่ได้หมายความว่ามันจะนำมาสู่ solution ที่ดีที่สุด (car) เสมอไป ซึ่งเรามักจะได้เห็นเรื่องราวการแก้ปัญหาแบบพลิกมุมมองแบบนี้เกิดขึ้นอยู่บ่อย ๆ โดยเฉพาะในวงการของการทำธุรกิจ

Peter Thiel หนึ่งใน PayPal Mafia เคยบอกว่าธุรกิจที่แก้ปัญหาบนมุมมองใหม่อย่างสิ้นเชิงแบบนี้ คือธุรกิจที่เรียกว่า “Zero To One” (ซึ่งหลายครั้งนำมาสู่ technology ที่หน้าตาแทบไม่เหมือน solution เดิมเลย) ในขณะที่ธุรกิจที่พยายามทำของเดิมให้ดีขึ้นหรือเอาอะไรที่มัน work อยู่แล้วมาทำต่อ คือธุรกิจที่เรียกว่า “One To N”

แน่นอนว่าการเปลี่ยนมุมมองของปัญหาแบบนี้ มันไม่ได้ใช้ได้กับแค่การทำธุรกิจ แต่มันคือ problem solving in general เลย ทั้งในวงการ math, computer science หรือ field อื่น ๆ ไม่ว่าอะไรก็ตาม ส่วนตัวเลยรู้สึกว่าเรื่องนี้เป็นเรื่องสำคัญที่เราควรหยิบมาพูดคุยกัน

แต่ว่าคำถามคือ การจะเปลี่ยนมุมมองมันต้องทำยังไง? กระบวนการของมันคืออะไร? แล้วการเปลี่ยนมุมมองจริง ๆ แล้วมันคืออะไรกันแน่ บทความนี้เลยอยากมาชวนหาคำตอบกัน

จำนวนเฉพาะตั้งแต่ 1 ถึง n

เพื่อให้เห็นภาพมากขึ้น ในบทความนี้เราจะลองยกตัวอย่างปัญหาง่าย ๆ มาแก้กันครับ

“กำหนดให้ n เป็นจำนวนเต็มบวกที่มีค่ามากกว่า 1 ขึ้นไป จงหาจำนวนเฉพาะ (prime number) ทั้งหมดตั้งแต่ 2 ถึง n”

ถึงตรงนี้เราอยากให้ทุกคนลองใช้เวลา 2 - 3 นาทีคิด solution ในการแก้ปัญหาดู แน่นอนว่าไม่จำเป็นต้องเป็น algorithm ที่มีประสิทธิภาพสูงที่สุดก็ได้ อาจจะลองคิดเล่น ๆ ว่าถ้าเราจะหา prime number ทั้งหมดตั้งแต่ 1 ถึงสักตัวเลข (เช่น 50) เราจะใช้วิธีไหนหาคำตอบออกมา

Naive Solution

ถ้าเราคิดอะไรไม่ออกวิธีการนึงที่เราสามารถทำได้ก็คือ การเริ่มคิดจากวิธีการที่ง่าย ตรงไปตรงมา และ obvious ที่สุดก่อน เรามักจะเรียก solution แบบนี้ว่า “Naive solution”

เช่นเราอาจจะเริ่มจากการไล่เช็คไปทีละเลขตั้งแต่ 2 ถึง \(n\) ว่าเลขนั้นเป็น prime number หรือไม่; เพื่อความง่าย เราจะตั้งชื่อให้เลขที่เราไล่เช็คว่า \(x\) โดยที่ \(2 \le x \le n\)

ถ้า \(x\) เป็น prime number ก็จดเก็บเอาไว้เป็นคำตอบ และเมื่อเราเช็ค \(x\) ครบทุกตัวแล้ว prime number ทั้งหมดก็คือที่เราจดเอาไว้

“For each x in the range 2 to n, check if x is prime.”

ส่วนวิธีการเช็คว่า \(x\) เป็น prime หรือไม่นั้น เราก็อาจจะมาไล่ดูว่าตั้งแต่เลข 1 จนถึง \(x\) มีเลขไหนที่หาร \(x\) ลงตัวไหม (ที่ไม่ใช่ 1 และ \(x\)) ถ้ามีเราก็สรุปได้เลยว่า \(x\) ไม่ใช่ prime number

16 มีตัวที่หารลงตัวคือ 1, 2, 4, 8, 16; ดังนั้น 16 ไม่ใช่ prime number

7 มีตัวที่หารลงตัวคือ 1, 7; สรุปได้ว่า 7 คือ prime number

ถ้าเราลองทำ analysis เพื่อเช็ค cost (หรือจำนวน operation) ในการรัน algorithm นี้คร่าว ๆ เราจะพบว่า การเช็คว่า \(x\) เป็น prime number หรือไม่นั้น ในกรณีที่แย่ที่สุดคือ \(x\) เป็น prime number เราต้องลองไล่หารจำนวนทั้งหมดตั้งแต่ 1 ถึง \(x\) ดังนั้นเราสรุปคร่าว ๆ ได้ว่า

Cost(isPrime(x))x\text{Cost}(\text{isPrime}(x)) \approx x

และในเมื่อเราต้องเช็ค prime number ทั้งหมดตั้งแต่ 2 ถึง \(n\) (ประมาณ​ \(n\) ครั้ง); ดังนั้น Cost ทั้งหมดโดยประมาณก็คือ

Cost=Cost(isPrime(2))+...+Cost(isPrime(n))\text{Cost} = \text{Cost(isPrime(2))} + ... + \text{Cost(isPrime(n))}
Cost2+3+...+nn(n+1)2=n2+n2\text{Cost} \approx 2 + 3 + ... + n \approx \frac{n(n + 1)}{2} = \frac{n^2 + n}{2}

ทั้งนี้เราไม่จำเป็นต้องคำนวณ operation cost ออกมาเป็ะ ๆ ก็ได้ เพราะจุดประสงค์ของการทำ analysis คือเราต้องการดูว่า algorithm ที่คิดมามันตอบโจทย์กับปัญหาไหม รวมถึงการนำไปเปรียบเทียบกับ algorithm อื่น ๆ ด้วย

ดังนั้นสิ่งที่สำคัญกว่าจำนวน cost เป็ะ ๆ คือการเติบโตของ cost “โดยประมาณ” โดยเฉพาะเมื่อ input (หรือในที่นี้คือค่า \(n\)) มีค่ามาก ๆ

ซึ่งเราสามารถใช้ mathematical notation ตัวหนึ่งที่ชื่อว่า “Big O Notation” ในการ express สิ่งที่ว่ามานี้ออกมาได้ ว่าในการรัน algorithm นี้มี Cost (หรือที่เรียกว่า Time Complexity) เป็น \(O(n^2)\) (เนื่องจากเราสนแค่การเติบโตเมื่อ input มีค่ามาก ๆ ดังนั้นพจน์ที่ไม่ significant เลยตัดออกไปได้)

อ่านต่อเรื่องการคำนวน Time complexity และ Big O Notation ได้ที่คอร์ส Data Data Structures and Algorithms Fundamentals, Chapter 2 เข้าสู่เนื้อหา

มีวิธีการที่ดีกว่านี้ไหม ?

แน่นอนว่าการจะตอบคำถามนี้ไม่ได้ง่าย แต่คำถามหนึ่งที่ตรงไปตรงมาที่จะถามมากที่สุดก็คือ “จาก solution ที่เรามีอยู่ตอนนี้ เราสามารถทำให้มันดีขึ้นได้ไหม ?”

สำหรับการเช็คว่า \(x\) เป็น prime number หรือไม่นั้น เราอาจจะสังเกตว่าในกรณีที่ \(x\) ไม่ได้เป็น prime number เราเช็คไปแค่ไม่กี่ค่าก็เจอค่าที่หารลงตัวตัวแรกแล้ว เช่น 16 เราเจอ 2 ก็รู้แล้วว่า \(x\) ไม่ใช่ prime หรือ 49 เราเจอ 7 ก็ไม่ต้องไล่หารต่อแล้ว

คำถามคือเพื่อที่จะเจอค่าที่หารลงตัวแรก เราจำเป็นต้องไล่เช็คไปเรื่อย ๆ จนถึง \(x\) ไหม ? หรือถ้าตั้งคำถามในทางกลับกัน

“เราต้องไล่ไปถึงเท่าไหร่ถึงจะเจอตัวที่หารลงตัวตัวแรก และถ้าหากว่าเราไม่เจอค่านั้น เราการันตีได้เลยไหมว่าจะไม่มีค่าที่หาร \(x\) ลงตัวอีกแล้ว (หรือสรุปได้ว่า \(x\) เป็น prime number)”

ถ้าหากว่าเราลองไล่หาตัวที่หาร \(x\) ลงตัวออกมาดูทั้งหมดเราจะพบว่า ถ้าเราหาค่าที่หารลงตัวได้ค่าหนึ่ง เราจะหาอีกคู่ที่หารลงตัวได้ด้วย เช่น

16 มีตัวที่หารลงตัวคือ 1, 2, 4, 8, 16; ถ้าเราลองไล่หารเราจะพบว่าเมื่อเจอ 1 เราจะรู้ว่า 16 ก็หารลงตัวด้วย เมื่อเจอ 2 จะเจอ 8 และเมื่อเจอ 4 อีกคู่นึงก็คือ 4

x=16;(1,16),(2,8),(4,4) x=100;(1,100),(2,50),(4,25),(5,20),(10,10); x=23;(1,23)x = 16; (1, 16), (2, 8), (4, 4) \\ \text{ } \\ x = 100; (1, 100), (2, 50), (4, 25), (5, 20), (10, 10); \\ \text{ } \\ x = 23; (1, 23)

จากการจับคู่ด้านบนเราจะเห็นว่า pattern ว่า \(x\) จะเกิดจากค่า \(a\) และ \(b\) คูณกันเสมอ

x=a×bx = a \times b

เมื่อเราเขียนออกมาแบบนี้ เราอาจจะสังเกตต่อว่า ถ้าหากว่าเราเพิ่มค่า \(a\); ค่า \(b\) จะลดลง และในทางกลับกัน ถ้าเพิ่ม \(b\); ค่า \(a\) ก็จะลด

ดังนั้นถ้าเราให้ค่า \(a\) เป็นค่าฝั่งน้อย และค่า \(b\) เป็นค่าฝั่งที่มาก ค่า \(a\) ที่มากที่สุดที่เป็นไปได้ก็คือกรณีที่ \(a = b\) (เพราะถ้า a มีค่ามากกว่านี้ค่า a ก็จะกลายเป็นฝั่งมาก และค่า b กลายเป็นฝั่งที่น้อยแทน)

สรุปในกรณีที่ \(a = b\) ก็คือ \(x = a \times b = a \times a = a^2\) ดังนั้นเราเลยสรุปได้ว่า \(a = \sqrt{x}\)

สรุปก็คือเราไล่หารถึง \(\sqrt{x}\) ก็พอแล้ว (ไม่จำเป็นต้องไล่ไปถึง \(x\)) ถ้าหากว่าไม่เจอค่าที่หารลงตัวในช่วงนี้ เราก็สรุปได้ทันทีว่า \(x\) เป็น prime number

Time Complexity ของ algorithm โดยรวมคือ \(O(n \sqrt{n})\)

Sieve of Eratosthenes

อีก algorithm ที่เราอยากเอามานำเสนอคือ Sieve of Eratosthenes เป็น algorithm ที่ถูกคิดมา 2000 กว่าปีแล้วโดยชาวกรีกคนหนึ่งที่ชื่อว่า Eratosthenes

นอกจากนี้ Eratosthenes ยังมีผลงานอื่น ๆ เยอะมาก อีกผลงานนึงที่ดัง ๆ ก็คือการคำนวณเส้นรอบวงของโลกจากการสังเกตของเงา ที่ผลลัพธ์คลาดเคลื่อนไปน้อยมาก ๆ เมื่อเทียบกับการคำนวณด้วยเทคโนโลยีปัจจุบัน

ก่อนอื่นเราอาจจะต้องโยน solution เก่าที่เราเคยมีทิ้งไปก่อน แล้วย้อนกลับมามอง มาตั้งคำถามที่ปัญหาของเราอีกครั้ง

“กำหนดให้ n เป็นจำนวนเต็มบวกที่มีค่ามากกว่า 1 ขึ้นไป จงหาจำนวนเฉพาะ (prime number) ทั้งหมดตั้งแต่ 2 ถึง n”

เราอาจจะเริ่มจากการจำลอง n ขึ้นมาตัวนึง ลองเขียนคำตอบที่เป็นไปได้ออกมาดู ลองเริ่มสังเกตและจดในสิ่งที่เห็นแบบง่าย ๆ เช่น เราอาจจะพบว่าคำตอบที่เป็น prime number ไม่มีเลขคู่เลยยกเว้น 2 เราอาจจะลองตั้งคำถามต่อว่าทำไมถึงเป็นอย่างนั้น หรือเราอาจจะลองกลับมาที่ปัญหาและถามว่า prime number คืออะไร

ทั้งหมดนี้ก็เพื่อทำให้ปัญหามันชัดมากขึ้น เพื่อที่เราจะสังเกตและ reasoning ต่อไปได้ง่ายขึ้น

“A prime number is a natural number greater than 1 that is not a product of two smaller natural numbers.”

เราอาจจะสังเกตว่า prime number มันคือจำนวนที่เป็นรากฐานของจำนวนอื่น ๆ (prime มาจากคำว่า primus ที่แปลว่า “first”) เราสามารถใช้ prime number (กำหนดให้เป็น \(p\)) สร้างจำนวนอื่น ๆ ที่ไม่ใช่ prime number ได้เช่น \(2p, 3p, 4p, …\)

จาก observation นี้เราสามารถคิดต่อได้ว่า จะเกิดอะไรขึ้นถ้าหากว่าเราตัดค่าที่ไม่ใช่ prime number ออกให้หมดจนถึง n โดยการนำ \(p\) มาคูณเลขไปเรื่อย ๆ

เราอาจจะลองสร้าง algorithm ขึ้นมา โดยการที่เราจะเริ่มจาก prime number ตัวแรก (ก็คือ 2) จากนั้นตัดค่า \(2p, 3p, 4p, …\) ออกให้หมดจนถึง \(n\) จากนั้นตัวถัดไปที่ยังไม่ถูกตัดก็คือ prime number ตัวถัดไป จากนั้นก็ตัดค่าที่ไม่ใช่ออกเหมือนเดิม ทำแบบนี้ไปเรื่อย ๆ จนกระทั้งไม่เหลือ prime number ที่น้อยกว่า \(n\) แล้ว

Sieve Of Eratosthenes เป็นหนึ่งใน algorithm ยอดฮิตที่ใช้ benchmark microcomputer ในยุค 1980s ซึ่ง computer ส่วนใหญ่ในยุคนั้นยังมี memory ไม่มาก (ประมาณ 64 - 128 KB) อ่านเพิ่มเติม

ลองกดเล่น Sieve Of Eratosthenes แบบ step by step ด้านล่างได้เลยครับ ⤵

Sieve Of Eratosthenes

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
1 / 65
กำหนดให้ n = 90; Initialize array ขนาด n + 1; กำหนดให้ค่าทุกค่ามีค่าเป็น true ยกเว้น 0 และ 1 (ค่อยมาตัดเลขที่ไม่ใช่ prime ออกทีหลัง)

Time Complexity: \(O(n\log\log{n})\)

ขยายความ: ที่บอกว่า “ตัวถัดไปที่ไม่ถูกตัดก็คือ prime number” เพราะว่า การที่ค่าใดถูกตัดออกไปแล้วมันหมายความว่าค่านั้น มีค่าที่น้อยกว่าตัวมันที่หารมันลงตัว ดังนั้นการที่ไม่ถูกตัดมันหมายความว่า ไม่มีมีค่าที่น้อยกว่าตัวมันที่หารมันลงตัว ดังนั้นเราเลยสรุปได้ว่า “ตัวถัดไปที่ไม่ถูกตัดก็คือ prime number”

การเปลี่ยนมุมมองคืออะไร ?

เราจะไม่สามารถคิด algorithm แบบ Sieve of Eratosthenes ออกได้เลยถ้าหากว่าเราไม่โยน naive solution ที่เราเคยมีทิ้งไป แล้วกลับมาทำความเข้าใจกับปัญหาอีกครั้ง

“สิ่งที่เราเรียกมันว่า naive solution มันก็คือความคุ้นเคย สิ่งที่เราเคยเห็นมันมาก่อน ไอเดียหรือความเชื่อที่เราเคยมีอยู่มาก่อน”

ซึ่งหลายครั้งเรามักจะถูก bias หรือไอเดียที่เคยมีอยู่แล้วพวกนี้บดบังจนทำให้เรามองไม่เห็นความเป็นไปได้อื่น ๆ

เราเรียกอาการที่เรามักจะเอาวิธีแก้ปัญหาที่เราเคยชินมาใช้แก้ปัญหาที่เจอ (ทั้ง ๆ ที่สามารถแก้ด้วยวิธีอื่นที่ดีกว่าได้) ว่า “Eistellung Effect”

เราเลยรู้สึกไม่ได้แปลกใจที่ในบางครั้ง คนที่เป็น beginners อาจจะสามารถหา solution ที่ดี หรือมองเห็นมุมมองใหม่ ๆ ได้มากกว่าคนที่มีประสบการณ์เยอะกว่า (ซึ่งนี่ก็เป็นอีกเหตุผลด้วยที่ทำไมการที่เราทำงานกับคนที่มี background หรือประสบการณ์ที่หลากหลาย อาจจะทำให้เราเห็นความเป็นไปได้ที่มากกว่า)

“In the beginner’s mind there are many possibilities, but in the expert’s there are few” - Shunryu Suzuki, Zen Mind, Beginner’s Mind

สำหรับเราแล้วการเปลี่ยนมุมมองมันคือ “การโยนสิ่งที่เราเคยมีทิ้งไป แล้วกลับมาทำความเข้าใจในปัญหา หรือ “แก่น” ของสิ่ง ๆ นั้นอีกครั้ง”

การตั้งคำถามจะทำให้เรา เข้าใจปัญหา ชัดขึ้นและกว้างขึ้น; การสังเกตจะทำให้เรา เห็น pattern (หรือ insights) ที่ทำให้เราสามารถคิดวิธีแก้ปัญหาที่ดีขึ้น; และการให้เหตุผลจะทำให้เรามองเห็นปัญหาและ insight ที่ลึกมากขึ้น ที่หลายครั้งเราอาจจะมองไม่เห็นเลยถ้าเราดูแบบผิวเผิน

เรารู้สึกว่า 3 อย่างนี้เป็นเครื่องมือที่สำคัญมาก ๆ ที่จะนำพาเราไปสู่มุมมองใหม่ ๆ

Chun Rapeepat

Rapeepat Kaewprasit (Chun)

คน ๆ หนึ่งที่ชื่นชอบในการสร้าง การทำธุรกิจ และการได้ลองทำอะไรใหม่ ๆ ไปเรื่อย ๆ