Showing posts with label Mathematics. Show all posts
Showing posts with label Mathematics. Show all posts

Mar 23, 2016

ใบเฟิร์น Barnsley

ความสวยงามอย่างหนึ่งของใบเฟิร์น คือ รูปทรงของส่วนย่อยๆ ภายในใบจะมีความคล้ายคลึงกับใบเฟิร์นทั้งใบ ภาษาคณิตศาสตร์มีชื่อเรียกพิเศษให้รูปทรงเหล่านี้เลยว่า fractal แต่ถ้าใครคุ้นเคยกับศัพท์สายคอมพิวเตอร์มากกว่า ก็อาจจะมองว่ามันเป็น recursion ก็ได้


การสร้าง fractal รูปใบเฟิร์นอย่างง่าย สามารถอธิบายคร่าวๆ ได้ด้วยขั้นตอนวิธีนี้


  1. ลากเส้นตรงความยาว $l$ ไม่ต้องเยอะมากนัก แล้วเลือกปลายข้างหนึ่งไว้เป็นปลายที่จะต่อยอดออกไป
  2. ที่ปลายยอดของเส้นตั้งต้น ทำสองขั้นตอนต่อไปนี้เพื่อลากเส้นเพิ่ม 3 เส้น
    • ลากเส้นตรงความยาว $0.2$ เท่าของความยาวเส้นตั้งต้น ในทิศทางตั้งฉากกับเส้นตั้งต้นทั้งสองข้าง
    • ลากเส้นตรงความยาว $0.8$ เท่าของความยาวเส้นตั้งต้น ในทิศทางต่อขึ้นไปตรงๆ จากเส้นตั้งต้น
  3. สำหรับแต่ละเส้นที่ลากต่อออกมาในข้อ 2 นั้น เลือกปลายด้านที่ว่างๆ ไว้เป็นปลายต่อยอด แล้ววนกลับไปทำขั้นตอนที่ 2 โดยเปลี่ยนมาใช้เส้นตั้งต้นเป็นเส้นที่ได้จากข้อ 2 แทน (คิดซะว่าเส้นใหม่นี้ยาว $l$) ทำไปเรื่อยๆ จนกว่าจะพอใจผลลัพธ์

วิธีการข้างต้นจะให้ผลลัพธ์เป็นรูปใบเฟิร์นเรียบๆ ตรงๆ ไม่หวือหวานัก แต่ก็มีนักคณิตศาสตร์นามว่า Michael Barnsley ได้เขียนสมการกำหนดมุมและจัดตำแหน่งของการต่อยอดให้สวยงามมีลูกเล่น เนื่องจากเขาเป็นคนแรกที่เขียนเรื่องนี้ไว้ตั้งแต่ปี 1993 ทุกคนเลยพากันเรียกใบเฟิร์นที่สวยงามเป็นเอกลักษณ์นี้ว่า ใบเฟิร์น Barnsley



พูดให้รัดกุมเป็นภาษาคณิตศาสตร์ก็คือ การสร้างใบเฟิร์น Barnsley เริ่มจากลากเส้นแรกยาว $1.6$ หน่วยจากจุด $(0,0)$ ขึ้นไปตามแกน $y$ แล้วเอาเส้นนั้นมาวาดซ้ำๆ อีกครั้ง จากเลื่อนและปรับขนาดด้วยสมการดังนี้


f_1(x, y) = [ 0.85, 0.04; -0.04, 0.85 ] [ x; y ] + [ 0; 1.6 ]
f_2(x, y) = [ 0.20 & -0.26 \\ 0.23 & 0.22 ] [ x; y ] + [ 0; 1.6 ]
f_3(x, y) = [ -0.15, 0.28; 0.26 & 0.24 ] [ x; y ] + [ 0; 0.44 ]

โดย $f_1, f_2, f_3$ คือสมการสำหรับคำนวณรูปร่างก้านที่ต่อยอดขึ้นข้างบน ก้านที่เอียงไปด้านซ้าย และก้านที่พลิกตัวแล้วเอียงไปด้านขวา ตามลำดับ


ผมแนบโค้ดในภาษา Python มาให้ด้วยสำหรับใครที่ต้องการนำไปเล่นต่อ ซึ่งวิธีการที่แนบนี้เป็นการสร้างแบบ recursive ต่อยอดออกไปเรื่อยๆ ตามแนวทางขั้นตอนวิธีที่ได้กล่าวไว้ข้างต้น หากไปค้นดูวิธีอื่นเพิ่มเติมจะพบว่าวิธีการสุ่มเลือกจุดจะให้ผลลัพธ์ที่รวดเร็วกว่า


from PIL import Image, ImageDraw

def transform(matrix, xy):
    return [sum(e * p for e, p in zip(row, xy)) for row in matrix]

def translate(vector, xy):
    return [e + p for e, p in zip(vector, xy)]

class BarnsleyFern(object):
    affines = [([[0.85, 0.04], [-0.04, 0.85]], [0, 1.6]),
               ([[0.20, -0.26], [0.23, 0.22]], [0, 1.6]),
               ([[-0.15, 0.28], [0.26, 0.24]], [0, 0.44])]

    def __init__(self, depth=10, size=(800,800)):
        self.image = Image.new('RGB', size)
        self.draw = ImageDraw.Draw(self.image)
        self.iterate([0.0, 0.0], [0.0, 1.6], depth)

    def canvas_coordinate(self, xy):
        width, height = self.image.size
        return [width/2 + xy[0]*width/10, height - xy[1]*height/10]

    def linespec(self, xy0, xy1):
        return self.canvas_coordinate(xy0) + self.canvas_coordinate(xy1)

    def draw_too_small(self, xy0, xy1, pos=2):
        return all(round(a, pos) == round(b, pos) for a, b in zip(xy0, xy1))

    def iterate(self, xy0, xy1, depth, affine=None):
        if depth == 0:
            return
        if affine is not None:
            xy0 = translate(affine[1], transform(affine[0], xy0))
            xy1 = translate(affine[1], transform(affine[0], xy1))
        if self.draw_too_small(xy0, xy1):
            return
        self.draw.line(self.linespec(xy0, xy1), fill='#0C3')
        for affine in self.affines:
            self.iterate(xy0, xy1, depth-1, affine)



fractal ยังมีรูปร่างอื่นๆ อีกมากมาย ทั้งที่พบได้ในธรรมชาติ เช่น ดอกทานตะวัน บรอคโคลี เกล็ดหิมะ หรือมากจากการสังเคราะห์ขึ้นอย่าง สามเหลี่ยม Sierpinski หรือ เซ็ต Mandelbrot


แต่ความมหัศจรรย์ของ fractal ที่แท้จริง คือเราไม่สามารถรู้ได้เลยว่าตอนนี้เรากำลังอยู่ตรงไหนของมันกันแน่ ลองคิดภาพว่าตัวเองเป็น Ant-Man ที่จะปรับขนาดตัวให้ไม่ใหญ่ไปกว่าก้านใบเฟิร์นที่ยืนอยู่ได้ แล้วก็ลองไปยืนอยู่บนใบเฟิร์นข้างต้นดู ณ ขณะหนึ่งเราอาจจะคิดว่าตัวเองอยู่บนก้านที่ใหญ่ที่สุดแล้ว แต่นอกจากขนาดที่แตกต่างกัน ก้านที่เราคิดว่าใหญ่ที่สุดนั้น ก็ไม่ได้มีอะไรที่แตกต่างจากก้านย่อยก้านอื่นๆ เลย หากเราเดินย้อนกลับไปเรื่อยๆ อาจพบว่าก้านที่เราคิดว่าใหญ่ที่สุดนั้น เป็นเพียงแค่ก้านย่อยที่แตกแขนงแยกออกมาก็เป็นได้


ในทางเดียวกัน เราก็ไม่สามารถแน่ใจได้ว่าตรงไหนคือจุดปลายของใบเฟิร์น เพราะยิ่งเดินค้นหาปลายใบเท่าไหร่ เราก็จะยิ่งตัวเล็กลงๆ และพบว่าใบเฟิร์นนั้นมีรูปร่างเหมือนเดิมไม่เปลี่ยนแปลง


ก็เปรียบเหมือนความรู้ เราจะรู้ได้ไงว่าอะไรคือพื้นฐานที่แท้จริงกันแน พันปีก่อนเราอาจคิดว่าถ้าได้รู้จักอะตอมก็คือรู้ถึงพื้นฐานแห่งทุกสรรพสิ่งแล้ว แต่ไม่กี่ร้อยปีก่อนเราเพิ่งพบว่าอะตอมยังแบ่งแยกย้อยลงไปได้อีกเป็นโปรตอน นิวตรอน อิเล็กตรอน และปฏิรูปพื้นฐานความรู้ด้านเคมีขึ้นใหม่ ส่วนปัจจุบันเรายังแบ่งย่อยสิ่งต่างๆ ลงไปได้อีกเป็นควาร์ก หรือแม้กระทั่งเสนอทฤษฎีสตริงซึ่งเป็นพื้นฐานของทุกอนุภาค


อีกนัยหนึ่ง พื้นฐานความรู้ที่ลึกเกินไปอาจเป็นสิ่งไม่จำเป็น หรือยิ่งไปกว่านั้น มันอาจเป็นตัวถ่วงให้เราไม่กล้าที่จะเดินหน้าสำรวจโลกใหม่ๆ ก็เป็นได้


Mar 14, 2016

มองสมการแล้วรู้สึกเป็นยาขม มองให้เห็นเป็นภาพสิจะได้สนุก

ตอนอยู่ม.ปลาย NutSnC เคยให้หนังสือ Proof Without Words มา คือตอนนั้นเรามองว่าคณิตศาสตร์สนุกอยู่แล้ว แต่พอได้เล่มนี้มาอ่านยิ่งรู้สึกสนุกเข้าไปใหญ่ เลยคิดว่ามันน่าจะช่วยให้ใครที่คิดว่าคณิตศาสตร์ไม่สนุก รู้สึกสนุกไปกับมันได้บ้าง

แนวคิดง่ายๆ ของหนังสือคือโยนสมการที่มีแต่ตัวหนังสือยั๊วเยี๊ยะทิ้งไปเลย แล้วเปลี่ยนมาคิดมันด้วยสิ่งที่จับต้องได้มาขึ้นอย่างรูปภาพแทน



ตัวอย่างหนึ่งในหนังสือ เช่น ต้องการพิสูจน์อนุกรมจำนวนเต็ม

1 + 2 + 3 + ... + n = n(n+1)/2

ก็ให้คิดด้วยลูกแก้วแทน ลูกแก้วแถวแรกมี 1 ลูก แถวที่สองมี 2 ลูก ลงไปเรื่อยๆ จนแถวสุดท้ายที่มี n ลูก

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


แต่ลองให้ใช้สมการอย่างเดียวอธิบายดู จะพบกับความยาวเฟื้อยเช่นนี้

sum  i  for i=1 to n = 1 + 2 + 3 + ... + n
    2 sum  i  for i=1 to n = 2(1 + 2 + 3 + ... + n)
                           = (1 + 2 + 3 + ... + n) + (1 + 2 + 3 + ... + n)
                           = (1 + 2 + 3 + ... + n) + (n + ... + 3 + 2 + 1)
                           = (1+n) + (2+n-1) + (3+n-2) + ... + (n+1)
                           = (n+1) + (n+1) + (n+1) ... + (n+1)
                           = n(n+1)
      sum  i  for i=1 to n = n(n+1)/2

เทียบกันแล้ว การพิสูจน์ด้วยรูปภาพง่ายและสวยงามกว่ากันเยอะเลย



วิธีข้างต้นยังนำไปประยุกต์ใช้กับสมการอื่นๆ ได้อีกมากมาย เช่น

  • อนุกรมจำนวนเต็มคี่
  • อนุกรมจำนวนเต็มยกกำลังสอง
  • อนุกรมจำนวนเต็มยกกำลังสาม
  • อนุกรมเรขาคณิต

ฝากไว้เป็นการบ้านให้ลองกลับไปวาดรูปเล่นกันดูครับ :3

Dec 29, 2015

เจองานจับฉลากแลกของขวัญที่มีคนได้ของตัวเอง ไม่ต้องตกใจไปโอกาสมีสูงเกินครึ่ง

วันก่อนเห็นเพื่อนแลกของขวัญปีใหม่แล้วดันจับได้ของตัวเองก็ฮาดี มองเผินๆ เหมือนว่าเรื่องแบบนี้จะเกิดขึ้นได้ยาก แต่เท่าที่เคยเล่นแลกของขวัญมา เรื่องประมาณนี้เกิดขึ้นบ่อยกว่าที่คิดนะ เลยลองมานั่งโซ้วสมการความน่าจะเป็นดู ว่าโอกาสที่ในงานเลี้ยงหนึ่งๆ จะมีคนจับของขวัญได้ของตัวเองเป็นเท่าไหร่

