• 17 min

วิเคราะห์โจทย์จาก TOI18 ข้อ Constellation

เป็นข้อที่รวมทั้ง Dynamic Programming ทั้งคณิตศาสตร์อยู่ในข้อเดียวกัน เหมาะกับคนที่อยากฝึกหลายๆคอนเซปต์ในข้อเดียวกัน

เป็ดไอคอนของเรื่องเล่าชาวอัลกอ Practical Algorithms
Practical Algorithms: เรื่องเล่าชาวอัลกอ
เพจที่อยากให้คนไทยมีเนื้อหาอัลกอริทึมดีๆ ให้ได้อ่านกัน
เป็ดชาวอัลกอกำลังส่องดูดาวบนท้องฟ้าผ่านกล้องโทรทรรศน์

โจทย์

มีแผนที่ท้องฟ้าขนาด R×CR\times C อยู่ แต่ละพิกัดจะถูกระบุด้วย 2 สัญลักษณ์

  • . แทนพิกัดนั้นไม่มีดาว
  • # แทนพิกัดนั้นมีดาวอยู่

เราต้องการหาจำนวนกลุ่มดาวทั้งหมดในแผนที่ท้องฟ้า โดยวิธีหากลุ่มดาวมีดังนี้

  1. เลือกจุดศูนย์กลางของกลุ่มดาวที่พิกัด (x,y)(x,y) โดยที่พิกัดนี้ต้องอยู่ในแผนที่
  2. เลือกดาวทั้งหมด KK ดวงที่ระยะห่างแบบ Manhattan(L1) จากจุดศูนย์กลางเท่ากัน ซึ่งระยะห่างตรงนี้จะยาวเท่าไหร่ก็ได้

โจทย์อยากทราบว่ามีวิธีการเลือกกลุ่มดาวได้ทั้งหมดกี่รูปแบบ โดยแสดงเศษจากหารจำนวนวิธีในการเลือกกลุ่มดาวด้วย 10000031\,000\,003


ข้อมูลนำเข้า

บรรทัดแรก ระบุจำนวนเต็ม R,C,KR,C,K จำนวนแถว จำนวนคอลัมน์ของแผนที่ดาว และขนาดของกลุ่มดาว (1R,C3001 ≤ R,C ≤ 300, 2K6002 \leq K \leq 600)
RR บรรทัดต่อมา ระบุสายอักขระความยาว CC ที่ทุกตัวอักษร . หรือ # เท่านั้น

ข้อมูลส่งออก

มีทั้งหมด 1 บรรทัด แสดงจำนวนเต็ม 11 จำนวน แทนค่าวิธีการเลือกกลุ่มดาว โดยแสดงเศษจากการหารด้วย 10000031\,000\,003


ตัวอย่างข้อมูลนำเข้าและข้อมูลส่งออก

Input Output
3 2 2
#.
.#
#.
8
3 3 2
#..
.#.
#..
11
5 4 3
.#.#
#.#.
.#.#
#.#.
.#.#
240
20 20 19
####################
####################
####################
####################
####################
####################
####################
####################
####################
####################
####################
####################
####################
####################
####################
####################
####################
####################
####################
####################
199545

วิเคราะห์โจทย์

ก่อนอื่นเลย เราจะมาดูก่อนว่าจะเลือกกลุ่มดาวยังไงดี

เลือกจากจุดศูนย์กลางก่อน

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

ปัญหา 1: เลือกดาวให้กลุ่มดาว

หลังจากเลือกจุดศูนย์กลางเสร็จแล้ว เรียกว่าเป็นจุด (x,y)(x,y) ละกัน ทีนี้เดี๋ยวเราจะต้องไปหาให้ได้ว่าดาวที่จะต้องเลือกมาทั้ง KK ดวงที่ระยะห่างจาก (x,y)(x,y) เท่ากันทั้งหมดมันมีกี่แบบ

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

รูปแสดงระยะห่างของดาวเทียบกับจุดศูนย์กลางโดยที่ยิ่งใกล้จุดศูนย์กลางสียิ่งเข้ม รูปแสดงระยะห่างของดาวเทียบกับจุดศูนย์กลาง

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

ปัญหา 2: แล้วเลือกได้กี่แบบล่ะ?

เราจะมาลองดูอีกปัญหานึงหลังจากเรื่องการหาดาวคือ ถ้าเรารู้แล้วว่ามีดาวที่เลือกได้ทั้งหมด pp ดวง แล้วต้องการจะเลือกมาเพียง qq ดวง จะทำยังไงดี?

