Just algorithm!
การทำให้ Function จำคำตอบได้! เอ๊ะ ยังไง!?

เคยเขียน Function ที่ต้อง Process นาน ๆ แต่เรียกใช้บ่อย ๆ ไหมครับ
เมื่อเจอเหตุการนี้ เรามักจะเอาผลลัพธ์ที่ได้จาก Function ไปเก็บไว้สักที่ เช่น ใน Dictionary
พอเวลาเรียกใช้ Function นั้นอีก ก็ไปหาคำตอบใน Dictionary แทน ซึ่งเร็วกว่า
การทำให้ Function จำคำตอบได้แบบนี้ เราเรียกกันว่า Memoization

ข่าวดีคือ คุณไม่ต้องไปแก้ไข Function ของคุณเพื่อทำ Memoization Smiley
ผมมีวิธีการง่าย ๆ มานำเสนอ ซึ่งจะทำให้ Function ของคุณจำคำตอบได้แบบไม่เคยเป็นมาก่อน
เพียงแค่คุณเพิ่ม Code ข้างล่างนี้ ไปไว้ใน Class สำหรับทำ Extension Methods ของคุณ

public static Func<T, TResult> Memoize<T, TResult>(this Func<T, TResult> func) {
    var dict = new Dictionary<T, TResult>();
    return arg => {
        TResult value;
        if (!dict.TryGetValue(arg, out value)) {
            value = func(arg);
            dict.Add(arg, value);
        }
        return value;
    };
}

แต่มีข่าวร้ายคือ Method ด้านบน ใช้ได้กับ Function ที่เป็น anonymous method เท่านั้น Smiley
มาดูวิธีใช้กัน สมมติว่าเรามี Function นึง ใช้สำหรับหาค่าของลำดับ Fibonacci
โดยที่ Input เป็น ลำดับของ Fibonacci

สูตรของ Fibonacci มีอยู่ว่า


(รูปจาก wikipedia.org)

ถ้าเราจะหาลำดับที่ 38 เขียนดังนี้

Func<int, int> fib = null;
fib = n => n > 1 ? fib(n - 1) + fib(n - 2) : n;
Console.WriteLine(fib(38));
//39088169
//ใช้เวลา 2.35 วินาที Smiley


ทำไมใช้ตั้ง 2 วินาทีกว่า !?
เนื่องจาก Function นี้ เป็น Function ที่เป็น Recursion มีการเรียกตัวเองหลายครั้ง
เรามาลองทำ Memoization กัน เผื่อช่วยได้

Func<int, int> fib = null;
fib = n => n > 1 ? fib(n - 1) + fib(n - 2) : n;
fib = fib.Memoize();
Console.WriteLine(fib(38));
//39088169
//ใช้เวลาแค่ 0.01 วินาที!! Smiley


เพิ่มแค่บรรทัดที่ 3 เข้าไป เร็วขึ้นกว่าเดิมทันตาเห็น!
เรามาดูกันว่ามันเร็วขึ้นได้อย่างไร
โดยการเพิ่มตัวแปร i เข้าไป โดยที่ i จะเพิ่มทุก ๆ ครั้งที่เรียก Function

Func<int, int> fib = null;
int i = 0;
fib = n => (n > 1 ? fib(n - 1) + fib(n - 2) : n) + (0 * i++);
Console.WriteLine(fib(38));
Console.WriteLine(i);
//39088169
//126491971


เมื่อดูผลลัพธ์ จะเห็นว่ามีการเรียก Function ถึง 126 ล้านครั้ง !!!
คราวนี้มาลองทำ Memoization ดูบ้าง

Func<int, int> fib = null;
int i = 0;
fib = n => (n > 1 ? fib(n - 1) + fib(n - 2) : n) + (0 * i++);
fib = fib.Memoize();
Console.WriteLine(fib(38));
Console.WriteLine(i);
//39088169
//39


หลังจากทำ Memoization มีการเรียก Function แค่ 39 ครั้ง!
น้อยกว่ากันกว่า 3 ล้านเท่า!!
ที่เป็นอย่างนี้เพราะว่า Function Fibonacci มีการเรียกตัวเองซ้ำไปซ้ำมา เช่น

fib(38) = fib(37) + fib(36)
fib(38) = [fib(36) + fib(35)] + [fib(35) + fib(34)]
fib(38) = [[fib(35) + fib(34)] + [fib(34) + fib(33)]] +
          [[fib(34) + fib(33)] + [fib(33) + fib(32)]]


จะเห็นว่านี่แตก Function ออกมาแค่ 3 ระดับ ก็พบการเรียกซ้ำมากมาย
เมื่อแตกไปเรื่อย ๆ จะมีการเรียกตัวเองถึง 126 ล้านครั้ง!!
การเติบโตของ Function นี้ เรียกได้ว่าเป็นการเติบโตแบบ Exponential เลยทีเดียว

คราวนี้มาดูการทำงานของ Memoization