แต่ก่อนอื่นต้องเข้าใจกฎการแลกของขวัญ ซึ่งมีขั้นตอนประมาณนี้
  1. ทุกคนซื้อของขวัญมาร่วมสนุกในงานคนละอย่าง โดยเอาของขวัญใส่กล่องไว้ไม่ให้ใครรู้ว่าด้านในเป็นอะไร
  2. พอแต่ละคนไปถึงงาน คนจัดงานจะแปะป้ายชื่อเจ้าของของขวัญไว้กับกล่อง พร้อมหย่อนฉลากรายชื่อของคนนั้นลงไห
  3. เมื่อถึงเวลาแลกของขวัญ ให้ประธานในพิธีหรือใครซักคนที่ใหญ่ที่สุดในงาน เป็นคนเริ่มจับฉลากรายชื่อจากไหเป็นคนแรก
  4. ประธานจับได้ชื่อใครก็เอากล่องของขวัญของคนนั้นไป แล้วก็ให้คนที่โดนจับชื่อเมื่อกี้ เป็นคนเริ่มจับฉลากในไหวนต่อไปเรื่อยๆ
  5. ถ้าเกิดมีคนจับได้ชื่อประธาน จะถือว่าเป็นการแลกของขวัญที่ครบรอบวง ก็ให้คนนั้นเอาของขวัญของประธานไป แล้วผู้จัดงานเลือกใครซักคนจากคนที่ยังไม่ได้จับของขวัญ มาเป็นหัวจับของขวัญรอบต่อไป
  6. การแลกของขวัญจะสิ้นสุดลงเมื่อไม่มีฉลากรายชื่อเหลือในไห
จากกฎนี้เห็นได้ชัดว่า เมื่อถึงตอนใกล้จบการแลกของขวัญที่มีฉลากรายชื่อในไหเหลือแค่ 2 ใบ ใบนึงจะเป็นของคนแรกและอีกใบเป็นของคนที่ยังไม่ได้จับฉลาก ดังนั้นโอกาสที่คนจับก่อนจะจับฉลากได้ชื่อคนแรกและบังคับให้คนสุดท้ายจับชื่อตัวเอง โอกาสนี้จะสูงถึง 50% เลย

ยกตัวอย่างให้ชัดขึ้นไปอีก สมมตินายประธานเป็นคนจับฉลากคนแรก แล้วก็มีคนจับตามๆ กันมาอีกหลายคน จนตอนสุดท้ายเหลือคนที่ยังไม่ได้จับฉลาก 2 คนคือสมศักดิ์กับมานี ถ้าคนก่อนหน้าจับฉลากได้ชื่อสมศักดิ์ แปลว่าสมศักดิ์จะเป็นคนจับฉลากคนต่อไป แต่รายชื่อในไหตอนนี้จะเหลือแค่มานีกับประธานเท่านั้น ที่โอกาสครึ่งๆ ถ้าสมศักดิ์จับได้ชื่อประธาน ก็จะเหลือคนไม่ได้จับฉลากคือมานี และฉลากที่เหลือในไหก็เป็นชื่อมานี นั่นก็คือมานีจะต้องจับฉลากได้ของขวัญของตัวเองแน่นอน

โอกาสที่ในงานแลกของขวัญงานหนึ่ง จะมีคนได้ของขวัญเป็นของตัวเองสูงถึง 50% ซึ่งจริงๆ แล้วโอกาสน่าจะมากกว่านี้อีกนิดหน่อยด้วยซ้ำ เพราะยังไม่ได้คิดโอกาสที่คนกลางๆ จะจับได้ของตัวเองทันทีหลังจากเกิดการจับฉลากครบรอบวงเลย



คิดได้แค่นี้ก็พอใจแล้ว แต่ก็โดน @NungNing ถามต่อว่า ถ้าไม่จับฉลากกันต่อไปเรื่อยๆ แบบนี้หละ แต่เปลี่ยนเป็นให้ทุกคนหยิบฉลากจากไหพร้อมกันเลย แล้วค่อยเปิดดูทีเดียวว่าใครได้ของขวัญจากใคร ยังจะมีโอกาสสูงอยู่มั้ยที่จะมีคนจับฉลากได้ของขวัญของตัวเอง?

ตอนนั้นหมดพลังแล้ว ให้คิดต่ออย่างเดียวคงไม่ออกแน่ๆ เลยเขียนโปรแกรมง่ายๆ มาดูค่าเอาเลยนั่นแหละ
#!/usr/bin/env python3
from random import shuffle
from collections import Counter

shuffled = lambda ls: shuffle(ls) or ls
own_gift = lambda n: [i for i, j in enumerate(shuffled(list(range(n)))) if i == j]
สมมติให้มีคนแลกของขวัญกัน 10 คน ทดลองไปล้านครั้ง พบว่ามีประมาณสามแสนหกหมื่นครั้งเท่านั้น (36%) ที่การแลกของขวัญนั้นจะไม่มีคนได้ของตัวเอง

ตอนนั้นก็ตกใจว่าเลข 36% มาจากไหน นึกว่าโอกาสไม่ได้ของตัวเองมันจะมีค่าสูงกว่า 50% ที่คิดได้จากการแลกของขวัญวนไปเรื่อยๆ ซะอีก นี่กลายเป็นว่าโอกาสไม่ได้ของตัวเองมันยิ่งเหลือน้อย เลยทดลองเพิ่มโดยนับแยกว่า ในแต่ละการทดลองแลกของขวัญนั้น มีคนกี่คนที่ได้ของขวัญของตัวเอง

ทดลองแลกของขวัญกัน 10 คนล้านครั้งเหมือนเดิม แล้วก็ตกใจเพิ่มกับหน้าตาผลลัพธ์ที่ไม่โกหกดังนี้

จำนวนคนที่จับได้ของขวัญของตัวเอง จำนวนครั้งของเหตุการณ์ที่เกิด ข้อคาดการณ์โอกาสการเกิด
0 368,243 1/0! x 1/e
1 367,640 1/1! x 1/e
2 183,800 1/2! x 1/e
3 61,406 1/3! x 1/e
4 15,269 1/4! x 1/e
5 3,014 1/5! x 1/e
6 545 1/6! x 1/e
7 67 1/7! x 1/e
8 16 1/8! x 1/e

ถึงตอนนี้ก็พอจะเข้าใจแล้วว่ามันเกิดอะไรขึ้น



แม้แค่เห็นข้อมูลข้างบนแล้วจะสรุปได้ว่าเกิดอะไรขึ้น แต่เรื่องที่ยากกว่าก็คือการพิสูจน์หาคำอธิบายเป๊ะๆ ว่าทำไมถึงเป็นอย่างนั้น

เริ่มจากกรณีที่ไม่มีใครจับของขวัญได้ของตัวเองก่อน จะได้ว่า
  • คนแรกมีโอกาสจะจับไม่ได้ของตัวเองที่ (n-1)/n
  • คนที่สองมีโอกาสที่จะจับไม่ได้ของตัวเอง แบ่งได้ 2 แบบ คือ
    • คนแรกจับได้ของคนที่สองไปแล้ว (โอกาส 1/n) ดังนั้นคนที่สองจับไม่ได้ของตัวเองแน่ๆ (โอกาส 1) รวมโอกาสได้เป็น 1/n
    • คนแรกจับไม่ได้ของคนที่สอง (โอกาส (n-1)/n) คนที่สองต้องจับหลบให้ไม่ได้ของตัวเอง (โอกาส (n-2)/(n-1)) รวมโอกาสได้เป็น ((n-1)/n)((n-2)/(n-1))
    • สรุปว่าโอกาสทั้งหมดที่คนที่สองจะจับไม่ได้ของตัวเองคือ 1/n + ((n-1)/n)((n-2)/(n-1))
  • โอกาสคนที่สามจะคล้ายๆ กับของคนที่สอง คือ 2/n + ((n-2)/n)((n-3)/(n-2))
  • คนที่ i มีโอกาส (i-1)/n + ((n-i-1)/n)((n-i)/(n-i-1)) ที่จะจับไม่ได้ของตัวเอง

