programming.in.th · ข้อ 2034
หั่นแผ่นช็อกโกแลตห้าสิบคูณห้าสิบ ทุกครั้งจ่ายเท่าลูกเกดบนบล็อกที่หั่น ท่าหั่นให้สองซีกหนักเท่ากันผ่านตัวอย่างในโจทย์แต่แพ้ บทนี้ตัดลำดับทิ้ง แล้วเก็บค่าของบล็อกทุกก้อนไว้ในตารางสี่มิติ
Bonny ทำขนมอยู่ที่เมือง Plovdiv เธอมีแผ่นช็อกโกแลตรูปสี่เหลี่ยมที่เรียงเป็นชิ้นเล็ก N แถว M คอลัมน์
บนแต่ละชิ้นมีลูกเกดอย่างน้อยหนึ่งเม็ด เธออยากหั่นมันจนเหลือเป็นชิ้นเดี่ยวทั้ง N × M ชิ้น
แต่ไม่ว่าง จึงจ้าง Sly Peter มาหั่นแทน
Peter หั่นได้ทีละครั้ง เป็นเส้นตรงจากขอบหนึ่งไปอีกขอบของบล็อกที่ได้รับ ตามแนวรอยต่อระหว่างชิ้น และคิดค่าแรงเป็นลูกเกด หั่นบล็อกไหน ก็ขอลูกเกดเท่ากับจำนวนลูกเกดทั้งหมดบนบล็อกนั้น Bonny เลือกได้ทุกอย่าง ทั้งจะส่งบล็อกไหนให้หั่นก่อน หั่นแนวนอนหรือแนวตั้ง และหั่นตรงรอยไหน งานของเราคือหาค่าแรงรวมที่น้อยที่สุด
อินพุต / ขอบเขต / เอาต์พุต
N และ M
อีก N บรรทัดถัดมา บรรทัดละ M จำนวน คือจำนวนลูกเกดบนแต่ละชิ้นของแถวนั้น เรียงจากซ้ายไปขวา
1 ≤ N, M ≤ 50 ลูกเกดบนแต่ละชิ้นอยู่ระหว่าง 1 ถึง 1,000 เม็ด
เวลา 5 วินาที หน่วยความจำ 128 เมกะไบต์
N และ M ไม่เกิน 7| Input | Output |
|---|---|
| 2 3 2 7 5 1 9 5 | 77 |
กระดานนี้มี 6 ชิ้น ทุกครั้งที่หั่นจะได้ชิ้นเพิ่มหนึ่งชิ้นพอดี จึงต้องหั่น 5 ครั้งเสมอ ไม่ว่าจะหั่นยังไง คำตอบ 77 คือค่าแรงของทั้ง 5 ครั้งนั้นรวมกัน
ใบ้
ครั้งแรกไม่มีทางเลือกเลย บล็อกที่ได้รับคือทั้งแผ่น จึงต้องจ่ายลูกเกดทั้งหมด 29 เม็ดเสมอ สิ่งที่เลือกได้คือหั่นตรงไหน และมันกำหนดว่าชิ้นไหนจะต้องไปอยู่ในบล็อกที่ถูกหั่นซ้ำอีกกี่รอบ
หลังหั่นครั้งแรกเสร็จ เราถือสองบล็อกที่ไม่เกี่ยวกันเลย ลองถามดูว่าบล็อกหนึ่งก้อนต้องจำอะไรบ้าง ถึงจะรู้ว่ามันควรจ่ายเท่าไร และบล็อกแบบนี้บนกระดาน 50 คูณ 50 มีได้กี่แบบ
เริ่มจากกระดานในโจทย์ก่อน ลองดูว่าหั่นยังไงถึงจ่ายแค่เท่าที่โจทย์ตอบ
กดรอยต่อระหว่างสองชิ้น เพื่อหั่นบล็อกที่รอยนั้นอยู่ ตลอดแนวทั้งบล็อก
จ่ายไปแล้ว 0 เม็ด
การหั่นหนึ่งครั้งจะวิ่งสุดขอบของบล็อกที่รอยนั้นอยู่เสมอ บล็อกอื่นไม่โดนด้วย
ลองหั่นให้ครบทั้งสามกระดานก่อนนะครับ พอเริ่มรู้สึกว่าเส้นแรกสำคัญที่สุด ค่อยเปิดเฉลย
ที่มาของแนวคิดนี้
ประโยคที่ทำให้โจทย์ดูยากคือ Bonny เลือกลำดับได้ ว่าจะส่งบล็อกไหนให้หั่นก่อน ถ้าลำดับมีผลจริง สถานะของปัญหาจะต้องจำทุกบล็อกที่ยังเหลืออยู่พร้อมกัน ซึ่งคือตัวตรวจแบบถุงที่ผมเขียนไว้ท้ายบท ผมวัดมันแล้ว จากกระดาน 3×4 ไป 4×4 เวลาโตขึ้นราว 18 เท่า ทั้งที่ชิ้นเพิ่มแค่สี่ชิ้น
แต่พอดูว่าค่าแรงหนึ่งครั้งขึ้นกับอะไร คำตอบคือขึ้นกับลูกเกดบนบล็อกที่ถูกหั่นเท่านั้น ไม่ได้ขึ้นกับว่าตอนนั้นมีบล็อกอื่นวางอยู่บนโต๊ะกี่ก้อน หรือหั่นกันไปแล้วกี่ครั้ง หั่นบล็อกซ้ายก่อนหรือขวาก่อน เงินที่จ่ายจึงเท่ากัน ลำดับถูกตัดทิ้งไปจากปัญหาได้ทั้งหมด
เหลือแค่ว่าแต่ละบล็อกถูกหั่นตรงเส้นไหน และบล็อกหนึ่งก้อนบอกได้ด้วยตัวเลขสี่ตัว คือแถวบน คอลัมน์ซ้าย แถวล่าง คอลัมน์ขวา ที่ขอบเขตเต็มมีบล็อกได้ 1,625,625 แบบ ซึ่งเก็บเป็นตารางได้สบาย ผมไม่ได้เชื่อข้ออ้างเรื่องลำดับเฉย ๆ แต่เอาไปเทียบกับตัวตรวจแบบถุงที่ไม่ได้ใช้ข้ออ้างนี้เลย ตรงกันหมด บทเรียนที่ยกไปข้ออื่นได้คือ เวลาโจทย์ให้เลือกลำดับ ให้ถามก่อนว่าต้นทุนแต่ละก้าวรู้ตัวหรือเปล่าว่าตัวเองมาก่อนหรือหลัง
มองอีกมุมก็ได้ภาพเดียวกัน ลูกเกดหนึ่งเม็ดถูกนับเข้าค่าแรงทุกครั้งที่บล็อกของมันถูกหั่น ค่าแรงรวมจึงเท่ากับลูกเกดของแต่ละชิ้น คูณจำนวนครั้งที่ชิ้นนั้นอยู่ในบล็อกที่ถูกหั่น ตัวอย่างในโจทย์หั่นแบบที่ดีที่สุดแล้วได้ 2×3 + 7×3 + 5×2 + 1×3 + 9×3 + 5×2 เท่ากับ 77 ตรงกับคำตอบ เป้าหมายจริงของเราจึงเป็นการจัดให้ชิ้นที่มีลูกเกดเยอะหลุดออกมาเป็นชิ้นเดี่ยวเร็ว ๆ
ท่าที่ฟังดูเข้าท่า แต่แพ้
ท่าที่ผุดขึ้นมาก่อนเพื่อนคือหั่นให้สองซีกมีลูกเกดใกล้กันที่สุดทุกครั้ง (ถ้าเสมอกันก็เลือกเส้นแรกที่เจอ) บนตัวอย่างในโจทย์มันได้ 77 ตรงคำตอบพอดี ซึ่งเป็นเหตุผลที่มันอันตราย บนกระดาน 2×4 ในเกม มันได้ 99 ขณะที่ค่าดีที่สุดคือ 86 และบนกระดาน 3×3 มันได้ 155 จากค่าดีที่สุด 147 การแบ่งให้เท่ากันไม่ได้สนใจว่าลูกเกดกองอยู่ตรงไหน ส่วนเป้าหมายจริงคือปล่อยชิ้นที่หนักให้หลุดออกไปก่อน
ให้ dp[B] คือค่าแรงน้อยที่สุดที่ต้องจ่าย เพื่อหั่นบล็อก B จนเหลือแต่ชิ้นเดี่ยว
และ sum[B] คือจำนวนลูกเกดบนบล็อกนั้น
อ่านสูตรนี้จากจังหวะที่เราถือบล็อก B อยู่ในมือ ครั้งถัดไปที่ Peter หั่นมัน
เราต้องจ่าย sum[B] ทันทีไม่ว่าหั่นตรงไหน ส่วน P กับ Q คือสองชิ้นที่ได้หลังหั่น
ซึ่งเล็กกว่า B ทั้งคู่ ค่าที่ดีที่สุดของมันจึงอยู่ในตารางแล้ว เส้นที่หั่นได้มีสองแบบ
k: P คือแถวบนสุดของบล็อกถึงแถว k Q คือแถว k+1 ถึงแถวล่างสุด ทั้งคู่กว้างเท่าเดิม
ค่าแรงรวมของทางนี้คือ sum[B] บวกค่าที่ดีที่สุดของสองชิ้น ทางนี้ใช้ไม่ได้เมื่อบล็อกสูงแถวเดียว
k: เหมือนกันแต่แบ่งตามคอลัมน์ สองชิ้นสูงเท่าเดิม
ทางนี้ใช้ไม่ได้เมื่อบล็อกกว้างคอลัมน์เดียว
ชิ้นเดี่ยวไม่มีเส้นให้หั่นเลย จึงมีค่าเป็นศูนย์ และเป็นจุดเริ่มของทั้งตาราง
ลำดับการเติมตารางคือจากเล็กไปใหญ่ ไล่ความสูงแล้วความกว้างของบล็อก เพราะชิ้นที่ได้หลังหั่นเตี้ยกว่าหรือแคบกว่าเสมอ บล็อกมีทั้งหมด 1,625,625 แบบ ถ้านับลูกเกดทีละช่องทุกครั้ง จะต้องแตะช่องรวม 488,410,000 ครั้ง ส่วนการลองทุกเส้นมีรวม 53,103,750 ครั้ง งานนับช่องจึงหนักกว่างานลองเส้นหลายเท่า และตัดทิ้งได้ด้วยผลรวมสะสมสองมิติ ซึ่งตอบลูกเกดของบล็อกไหนก็ได้ในการบวกลบสี่ตัว (บทปูพื้นอยู่ที่ ผลรวมสะสมกับต้นไม้เฟนวิก ฝึกข้อ 1)
ถ้าเคยทำ DP บนช่วงของอาเรย์หนึ่งมิติมาก่อน ข้อนี้คือท่าเดียวกันที่ขยายเป็นสองมิติ ช่วงหนึ่งช่วงจำด้วยขอบซ้ายขวา บล็อกหนึ่งก้อนจำด้วยขอบสี่ด้าน (บทปูพื้นอยู่ที่ DP บนช่วง)
โน้ต · int พอไหม
ค่าแรงโตตามจำนวนลูกเกดบนทุกชิ้น กรณีหนักสุดคือทุกชิ้นมี 1,000 เม็ดบนกระดาน 50×50 ซึ่งผมรันแล้วได้ 28,600,000 ช่องในตารางทุกช่องไม่เกินค่านี้ และผลบวกของสองช่องก็ยังห่างจากขีดของ int มาก
ลองทุกเส้นแรกของกระดาน 2×4 ในเกม โดยใช้ค่าของบล็อกที่เล็กกว่าจากตาราง
| เส้นแรก | ชิ้นแรก | ค่าแรง | ชิ้นที่สอง | ค่าแรง | รวม |
|---|---|---|---|---|---|
| แนวนอน ใต้แถว 1 | 1×4 | 19 | 1×4 | 34 | 86 |
| แนวตั้ง หลังคอลัมน์ 1 | 2×1 | 10 | 2×3 | 52 | 95 |
| แนวตั้ง หลังคอลัมน์ 2 | 2×2 | 38 | 2×2 | 28 | 99 |
| แนวตั้ง หลังคอลัมน์ 3 | 2×3 | 55 | 2×1 | 11 | 99 |
เส้นของท่าหั่นให้สองซีกเท่ากันคือแนวตั้ง หลังคอลัมน์ 2 แม้หั่นสองชิ้นที่เหลือได้ดีที่สุดแล้ว ทางนี้ก็ยังได้ 99 ส่วนเส้นที่ดีที่สุดคือแนวนอน ใต้แถว 1 ได้ 86 ทั้งที่มันแบ่งลูกเกดได้ไม่เท่ากันเลย (13 กับ 20)
เวลาที่วัดได้บนเครื่องผม กระดาน 50×50 ลูกเกดสุ่มถึงพันเม็ด โค้ดเต็มใช้ 0.07 วินาที จากลิมิต 5 วินาที
เวลาโจทย์ยื่นอิสระให้เลือกลำดับ ให้ถามก่อนว่าต้นทุนของแต่ละก้าวรู้ไหมว่าตัวเองมาก่อนหรือหลัง ถ้าไม่รู้ ลำดับก็ทิ้งได้ และสิ่งที่เหลือมักเป็นการแบ่งของก้อนหนึ่งออกเป็นสองก้อนที่ไม่ยุ่งกันอีก ซึ่งคือ DP บนช่วง ในข้อนี้ช่วงกลายเป็นบล็อกสี่เหลี่ยม สถานะจึงมีสี่ตัวแทนที่จะมีสอง
| ทาง | ขอบเขตที่มันไหว | เวลาที่วัดได้ |
|---|---|---|
| ถุงของบล็อก ลองทุกลำดับการหั่น (ตัวตรวจ) | ทดสอบถึง 4×4 | 4×4 ใช้ 0.18 วินาที |
| จำบล็อกด้วยขอบสี่ด้าน นับลูกเกดทีละช่อง | ถึง 50×50 | 50×50 ใช้ 0.32 วินาที |
| จำบล็อกด้วยขอบสี่ด้าน นับลูกเกดด้วยผลรวมสะสม | ถึง 50×50 | 50×50 ใช้ 0.07 วินาที |
ค่าแรงหนึ่งครั้งขึ้นกับบล็อกที่ถูกหั่นเท่านั้น ลำดับจึงไม่มีผล และบล็อกหนึ่งก้อนจำได้ด้วยขอบสี่ด้าน ค่าที่ดีที่สุดของมันคือลูกเกดบนบล็อก บวกผลรวมที่ถูกที่สุดของสองชิ้นที่ได้จากเส้นใดเส้นหนึ่ง
ในหน้านี้