fib(38) = fib(37) + M(36)
fib(38) = [fib(36) + M(35)] + M(36)
fib(38) = [[fib(35) + M(34)] + M(35)] + M(36)


ดูเรียบง่ายมากเลยใช่ไหมครับ
M หมายถึง การอ่านจาก Dictionary ไม่ต้อง Process Function
ทำให้เกิดการเรียกตัวเองแค่ 39 ครั้งเท่านั้น
เปลี่ยนจากการเติบโตแบบ Exponential เป็นแบบ Linear แทน

Code Memoization ด้านบน เป็น Code สำหรับ Function ที่มี Parameter แค่ตัวเดียว
ถ้าจะเพิ่ม Parameter เป็น 2 ตัว ก็เพิ่ม Method นี้เข้าไปใน Class สำหรับทำ Extension Methods

public static Func<T1, T2, TResult> Memoize<T1, T2, TResult>(this Func<T1, T2, TResult> func) {
    Func<T1, Func<T2, TResult>> curried = arg1 => func.Partial(arg1).Memoize();
    curried = curried.Memoize();
    return curried.Uncurry();
}


Code ด้านบนต้องการ Function ใน Post ที่แล้ว ด้วย
ในที่นี้เราใช้ Curry สำหรับแบ่ง Func(a1,a2) เป็น Func(a1)(a2)
ซึ่งในส่วนของ Curry มีการทำ Memoization กับ Func(a2) ก่อน 1 ที
แล้วค่อยมาทำ Memoization กับ Func(a1) อีก 1 ที
ทำให้ Func(a1)(a2) เป็น Memoization โดยสมบูรณ์
เสร็จแล้วก็ทำ Uncurry เพื่อแปลง Func(a1)(a2) กลับไปเป็น Func(a1, a2) Smiley

มาดูตัวอย่าง Memoization แบบ 2 Parameters กัน

คำถามจาก projecteuler.net นะครับ
สมมติว่ามีขี้เมาคนนึง เดินกลับบ้านซึ่งจากจุดเริ่มต้น ถึงบ้าน ห่างกัน 2x2 ซอย
ขี้เมาสามารถเดินกลับบ้าน (แบบไม่เดินวกกลับ) ได้ 6 วิธี ดังนี้


(รูปจาก projecteuler.net)

แต่ถ้าขี้เมาคนนั้นอยู่ห่างจากบ้าน 20x20 ซอยล่ะ
มีวิธีเดินกลับบ้านกี่วิธี?

จริง ๆ ข้อนี้มีสูตรการคิดคือ [(m + n)!]/[m! * n!]
แต่เราจะไม่ใช้สูตรนี้ เนื่องจากมันง่ายไป Smiley

เราลองมาสร้างสูตรคล้าย ๆ Fibonacci ด้านบนดู
ยึด Fm,n เป็นหลัก m คือ ความห่างจากบ้านในแนวนอน n คือ ความห่างจากบ้านในแนวตั้ง
แล้วดูความสัมพันธ์ที่จุด Fm,n กับจุดอื่น ๆ
จะพบว่าจุด Fm,n จะเชื่อมโยงกับจุด Fm-1,n และ Fm,n-1
นั่นคือการเดินไปทางขวา และการเดินลง
ดังนั้น ผลลัพธ์ของ Fm,n มาจาก 2 แหล่งคือ Fm-1,n กับ Fm,n-1
ทำให้เกิดสูตรดังนี้



เขียนเป็น Function ได้ดังนี้

Func<int, int, long> func = null;
func = (m, n) => func(m - 1, n) + func(m, n - 1);
func = func.When((m, n) => n == 0, (m, n) => func(m - 1, 0));
func = func.When((m, n) => m == 0, (m, n) => func(0, n - 1));
func = func.When((m, n) => m == 0 && n == 0, (m, n) => 1L);
func = func.Memoize();
Console.WriteLine(func(20, 20));
//137846528820


Method ชื่อ When เอามาจาก Post ที่แล้ว
Function ด้านบน ถ้าไม่มี Memoize จะเกิดการ Recursion กันมหาศาล
ซึ่งอาจใช้เวลาคำนวนนานเป็นวัน!!!
แต่ถ้ามี Memoize แล้ว ใช้เวลาคำนวนแค่เสี้ยววินาที Smiley

ถ้าใครสนใจวิธีการแตก Function เป็น Function ย่อย ๆ
แล้วใช้ Memoization ในการบันทึกค่าเพื่อเพิ่มความเร็ว แบบด้านบน
วิธีการแบบนี้เค้าเรียกกันว่า Dynamic Programming (DP) ครับ
สนใจลองศึกษาเพิ่มดูครับ :)

Post หน้า เกี่ยวกับ Linq แบบถึงกึ๋น!!

* อ่านจบแล้ว กรุณา Comment ด้วยครับ เพื่อเป็น Feedback และเป็นกำลังใจ
* บทความนี้คัดลอกได้ แต่ต้องมี Link หรือ Url กลับมาที่หน้านี้ และเมื่อคัดลอกแล้วกรุณา Comment แจ้งด้วยครับ