เนื่องจากเหตุการณ์ทั้งหมด ต้องเกิดขึ้นพร้อมกัน ดังนั้นสรุปได้ว่า
P(n คนไม่มีใครได้ของตัวเอง) = (n-1)/n x (1/n + ((n-1)/n)((n-2)/(n-1))
                                  x ... x ((i-1)/n + ((n-i-1)/n)((n-i)/(n-i-1))
                                  x ... x ((n-n)/n + 0)
                        = (n-1)/n x (1/n + (n-2)/n) x .... x ((i-1)/n + (n-i)/n) x ... x 1
                        = Product  (n-1)/n  for integer i from 1 to n-1
                        = Product  1 - 1/n  for integer i from 1 to n-1
                        = (1-1/n)^(n-1)
จากสมการข้างต้น ถ้าลองแทนตัวเลขน้อยๆ เช่น n=1,2,3,4 เข้าไป จะได้คำตอบเป็น 1, 0.5, 0.445, 0.422 ตามลำดับ แต่เมื่อลองแทนเลขมากๆ แล้ว ผลลัพธ์จะลู่เข้าหาค่าเดียวกันคือ 0.367879... นั่นก็เพราะ
limit  (1-1/n)^(n-1)  when n -> infinity = limit  (1-1/n)^n  when n -> infinity
                                         = limit  ((n-1)/n)^n  when n -> infinity
                                         = limit  (n/(n+1))^n  when n -> infinity
                                         = limit  1/((n+1)/n)^n  when n -> infinity
                                         = 1/(limit  ((n+1)/n)^n  when n -> infinity)
                                         = 1/(limit  (1+1/n)^n  when n -> infinity)
                                         = 1/e
สวยมั้ย อยู่ดีๆ ก็ได้ค่า e ออกมาเฉยเลย

ส่วนการพิสูจน์กรณีมีคนได้ของขวัญของตัวเองแค่คนเดียว จะมีฟอร์มที่ต่างไปเล็กน้อยเนื่องจากเหตุการณ์จะไม่เกิดขึ้นต่อเนื่องกันแล้ว เขียนสมการออกมาได้เป็น
P(n คนมี 1 คนได้ของตัวเอง) = (1/n)P(n-1 คนไม่มีใครได้ของตัวเอง)
                              + ((n-1)/n)(1/(n-1))P(n-2 คนไม่มีใครได้ของตัวเอง)
                              + ...
                              + ((n-(n-1))/n)(1/(n-(n-1)))P(n-n คนไม่มีใครได้ของตัวเอง)
                        = (1/n)P(n-1 คนไม่มีใครได้ของตัวเอง)
                              + (1/n)P(n-2 คนไม่มีใครได้ของตัวเอง)
                              + ...
                              + (1/n)P(n-n คนไม่มีใครได้ของตัวเอง)
                        = (1/n)(Sum  P(n-i คนไม่มีใครได้ของตัวเอง)  for integer i from 1 to n)
สมมติว่ามีคนมาร่วมงานแลกของขวัญเยอะ อาจจะมองว่า P(n-i คนที่ไม่มีใครได้ของตัวเอง) มีค่าไม่แตกต่างกันสำหรับแต่ละค่า i เลยก็ได้ ทำให้เราสามารถยุบผลรวมยุ่งๆ ด้านหลัง ให้เหลือแค่เอาความน่าจะเป็นไปคูณด้วย n ก็พอ ซึ่งมันก็จะโดนหักล้างทิ้งไปกับ (1/n) ด้านหน้าทันที

ดังนั้นจะได้ว่า ค่าความน่าจะเป็นของงานเลี้ยงแลกของขวัญที่จะมีคนหนึ่งคนได้จับได้ของตัวเอง เป็น 1/e เท่ากันกับกรณีไม่มีใครจับได้ของตัวเองเลย

สำหรับกรณีที่มีคนสองคนจับได้ของตัวเอง จะเริ่มพิสูจน์ยุ่งยากมากขึ้นไปอีก แต่ใจความจะคล้ายๆ กับการพิสูจน์คนเดียวได้ของตัวเอง ซึ่งได้แก่การมองแยกกรณี โดยแบบที่หนึ่งจะให้คนแรกจับได้ของตัวเองไปแล้ว หลังจากนั้นจึงหาโอกาสของคนที่เหลือที่จะมีหนึ่งคนจับได้ของตัวเองเป็นเท่าไหร่ กับแบบที่สองที่ให้คนแรกจับไม่ได้ของตัวเอง แล้วหาโอกาสของคนที่เหลือที่จะมีสองคนจับได้ของตัวเองเป็นเท่าไหร่ (ถ้าคิดด้วยวิธีนี้ไม่ผิด จะได้หน้าตาสมการเป็นการหาผลรวมของเทอม ((n-k)/n)((n-k+1)/(n-1))((n-k+2)/(n-2))...(1/(n-k+1))P(n-k คนมี 1 คนได้ของตัวเอง))

กรณีอื่นๆ ที่เหลือเนื่องด้วยพื้นที่กระดาษไม่พอเขี.... เฮ้ย ไม่ใช่แฟร์มาต์! จริงๆ แล้วต้องบอกว่าการพิสูจน์ด้วยท่านี้มันเริ่มยุ่งยากซับซ้อนจนไปต่อไม่ไหวแล้ว แต่ถ้ายังจำกันได้ นี่มันการแจกแจงปัวซองที่มีค่า λ=1 ชัดๆ ก็เอาเป็นว่าใครอยากพิสูจน์ต่อก็ลองใช้ท่านี้ดูนะ อาจจะได้บทพิสูจน์ที่สั้นและสวยงามกว่าด้วย



เห็นแล้วว่าการสุ่มจับฉลากทั้งหมดทีเดียวพร้อมกันมันไม่เวิร์ค กลับไปดูวิธีดั้งเดิมที่ค่อยๆ ให้จับฉลากทีละคน จับได้ชื่อใครก็ให้คนนั้นไปจับฉลากต่อ วิธีนี้เขียนเป็นโค้ดออกมาได้ว่า
from random import randint

def exchange(ls):
    owned = [None for _ in ls]
    current = 0
    while ls:
        if owned[current] is not None:
            current = owned.index(None)
        gone = randint(0, len(ls)-1)
        owned[current] = ls[gone]
        current = ls[gone]
        del ls[gone]
    return owned
เอาฟังก์ชัน exchange ไปแทนที่ฟังก์ชัน shuffled ข้างบนแล้วทดลองใหม่ ก็ได้ผลลัพธ์ที่ไม่ต่างไปจากตารางแรกซักเท่าไหร่เลย



ดังนั้นไม่ว่าจะให้จับฉลากพร้อมกันหมดในทีเดียว หรือจะค่อยๆ จับทีละคน พอจับได้ชื่อใครก็ให้คนนั้นจับต่อ ทั้งสองวิธีนี้ยังไงก็จะมีคนซวยโชคดีอย่างน้อยหนึ่งคน ที่จับได้ของขวัญของตัวเองด้วยโอกาสสูงเท่ากันที่ 1-1/e หรือประมาณ 63.21% ครับ โดยที่(แทบ)ไม่ต้องสนใจด้วยว่ามีคนมาแลกของขวัญกันกี่คน

แต่วิธีแก้ก็ไม่ได้ยากเกินไป ใช้วิธีค่อยๆ ไล่จับฉลากไปทีละคนนั่นแหละ (จะได้เล่นมุกคั่นเวลา ดูรีแอคชันของคนได้ของขวัญด้วย) แล้วเพิ่มกฎเข้าไปสองข้อดังนี้
  1. ถ้าจับได้ชื่อตัวเอง ให้จับฉลากเพิ่มอีกใบเพื่อจะได้เอาของขวัญจากคนนั้น แต่อย่าลืมหย่อนฉลากชื่อตัวเองที่จับขึ้นมาเมื่อกี้ใส่กลับลงไหก่อนเอาไปให้จับฉลากต่อ
  2. ตอนเหลือคนยังไม่ได้จับฉลาก 2 คน ไม่ต้องให้จับฉลากแล้ว ให้คนที่กำลังจะจับฉลากไปเอาของขวัญจากคนสุดท้ายได้เลย ส่วนคนสุดท้ายก็ไปเอาของขวัญจากคนแรก
อย่างไรก็ตาม หากรู้สึกว่ากฎพวกนี้มันวุ่นวายจนทำให้งานกร่อยก็ไม่ต้องไปทำตามหรอก เพราะการมีคนจับได้ของขวัญของตัวเองก็ไม่ใช่เรื่องเลวร้ายแต่อย่างใด หนำซ้ำมันยังเป็นประสบการณ์สุดประหลาดที่หาได้ยาก และเป็นเรื่องขำขันชั้นดีที่สามารถเก็บเอาไว้เล่นได้เรื่อยๆ ยันลูกบวชเลยหละ

สวัสดีปีใหม่ 2016 ล่วงหน้าครับ

Feb 14, 2014

ประสบการณ์สร้างเกมจริงจังครั้งแรก

ไอเดียมันเริ่มมาจากทวีตนี้


(ว่าแต่ใครมันจุดกระแส #ถีบเนยสด ฟระ Orz)

เลยจัดการซะคืนนั้นเลย ... ถ้าไล่ดูตาม commit log จะเห็นว่าใช้เวลาไป 2 เดือนพอดีจนปิดโปรเจคได้ ก็นับว่าใช้เวลาเยอะโขอยู่กับโปรเจคขำๆ

ทำให้รู้เลยว่า ส่วนที่ยากที่สุดของการทำเกมนั้น ไม่ใช่การ coding ไม่ใช่การ debug แต่เป็นการออกแบบ game play ให้น่าสนใจ ผู้เล่นต้องรู้สึกว่าไม่ยากเกินความสามารถ ในขณะเดียวกัน เมื่อเล่นจบแล้วก็ยังกลับมาเล่นซ้ำๆ ได้อีกโดยไม่เบื่อ

ซึ่งเวลาส่วนใหญ่ที่หมดไปก็เพราะเจ้าเนี่ยแหละ จริงๆ ถ้าดูตาม log อย่างละเอียดแล้วลองคำนวณเวลาที่ใช้ จะพบว่าโปรเจคนี้ทำเสร็จภายใน 42 ชั่วโมง (บวกลบไม่เกิน 10%) ถ้ามีไอเดียเจ๋งๆ เกี่ยวกับ game play เตรียมไว้อยู่แล้ว

แต่ก่อนที่จะไปดูการออกแบบเกมนี้ ลองเรียนรู้จากเกมอื่นๆ ก่อน



Flappy Bird

  • infinity - เล่นได้เรื่อยๆ จนกว่าจะพลาดหรือเบื่อ
  • social - อวดคะแนนแข่งเพื่อน
  • reflex - ตอบสนองทันทีต่อสิ่งใหม่
(เกาะกระแสซักหน่อยครับ) เกมนี้ใช้ระบบที่เรียบง่ายมากๆ คือพานกบินลอดท่อไปเรื่อยๆ ลอดผ่านได้ 1 ท่อก็เพิ่ม 1 คะแนน อย่างไรก็ตามเกมประเภทนี้จะสุ่มด่านใหม่มาให้เสมอ ผู้เล่นแต่ละคน (หรือแม้แต่คนเดียวกันเมื่อเล่นรอบใหม่) ก็จะพบกับด่านที่ไม่เหมือนเดิม การเพิ่มคะแนนแบบไม่เกี่ยวข้องกับความยากของด่านจึงเหมาะสม เพราะจะกดดันให้เล่นเกมซ้ำๆ เผื่อฟลุ๊คเจอด่านง่ายแล้วได้คะแนนเยอะโดยออกแรงไม่มากไปกว่าเดิมนัก ซึ่งการเล่นซ้ำๆ (ด้วยความโมโหว่าทำไมคะแนนไม่ขึ้นซักที 555) จะย้อนกลับมาช่วยให้เราเรียนรู้ระบบเกมและฝึกประสาทตอบสนอง ส่งผลให้ผู้เล่นได้คะแนนง่ายขึ้นเรื่อยๆ ตามเวลาที่ฝึก ยิ่งผสมกับการนับคะแนนตรงไปตรงมา (1 ท่อ = 1 คะแนน) ยิ่งทำให้เรากะถูกว่าต้องใช้ความพยายามเพิ่มอีกแค่ไหนเพื่อจะทำคะแนนให้ชนะเพื่อนได้

Super Hexagon

  • goal set - เกมตั้งจุดมุ่งหมายให้แล้ว
  • social - แต่จะแข่งกับเพื่อนก็ได้
  • reflex - ตอบสนองทันทีต่อสิ่งใหม่
แนวคิดของเกมนี้ก็ง่ายเช่นเดียวกัน คือพาเจ้าตัวสามเหลี่ยมวิ่งวนคอยหลบไม่ให้โดนกำแพงอัดแบน และใช้ "เวลาที่อยู่รอด" เป็นคะแนนนั่นเอง งานนี้ไม่ต้องคิดมากว่าจะเลือกท่าหลบให้ให้สวยงามได้คะแนนโบนัสเพิ่มหรือเปล่า เช่นเดียวกับ Flappy Bird คือต้องฝึกเยอะๆ ฝึกไปเรื่อยๆ และแม้ว่าจะกำหนดเวลาผ่านด่านไว้ที่ 60 วินาที แต่ถ้าจะทำคะแนนไว้อวดเพื่อน ก็สามารถเล่นต่อหลัง 60 วินาทีนั้นไปได้เรื่อยๆ ครับ

Jubeat

  • goal set - เกมตั้งจุดมุ่งหมายให้แล้ว
  • social - แต่จะแข่งกับเพื่อนก็ได้
  • rhythm - เกมเข้าจังหวะ
Jubeat เป็นเกมแนวดนตรีที่จะให้กดปุ่ม 16 ปุ่ม (บน grid ขนาด 4x4) ตามจังหวะของแต่ละปุ่ม แต่ละเพลงมีรูปแบบที่ตายตัว แถมเกมยัง cap แต้มไว้ที่หนึ่งล้านแต้มเสมอไม่ว่าจะเป็นเพลงยากหรือง่าย ดังนั้นรูปแบบการฝึกจะไม่เหมือน 2 เกมข้างบนเท่าไหร่ เช่นฝึกซ้ำบ่อยๆ เฉพาะรูปแบบที่ยาก ไปจนถึงการจำรูปแบบจังหวะของเพลงทั้งเพลง เกมแบบนี้ต้องให้คนรักในความ perfect จริงๆ มาเล่น ไม่งั้นคงรู้สึกเหมือนฟินไม่สุด

The Typing of the Dead

  • goal set - เกมตั้งจุดมุ่งหมายให้แล้ว
  • co-op - ร่วมมือกันเล่นได้
  • reflex - ตอบสนองทันทีต่อสิ่งใหม่
จริงๆ เกมนี้จะนับเป็นแนวกึ่ง rhythm กึ่ง reflex ก็ได้ เพราะเราไม่ได้สนใจแค่ตัวอักษรที่โผล่มาให้พิมพ์เพียงอย่างเดียว ในบางจังหวะเราสามารถจำว่าต้องยิงซอมบี้ตัวไหนก่อน เพื่อในกลับมาเล่นด่านเดิมด้วยคะแนนที่สูงขึ้นได้ จุดเด่นอีกอย่างในเกมแบบนี้คือความสามารถในการร่วมมือกันเล่น ที่ผู้เล่นต้องสมดุลระหว่างการยิงรัวๆ กันโดนซอมบี้กัดตาย กับการเหลือซอมบี้ไว้ให้เพื่อนร่วมทีมยิงเพื่อให้คะแนนไม่ห่างกันนักด้วย (หรือใครเป็นพวกซาดิสม์ ชอบทำคะแนนให้ห่างกันเยอะๆ ก็ไม่ว่ากัน 55+)

Diablo II

  • goal set - เกมตั้งจุดมุ่งหมายให้แล้ว
  • co-op - ร่วมมือกันเล่นได้
  • leveling - ตัวละคร/ด่านมีพัฒนาการ
ผมจำได้เลยว่าตอนอยู่ม.ต้นเล่น Diablo II ก็รู้สึกหนุกดีนะ เดินผ่านด่านไปเรื่อยๆ ไม่คิดอะไรมาก ถ้าตรงไหนดูยากก็กลับไปเล่นด่านเก่าอัพเลเวลซักหน่อย เพื่อที่จะได้ใช้ท่าใหม่ๆ ตีมอนได้แรงขึ้น เล่นไปเล่นมาก็รู้สึกว่า เอ๊ะทำไมเหมือนฟาร์มเลเวลได้ช้าลง ค่าประสบการณ์สำหรับเลเวลถัดไปที่เพิ่มขึ้นเร็วเป็นเอกซ์โพเนนเชียลก็ยังไม่น่าทำให้รู้สึกได้ขนาดนี้ จนเมื่อได้อ่านสูตรการคิดค่าประสบการณ์ที่ได้จากมอนตัวนึงแล้วถึงบางอ้อ ความต่างของเลเวลมอนกับผู้เล่นก็มีผลต่อค่าประสบการณ์ด้วย นั่นทำให้ผมรู้สึกประทับใจในรายละเอียดและความเชื่อมโยงกันของค่าต่างๆ ภายในเกม ไม่ใช่การที่ developer สักแต่คิดว่าใส่ factor นี้เข้ามาแล้วเกมมันน่าจะสนุกขึ้น ซึ่งในความเป็นจริงเกมมันอาจจะสนุกขึ้น 10 นาที แต่หลังจากนั้นก็กลายเป็นหายนะเพราะเสียสมดุลไปเรียบร้อย



จริงๆ ตอนเอาไอเดียมาแปลงเป็น game play ก็ไม่ได้ไล่คิดถึงเกมอื่นเยอะขนาดนี้หรอก (เอ หรือมันจะเป็น unconscious?) แต่อยากได้แบบ infinity+leveling และคิดว่าต้องปรับแน่ๆ ดังนี้

  • เกมฝึกพิมพ์ภาษาไทยยากมาก ทั้ง font เล็ก ทั้งสระบนล่าง ... ทำภาษาอังกฤษง่ายกว่า
  • 1 นาทีจะว่านานก็นานเกินไปสำหรับเกมฝึกพิมพ์ ยิ่งเจอแต่คำซ้ำๆ นี่ยิ่งน่าเบื่อ
  • แต่ 1 นาทีก็สั้นเกินไปถ้า random คำมาได้ไม่ซ้ำกันเลย
  • เลยคิดว่าทำเป็นระบบหัวใจดีกว่า พิมพ์ไม่ทันก็โดนโจมตี หัวใจหมดเมื่อไหร่ก็จบเกม
  • ถึงแม้ส่วนกริยาจะไม่ซ้ำจากการ random แล้ว แต่ให้พิมพ์ชื่อ (@neizod) บ่อยๆ ก็เบื่อได้เช่นกัน เลยทำระบบเก็บคำกริยาหลายๆ คำไว้ก่อน แล้วค่อยพิมพ์ชื่อเมื่อต้องการจบประโยค
  • อนึ่ง ในความเป็นจริงเกมก็ไม่ได้มี word list ใหญ่ขนาดนั้น เลยหาแผนมารับมือเวลาเจอคำซ้ำไม่ให้รู้สึกเบื่อ คือเมื่อพิมพ์ศัพท์ซ้ำจะทำการดึงคำนั้นออกจากประโยคที่กำลังสร้างซะเลย

ตอนนี้ก็ได้ game play หลักมาครบแล้ว ซึ่งเอาจริงๆ prototype พวกนี้ให้ทำ 3-4 วันก็เสร็จ

แต่ก่อนหน้านั้นก็มีสิ่งที่สำคัญไม่แพ้กัน คือการเลือก engine สำหรับโปรเจคนี้ครับ

  • เลือกเป็นเว็บเพราะทำ GUI ได้ง่ายและเร็วสุด (จริงๆ คือเป็นอยู่แค่เว็บ 555+)
  • ตอนแรกก็ว่าจะใช้ canvas เพราะทุกอย่างสามารถยัดเป็น JavaScript ล้วนเลยได้ แต่ก็จะไม่ได้ฝึก CSS อีกทั้งเกมนี้ยังเป็นแบบ text-base ไม่ใช่เกมตัวละครลุยด่าน เลยคิดว่าเล่นกับ DOM ดีกว่า
  • จะเล่นกับ DOM ทั้งที ก็ต้อง jQuery และ position: absolute สิครัช
  • ถ้า JavaScript เขียนสั้นๆ ก็พอไหว แต่มาเขียนซับซ้อนๆ แบบนี้ CoffeeScript เถอะครัฟ จะได้ไม่เป็นภาระ @plynoi
  • ไม่ได้ใช้ framework อื่นใดช่วยอีกเพราะไม่ค่อยรู้จัก + learning curve ท่าทางจะสูงน่าดู รอลุยโปรเจคนี้เสร็จพลังวัตรน่าจะกระโดดข้ามไปอีกขั้น
  • ไม่อยากแตะ database เท่าไหร่เพราะต้องใช้ host แยก ไม่งั้นก็ไปขอ AppEngine ที่ตอนนี้เก็บครบโควต้าแล้ว
  • เลยกะว่าเอาง่าย เล่นเกมเสร็จก็ทวีตคะแนนซะเลย ใครจะโกงก็ช่างมัน (แต่ประสบการณ์ก็บอกว่าไม่ค่อยมีใครโกงหรอก เท่าที่สังเกตจาก Temple Run)
  • พอไม่แตะ database ทุกอย่างเป็น static หมด ก็หา host ง่ายขึ้นจม แต่มีที่เดียวที่ผุดมาในใจคือ GitHub Pages

พอรู้ spec env ครบก็สบายใจไปกว่าครึ่งแล้ว

จุดต่อมาคือระบบคิดคะแนนต่างๆ (ที่อู้ไปเป็นเดือนเพราะคิดส่วนนี้ไม่ออก) การจะออกแบบตรงนี้ได้ถ้ารู้จักแนวโน้มกราฟแบบต่างๆ จะได้เปรียบพอสมควร ผลสุดท้ายผมออกแบบดังนี้

  • ค่าหลักของเกมคือคะแนน ซึ่งควรคิดตามจำนวนตัวอักษรต่อคำที่พิมพ์เข้าไป เช่น kick ได้ 4 คะแนน เพราะมี 4 ตัวอักษร
  • อย่างไรก็ตาม การคิดคะแนนที่ 1 ตัวอักษร = 1 คะแนน อาจทำให้เบื่อได้ และยังไม่ได้ใช้ประโยชน์จากการบังคับพิมพ์ชื่อ (@neizod) เท่าไหร่ด้วย
  • คิดว่าอีกปัจจัยที่ควรจะมีผลต่อคะแนนคือเลเวล (เหมือนเกมอื่นๆ) ยิ่งผู้เล่นเลเวลสูงก็น่าจะได้คะแนนเยอะ เลยเอาคะแนนที่ได้ไปคูณด้วยเลเวลเมื่อพิมพ์จบประโยค
  • แต่ก็ไม่อยากให้ผู้เล่นเก็บคำศัพท์ไว้เป็น 20 คำแล้วค่อยจบประโยคเพื่อคูณคะแนนตูมเดียว เลยให้แค่คำแรกของประโยคที่ได้คูณเลเวลเต็มๆ คำถัดๆ มาก็คูณกับค่าเลเวลที่ลดลงเรื่อยๆ จนคำท้ายๆ ไม่ได้ประโยชน์จากการเก็บดองคำศัพท์ไว้ไม่ยอมสร้างเป็นประโยคซักที
  • ส่วนการเพิ่มเลเวลนั้นก็คิดจากคะแนน ตรงนี้ไม่อยากให้คะแนนต่อเลเวลเป็นกราฟเส้นตรง เพราะจะทำให้ตัวคูณเพิ่มเร็วมาก และเนื่อจากไม่ค่อยชอบกราฟกำลังสอง เลยเลือกค่าคะแนนสำหรับเพิ่มเลเวลต่อไปเป็นแบบเอกซ์โพเนนเชียลซะ
  • หัวใจลดได้ก็ย่อมเพิ่มได้ ง่ายสุดไม่คิดอะไรแล้ว เลเวลเพิ่มเมื่อไหร่ หัวใจเพิ่มเมื่อนั้น (เชื่อว่าระบบการเพิ่มเลเวลถูกออกแบบมาเป็นอย่างดี 555+)
  • แต่แน่นอนว่าเพิ่มเลเวล = เพิ่มความยาก ในที่นี้ก็คือเพิ่มจำนวนคำศัพท์บนหน้าจอให้ออกมาเท่ากับเลเวล และเพิ่มความเร็วให้มันทีละนิดๆ

ถ้าแม่น math หน่อย ระบบพวกนี้ implement เข้าไปไม่นานครับ ดีไม่ดีเร็วกว่าออก prototype ข้างบนอีก ปัญหาคือจะทำได้เร็วๆ นี่ต้องไอเดียบรรเจิดมาก ควรหา paper ด้านจิตวิทยาและการออกแบบเกมอ่านตุนไว้เป็นอย่างยิ่ง :p

พอ core ทั้งหลายเสร็จแล้ว ก็แต่งสวยครับ

  • จริงๆ ก็อยากให้เจ้าแมวสดโดนชก/ถีบหน้าน่วมนะ แต่มันทำยากซะเหลือเกิน เลยเลือกๆ อารมณ์มาตรฐานที่เว็บมันมีให้มาให้แทน ได้มา 8-9 แบบก็น่าจะพอ
  • พื้นหลังนี้ก็เลือกนานเหมือนกัน คิดไว้ว่าอยากได้โทนสว่างๆ ก็เลือกเหลืองอ่อน เลยได้น้ำเงินอ่อนติดมาด้วยในตอนแรก (สีคู่ตรงข้ามกันพอดี) ทำไปทำมาได้สีแดงตอนจบเกมมาด้วย เลยต้องเลื่อนสีน้ำเงินให้ออกไปฟ้าๆ หน่อย โทนสีจะได้เป็นรูปสามเหลี่ยม
  • ตอนแรกไม่ได้คิดว่าจะเอาเจ้าแมวสดมาไว้ด้านล่าง แต่จะเอาไว้เป็น background เลย ซึ่งคิดไปคิดมาก็คงไม่เหมาะเพราะรบกวนสมาธิผู้เล่นเกินไป เลยย้ายมาอยู่มุมล่างขวา ด้านที่คำศัพท์ต่างๆ พรั่งพรูออกมานั่นเอง
  • ทำให้พวก status ผู้เล่นอย่างคะแนนสะสม/เลเวล/หัวใจที่ตอนแรกวางไว้ว่าจะให้กระจัดกระจายตามขอบต่างๆ ของจอ ถูกจับมาอัดรวมกันไว้เป็นกล่องทางด้านซ้ายในสไตล์เดียวกัน
  • ที่เหลือก็จับปุ่มโน่นนี่นั่นยัดเข้ามาไม่ให้มันดูโล่ง
  • สุดท้ายคือ tutorial เกม หัวใจสำคัญที่ขาดไม่ได้แม้เกมจะง่ายแค่ไหน ใครจะไม่ดูก็ช่างเขาแต่เราจะพรีเซนต์!

หลังจาก implement เสร็จหมดก็ได้ฤกษ์ปล่อยเกม ตามไปเล่น (ด้วยความรัก) ได้ที่ neizod.github.io/kick เลยครับ \(;w ;)/

Jan 5, 2014

0!=1

สัญลักษณ์คณิตศาสตร์ที่น่าฉงนปนตกใจมากที่สุดคงหนีไม่พ้น factorial (!) และสิ่งที่น่าตกใจยิ่งกว่าคือคำตอบที่ว่า 0!=1 เมื่อถามต่อว่าทำไมก็จะได้รับคำตอบว่ามันเป็นนิยาม ซึ่งจริงๆ ก็ถูกต้องแล้วแหละ แต่หลายคนคงอยากให้มันมีความหมายที่ลึกซึ้งกว่านั้น

เมื่อลองพิจณานิยามพื้นฐานสุดของ factorial การจะได้มาซึ่ง n! นั้นคือการหาผลคูณจาก 1 ขึ้นไปเรื่อยๆ จนถึง n การหา 1! ได้จึงไม่แปลก เพราะมันคือผลคูณจาก 1 ไปจนถึง 1 (ซึ่งก็คือไม่ต้องคูณเลยซักตัว) แต่ถ้าคิดตามนิยามนี้ 0! ก็ไม่มีตัวตนเพราะว่า 1 มีค่ามากกว่า 0 (แล้วเราจะคูณขึ้นไปเรื่อยๆ ได้ยังไง!?) ตอนนี้ถ้าเราอยากให้ 0! มีค่าก็ต้องหาวิธีคำนวณ/นิยายามที่มีเหตุผลขึ้นมาเพื่อให้คนอื่นยอมรับค่านั้นๆ ... ซึ่งแน่นอนว่ามันไม่ใช่การเปลี่ยนนิยามข้างต้นเป็น n! คือผลคูณจาก 0 ขึ้นไปเรื่อยๆ จนถึง n แน่ :P



ความพยายามแรกที่จะหาคำตอบนี้ ก็คือการกล่าวถึง factorial ในรูปของ n!=n(n-1)! จะเห็นว่าสูตรนี้เป็น recursive การจะหาผลลัพท์ได้นั้น ต้องมองย้อนกลับไป 1 ขั้นเสมอ ในทำนองเดียวกัน ถ้าเรารู้ผลลัพท์ของขั้นตอนปัจจุบัน เราอาจจะสามารถย้อนกลับไปคำนวณผลลัพท์สำหรับขั้นตอนก่อนหน้าได้ ดังนั้น
5! = 120 = 5 * 4!
4! =  24 = 4 * 3!
3! =   6 = 3 * 2!
2! =   2 = 2 * 1!
1! =   1 = 1 * 0!
เลขตัวเดียวที่คูณหนึ่งแล้วยังได้หนึ่งก็คือหนึ่ง ดังนั้น 0!=1 อย่างไม่ต้องสงสัย...

ใครตอบมาอย่างนี้บอกได้เลยว่าเป็นนักคณิตศาสตร์เกรด C เพราะถ้าเราลองทำต่อไปจะได้ว่า
0! = 1 = 0 * (-1)!
แน่นอนว่าสมการนี้หาคำตอบไม่ได้ (เลขใดๆ คูณศูนย์ย่อมได้ศูนย์) แม้ว่าสมการข้างต้นจะทำงานได้ดีไม่มีผิดถ้าหาก n เป็นเลขจำนวนเต็มบวก แต่ก็ทำให้เกิดข้อสงสัยว่า ในเมื่อ 0! ไม่สามารถคำนวนมาได้จากฝั่ง 0!=0*(-1)! แล้วเราจะแน่ใจได้หรือ ว่าสมการข้างต้นนี้สามารถใช้ได้ตั้งแต่ n=1 จริงๆ (เพราะตอนนี้เรามั่นใจแค่ว่ามันใช้ได้ตั้งแต่ n=2)



ความพยายามต่อมาคือ gamma function ที่ให้สมการอินทิเกรตยากๆ มาอันนึง (ซึ่งแก้ออกได้ด้วยการทำอินทิเกรตแยกส่วน แล้วลดรูปกลับมาให้อยู่ในรูปเดิม) สมการนี้เมื่อหาค่าทุกจุดแล้วนำมาพล็อตกราฟ จะได้ภาพการลากเส้นโค้งเชื่อมจุดต่างๆ ของ factorial นั่นเอง กราฟนี้บอกเราว่า 0!=1 และ factorial ของจำนวนเต็มลบใดๆ หาค่าไม่ได้ ซึ่งไม่ขัดแย้งกับความพยายามก่อนหน้า


แม้ว่าความพยายามนี้จะเลิศเลอเพอร์เฟคแค่ไหนก็ตาม (ทั้งสมการยากๆ ที่ทำให้ดูเป็นมือโปร ทั้งการขยายขอบเขต factorial ให้ไปอยู่บนจำนวนจริงได้) แต่มันก็เกิดขึ้นหลังจากที่เรานิยามได้แล้วว่า 0!=1 ครับ



ความพยายามสุดท้าย คือการกลับไปยังต้นกำเนิดของการที่เราอยากนิยาม factorial มาใช้เพื่อความสะดวกในการเขียนและคำนวณ นั่นก็คือเรื่องของโอกาสและความน่าจะเป็น เช่นตัวอย่างที่ว่า ถ้ามีนักเรียน 5 คน จะจัดให้ยืนเรียงต่อแถวกันได้ทั้งหมดกี่แบบ เราก็จะตอบได้ว่า คนแรกเลือกที่ยืนได้ 5 ที่ คูณกับคนที่สองเลือกยืนได้ 4 ที่ (เพราะคนแรกจองไปที่นึงแล้ว) คูณกับคนต่อมามีที่ยืนแค่ 3 ที่ (สองคนแรกจองไปสองที่) ... เมื่อคิดอย่างนี้ต่อไปเรื่อยๆ เราจะเห็นได้ว่ามันย้อนกลับไปหานิยามตั้งต้นของ factorial นั่นเอง

แล้วถ้านักเรียนเหลือแค่คนเดียวหละ? เราก็จะได้ว่ามีวิธีเดียวที่จะยืน คือยืนอยู่คนเดียวให้เปล่าเปลี่ยวหัวใจเล่น ไม่มีใครมายืนนำหน้าหรือต่อแถวข้างหลัง

ส่วนที่ยากก็คือว่าถ้าไม่มีนักเรียนเหลือซักคนเลย?? คำตอบนี้อาจดูแปลก เพราะมันคือการลอกคำตอบข้างบนมาตอบในทำนองเดียวกันว่ามีวิธีเดียวที่จะยืน คือไม่ต้องมีนักเรียนยืนเลยซักคนนั่นเอง

ความพยายามนี้แม้แลดูเป็นปรัชญาอันสูงส่งเข้าใจยาก (ที่พยายามบอกเป็นนัยเชิงปฏิทรรศน์ว่า ความว่างเปล่าแท้จริงแล้วก็ยังไม่ว่าง) มันนับว่าเป็นการอธิบายที่ถูกต้องตรงจุดที่สุดในบรรดาความพยายามอธิบายที่ผ่านๆ มาเลยทีเดียว



ส่วนเรื่องจริงนั้นก็อย่างที่บอกแต่แรกสุดแล้ว ว่า 0!=1 มันเป็นนิยามนั่นแหละ

เหตุผลก็ไม่พิศดารอะไรเลย แค่การนิยามแบบนี้แล้วจะทำให้เขียนสมการง่ายขึ้นเท่านั้น ลองคิดดูว่าถ้าเราไม่นิยามแบบนี้จะเกิดอะไรขึ้น
  • สัมประสิทธิ์ทวินาม หรือที่หลายๆ คนคุ้นเคยในรูปของสามเหลี่ยมปาสคาลมากกว่า มันมีสูตรว่า
    (x+y)^n = sigma C(n,k) x^(n-k)y^k for k from 0 to n
    
    ซึ่งถ้าไม่ได้นิยาม factorial ตามข้างต้น ก็ต้องเปลี่ยนสูตรนี้ใหม่เป็น
    (x+y)^n = x^k + (sigma C(n,k) x^(n-k)y^k for k from 1 to n-1) + y^k
    
  • นิยาม permutation กับ combination ต้องกลับไปเขียนข้อยกเว้นไว้ที่
    • C(n,0) (เลือกของ 0 ชิ้นจากของ n ชิ้นได้กี่วิธี) ซึ่งก็ยังเข้าใจได้เพราะการไม่เลือกของซักชิ้นเลยอาจเป็นข้อถกเถียง
    • C(n,n) (เลือกของ n ชิ้นจากของ n ชิ้นได้กี่วิธี) อันนี้เริ่มแปลกๆ แล้ว ทำไมเลือกของทุกชิ้นต้องมีข้อยกเว้นด้วยหละ?
  • การหาค่า e จากการกระจายเทย์เลอร์ จากเดิมที่เราสามารถสรุปสั้นๆ อย่างสวยงามได้ว่ามันคือ
    e = sigma 1/k! for k from 0 to infinity
    
    ก็จะกลายเป็นสมการหน้าเกลียดๆ ที่ไม่สามารถอธิบายได้ว่าเลข 1 ตัวแรกนั้นโผล่มาจากไหน/โผล่มาทำไม เช่นนี้
    e = 1 + (sigma 1/k! for k from 1 to infinity)
    
เพราะคณิตศาสตร์คือความสวยงามครับ :)

Apr 27, 2013

Code Jam 2013 รอบ 1A

รอบนี้ตอนแรกว่าจะปล่อยผ่านเพราะอุตสาห์มากทม.ทั้งที อยากไปร่วมฟาร์มเสา 8 กับเหล่า agent ทั้งหลายด้วย

แต่คิดไปคิดมา คราวก่อนก็วืดเพราะมีแต่ไปคาราโอเกะ เลยคิดว่าควรให้ importance และ energy กับการแข่งให้มากที่สุดจะดีกว่า

รอบนี้เหมือนจะเป็น math จ๋าเลย แค่ข้อแรกที่ถามว่าจะระบายธนูได้กี่วง ก็ต้องใช้ความรู้เรื่องพื้นที่วงกลม + สมการกำลังสอง + อนุกรมเข้ามาช่วย

เราเริ่มจากสังเกตว่าวงกลมแรกจะใช้สีระบายไป 2r + 1 วงกลมถัดๆ มาใช้ 2r + 1 + 4 และ 2r + 1 + 8 ดังนั้นจะตั้งเป็นสมการได้ว่า

used_color(n) = (2r+1) + (2r+1 + 4) + (2r+1 + 8) + ... + (2r+1 + 4(n-1))
              = summation (2r+1 + 4k) for k in [0 .. n-1]
              = n(2r+1) + 4 * summation k for k in [0 .. n-1]
              = n(2r+1) + 4n(n-1)/2
              = n(2r+1) + 2n(n-1)

แต่เนื่องจากสิ่งที่เรารู้คือ t = used_color(n) และต้องการคำนวณกลับเพื่อหา n ดังนั้น

0 = n(2r+1) + 2n(n-1) - t
  = 2n^2 - (2r-1)n - t

ก็จะได้ว่า

a = 2
b = 2r - 1
c = -t
n = (-b ± sqrt(b^2 - 4ac)) / 2a

ถึงตอนนี้ก็แค่แก้สมการข้างบน ได้คำตอบมา 2 อันก็เลือกเอาคำตอบที่มากกว่า 0 แล้วปัดเศษทิ้งครับ



ข้อสองคิดวิธี optimize ไม่ออก เลยเขียน recursive ไป (loop ใหญ่สุดของแบบเล็กคือ 60 ล้าน ก็ยัง brute-force ออกในเวลาที่รับได้) แต่เสียดายที่คิดผิด debug ไม่ทัน

ส่วนข้อสามแนวคิดคือหาตัวคูณร่วมน้อยของเลขทั้ง set ที่เค้าให้ แล้วก็แยกตัวประกอบมันออกมาเป็น list นึง ทีนี้ถ้า list มันใหญ่กว่าขนาดของการ์ดทั้งหมดก็ merge ตัวเลขใน list นี้ด้วยการคูณจนกว่ามันจะมีขนาดเท่ากับการ์ด เท่านี้เอง (อันนี้ออกแค่ข้อเล็ก)

อันดับระหว่างแข่งคือ 18xx พอจบรอบตรวจคะแนนจริงก็เด้งกลับมาที่ 1377 เพราะข้อที่ส่งไปถูกหมด (แต่ก็ยังไม่เพียงพอสำหรับผ่านเข้ารอบต่อไปอยู่ดี) ... ไม่เป็นไรรอบหน้าเอาใหม่ เนอะ ^^)v

Feb 25, 2013

ลำดับ Farey และ วงกลม Ford

ปีที่แล้วเล่น Project Euler ไปได้หลายข้อ แล้วก็สะดุดตากับข้อที่น่าสนใจ เลยเอามาขยายผลต่อจนได้เป็นเรื่องนี้ขึ้นมา

อธิบายสั้นๆ คือลำดับ Farey เป็นลำดับของเศษส่วนอย่างต่ำตั้งแต่ 0/1 ไปจนถึง 1/1 เพราะฉะนั้นตัวเลขอย่าง 2/4 จึงไม่นับ (หรือถ้าจะนับ ก็ทำให้มันเป็น 1/2 ก่อน)

อย่างไรก็ดี ถ้านิยามอย่างนี้เราจะมีปัญหาว่าลำดับ Farey มันดันเป็นลำดับอนันต์ซะหนิ ดังนั้นเลยเพิ่มข้อจำกัดไปว่า ลำดับ Farey อันดับ n จะบรรจุเศษส่วนอย่างต่ำ ที่ตัวส่วนมีค่าน้อยกว่าเท่ากับ n เท่านั้น

พอมันเป็นลำดับจำกัดแล้ว ก็สามารถเพิ่มข้อกำหนดสุดท้ายได้ว่า สมาชิกแต่ละตัวในลำดับนี้ ต้องเรียงค่าจากน้อยไปมากด้วย

ส่วนวงกลม Ford ก็เป็นวงกลมที่นำเอาลำดับ Farey ไปวาดแสดงผลได้อย่างสวยงาม ;)