เพื่อความง่าย เราจะลองเอาดาวมาเรียงเป็นเส้นตรง แล้วเลือกดาวในแต่ละตำแหน่งดู

มีดาวอยู่ 5 สี ที่ต้องการเลือกมาลงล๊อกที่มีแค่ 3 ช่อง มีดาวให้เลือกได้ 5 ดวง และต้องการเลือกมา 3 ดวง

ตำแหน่งที่ 1 เราจะเลือกดาวได้ทั้งหมด pp รูปแบบ
ตำแหน่งที่ 2 เราจะเลือกดาวได้ทั้งหมด p1p-1 รูปแบบ
ตำแหน่งที่ 3 เราจะเลือกดาวได้ทั้งหมด p2p-2 รูปแบบ

ถ้าคิดต่อไปจนถึง
ตำแหน่งที่ qq เราจะเลือกดาวได้ทั้งหมด pq1p-q-1 รูปแบบ

ซึ่งรวมๆแล้วแปลว่าทำได้ทั้งหมด p!(pq)!\frac{p!}{(p-q)!} แบบเมื่อ p!p! คือผลคูณตั้งแต่ 1 ถึง pp

แต่การทำแบบนี้จะเห็นได้ว่าลำดับการเลือกในแต่ละตำแหน่งส่งผลต่อจำนวนรูปแบบที่เกิดขึ้นได้ (permutation) ซึ่งเราไม่ต้องการแบบนั้น

เลือกดาวสีเหลือง ฟ้าและ แดง เทียบกับอีกแบบที่เลือกเป็น ฟ้า แดงและเหลืองว่ามันไม่เท่ากัน ลำดับการเลือกดาวมีผลกับจำนวนรูปแบบที่เกิดขึ้นได้ ซึ่งเราไม่สนใจลำดับในการเลือกดาว

เราก็เลยต้องหาต่อว่าการสลับที่จากดาวที่เลือกมามีกี่รูปแบบ ในที่นี้เราเลือกดาวมา qq ดวง ก็เลยสลับได้ q!q! แบบ (เลือก qq ช่องจาก qq ช่องโดยคิดเรื่องลำดับ)

ทีนี้เราจะเอาจำนวนรูปแบบทั้งหมด (p!(pq)!\frac{p!}{(p-q)!}) มาหารด้วยจำนวนลำดับจากการ permutation ของในกลุ่มเดียวกันทิ้ง (q!q!) ทำให้สุดท้ายเหลือจำนวนรูปแบบเป็น p!q!(pq)!\frac{p!}{q!(p-q)!} (ในทางคณิตศาสตร์จะเรียกว่า Combination)

เราสามารถเรียก Combination ตรงส่วนนี้ว่า (pq)\binom{p}{q} เป็นการเลือกดาว qq ดวงจากดาว pp ดวง ซึ่งในส่วนนี้เราสามารถคำนวณเอาไว้แล้วเก็บค่าลงในอาเรย์เอาไว้เพื่อใช้ต่อทีหลังได้ จะได้ไม่ต้องคำนวณใหม่เรื่อยๆ

แต่ทีนี้เราจะคำนวณ (pq)\binom{p}{q} ให้เร็วๆได้ยังไง?

Preprocess ค่าวิธีเลือก

การหาค่า (pq)\binom{p}{q} สามารถทำได้จากการใช้สามเหลี่ยม Pascal ได้เลย ซึ่งก็คือ (pq)=(p1q1)+(p1q)\binom{p}{q} = \binom{p-1}{q-1}+\binom{p-1}{q}

เนื่องจากว่าจำนวนดาวมากสุดที่เป็นไปได้ในการเลือกมีอยู่ประมาณ R+CR+C ดวง และเราก็เลือกแค่ KK ดวงอยู่แล้ว

ทำให้เราไม่ต้องคิดทั้งสามเหลี่ยม Pascal ทั้งอัน และคำนวณ preprocess แค่ถึง (2(R+C)K)\binom{2*(R+C)}{K} ซึ่งทำได้ใน O((R+C)K)\mathcal{O}((R+C)K)

สามเหลี่ยม Pascal

ถ้าสนใจว่าค่าจากสามเหลี่ยม Pascal มายังไงก็สามารถอ่านต่อได้เลย หรือจะข้ามไปส่วนต่อไปเพื่อไปใช้ดูว่าเอาสามเหลี่ยม Pascal ไปใช้ในโจทย์ยังไงดี