Create Date : 09 สิงหาคม 2551
Last Update : 23 สิงหาคม 2551 15:55:18 น. 7 comments
Counter : 2429 Pageviews.

 
ดีเยี่ยมครับ...
เคยเขียนแต่ใน MsAccess

แล้วจะกลับมาศึกษาอีกนะครับ


[[เป็นสมาชิกแต่ไม่ได้ login]]


โดย: แดนน้อย IP: 58.8.44.233 วันที่: 10 สิงหาคม 2551 เวลา:14:20:36 น.  

 
ขอบคุณนะครับ เขียน blog มีประโยชน์ จริง ๆ

เป็นกำลังใจให้นะครับ


โดย: PureSnow IP: 124.120.250.171 วันที่: 11 ตุลาคม 2551 เวลา:22:16:20 น.  

 
เป็นบล๊อกที่มีประโยชน์มากครับ

ขอให้ทำ(เฉลย)เวปproject euler ไปเรื่อยๆนะคับ
จะติดตามผลงาน


โดย: programoo IP: 202.176.102.123 วันที่: 10 พฤศจิกายน 2551 เวลา:21:39:27 น.  

 
ขอบคุณที่เข้ามาอ่านครับ

คุณ programoo ครับ
เรื่อง Dynamic Programming เนี่ยแก้ปัญหาประมาณ 10-20 ข้อ
ใน ProjectEuler ได้เลยนะครับ
ลองทำดูนะครับ


โดย: chaowman วันที่: 19 พฤศจิกายน 2551 เวลา:10:25:26 น.  

 
ขอถามหน่อยครับว่า ไม่ทราบว่า ในภาษาจาว่ามี method,function ที่เป็น memorize หรือเปล่าครับ แล้วถ้าสมมุติว่ามี การที่จะใช้ method นี้ได้นั้น ต้องเขียนโค๊ดในรูปแบบของ recursion ถึงจะใช้ได้ใช่หรือเปล่าครับ ถ้าเขียนแบบloopก็ไม่สามารถใช้เทคนิคนี้้ได้ใช่มั้ยครับ ? ช่วยอธิบายเป็นแบบอัลกอรึทึ่มให้หน่อยคับ(อ่านโค๊ดไม่ค่อยออกครับ - -")
(พอดีว่าที่ ม สอนภาษา java ครับ) ขอบคุณล่วงหน้าครับ
ps.ทำไปได้ 14(projecteuler) แต่ล่ะข้อ compile เป็นชาติเลยครับ !!


โดย: programoo IP: 202.176.103.12 วันที่: 25 ธันวาคม 2551 เวลา:22:19:38 น.  

 
Java ไม่น่ามี Memoize method ครับ
แต่คิดว่าเขียนขึ้นมาเองได้ครับ
ที่ผมเขียนด้านบน เค้าเรียกว่า auto-memoization
คือ ทำให้ function นั้นจำค่าได้เองเลย
ถ้าไม่ถนัด ให้เขียนแยกส่วนที่เป็น function กับ memoize ก็ได้ครับ

Procedure ของ Memoize ง่าย ๆ ประมาณนี้ครับ
1.ต้องการหาผลลัพธ์จาก input x
2.เอาค่า x ไปหาใน dictionary ก่อนว่ามีผลลัพธ์เก็บไว้รึเปล่า
3.ถ้ามีก็เอาผลลัพธ์จาก dictionary ไม่ต้องคำนวนใหม่
4.ถ้าไม่มีก็เอาค่า x ไป input ใน function เพื่อให้ได้ผลลัพธ์
5.เอาค่า x และผลลัพธ์ไปเก็บใน dictionary
จบขั้นตอน

auto-memoization จะเหมาะกับ function ที่เป็น recursive
ที่มีการคำนวนค่าเดิมซ้ำไปซ้ำมา
แต่ถ้าทำ Memoize แบบเขียนเอง
เป็น loop ก็ทำงานได้ครับ

เอาใจช่วยนะครับ สำหรับ ProjectEuler


โดย: chaowman วันที่: 28 ธันวาคม 2551 เวลา:2:08:47 น.  

 
แอบงง แต่ก็ขอบคุณมากครับที่ตอบ
จะลองศึกษาดูครับ


โดย: programoo IP: 203.144.187.19 วันที่: 29 ธันวาคม 2551 เวลา:12:58:21 น.  

ชื่อ :
Comment :
  *ใช้ code html ตกแต่งข้อความได้เฉพาะสมาชิก
 

chaowman
Location :
กรุงเทพฯ Thailand

[Profile ทั้งหมด]

ฝากข้อความหลังไมค์
Rss Feed
Smember
ผู้ติดตามบล็อก : 8 คน [?]





New Comments
Group Blog
 
All Blogs
 
Friends' blogs
[Add chaowman's blog to your web]
Links
 

 Pantip.com | PantipMarket.com | Pantown.com | © 2004 BlogGang.com allrights reserved.