รายละเอียดทั้งหมดอยู่ในเอกสารฉบับนี้ ไม่ต้องกลัวว่าเป็นภาษาคณิตศาสตร์แล้วจะเข้าใจยาก เพราะมีโปรแกรมที่เขียนด้วย Python ให้ไปแกะโค้ดเล่นกันด้วย

ปล. blog ตอนนี้ เขียนเป็นพิเศษให้ @flurrywong @FordAntiTrust ฮะ <3

Jan 2, 2013

ทำ Optional Argument แบบ LaTeX

command ใน LaTeX นั้นมีวิธีเรียกใช้ที่ต่างจากภาษาโดยทั่วไปอยู่บ้าง ลองดู
\sqrt{x}
\sqrt[3]{x}
อันด้านบนจะวาด √x (รากที่สองของ x -- โดยปรกติ รากที่สองไม่จำเป็นต้องมีเลข 2 กำกับ) ส่วนอันล่างจะวาด ∛x

รูปร่างของ syntax ที่น่าสนใจคือ ส่วนที่เป็น optional argument จะถูกเรียกขึ้นมาก่อน main argument แถมยังใช้วงเล็บแยกกันอีก นี่ทำให้การ currying เป็นไปได้อย่างง่ายดาย
\newcommand{\cbrt}{\sqrt[3]}
...
\cbrt{x}
จริงๆ แล้วทาง math ก็มีฟังก์ชันในแนวคิดนี้อยู่เยอะ อย่างเช่น σx(n) ซึ่งเขียนแบบนี้แล้วเข้าใจง่ายกว่า σ(n, x) และยังไม่สับสนอีกว่า argument ตัวไหนที่ควรเป็น n หรือ x กันแน่