ทีนี้เราจะมาโฟกัสที่ปัญหานึงกัน ปัญหาที่ว่าคือเราต้องการเลือกของจากทั้งหมด pp ชิ้นมาจำนวน qq ชิ้นโดยไม่สนใจลำดับในการหยิบ ซึ่งจะมาคิดแยกเป็น 2 วิธีกัน

  1. ใช้นิยามการเลือกตรงๆ ไปเลย ก็จะได้ว่ามีค่าเป็น (pq)=p!q!(pq)!\binom{p}{q} = \frac{p!}{q!(p-q)!}
  2. คิดว่าการเลือกที่ว่านั้นมีความเป็นไปได้ 2 รูปแบบที่ครอบคลุมตามการเลือกเริ่มต้น แล้วมารวมกัน
    • มีของทั้งหมด p1p-1 ชิ้นแล้วเลือก q1q-1 ชิ้นไปแล้วก่อนหน้านั้น ทำให้ชิ้นที่ pp ต้องเลือกแน่ๆ ((p1q1)\binom{p-1}{q-1})
    • มีของทั้งหมด p1p-1 ชิ้นแล้วเลือกครบ qq ชิ้นไปแล้วก่อนหน้านั้น ทำให้ชิ้นที่ pp ยังไงก็ไม่เลือกแน่ๆ ((p1q)\binom{p-1}{q})

ซึ่งวิธีพิสูจน์แบบนี้ก็มีชื่อเรียกเฉพาะก็คือ Combinatorial Proof FaviconCombinatorial Proof ถ้าสนใจก็ไปอ่านต่อกันได้ 😁

กลับมาที่การหาจำนวนดาวในระยะต่างๆ

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

ทางที่ง่ายที่สุดก็คือในทุกๆ จุดศูนย์กลาง (x,y)(x,y) เราสามารถไล่หาทั้งตารางเทียบกับ (x,y)(x,y) ที่กำลังดูอยู่ว่า ดาวแต่ละพิกัดมันห่างจากจุดศูนย์กลางเท่าไหร่ แล้วก็เก็บว่าแต่ละระยะมีดาวทั้งหมดกี่ดวง ซึ่งในการไล่ตรวจทั้งตารางจะใช้เวลา O(R×C)\mathcal{O}(R\times C) เพราะทั้งตารางมีขนาด R×CR\times C

แล้วเราก็แค่ต้องทำแบบนี้ไปเรื่อยๆทุกจุดศูนย์กลางเอง ซึ่งก็มีอีก R×CR \times C จุด ดังนั้นแค่จะมองหาว่ามีดาวกี่ดวงอยู่รอบๆก็ใช้ไปแล้ว O(R2C2)\mathcal{O}(R^2C^2)

ต่อมาตอนเราจะคำนวณจำนวนวิธี ก็จะไปดูในทุกระยะจากจุดศูนย์กลางว่า แต่ละระยะเลือกนั้นเป็นไปได้กี่รูปแบบ ทำให้เรียกใช้ค่า (SdK)\binom{S_d}{K} ได้ เมื่อ SdS_d คือจำนวนดาวที่ระยะห่าง dd จากพิกัด (x,y)(x,y) เพราะเราต้องการเลือกดาวมาแค่ KK ดวง จาก SdS_d ดวงทั้งหมดในระยะนั้นๆ

ซึ่งระยะทางที่ว่ามีค่ามากที่สุดได้ถึง R+C2R+C-2 ฃดังนั้นเราจะต้องคำนวณค่า (SdK)\binom{S_d}{K} ในทุกระยะที่เป็นไปได้ ซึ่งก็จะใช้เวลา O(R+C)\mathcal{O}(R+C) ส่วนค่าที่ต้องคำนวณก็ไปดึงเอามาจากอาเรย์ที่เก็บไว้จากที่คิดเมื่อในสเต็ปที่แล้วได้เลย

ละก็เหมือนเดิมที่ก็ต้องไปคิดแบบนี้ในทุกๆจุดศูนย์กลาง ก็เลยใช้เวลาในพาร์ทนี้เป็น O(RC(R+C))\mathcal{O}(RC(R+C))

