programming.in.th · ข้อ 2024
ทุ่งพันห้าร้อยคูณพันห้าร้อย ที่ถ้าไล่ดูช่องที่กระโดดถึงตรง ๆ จะเป็นหมื่นล้านครั้ง บทนี้เล่าทั้งทางที่ทิ้ง คือต้นไม้ช่วงสามพันต้นซึ่งกินหน่วยความจำเกินโควตา และรุ่นที่ผิดเพราะดอกกลีบเท่ากันมองเห็นกันเอง
ทุ่งดอกไม้เป็นตาราง N คูณ N แต่ละดอกมีจำนวนกลีบกำกับไว้
ตั๊กแตนเริ่มที่ดอกในแถว R คอลัมน์ C และอยากแวะดอกไม้ให้ได้มากที่สุด
ภายใต้กติกาสองข้อ
(r1, c1) ไป (r2, c2) ได้ก็ต่อเมื่อ
|r1 - r2| = 1 และ |c1 - c2| > 1 หรือกลับกัน
อินพุต / ขอบเขต / เอาต์พุต
N บรรทัดที่สองคือ R กับ C
จากนั้นคือตารางกลีบดอก N บรรทัด บรรทัดละ N จำนวน
1 ≤ N ≤ 1,500, จำนวนกลีบเป็นจำนวนเต็มบวกที่น้อยกว่า 1,000,000
เวลา 4 วินาที หน่วยความจำ 35 เมกะไบต์
N ไม่เกิน 100 และมูลค่า 80% มี N ไม่เกิน 1,000
| Input | Output |
|---|---|
| 4 1 1 1 2 3 4 2 3 4 5 3 4 5 6 4 5 6 7 | 4 |
| 5 3 3 20 16 25 17 12 11 13 13 30 17 15 29 10 26 11 27 19 14 24 22 23 21 28 18 13 | 21 |
ใบ้
กลีบต้องมากขึ้นทุกก้าว แปลว่าตั๊กแตนย้อนกลับมาที่เดิมไม่ได้เลย ทุ่งนี้จึงไม่ใช่ปัญหาเดินวน มันคือปัญหาหาทางที่ยาวที่สุดบนกราฟที่ไม่มีวงจร ซึ่งคิดจากดอกใหญ่ไล่ลงมาหาดอกเล็กได้เลย
ปัญหาที่แท้จริงอยู่ที่ราคา ดอกหนึ่งดอกกระโดดต่อได้ราว 4N ที่
ทั้งทุ่งมี N ยกกำลังสองดอก คูณกันแล้วที่ N เท่ากับ 1,500
จะเป็นหมื่นล้านครั้ง แล้วจะตัดตัวเลือกทิ้งยังไงดี
ทุ่งนี้กลีบเพิ่มขึ้นเป็นระเบียบ จึงเหมาะกับการทำความคุ้นเคยกับกฎการกระโดดก่อน
ตั๊กแตนยืนอยู่ที่ช่องสีทอง กดดอกถัดไปที่อยากกระโดดไป กลีบต้องมากขึ้นทุกก้าว
ตอนนี้แวะไปแล้ว 1 ดอก
จากช่องที่ยืนอยู่ ให้มองแถวบนกับแถวล่างที่คอลัมน์ห่างออกไปอย่างน้อยสองช่อง และมองคอลัมน์ซ้ายกับขวาที่แถวห่างออกไปอย่างน้อยสองช่อง
ถ้าลองเล่นทุ่งที่สองสักสองสามรอบจะเริ่มรู้สึกว่า ดอกที่ควรกระโดดไปไม่ใช่ดอกที่กลีบมากที่สุด แต่เป็นดอกที่ยังมีอนาคตยาวที่สุด ซึ่งเป็นความรู้สึกที่ตรงกับสิ่งที่โปรแกรมต้องคำนวณพอดี
สองทางที่ผมลองก่อน แล้วทิ้ง
ทางแรกคือท่าตรงไปตรงมา ไล่ดูทุกช่องบนกระดานว่ากระโดดถึงไหม เขียนสิบบรรทัดจบ
และตอบตัวอย่างทั้งสองชุดถูก แต่พอเอา N เท่าที่โจทย์ให้ได้จริงมาคูณ
คือ 1,500 ยกกำลังสอง คูณอีก 4N ช่องที่ต้องไล่ดู มันออกมาเป็น
หมื่นล้านครั้ง ซึ่งไม่มีทางจบใน 4 วินาที ผมเลยต้องหาทางลดตัวเลือก
ทางที่สองที่ผมคิดจริงจังกว่าคือใส่ต้นไม้ช่วงให้ทุกแถวและทุกคอลัมน์ เพราะสิ่งที่ต้องการคือค่าสูงสุดของแถวหนึ่งยกเว้นช่วงสามคอลัมน์ตรงกลาง ซึ่งก็คือถามช่วงซ้ายหนึ่งครั้งกับช่วงขวาอีกหนึ่งครั้ง คิดราคาแล้วได้ราวร้อยล้านครั้ง พอไหวอยู่ แต่ต้องสร้างต้นไม้สามพันต้น ซึ่งกินหน่วยความจำเกินสามสิบห้าเมกะไบต์ที่โจทย์ให้
สิ่งที่ทำให้เลิกใช้ต้นไม้ช่วง คือผมนับดูว่าช่วงที่ถูกห้ามมันกว้างแค่ไหน คำตอบคือ สามคอลัมน์ เท่านั้น ไม่ว่าทุ่งจะใหญ่แค่ไหน พอเป็นเลขคงที่แบบนี้ เราไม่ต้องการโครงสร้างที่ตอบช่วงไหนก็ได้ เราต้องการแค่ผู้สมัครสี่คน เพราะห้ามได้มากสุดสามคน จึงเหลือรอดอย่างน้อยหนึ่งคนเสมอ
บทเรียนที่ยกไปข้ออื่นได้คือ ก่อนจะหยิบโครงสร้างข้อมูลมาแก้ปัญหา "ถามค่าสูงสุดของช่วง" ให้นับก่อนว่าสิ่งที่ถูกตัดออกมีกี่ตัว ถ้ามันเป็นจำนวนคงที่เล็ก ๆ คำตอบมักเป็นการเก็บอันดับต้น ๆ ไว้ไม่กี่ตัว ซึ่งเร็วกว่าและกินที่น้อยกว่ามาก
กติกาข้อสองบอกว่ากลีบต้องมากขึ้นทุกก้าว แปลว่าถ้ามองดอกไม้เป็นปมและการกระโดดเป็นเส้นเชื่อม กราฟนี้ไม่มีวงจรเลย เพราะเดินวนกลับมาที่เดิมแปลว่ากลีบมากกว่าตัวเอง
ให้ dp[r][c] คือจำนวนดอกมากที่สุดที่แวะได้ ถ้าเริ่มที่ช่องนี้
ค่านี้ขึ้นกับช่องที่กลีบมากกว่าเท่านั้น ดังนั้นถ้าเราคิดจากดอกกลีบมากไล่ลงมาหากลีบน้อย
ตอนที่ถึงคิวช่องหนึ่ง ทุกช่องที่มันกระโดดไปได้ก็คิดเสร็จไปแล้วทั้งหมด
คำตอบของช่องหนึ่งคือหนึ่ง บวกกับค่าที่ดีที่สุดของช่องที่กระโดดไปถึงและกลีบมากกว่า
จากช่อง (r, c) ปลายทางที่กระโดดถึงมีแค่สี่กลุ่ม
r - 1 ทุกคอลัมน์ ยกเว้น c - 1, c และ c + 1r + 1 โดยห้ามสามคอลัมน์เดียวกันc - 1 ทุกแถว ยกเว้น r - 1, r และ r + 1c + 1 โดยห้ามสามแถวเดียวกันแต่ละกลุ่มคือค่าสูงสุดของแถวหนึ่ง โดยห้ามใช้สามตำแหน่ง ตรงนี้คือจุดที่ทั้งข้อพลิก เพราะจำนวนตำแหน่งที่ถูกห้ามเป็นเลขคงที่ ไม่ว่าทุ่งจะกว้างแค่ไหน เราจึงไม่ต้องเก็บทั้งแถว เก็บแค่สี่อันดับแรกที่คอลัมน์ไม่ซ้ำกันก็พอ เพราะห้ามได้มากสุดสามคอลัมน์ อันดับที่สี่จึงรอดเสมอ
// ห้ามได้มากสุดสามดัชนี คือ c-1, c และ c+1
// เก็บสี่อันดับแรกที่ดัชนีไม่ซ้ำกัน จึงเหลือตัวที่ใช้ได้อย่างน้อยหนึ่งตัวเสมอ
int best(int b1, int b2, int b3) const {
for(int i = 0; i < n; i++) if(k[i] != b1 && k[i] != b2 && k[i] != b3) return v[i];
return 0;
} ระวัง · ดอกที่กลีบเท่ากันต้องมองไม่เห็นกัน
กติกาบอกว่ากลีบต้องมากกว่าอย่างเคร่งครัด ดอกสองดอกที่กลีบเท่ากันจึงกระโดดหากันไม่ได้ ถ้าเราคิดทีละดอกแล้วใส่ค่าลงตารางทันที ดอกที่กลีบเท่ากันซึ่งคิดทีหลังจะเห็นค่าของเพื่อนที่คิดไปก่อน และใช้มันเป็นปลายทาง ซึ่งผิดกติกา
ทางแก้คือคิดทั้งกลุ่มที่กลีบเท่ากันให้จบก่อน แล้วค่อยใส่ทั้งกลุ่มเข้าตารางพร้อมกัน
คิดทีละดอก จากกลีบมากไปกลีบน้อย บนทุ่ง 4 x 4
| ลำดับ | ดอกที่ | กลีบ | กระโดดต่อไปที่ | แวะได้ทั้งหมด |
|---|---|---|---|---|
| 1 | (4, 4) | 16 | ไม่มี | 1 |
| 2 | (1, 3) | 15 | (4, 4) | 2 |
| 3 | (3, 4) | 14 | (1, 3) | 3 |
| 4 | (2, 1) | 13 | (3, 4) | 4 |
| 5 | (1, 4) | 12 | (2, 1) | 5 |
| 6 | (4, 2) | 11 | (2, 1) | 5 |
| 7 | (2, 3) | 10 | (4, 2) | 6 |
| 8 | (4, 1) | 9 | (3, 4) | 4 |
| 9 | (1, 2) | 8 | (4, 1) | 5 |
| 10 | (3, 1) | 7 | (2, 3) | 7 |
| 11 | (3, 3) | 6 | (1, 2) | 6 |
| 12 | (4, 3) | 5 | (3, 1) | 8 |
| 13 | (2, 2) | 4 | (4, 3) | 9 |
| 14 | (3, 2) | 3 | (1, 3) | 3 |
| 15 | (1, 1) | 2 | (2, 3) | 7 |
| 16 | (2, 4) | 1 | (4, 3) | 9 |
ทุ่งสาธิตนี้คือทุ่งที่สองในเกมข้างบน ตั๊กแตนเริ่มที่ช่อง (1, 1) ซึ่งมี 2 กลีบ และแวะได้มากที่สุด 7 ดอก ตัวเลขทุกตัวในตารางนี้คำนวณตอนสร้างหน้า ด้วยท่าที่บทความสอน และถูกเทียบกับท่าตรงไปตรงมาแล้วว่าตรงกัน
ทั้งโปรแกรมมีลูปใหญ่รอบเดียว วนตามลำดับกลีบที่เรียงไว้แล้ว
แต่ละดอกถามตารางสี่ครั้ง ครั้งละไม่เกินสี่ก้าว รวมเป็นราวเก้าล้านครั้งที่ N เท่ากับ 1,500
เวลาที่วัดได้บนเครื่องผม ทุ่งขนาด 1,500 คูณ 1,500 ที่กลีบสุ่มเต็มช่วง ใช้เวลา 0.86 วินาที จากลิมิต 4 วินาที
คำถามที่ควรถามคือ เก็บสามอันดับพอไหม เพราะเราห้ามแค่สามคอลัมน์
คำตอบคือไม่พอ เพราะถ้าสามอันดับที่เก็บไว้ดันอยู่ที่คอลัมน์ c - 1,
c และ c + 1 พอดี ก็จะไม่เหลืออะไรเลย ทั้งที่คอลัมน์อื่นในแถวนั้นยังมีค่าอยู่
อันนี้ไม่ใช่การเดา ผมคอมไพล์รุ่นที่เก็บสามอันดับแล้วสุ่มเทียบกับตัวตรวจ มันพังที่ทุ่งขนาด 5 คูณ 5 ซึ่งคำตอบจริงคือ 8 แต่รุ่นสามอันดับตอบ 7 ส่วนรุ่นสี่อันดับผ่านทุกเคสในชุดเดียวกัน
ในทางกลับกัน เก็บห้าอันดับก็ไม่ได้ปลอดภัยขึ้น เพราะจำนวนที่ถูกห้ามคือสามเสมอ สี่จึงเป็นเลขที่พอดีและพิสูจน์ได้ ไม่ใช่เลขที่เผื่อไว้
ท่าของข้อนี้คือเก็บอันดับต้น ๆ ไว้เท่าที่ข้อห้ามจะกินได้ ซึ่งใช้ได้ทุกครั้งที่โจทย์ถาม
"ค่าสูงสุดของกลุ่มหนึ่ง โดยห้ามใช้สมาชิกไม่กี่ตัว" ถ้าจำนวนตัวที่ห้ามคือ k
เก็บ k + 1 อันดับที่ดัชนีไม่ซ้ำกันก็เพียงพอเสมอ
ทบทวนพื้นฐาน · ทางยาวที่สุดบนกราฟไร้วงจร
ปัญหาหาทางยาวที่สุดบนกราฟทั่วไปเป็นปัญหายาก แต่พอกราฟไม่มีวงจร มันกลับง่ายมาก เพราะเรียงปมให้เส้นเชื่อมทุกเส้นชี้ไปข้างหน้าทางเดียวได้ แล้วคิดย้อนจากท้ายมาหน้า
ข้อนี้ไม่ต้องเรียงลำดับทอพอโลยีเองด้วยซ้ำ เพราะจำนวนกลีบทำหน้าที่นั้นให้แล้ว เส้นเชื่อมทุกเส้นวิ่งจากกลีบน้อยไปกลีบมากเสมอ การเรียงตามกลีบจึงเป็นลำดับที่ถูกต้องอยู่แล้ว ท่านี้ใช้ได้ทุกครั้งที่โจทย์มีค่าที่ต้องเพิ่มขึ้นทุกก้าว
ถ้าอยากเห็นการยุบกราฟให้ไม่มีวงจรก่อนจะคิดแบบนี้ ลองอ่าน กราฟที่ทุกปมมีทางออกทางเดียว
| ทาง | ขอบเขตที่มันไหว | งานคร่าว ๆ |
|---|---|---|
| ไล่ดูทุกช่องที่กระโดดถึง | N ไม่เกินราวร้อย | 13.5 หมื่นล้านครั้งที่ N = 1500 |
| ต้นไม้ช่วงประจำแถวและคอลัมน์ | N ถึงหลักพัน | ราว 100 ล้านครั้ง บวกโครงสร้าง 3,000 ต้น |
| เก็บสี่อันดับแรกของแถวและคอลัมน์ | N ถึง 1500 | 9 ล้านครั้ง |
กลีบที่ต้องมากขึ้นทุกก้าวทำให้ทุ่งกลายเป็นกราฟไร้วงจรที่คิดย้อนได้ และเพราะข้อห้ามของการกระโดดกว้างแค่สามตำแหน่งเสมอ ค่าสูงสุดของทั้งแถวจึงเก็บไว้แค่สี่อันดับก็พอ
ในหน้านี้