แล้วถ้าอยากได้แบบนี้ใน Python บ้าง? ง่ายนิดเดียวเพราะใช้เทคนิคเดียวกับ Infinite List นั่นเอง
@singleton
class sigma:
    def __call__(self, n, x=1):
        if n == 1:
            return 1
        return product( sum((k**x)**i for i in range(v+1))
                            for k, v in Counter(factor(n)).items() )
    def __getitem__(self, x):
        def partial_sigma(n):
            return self(n, x)
        return partial_sigma
คราวนี้จะเขียน
6 == sigma[1](6) - 6
หรือกระทั่ง
tau = sigma[0]
...
[tau(i) for i in range(return 1, 10)]
ก็ย่อมได้

Dec 14, 2012

9/3(2+1) = ?

เวลามีคนถามว่า

9/3(2+1) = ?

จะตอบโดยไม่ลังเลเลยว่า 1 แถมถ้าใครมาบอกว่าต้องคิดหารทางด้านซ้ายมือ แล้วค่อยมาคูณแบบคอมพิวเตอร์นะ จะตอบกลับไปเลยว่ามัน syntax error โว้ย ภาษาคอมพิวเตอร์ที่ไหนเค้ายอมรับการเขียนเลขติดกันว่าเป็นการคูณเลขบ้าง?

อนึ่ง ถ้ามองว่าการเขียนเลขติดกัน (คั่นด้วยวงเล็บ) เป็นการคูณจริงๆ ตัวที่ติดกับวงเล็บก็ต้องมี precedence สูงกว่า (ทำงานก่อน) เครื่องหมายหารอยู่แล้ว นั่นหมายความว่าต้องคิดส่วน 3(2+1) ให้เสร็จก่อนเอาไปคำนวณต่ออยู่ดี

เจอบ่อยๆ ก็เริ่มขี้เกียจอธิบาย เลยเขียนโปรแกรมที่มันแปลสมการด้านบนนี้ได้ถูกต้องเลยละกัน -> จิ้มโลด

ตัวอย่างผลลัพท์จากโปรแกรม

>>> 9/3(2+1)
1.0
>>> a, b, c, d = 48, 2, 9, 3
>>> a/b(c+d)
2.0
>>> 10(9(8(7(6(5(4(3(2(1)))))))))
3628800
>>> (1+2j)(3-4j)
(11+2j)
>>> 1 + 1/phi
1.618033988749895
>>> phi(43)
42

อันที่จริงก็คิดว่าจะทำมาได้ซักพักละ แต่ด้วยความที่ขี้เกียจวางกฎ grammar ด้วย flex bison ทั้งที่หลายๆ กฎนั้นภาษาส่วนใหญ่ก็ทำไว้ให้แล้ว เลยดองมาเรื่อยๆ จนทำ Infinite List เสร็จ และไปเจอ Python's Magic Methods เลยได้ฤกษ์เริ่มจากตรงนั้น

แนวคิดก็ง่ายนิดเดียว แค่เพิ่มเมธอด __call__ เข้าไปให้ class ของตัวเลข (เป็น Monkey Patching เหมือนใน Ruby) โดย define มันกว่าถ้าเรียก a.__call__(b) แล้วจะมีค่าเท่ากับเอามันมาคูณกันซะ