ทีนี้เราจะมาค่อยๆดูว่าจะ optimize วิธีคิดจากส่วนไหนกันต่อได้บ้าง เพื่อให้ผ่านโจทย์ข้อนี้ไปได้ 😅

  • การไล่ลูปหาจุดศูนย์กลาง: ใช้เวลา O(RC)\mathcal{O}(RC) ซึ่งก็น่าจะดีที่สุดแล้ว เพราะไม่มีวิธีคิดรวบทีเดียวหลายๆจุด เนื่องจากแต่ละจุดมีจำนวนดาวรอบๆไม่เท่ากัน
  • หาจำนวนวิธีเลือกดาวในแต่ละระยะ: ก็เรียกใช้จากที่ preprocess จากสามเหลี่ยม Pascal มาได้เลย ซึ่งเรียกใน O(1)\mathcal{O}(1) และ preprocess ใน O((R+C)K)\mathcal{O}((R+C)K) ตอนแรกสุด ซึ่งก็ไม่ได้แย่อีก
  • นับจำนวนดาวที่อยู่ในแต่ละระยะ: ใช้เวลาคำนวณระยะทุกช่องในตาราง O(RC)\mathcal{O}(RC) เพื่อให้ได้ทุกระยะต่อ 1 จุดศูนย์กลาง ซึ่งดูแย่ที่สุด ทำให้เราจะมา optimize ต่อในส่วนนี้

หาจำนวนดาวแบบเร็วๆ

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

สี่เหลี่ยมที่เป็นแนวทแยงรอบจุดศูนย์กลางในระยะห่าง d เส้นรอบจุดศูนย์กลางที่เราสนใจ ในระยะห่าง d

มองเพิ่มอีกนึดนึงเราจะเห็นว่าเส้นทแยงมุมทั้ง 4 เส้นเนี่ย จริงๆมันมีแค่ 2 แนวเอง คือทแยงจากบนซ้ายไปล่างขวา กับบนขวาไปล่างซ้าย

เส้นทแยงจากบนซ้ายไปล่างขวาที่เราสามารถใช้เป็น 2 ด้านของสี่เหลี่ยมได้ เส้นทแยงจากบนซ้ายไปล่างขวา

เส้นทแยงจากบนขวาไปล่างซ้ายที่เราสามารถใช้เป็น 2 ด้านของสี่เหลี่ยมได้ เส้นทแยงจากบนขวาไปล่างซ้าย

เราจะทำการเก็บผลรวมของดาวในตามแนวทแยง (Quicksum) ทั้งสองแนวกันก่อน ซึ่งก็จะสร้างอาเรย์ 2 มิติเก็บผลรวมมาทั้งหมด 2 อาเรย์ สมมติว่าเป็น qs1,qs2qs1, qs2 และให้จำนวนดาวที่พิกัด (i,j)(i,j) คือ Si,jS_{i,j} (มีค่าเป็น 0 หรือ 1)

  • มุมจากซ้ายบนไปขวาล่าง: สำหรับพิกัด (i,j)(i,j) ใดๆ เราจะเก็บค่า qs1[i][j]qs1[i][j] เป็น qs1[i1][j1]+Si,jqs1[i-1][j-1]+S_{i,j}
  • มุมจากขวาบนไปซ้ายล่าง: สำหรับพิกัด (i,j)(i,j) ใดๆ เราจะเก็บค่า qs2[i][j]qs2[i][j] เป็น qs2[i1][j+1]+Si,jqs2[i-1][j+1]+S_{i,j}

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

รวมทุกอย่างเข้าด้วยกัน

ทีนี้เราก็จะใช้ค่าที่ preprocess ได้ มาคู่กับลูปเรื่องระยะ ทำให้ในแต่ละระยะเราแค่เรียกผลรวมแนวทแยงทั้ง 4 ด้านมาใช้ได้โดยใช้เวลาใน O(1)\mathcal{O}(1) ซึ่งลดเวลาจาก O(RC)\mathcal{O}(RC) เหลือแค่ O(R+C)\mathcal{O}(R+C) ต่อ 1 จุดศูนย์กลาง เพราะเหลือแค่ต้องลูปเรื่องระยะแทน

สุดท้ายทำให้หาค่าจำนวนกลุ่มดาวได้ภายใน O(RC(R+C))\mathcal{O}(RC(R+C)) ซึ่งก็ทำให้ผ่านข้อนี้ได้แล้ว 🎉

ลิงก์

ลิ้งโจทย์ TOI18 - Constellation Combinatorial Proof
0

บทความอื่นๆ