วิเคราะห์โจทย์ข้อ The Wall-The Sequel จาก TOI17
โจทย์
มีอิฐทั้งหมด ก้อน เริ่มตั้งแต่ก้อนที่ 1 แต่ละก้อนมีคุณภาพ
มีคำสั่งทั้งหมด คำสั่ง แต่ละคำสั่งจะให้ค่ามาทั้งหมด 3 ค่าคือ
คำสั่งแต่ละครั้ง จะถามว่าคุณภาพของอิฐก้อนที่ รวมกันไปเรื่อยๆ(แต่ไม่เกินก้อนที่ ) มีมูลค่ารวมเท่าไหร่
โจทย์อยากให้เราคำนวณว่าค่าคุณภาพรวมของอิฐ ที่คำนวณได้ในแต่ละคำสั่งเป็นเท่าไหร่
ข้อมูลนำเข้า
บรรทัดแรก ระบุจำนวนเต็ม แทนจำนวนอิฐและจำนวนคำสั่ง ()
บรรทัดต่อมา ระบุจำนวนเต็ม จำนวน แทนคุณภาพของอิฐ () ของอิฐก้อนที่ ()
บรรทัดต่อมา ระบุจำนวนเต็ม แทนค่าของแต่ละคำสั่ง ()
ข้อมูลส่งออก
มีทั้งหมด 1 บรรทัด แสดงจำนวนเต็ม จำนวนโดยคั่นด้วยช่องว่าง แทนค่าคุณภาพรวมของอิฐที่คำนวณได้ในแต่ละคำสั่ง
ตัวอย่างข้อมูลนำเข้าและข้อมูลส่งออก
Input | Output |
---|---|
11 2 1 5 2 4 7 3 9 2 2 1 5 3 2 8 2 5 8 | 18 14 |
5 7 4923 4743 -3836 -484 -1009 1 1 2 1 4 3 1 3 4 2 4 3 1 5 2 4 5 5 2 5 4 | 9666 4923 4439 4743 4923 -484 4743 |
9 3 7 8 -2 45 2 1 2 -8 11 2 3 9 4 2 8 3 1 9 | 2 38 51 |
วิเคราะห์โจทย์
ก่อนที่เราจะคิดปัญหาเต็ม เราจะเริ่มมองวิธีที่ที่คิดง่ายๆ ที่แก้ได้เหมือนกัน แต่ทำไม่ทันเวลามา 2 วิธีก่อน
วิธีที่ 1 - กระโดดบวกไปตรงๆ
เริ่มแรกคือเราจะเอาค่าของที่ L
และไล่บวกค่าไปเรื่อยๆ ตั้งแต่ L, L+M, L+2M, L+3M
ไปถึง R
สมมติว่าให้ L=3
, M=2
, R=9
แล้วเราจะได้ว่าค่าที่เราต้องการคือ
ตัวอย่างอิฐ 10 ช่อง โดยเริ่มต้นที่ ตำแหน่ง 3 ข้ามทีละ 2 ก้อน และคิดถึงแค่ช่องที่ 9
ถ้าลองมาดูอีกตัวอย่าง: ถ้าเราให้ L=1
, M=6
, R=7
ค่าที่เราต้องการคิดแค่ เอง
ตัวอย่างอิฐ 10 ช่อง โดยเริ่มต้นที่ ตำแหน่ง 1 ข้ามทีละ 6 ก้อน และคิดถึงแค่ช่องที่ 7
จะเห็นได้ว่าเวลาในการทำแต่ละคำสั่งคือ ซึ่งถ้าค่า น้อยๆ ในทุกคำสั่งจะทำให้เวลาขึ้นสูงไปถึง ได้ ซึ่งพอถามทั้งหมด ครั้ง จะทำให้ใช้เวลา เลย (ซึ่งเยอะเกินไป 😭)
ทีนี้เราจะมาลองดูอีกวิธีนึง โดยใช้เทคนิคการ preprocess (คำนวณบางอย่างไว้ก่อน) เพื่อช่วยลดเวลาในการหาค่าแต่ละคำสั่งในวิธีถัดมากัน
วิธีที่ 2 - preprocess ข้อมูลเพื่อเรียกตอบ
วิธีนี้เราจะจำค่าบางอย่างลง array เพื่อให้สามารถตอบค่าผลรวมในเวลา ได้โดยแลกกับการใช้เวลา preprocess ข้อมูลผลรวมมาไว้ก่อน
เราจะให้ แทนผลรวมของอิฐทั้งหมดจากที่ให้กระโดดข้ามอิฐทีละ ก้อน และสิ้นสุดที่ก้อนที่ พอดี
จะได้ว่า จะเป็นไปได้ 2 แบบคือ
- ถ้ามีอิฐก่อนหน้า เราจะรวมกับค่าอิฐก่อนหน้านั้น ทำให้ได้ว่า หรือ
- ถ้าอิฐก้อนที่ เป็นก้อนแรก (ไม่มีอิฐก้อนก่อนหน้า ที่กระโดดมาได้) เราจะใช้ เลย
ซึ่งสามารถเขียนเป็นสมการได้ดังนี้
ทีนี้พอเวลาเราจะหาค่าของคำสั่ง เราก็สามารถหาอิฐก่อนสุดท้ายก่อนที่จะเกินช่วง () ได้โดยนับจำนวนการกระโดด () คูณด้วยระยะกระโดด และบวกด้วยตำแหน่งเริ่มต้นที่กระโดด จนเป็นสมการดังนี้
ต่อมาเราจะใช้ค่าของ และ มาหาค่าผลรวม ซึ่งก็คือเรียก ได้เลย ทำให้สามารถตอบทุกคำสั่งได้ภายใน
อย่างเช่นเมื่อ L=3
, M=2
, R=9
เราจะไปดูผลรวมที่คำนวณไว้ก่อน โดยจะไปหาค่า และ แล้วนำมาลบกันและบวกค่า ได้ผลรวมที่ต้องการ
โดยที่ช่อง จะเป็นค่าผลรวมของอิฐตั้งแต่ 1, 3, 5, 7 และ 9 อย่างในตารางด้านล่าง
ค่าผลรวมของช่อง S_2,9
เราก็เลยจะมาลบกับ ที่เป็นค่าผลรวมของอิฐช่องที่ 1 และ 3
ค่าผลรวมของช่อง S_2,3
แล้วบวก ที่เป็นค่าของอิฐที่ 3 คืนกลับไปเพื่อให้ได้ผลรวมที่ต้องการ
ตัวอย่างอิฐ 10 ช่อง โดยเริ่มต้นที่ ตำแหน่ง 3 ข้ามทีละ 2 ก้อน และคิดถึงแค่ช่องที่ 9
ดังนั้นถ้าเรามีข้อมูลผลรวมพวกนี้เก็บไว้อยู่แล้ว จะคำนวณหาค่าของแต่ละคำสั่งก็ไม่ใช่เรื่องยาก และใช้เวลาแค่ ต่อคำสั่งเอง
ทีนี้จะติดปัญหาต่อมาว่า ขนาด array ต้องมีขนาด เพื่อรองรับคำสั่งทุกรูปแบบที่เป็นไปได้ ทั้งจบที่อิฐก้อนไหน และกระโดดกี่ก้อนต่อครั้ง
ถ้าเราจะสร้างข้อมูลตรงนี้มาก่อน เวลาที่ใช้สำหรับ preprocess และ memory ก็เลยจะกลายเป็น เลย ซึ่งก็ทำไม่ทันอยู่ดี (แถม memory เกินกว่าที่โจทย์ต้องการเยอะด้วย 🥲) แล้วจะทำยังไงดีล่ะ
ข้อสังเกต
ในวิธีแรกการกระโดดยาวๆนี่ดีมาก เพราะแทบไม่ต้องคำนวณเลย แต่ถ้าค่า น้อยๆ จะเสียเวลาค่อยๆคิดมากๆ
แต่ในวิธีที่ 2 ช่วยแก้ปัญหาได้แค่เรื่องกระโดดทีละสั้นๆ( น้อยๆ) แต่ตอนที่ต้องกระโดดข้ามหลายๆก้อนนี่แทบไม่มีประโยชน์เลย
ถ้างั้นเราควรต้องสร้าง array มาเพื่อ preprocess ข้อมูลก่อนแต่ไม่ใช่ทั้งหมด แล้วทีนี้เราจะเลือกสร้างขนาดเท่าไหร่ดีล่ะ
เราก็เลยจะเอาทั้ง 2 วิธีก่อนหน้านั้นมารวมร่างกัน แล้วแยกว่าเราจะตัดสินใจใช้วิธีไหนโดยใช้ค่า เป็นตัวตัดสินในการแบ่งในแต่ละคำสั่ง
วิธีที่ 3 - รวมร่าง 2 วิธีแรก
ลองสมมติให้ค่า เป็นตัวแบ่งแทนว่าจะใช้วิธีไหน หรือก็คือถ้าค่า มากกว่า จะทำวิธีแรก ถ้าไม่ใช่จะทำวิธีที่สอง
จากที่วิเคราะห์เวลาของทั้ง 2 วิธีจะได้ว่า
- วิธีที่ 1: เวลาขึ้นอยู่กับค่า ต่อคำสั่ง ()
- วิธีที่ 2: เวลาขึ้นอยู่กับขนาดตาราง (preprocess เมื่อ คือขนาดตาราง)
ทีนี้การแบ่งว่าเราจะให้คำสั่งไหนทำวิธีที่ 1 หรือ 2 นั้น เราจะทำให้ทั้งสองวิธีใช้เวลาในกรณีที่แย่ที่สุด (Worst case) พอๆ กัน ซึ่งค่า ที่แบ่งดีที่สุดจะหาได้จาก
- วิธีแรกจะยิ่งใช้เวลาเยอะถ้าค่า มันน้อย แล้วถ้าจะให้ เป็นค่าน้อยสุดก่อนจะเปลี่ยนไปใช้อีกวิธี แปลว่าเวลาแย่สุดต่อคำสั่งนึงก็จะเป็น ถ้าสมมติมี คำสั่งไปเลย ก็จะใช้เวลา
- วิธีที่สอง ต้องคำนวณตาราง array ไว้ก่อนเลยซึ่งยังไงก็ใช้ แน่ๆ
เราต้องการให้สองค่านี้มันใกล้เคียงกันก็เลยจะได้ว่า:
ทำให้ได้ค่า ที่เหมาะสมคือประมาณ
ทำไมถึงเวิร์คล่ะ?
คำถามหลักที่จะตามมาคือ วิธีที่ 3 จะทำผ่านได้จริงเหรอ ทีนี้เราจะมาพิสูจน์คร่าวๆ กัน
กรณีที่ 1 -
ในกรณีนี้ จะใช้วิธีที่ 2 ทำให้ใช้เวลาคงที่คือ ต่อคำสั่ง และใช้ ในการ preprocess ผลรวมของอิฐ
กรณีที่ 2 -
ในส่วนที่เหลือจะเข้าไปทำวิธีที่ 1 เพราะใช้การกระโดดไปถึงจุดจบ () น้อยลงแล้ว ทำให้กรณีแย่ที่สุดคือ ทำให้ใช้เวลาแย่ที่สุดคือ ต่อคำสั่ง ซึ่งถึงทุกคำสั่งถามแบบเดียวกันหมดก็จะใช้เวลาแค่ อยู่ดี
สรุป
ที่เกริ่นไปซะยาวนี่เพื่อจะแนะนำเทคนิคที่ชื่อว่า Square Root Decomposition ซึ่งอันนี้จะเป็นวิธีนึงการแบ่งคำถามเป็นหลายๆ ส่วน เพื่อใช้วิธีที่ดีเฉพาะในบางคำถาม และเลือกใช้วิธีที่เหมาะสมที่สุดกับคำถามนั้นๆ มาหาค่าให้เร็วที่สุด
จริงๆ แล้วตัว Square Root Decomposition เนี่ยสามารถใช้ได้หลากหลายมากๆ ตัวอย่างเช่น
- เก็บค่าที่จะ update แยกลงอีก array นึง พอมีค่า มากพอแล้วค่อยไปแค่หาใน array หลักรวดเดียว
- สร้างช่วงใหม่ไว้เก็บค่า operation บนช่วงของช่อง (ช่วง 1 เก็บผลรวมช่อง 1 ถึง , ช่วง 2 เก็บผลรวมช่อง ถึง ไปเรื่อยๆ)
- เก็บ version ของการ update ว่าถ้า update ค่าถึงคำสั่งเท่านี้แล้วจะมีค่าเท่าไหร่ (Persistent Data Structure)
- เอาคำสั่งหลายๆ อันมา sort เป็นกลุ่มๆ เพื่อลดเวลาหาค่าโดยรวม (Mo’s algorithm)
ซึ่งยังมีอีกหลายวิธีที่ดัดแปลงได้จาก Square Root Decomposition เช่นบางครั้งเราไม่จำเป็นต้องแบ่ง 2 กรณีโดยใช้ Square Root ก็ได้ ซึ่งก็ขึ้นกับเวลาทั้งสองกรณีอีกว่ามีจุดกึ่งกลางที่เท่าไหร่
ไว้มีคนมาถามเพิ่ม หรือมีอารมณ์เขียนเรื่องนี้เดี๋ยวจะมาเขียนใหม่นะฮะ
สุดท้ายนี้อยากจะฝากว่า บางครั้งเราไม่จำเป็นต้องแก้โจทย์ด้วยอัลกออันเดียวก็ได้ ขอแค่เราเอาอัลกอง่ายๆ มารับมือกับปัญหาเฉพาะส่วนให้สุดท้ายแล้วมันครอบคลุมก็พอ 😁
ลิงก์
ลิงก์โจทย์ TOI17 - The Wall: The Sequel ของ Programming.in.thExtra Challenge
- ถ้าคำสั่งมีอีกประเภทคือเปลี่ยนค่าได้ 1 ช่องเป็นอะไรก็ได้ จะมีวิธีคิดที่แตกต่างจากเดิมอย่างไรบ้าง