ปัญหาที่ต้องไล่เก็บก็คือ
  1. Python ไม่อนุญาติให้แก้ไขพวก builtins -- ที่จริงปัญหานี้ก็เหมือนกับคราวที่ทำ Infinite List นั่นแหละ ซึ่งแก้ไม่ยาก แค่ inherit class นั้นๆ ออกมาแล้วไล่ define magic method ซะ
  2. ปัญหาจริงๆ คือ Python parser มันจะแปลตัวเลขไปเป็น class ตัวเลขใน builtins เท่านั้น ดังนั้นก็ต้องแปลง token เองโดยหาว่าตรงไหนคือเลข แล้วก็เอา class ตัวเลขที่ inherit มาแล้วครอบไว้
  3. งานนี้ได้โมดูล tokenize มาช่วยแปลตัวเลขให้ ไม่ต้องเขียน regex เอง เพราะมันจะมีท่าประกาศตัวเลขประหลาดๆ ทำให้ tokenize เองพลาดได้
  4. ที่เหลือก็เอา code ที่แก้ token เรียบร้อยไปทำงาน ตรงนี้ดึงโมดูล code ที่ทำทุกอย่างเตรียมไว้แล้วมาใช้ฟังก์ชันเดียวจบ
  5. แต่ตอนที่เอา tokenize มาใช้ร่วมกับ code จะมีปัญหาที่พิมพ์ได้ทีละบรรทัด เนื่องจาก tokenize มันปรับแต่งอะไรไม่ได้เลย ก็ต้องหลอกมันเอานิดหน่อย
  6. ครบถ้วนกระบวนความแล้วก็ clean up โดยจุดที่เอา code ออกไปได้เยอะมากๆ คือส่วน define magic method อันนี้ใช้ meta-programming บอกแค่ว่าจะเพิ่ม method ไหนบ้างก็พอ แล้วปล่อยให้มัน generate ตัวเองไปซะ
  7. อยู่ๆ ก็เขียน function decorator เป็นเฉยเลย งงตัวเอง 555+
อย่างไรก็ตาม แนวคิดของ callable number นี้ไม่ควรเอาไปใช้เขียนโปรแกรมในโลกจริง เพราะนักคณิตศาสตร์ชอบตังชื่อเดียว แต่ใช้ในคนละบริบท เช่น φ (phi) ที่อาจหมายถึง golden radio หรือ Euler's totient ก็ได้ ซึ่งแม้ว่าทั้ง 2 รูปของ φ นี้จะไม่ overlap กัน (เช่น 1+1/φ จะถูกมองว่าเป็นตัวเลข ในขณะที่ φ(43) คือการใช้ฟังก์ชัน) แต่ด้วยความที่ Python เป็น 1st class function ที่ยอมให้เราส่งผ่านฟังก์ชันเป็นตัวแปรของฟังก์ชันอื่นๆ ได้ ดังนั้นถ้าเราเจออะไรอย่าง δ(φ,ε) เราจะไม่สามารถแน่ใจได้เลยว่า φ ในบริบทนี้คืออะไรครับ

Nov 20, 2012

Topology 101: กลับด้านในของลูกบอลออกมา

ในวิชา topology มันมีบทพิสูจน์หนึ่งที่ไม่น่าเชื่อเอาซะมากๆ นั่นคือ เราสามารถกลับด้านเอาด้านในของลูกบอลออกมาด้านนอกได้ (Smale's paradox)



เอ่อ มันทำได้จริงแฮะ

Oct 23, 2012

ถ้าฝนตกแล้วเก็บผ้า

เวลาพูดว่า "ถ้า ก แล้ว ข" เนี่ย มันเป็นการพูดแบบสนใจตอนที่เงื่อนไข ก เป็นจริงอย่างเดียว ถ้าเงื่อนไข ก ไม่เกิด เหตุการณ์ ข ที่ตามมาจะเกิดหรือไม่เกิดก็ได้

ตัวอย่างเช่น "ถ้าฝนตก ผ้าที่ตากไว้จะเปียก" จะเห็นได้ว่า ฝนตกแล้วยังไงผ้าต้องเปียกแน่ๆ ไม่มีทางที่ฝนตกแต่ผ้าไม่เปียกได้เลย แต่เราจะไม่สามารถระบุได้เลยว่าหากฝนไม่ตกแล้วผ้าจะเปียกหรือไม่ (มันไม่เกี่ยวโยงกันเลย)

สิ่งที่น่าประหลาดใจมาก คือหลายครั้งเรามักจะคิดว่ามันเชื่อมโยงกันได้ คือคิดว่า "ถ้าไม่ ก แล้วไม่ ข" ตามไปด้วย อย่างเช่นจากด้านบนนี้ เราอาจคิดว่า "ถ้าฝนไม่ตก ผ้าที่ตากไว้จะไม่เปียก" ซึ่งจะเห็นได้ว่าผิดถนัด เพราะเงื่อนไขของการที่ผ้าเปียกอาจไม่ได้เกิดจากฝนตกอย่างเดียว ตอนรดน้ำต้นไม้อาจสะบัดสายน้ำไปโดนผ้าก็เป็นได้

ดังนั้นถ้าจะหัวหมอหน่อย เวลามีคนบอกว่า "ถ้าฝนตก แล้วเก็บผ้า" เนี่ย เราสามารถไปเก็บผ้าได้ทันทีเลย ไม่ต้องรอให้ฝนตกนะ (เพราะสาเหตุมันไม่เกี่ยวโยงกัน ดังนั้นเมื่อฝนไม่ตก จะเก็บผ้าหรือไม่เก็บผ้าก็ได้)

ทางแก้คือเปลี่ยนไปใช้รูปประโยค "ก ก็ต่อเมื่อ ข" แทน ซึ่งหมายความคือ "ถ้า ก แล้ว ข" และ "ถ้า ข แล้ว ก" ทั้งสองอย่าง

นั่นก็คือเปลี่ยนประโยคนั้นเป็น "ฝนตกก็ต่อเมื่อเก็บผ้า" หรือเพื่อให้อ่านได้เข้าใจง่ายขึ้น "เก็บผ้าก็ต่อเมื่อฝนตก" แบบนี้แล้ว การจะเก็บผ้าต้องทำตอนที่ฝนตกเท่านั้น ถ้าฝนไม่ตกห้ามเก็บผ้าเด็ดขาดครับ

Sep 18, 2012

1000^1000 กับ 1001^999 อะไรมากกว่ากัน?

สืบเนื่องจาก blog ตอนที่แล้ว มีหลายท่านท้วงเข้ามาว่าโจทย์ผิดหรือเปล่า ซึ่งผมได้ไปเช็คกับคำถามอีกรอบแล้วก็พบว่า โจทย์ที่ได้รับมานั้นไม่ผิดจริงๆ แต่ก็น่าสงสัยอย่างมากว่าคนตั้งโจทย์นั่นแหละที่เขียนเลขตกหล่นไปเอง

จริงๆ ตอนก่อนก็แอบเกรียนไปหน่อย คือรู้แหละว่าตั้งใจจะให้พิสูจน์ทางคณิตศาสตร์ งั้นมาไถ่บาปกับคำถามที่ว่า 1000^1000 กับ 1001^999 (เวอร์ชั่นแก้คำผิดแล้ว) อะไรมากกว่ากันอีกรอบดีกว่า ;)



ก่อนอื่น สังเกตว่า
1000^1000 = 1000 * 1000^999
ที่เราต้องการคือ จัดรูปของ 1001^999 ให้กลายเป็น 1000^999 ให้ได้ ซึ่งก็ง่ายๆ โดนทำการกระจายทวินาม
1001^999 = (1000 + 1)^999
         = summation   C(999, i) * 1000^(999-i) * 1^i   for i from 0 to 999
         = summation   C(999, i) * 1000^(999-i)         for i from 0 to 999
         = 1000^999 + C(999, 1) * 1000^998 + C(999, 2) * 1000^997 + ... + 1
จะเห็นว่า พอกระจาย 1000^999 ออกมาแล้ว จะมี 1000 พจน์พอดี (index ไล่จาก 0 จนถึง 999) ซึ่งมันสามารถเอาไปจับคู่กับ 1000*1000^999 ได้ ดังนี้
pair(1000^1000, 1001^999) = [ (1000^999, 1000^999),
                              (1000^1 * 1000^998, C(999, 1) * 1000^998),
                              (1000^2 * 1000^997, C(999, 2) * 1000^997),
                              ...
                              (1000^i * 1000^(999-i), C(999, i) * 1000^(999-i)),
                              ...
                              (1000^999 * 1000^0, C(999, 999) * 1000^0) ]
จะเห็นว่า สิ่งที่ต้องการตรวจสอบคือ 1000^i กับ C(999, i) อะไรมากกว่ากัน

จาก
P(n, r) = n!/r!
C(n, r) = n!/(r!(n-r)!) = P(n, r)/(n-r)!
จะเห็นว่า P(n, r) มีค่ามากกว่าหรือเท่ากับ C(n, r) เสมอ

ถึงตรงนี้ก็เห็นชัดแล้วว่า
1000^0 = P(999, 0) = 999!/999! = 1
1000^1 > P(999, 1) = 999!/998! = 999
1000^2 > P(999, 2) = 999!/997! = 999 * 998
1000^3 > P(999, 3) = 999!/997! = 999 * 998 * 997
...
1000^n >= P(999, n) = 999!/(999-n)! = 999 * 998 * ... * (999-n)
ดังนั้น
1000^i >= P(999, i) >= C(999, i)   for all i in [0..999]
เพราะฉะนั้น
for i in [1..999]:
    summation 1000^i * 1000^(999-i) >= summation C(999, i) * 1000^(999-i)
    summation 1000^(i+999-i) >= summation C(999, i) * 1000^(999-i) * 1^i
    1000 * 1000^999 >= (1000 + 1)^999
    1000^1000 >= 1001^999
แต่เนื่องจาก มีบางพจน์ 1000^i ที่มีค่ามากกว่า C(999, i) ดังนั้น
1000^1000 > 1001^999
จบการพิสูจน์ครับ

Sep 13, 2012

ทำ Recursion บนการ Integral

ฟังก์ชัน factorial นั้นมีนิยามง่ายๆ ตรงไปตรงมาคือ ผลคูณของจำนวนเต็มบวกตั้งแต่ 1 ไปจนถึง n
factorial n = product [1..n]
ซึ่งถ้าสังเกตดูซักหน่อย จะพบว่ามันสามารถเขียนนิยามเป็น recursion ได้
factorial 1 = 1
factorial n = n * factorial (n - 1)
ตอนนี้เราอาจเพิ่มนิยามที่ว่า 0! = 1 เพื่อความสะดวกในการกระจายพจน์สำหรับคำนวณความน่าจะเป็น

แต่ทั้งหมดนี้จะมีปัญหาอยู่อย่างนึง ตรงที่ทุกอย่างเป็น discrete math หมดเลย นั่นคือเราไม่สามารถหาอะไรอย่าง 0.5! ได้

ถ้าดูจากที่มาและการใช้งานของมัน (ส่วนมากเป็นเรื่องความน่าจะเป็นนั่นแหละ) ก็อาจนับว่าไม่มีปัญหา แต่สำหรับ pure math แล้ว มันก็น่าจะมีอะไรซักอย่างมาตอบคำถามตรงนี้ได้นะ



โชคดี (?) ที่เรามีเลขมหัศจรรย์อย่าง e ซึ่งมีสมบัติประหลาดอย่าง
e^x = d/dx e^x
ดูๆ ไปแล้ว มันก็คล้ายกับ fixed-point combinator ที่เคยพูดไว้ในตอนก่อนๆ เพียงแต่เปลี่ยนจาก function call เป็นการ diff-integral แทน โดยมี terminate point อย่าง
integral 1/e^x dx from 0 to infinity = - 1/e^infinity + 1/e^0
                                     = 1
นี่ทำให้เราสามารถเขียน recursion ในรูปของ integral ได้ เช่น
Gamma(n) = integral 1/e^t t^(n-1) dt from 0 to infinity
ลองดูสมบัติของมันโดยทำการ integral เข้าไป จะได้ว่า
Gamma(n) = integral 1/e^t t^(n-1) dt from 0 to infinity
         = integral u dv from 0 to infinity            ; let u = 1/e^t, dv = t^(n-1) dt
         = (limit u*v for x from 0 to infinity) - integral v du from 0 to infinity
         = (1/n)(1/e^infinity infinity^n - 1/e^0 0^n) - integral v du from 0 to infinity
         = 0 - integral v du from 0 to infinity
         = (1/n) integral 1/e^t t^n dt from 0 to infinity
         = Gamma(n+1)/n
ดังนั้น จะเห็นว่า
Gamma(1) = 1
Gamma(n) = (n-1) * Gamma(n-1) = (n-1)(n-2) * Gamma(n-2) = ...
ซึ่งก็คือ
factorial(n) = Gamma(n+1)


ถึงตอนนี้ก็คงตอบคำถามได้แล้วว่า 0.5! นั้น สามารถหาค่าได้จาก
Gamma(3/2) = (1/2) * Gamma(1/2)

Gamma(1/2) = integral 1/e^t 1/sqrt(t) dt from 0 to infinity
           = 2 * integral 1/e^u^2 du from 0 to infinity      ; let du = 1/sqrt(t)
           = integral 1/e^u^2 du from -infinity to infinity
           = sqrt(pi)

Gamma(3/2) = sqrt(pi)/2
ตอนพยายามหาค่าของ Gamma(1/2) ต้องใช้ความรู้เรื่อง double integral และการแปลงระนาบเข้าช่วย ลองศึกษาเทคนิคที่นี่

ข้อดีของการเขียนในรูป integral นี่มีอีกอย่าง คือถ้ารู้สึกว่าพิสูจน์ห่าค่าตรงๆ แบบนี้มันยากไป จะเลี่ยงไปใช้ Riemann integral เพื่อประมาณค่าแทนก็ย่อมได้ครับ

Aug 30, 2012

ทำ Infinity List ใน Python

พอดีไปขุด Project Euler มาเล่น แล้วมันต้องได้ยุ่งกับพวก infinity sequence อย่างจำนวนเฉพาะ, ฟีโบนักชีบ่อยๆ ตอนแรกก็ใช้แค่ list ธรรมดาเนี่ยแหละ แล้วถ้า index ที่อยากได้ไม่มีก็ค่อยไปเรียก function แยกมาคำนวณแล้วเก็บข้อมูลกลับเข้า list เอา

ทีนี้เขียนไปเขียนมารู้สึกว่า code มันไม่สวยเอาซะเลย เพราะไอ้จำนวนเหล่านี้เนี่ย เวลาหามันจะกระโดดข้ามไป index ที่ต้องการไม่ได้อยู่แล้ว บวกกับจำนวนพวกนี้มันหาเพิ่มได้เรื่อยๆ ไม่มีที่สิ้นสุด ดังนั้นในสายตาของนักคณิตศาสตร์อย่างผม ถ้าเขียน prime[100] แล้วเจอฟ้องว่า index out of range นี่คงเซ็งน่าดู เลยคิดว่ามันน่าจะมีวิธี hack ให้ใช้ syntax นี้หาค่าใน index ที่ต้องการแม้ต้อนนี้จะยังไม่มีได้

ค้นไปค้นมาก็เจอ list.__getitem__ เลยจัดการลงมือลุย
class InfinityList(list):
    def __iter__(self):
        n = 0
        while True:
            yield self[n]
            n += 1

    def __repr__(self):
        return super(InfinityList, self).__repr__()[:-1] + ', ...]'


class fibonacci(InfinityList):
    def __getitem__(self, n):
        while True:
            try:
                return super(InfinityList, self).__getitem__(n)
            except IndexError:
                self.append( super(InfinityList, self).__getitem__(-1) +
                             super(InfinityList, self).__getitem__(-2) )

    def __init__(self):
        super(InfinityList, self).__init__([1, 1])


class prime(InfinityList):
    def __getitem__(self, n):
        while True:
            try:
                return super(InfinityList, self).__getitem__(n)
            except IndexError:
                c = super(InfinityList, self).__getitem__(-1)
                while True:
                    c += 2
                    for p in self[:]:
                        if not c % p:
                            break
                    else:
                        self.append(c)
                        break

    def __init__(self):
        super(InfinityList, self).__init__([2, 3])

fibonacci = fibonacci()
prime = prime()
กรอเวลาไปข้างหน้า 8 ชั่วโมง ก็มีของเล่นใหม่ตามที่แปะเป็นตัวอย่างไว้ด้านบน
  1. ใน __getitem__ นี่ลืมไปว่าถ้าแก้ตรงนี้แล้วมันจะทำ recursion กับตัวเอง ทำให้ไม่สามารถใช้ syntax อย่าง fibonacci[-1] เพื่อดึงเอาตัวเลขออกมาได้ ก็เลยต้องเลี่ยงไปเรียก method จาก super class เอา
  2. จะให้ syntax แบบ list[n] ทำงานโดยเรียก __getitem__ ต้อง inherit class มาแล้ว define ไว้ตอนสร้าง class เท่านั้นด้วย มาสั่ง class.__getitem__ = outer_function ไม่ได้ (ป้องกัน injection เข้าระบบ)
  3. ตัว __getitem__ มันรับ parameter ได้อันเดียวก็จริง แต่ไม่ใช่แค่ตัวเลขเท่านั้น เพราะยังมี slice object อีกด้วย (นึกถึงเวลาเขียน some_list[4:-4] ไอ้ [4:-4] นั่นแหละ slice object) ดังนั้นงานนี้ play safe ใส่ while True ไปดีกว่า ช้าหน่อยยอมรับได้
  4. สุดท้ายก็คืออยากให้ class พวกนี้เป็น static ไปซะ (ตอนแรกออกแบบว่าจะให้เขียนเป็น prime.numbers[n] ด้วยซ้ำ) แต่เหนื่อยมากขี้เกียจทำแล้ว เลยจับประกาศชื่อ fibonacci, prime ทับกับชื่อ class ตัวมันเองไปซะเลย ยังไงก็มีได้แค่ instance เดียวอยู่แล้วหนิ 555+
เพียงเท่านี้ ถ้าอยากได้ prime ตำแหน่งที่ 101 ก็แค่สั่ง
prime[100]
จบเลย ง่ายมั้ย? อยากลองเล่นเองแล้ว? ไปจิ้ม code ได้จากที่นี่เลย XD

Aug 28, 2012

สมการรัก

เขียน Haskell อยู่ดีๆ แล้วก็ได้ไอ้นี่ออกมา...
let me = (<3) u where (u,me) = (1,(<3))

Aug 17, 2012

Function Composition กับ Programming


ตอนม.ปลาย คงเคยเห็นฟังก์ชั่นที่เขียนแทนด้วยสัญลักษณ์ g o f กันมาแล้ว ซึ่งมันหมายความง่ายๆ เช่นนี้
(g o f) (x) = g(f(x))
(นิยามเต็มๆ ของมันคือ ถ้า f: X -> Y และ g: Y -> Z แล้ว g o f: X -> Z ---- ที่ต้องนิยามเช่นนี้เพราะเราจะสามารถละการเขียน argument x ติดไปกับนิยามได้)

ในการเขียนโปรแกรม (โดยเฉพาะเชิง functional) เรามักต้องทำงานแบบฟังก์ชันต่อเนื่อง คือ output จาก function หนึ่ง จะถูกนำมาใช้เป็น input ให้ฟังก์ชันถัดไปเป็นลูกโซ่

เขียนอธิบายเป็นภาษาโปรแกรมได้คือ
first_input = x
first_output = f(first_input)
second_input = first_output
second_output = g(second_input)
y = second_output
หรือเพื่อไม่ให้เปลืองตัวแปร ทั้งหมดนี้สามารถย่อได้เหลือ
y = g(f(x))
คำถามคือ ถ้าเราต้องการประกาศแค่ฟังก์ชั่นที่ทำงานต่อกันไปเรื่อยๆ เช่นนี้ โดยที่ไม่ต้องการใส่ input ให้ฟังก์ชั่นโดยทันที เราจะทำอย่างไร?

ทางออกหนึ่งคือใช้ lambda เข้าช่วย
h = lambda x: g(f(x))
แล้วเวลาจะเรียกใช้ฟังก์ชั่นนี้ ก็แค่สั่ง
y = h(x)
ฟังดูง่ายดี แต่นึกดูอีกที ทำไมเราถึงต้องทำอะไรให้มันยุ่งยากด้วยการเอา lambda เข้ามาเกี่ยวด้วย?

ในภาษา imperative คงไม่มีทางเลือกอื่น แต่สำหรับภาษา functional จ๋าอย่าง Haskell เราสามารถเขียนแบบนี้ได้
let h = g . f
จะเห็นว่าง่ายดายเหมือนกับ g o f ตอนแรกเลย เวลาใช้ก็แค่
h x
หรือถ้าจะละการประกาศฟังก์ชั่น h ทิ้งไป เพื่อหาผลลัพท์ทันทีเลย ก็ทำได้โดย
(g . f) x
อ่าห์... นี่มันคณิตศาสตร์ชัดๆ!!

Nov 30, 2009

อยากทำ อย่างที่ 3 เขียนนิยายคณิตศาสตร์ให้ดังก้องโลก

เมื่อวันก่อน ขณะอ่านหนังสือการทดลองทางวิทยาศาตร์อยู่
ก็เกิดไอเดียแจ่มแมวแว๊บขึ้นมาในหัว นั่นก็คือนิยายคณิตศาสตร์

ไม่รู้ว่าทำไมสมองเราถึงทำงานเร็วมาก ในเวลาที่ไม่คิดว่าจะทำงาน
แค่เห็นคำว่า paradox เข้าไป ก็คิดไปไกลถึงหนัง the metrix
แล้วก็คิดว่า เอ่อ หนังเรื่องนั้นมันก็แทรกปรัชญาเยอะโครตเลยนี่หว่า
(เอ๋... หรือว่าเป็นหนังปรัชญาที่แทรกแอคชันกันแน่ อิอิ)

แล้วก็คิดถึงอลิซในดินแดนมหัศจรรย์ ของลิวอิส คาร์โรล
ซึ่งผู้ประพันธ์นั้นเป็นนักคณิตศาสตร์ (อ่านเพิ่ม my math ฉบับ 19)
และถ้าอ่านอย่างตั้งใจแล้ว จะพบว่ามีคณิตศาสตร์ซ่อนตัวอยู่มากมาย

แล้วก็คิดไปเรื่อยๆ (ภายในเวลาไม่กี่วิ) จนไอเดียบังเกิดนั่นแล
ก็คือ นิยายคณิตศาสตร์ ที่ใช้คณิตศาสตร์เป็นแก่นของเรื่อง
แล้วแต่งเติมเรื่องราวอื่นๆ ลงไป ให้น่าสนใจเหมือนกับนิยายทั่วไป

ฝันอยากให้มันเป็นหนังนะ เอาให้ดังเป็นพลุแตกเหมือน the matrix เลย!

Nov 19, 2009

อยากทำ อย่างที่ 1 เปิด Lab วิชาคณิตศาสตร์

ถ้าได้เป็นอาจารย์มหา'ลัยนะ จะเปิดวิชา Lab เกี่ยวกับคณิตศาสตร์เลย
ไม่ต้องมหา'ลัยก็ได้ แค่อาจารย์มัธยมก็พอ จะแอบเอาวิชาเรียนมาบูรณาการ
แล้วก็เปิดสอนแบบ Lab ซะเลย ฮาๆๆ

ที่อยากสอน Lab วิชาคณิตศาสตร์มากๆ เลย นั่นก็เป็นเพราะว่า
วิชาอื่นๆ อย่าง ฟิสิกส์ เคมี ชีวะ ก็ทำ Lab กันเป็นเรื่องปรกติ
ไฉนเลย วิชาคณิตศาสตร์จะไม่มี Lab หละ? อิอิ

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

คาบถัดมา อะ สร้างรูปสามเหลี่ยมด้านเท่าสิ
สร้างวงกลมแนบใน-แนบนอกสิ
เอ้า ลองหาสมบัติบางประการของมุมในสามเหลี่ยมแนบในสิ

คาบอื่นๆ อาจแจกเครื่องคิดเลข
เอ้า ลองหารูทของสองคูณรูทของสองคูณรูทของสอง... ไปเรื่อยๆ สิ
ทำได้แล้ว งั้นก็ลองเปลี่ยนเลขสองเป็นเลขสามมั่ง ดูสิว่าเกิดอะไรขึ้น
อะ เปลี่ยนตัวเลขไปเรื่อยๆ สิ อึ่ม แล้วสรุปได้มั้ยว่ารูปทั่วไปเป็นยังไง

ฮ่าๆๆ น่าจะเป็นวิชาที่สนุกน่าดูเลย ^^

(แอบบ่นเล็กน้อย หลังจากเสียใจที่รู้สึกคล้ายว่าคณิตศาสตร์จะไม่ใช่ศาสตร์ที่ถนัด)
(แต่ตอนนี้ดันชอบทำ Lab มากกว่า แล้วก็น้อยใจว่าทำไมคณิตถึงไม่มี Lab)
(ถ้าได้เป็นใหญ่เป็นโต จะปฏิรูปการเรียนคณิตศาสตร์ให้หมดเลย คอยดูสิ หุหุหุ)


ทดไว้ก่อน ถึงเวลาจริงๆ จะได้ไม่ลืมทำ!

Apr 25, 2009

Knowledge จับตัวเลขมาตัดแบ่งยังไงให้ได้ผลคูณมากที่สุด?

เมื่อต้นเดือน ได้มีโอกาสไปเป็นพี่ค่ายโอลิมปิกฟิสิกส์ดาราศาสตร์
เจอน้องกวนๆ คนนึง เป็นคนที่ต(ร)งมากๆ ได้ฝากคำถามมาว่า

"ถ้านำเลขจำนวนเต็มบวกมาเขียนในรูปการบวกของจำนวนเต็มบวก
แล้วเอาจำนวนที่กระจายนั้นมาคูณกัน ทำยังไงให้ได้ผลลัพท์มากที่สุด?"

อันนี้ไม่เคยทำ แต่จำได้ลางๆ ว่าเห็นผ่านๆ ใน My Math เลยตอบว่าทำได้
คุณน้องเลยตั้งคำถามต่อไปว่า "ถ้าขยายขอบเขตเป็นจำนวนจริงหละ"

คำตอบแรกที่แว๊บขึ้นมาในใจทันทีเลยคือ มันต้องเกี่ยวข้อกับค่า e แน่ๆ
แต่ด้วยความขี้เกียจ+ค่ายจัดบนดอยอินทนนท์ จะให้ขบปัญหาก็กระไรอยู่
ไปเที่ยวดอยให้สนุกดีกว่า (แม้จะมาเป็นรอบที่สี่ของปีนี้แล้วก็ตาม)

หลังจากลงดอยเสร็จ ก็ลืมเรื่องนี้ไปเลย ...จนได้ไปฟังโปรเจค JSTP
ตอนนั้นอยู่ในอารมณ์ไหนไม่รู้ สงสัยเซ็งที่ฟังเด็กพรีเซนท์ไม่รู้เรื่องมั้ง
ก็แว๊บนึกถึงโจทย์ข้อนี้ขึ้นมาได้ เลยทดลองหาคำตอบตรงนั้นเลย
(นักคณิตศาสตร์เค้ามีแต่นั่งพิสูจน์ ไอ่นี่นั่งทดลอง 555)
(บ่นอีกนิด เซ็งมากลืมเครื่องคิดเลข ส่วนมือถือคิด ln ได้ แต่ไม่มี e ให้ใช้)

สรุปว่า ทดลองยังไม่ทันเสร็จ ก็ต้องได้ฤกษ์กลับบ้านซะก่อน
เลยมาทำต่อที่บ้าน คราวนี้ได้ใช้ Excel แล้ว ง่ายขึ้นมากๆ เลย ^^
ทดลองเสร็จก็มานั่งพิสูจน์ต่อ ดังนี้

ก่อนอื่น แบ่งจำนวนจริงบวกเป็นสองส่วน ผลคูณที่มีค่ามากที่สุดหาได้จาก
ให้ m2(k) = k (n-k) เป็นฟังก์ชันผลคูณของเลขที่แบ่งส่วน
โดยให้ n คือจำนวนจริงที่ต้องการแบ่งส่วน
และ k คือตัวแปรที่ใช้เพื่อแบ่งส่วนจาก n
d/dx[m2(k)] = n - 2k
0 = n - 2k
k = n/2
หมายถึง เมื่อต้องการให้ได้ผลคูณมากที่สุด ต้องแบ่งทั้งสองสวนนี้เท่ากัน
ซึ่งสำหรับการแบ่งมากกว่าสองส่วน พิสูจน์ได้ในทำนองเดียวกันครับ ^^"
(ไม่ลงพิสูจน์ไว้ละ มันยาก+ยาว+ขี้เกียจ+ยังไม่ได้ลองทำ ...เอ๋ ยังไงเนี่ย)

หลังจากที่ทราบวิธีแบ่งส่วนที่ดีที่สุดแล้ว คำถามต่อมาคือแบ่งกี่ส่วนดี?
ทำได้โดยกำหนดสมการ mx(k) = (k/x)x
โดยที่ x คือจำนวนส่วนที่ต้องการแบ่ง
d/dx[mx(k)] = (k/x)x(ln(k) - ln(x) - 1) ดิฟยากหน่อย เพราะมีพจน์ xx
0 = (k/x)x(ln(k) - ln(x) - 1)
e = k/x
แปลเป็นภาษาชาวบ้านๆ ก็คือ แบ่งให้แต่ละส่วนมีค่าเท่ากับ e นั่นเอง

แต่ในความเป็นจริงนั้น จำนวนจริงทุกตัวไม่ได้หาร e ลงตัว
ตรงนี้ก็ไม่ยากอะไร จาก x = n/e ได้ค่ามาเท่าไหร่ก็ปัดให้ใกล้ค่านั้น
ที่ต้องระวังคือ เมื่อหลังจุดทศนิยมมีค่าประมาณเลข 5 ต้องตรวจสอบให้ดี
เช่น x = 1.48 ปัดลงไม่ได้ เพราะแบ่ง 2 ส่วนแล้วผลคูณมีค่ามากกว่า
ทั้งนี้ก็เพราะว่า ฟังก์ชัน ex ไม่ใช่เส้นตรง จึงแบ่งครึ่งพอดีไม่ได้

เขียนทั้งหมดด้วยความมันส์ ก็หวังว่าท่านผู้อ่านคงจะมันส์ไปด้วยนะครับ
บวกกับแอบหวังเล็กๆ ว่าผู้อ่านจะได้รับความรู้กลับไปบ้าง ซักนิดก็ยังดี เนาะ!
แล้วพบกันใหม่เมื่อไอเดียบรรเจิดอีกรอบ สวัสดีครับ ^^

Nov 24, 2007

Knowledge ปัญหาย้ายเหรียญไปมา ปัญหาที่น่าป๊วดหัว!

จากที่เคยอัพบล๊อกประจำทุกสัปดาห์...
กลายมาเป็นหยุดอัพไปซะเฉยๆ
รู้สึกแปลกๆ แฮะ... (มันว่างๆ โล่งๆ)

เอาเถอะ ยังไงวันนี้ก็กลับมาอัพแล้ว
ยากๆ หน่อยคงไม่ว่ากันนะครับ ^^"

1 สัปดาห์ที่ก่อน ผมพบโจทย์ข้อหนึ่ง
เป็นโจทย์วัดเชาว์ปัญญาทางคณิตฯ
คำถามของโจทย์ข้อนั้น มีอยู่ว่า
"หาก้อนหินที่แตกต่าง จาก 12 ก้อน"

ครับ โจทย์นี้ก็คงไม่ยากอะไรมาก
ถ้าไม่มีข้อกำหนดปวดหัวอีก 2 ข้อ
ก็คือ หินที่ต่างจะหนักหรือเบาก็ได้
และให้ใช้ตาชั่งยาชั่งภายใน 3 ครั้ง!

...โจทย์แนวนี้ผมเคยเล่นมาก่อนแล้ว
แต่ง่ายกว่านี้ คือบอกเลยว่าเบากว่า
พอเจอข้อนี้ก็คิดทางเดิมไม่ได้ละ...

ต่อไปนี้เป็นแนวคิดของผมนะครับ
ถ้าอยากจะคิดเอง ก็ยังไม่ต้องอ่าน
เดี๋ยวจะไม่สนุกนะครับ

!!! Spoiler warning !!!

ถึงแม้ในโจทย์จะเขียนว่าใช้หิน
แต่ผมขออธิบายโดยใช้เหรียญนะครับ
เพราะว่าโจทย์แรกที่ผมเคยเล่นนั้น
เค้าใช้ "เหรียญ" เล่นกันครับ
(เหรียญกับตาชั่งจริงๆ เลย)

และผมขอเพิ่มอุปกรณ์ 2 อย่างนะครับ
(เอาไว้กันงงเฉยๆ ครับ)
มีสก๊อตเทปสีและปากกาเมจิกในอุดมคติ
(ใช้กับเหรียญแล้วไม่ทำให้น้ำหนักเพิ่ม)
สก๊อตเทปสีขอใช้สีแดง กับน้ำเงินกันครับ

เริ่มเลยนะครับ
ผมสุ่มเลือกเหรียญมา 8 เหรียญ แบ่งออก 2 กองเท่ากัน แล้วเอาไปชั่ง
ผมเอาสติกเกอร์สีแดงและสีน้ำเงินติดบนเหรียญแต่ละกลุ่ม

1. ตาชั่งไม่เอียง หมายความว่าเหรียญที่ผิดอยู่ใน 4 เหรียญที่เหลือ (ไม่ติดสติกเกอร์)
ที่จริงตรงนี้ง่ายนะ... แต่ไหนๆ ก็พิมพ์มาตั้งยาวละ พิมพ์อีกนิดคงไม่เป็นไร ^^
ผมเขียนเลข I-IV (เลขโรมัน) กำกับบนเหรียญที่ยังไม่แน่ใจ แล้วเอา I กับ II ไปชั่ง

1.1. ถ้าตาชั่งไม่เอียง หมายถึงเหรียญที่ผิดคือ III หรือ IV
ผมชั่งต่อโดยเปลี่ยนเหรียญ II ไปเป็นเหรียญ III

1.1.1. ถ้าตาชั่งไม่เอียง หมายถึงเหรียญที่ผิดยังไม่ได้ถูกชั่ง ซึ่งก็คือเหรียญ IV
1.1.2. ถ้าตาชั่งเอียง หมายถึงเหรียญที่ผิดอยู่บนตาชั่งแล้ว ซึ่งก็คือเหรียญ III

1.2. ถ้าตาชั่งเอียง หมายถึงเหรียญที่ผิดคือ I หรือ II
ผมชั่งต่อโดยเปลี่ยนเหรียญ II ไปเป็นเหรียญ III

1.2.1. ถ้าตาชั่งไม่เอียง หมายถึงเหรียญที่ผิดถูกเอาออกไปแล้ว ซึ่งก็คือเหรียญ II
1.2.2. ถ้าตาชั่งเอียง หมายถึงเหรียญที่ผิดยังอยู่บนตาชั่ง ซึ่งก็คือเหรียญ I

2. ตาชั่งเอียง หมายความว่า เหรียญที่ผิดอยู่ในกลุ่มที่ชั่งนี้ (แดงหรือน้ำเงิน)
ตรงนี้ 4 เหรียญที่ไม่ได้ชั่งนั้น เรารู้ว่ามันมาตฐานครับ ซึ่งจะมีบทบาทสำคัญมาก
ผมเขียนหมายเลข ๑ ถึง ๓ บนเหรียญสีแดง 3 เหรียญ (ไม่เขียน 1 เหรียญ)
และทำอย่างนี้เช่นเดียวกันกับกลุ่มเหรียญสีน้ำเงิน เพียงแต่เปลี่ยนไปใช้เลขอาราบิกแทน
เอาหละ... ถึงตรงนี้จะยากแล้วนะครับ ^^" (สูดลมหายใจลึกๆ...)
ด้านซ้ายของตาชั่ง (เคยชั่งสีแดงล้วนมาก่อน) ผมใส่เหรียญ ๑ ๒ แดง และ 1 2 น้ำเงิน
ด้านขวาของตาชั่ง ผมใส่เหรียญ ๓ แดง, 3 น้ำเงิน และเหรียญมาตฐาน 2 เหรียญ

2.1. ถ้าตาชั่งไม่เอียง หมายถึงเหรียญที่ผิดเป็นเหรียญแดงหรือน้ำเงินที่ไม่ได้เขียนเลขกำกับ
ผมเอาเหรียญสีแดงที่ยังไม่ได้เขียนไปชั่งกับเหรียญมาตฐาน

2.1.1. ถ้าตาชั่งไม่เอียง หมายถึงเหรียญที่ผิดไม่ได้ถูกนำมาชั่ง ซึ่งก็คือเหรียญน้ำเงินไม่กำกับ
2.1.2. ถ้าตาชั่งเอียง หมายถึงเหรียญที่ผิดอยู่บนตาชั่งแล้ว ซึ่งก็คือเหรียญแดงไม่กำกับ

2.2. ถ้าตาชั่งเอียงข้างเดิม หมายถึงเหรียญที่ผิดไม่ได้ถูกย้าย ก็คือ ๑ ๒ แดง และ 3 น้ำเงิน
ด้านซ้ายของตาชั่ง ผมเอาเหรียญ ๒ แดง กับ 3 น้ำเงินใส่ ส่วนด้านขวาใส่มาตฐาน 2 เหรียญ

2.2.1. ถ้าตาชั่งไม่เอียง หมายถึงเหรียญที่ผิดถูกเอาออกไปแล้ว ซึ่งก็คือเหรียญ ๑ แดง
2.2.2. ถ้าตาชั่งเอียงข้างเดิม หมายถึงเหรียญที่ผิดยังอยู่ข้างเดิม ซึ่งก็คือเหรียญ ๒ แดง
2.2.3. ถ้าตาชั่งเอียงข้างใหม่ หมายถึงเหรียญที่ผิดถูกสลับด้าน ซึ่งก็คือเหรียญ 3 น้ำเงิน

2.3. ถ้าตาชั่งเอียงข้างใหม่ หมายถึงเหรียญที่ผิดถูกสลับด้าน คือ 1 2 น้ำเงิน และ ๓ แดง
ด้านซ้ายของตาชั่ง ผมเอาเหรียญ 2 น้ำเงิน กับ ๓ แดงใส่ ส่วนด้านขวาใส่มาตฐาน 2 เหรียญ

2.3.1. ถ้าตาชั่งไม่เอียง หมายถึงเหรียญที่ผิดถูกเอาออกไปแล้ว ซึ่งก็คือเหรียญ 1 น้ำเงิน
2.3.2. ถ้าตาชั่งเอียงข้างเดิม หมายถึงเหรียญที่ผิดยังอยู่ข้างเดิม ซึ่งก็คือเหรียญ 2 น้ำเงิน
2.3.3. ถ้าตาชั่งเอียงข้างใหม่ หมายถึงเหรียญที่ผิดถูกสลับด้าน ซึ่งก็คือเหรียญ ๓ แดง

ถ้ายังไม่เข้าใจก็ลองหาเหรียญมาเล่นจริงๆ นะครับ

End of spoiling.

วิธีที่ผมใช้ บอกไม่ได้ว่าหนักหรือเบากว่า
(กว่าจะคิดได้ขั้นนี้ก็จะตายอยู่แล้วครับ)

ถ้าอยากดูแบบขั้นเทพ เชิญที่พันทิพครับ
(ลิงก์อยู่ตรง 'อ้างอิง' นะครับ)

เอนทรีนี้ควรจะเป็นอันสุดท้ายของเดือนละ
(และเกือบจะเป็นเอนทรีแรกซะด้วย...)

เพราะผมต้องกลับไปอ่านหนังสือละครับ
แล้วเจอกันใหม่ เมื่อสอบเสร็จครับ ^^
bye nii~

อ้างอิง :
http://www.vcharkarn.com/include/vcafe/showkratoo.php?Pid=128423
http://topicstock.pantip.com/wahkor/topicstock/2007/11/X6030804/X6030804.html