programming.in.th · ข้อ 2012
ข้อที่ควรอ่านถ้าเคยส่งแล้วตกทั้งที่ตัวอย่างผ่านหมด เพราะสูตรโลภที่ฟังดูสมเหตุสมผลที่สุดของข้อนี้ผิดตรงกรณีสุดท้ายพอดี และสิ่งเดียวที่จับได้คือตัวไล่วางทุกความเป็นไปได้แล้วจำลองจริง
เราเดินอยู่บนเส้นตรงจากตะวันตกไปตะวันออก เริ่มที่ตำแหน่ง 0 และต้องไปให้ถึงปลายทางที่ตำแหน่ง 2,000,001 กฎเดียวคือเดินไปทางตะวันออกเสมอ
บนเส้นนี้มีเครื่องเคลื่อนย้ายมวลสาร (teleporter) อยู่ N เครื่อง
เครื่องหนึ่งมีจุดปลายสองจุด พอเราเดินไปเหยียบจุดปลายข้างหนึ่ง เราจะถูกโยนไปโผล่ที่จุดปลายอีกข้างทันที
แล้วเดินต่อไปทางตะวันออกจากจุดที่โผล่ เราหลบจุดปลายที่ขวางอยู่ไม่ได้ และทุกครั้งที่ถูกโยนได้หนึ่งคะแนน
แกะคำศัพท์
teleporter อ่านว่า "เทเลพอร์เตอร์" แปลว่าเครื่องย้ายตัวข้ามที่ ประกอบจาก tele- ที่แปลว่าไกล (ตัวเดียวกับใน telephone และ television) กับ portare ในภาษาละตินที่แปลว่าขนหรือแบก
ก่อนออกเดินทาง เราวางเครื่องเพิ่มได้อีกไม่เกิน M เครื่อง จะวางจุดปลายไว้ที่ไหนก็ได้
(ตำแหน่งไม่ต้องเป็นจำนวนเต็มก็ได้) ขอแค่ไม่ซ้ำกับจุดปลายที่มีอยู่แล้ว งานของเราคือทำคะแนนให้ได้มากที่สุด
อินพุต / ขอบเขต / เอาต์พุต
N บรรทัดที่ 2 คือ M จากนั้น
N บรรทัด แต่ละบรรทัดคือตำแหน่งปลายด้านตะวันตกและด้านตะวันออกของเครื่องนั้น
N, M ≤ 1,000,000 และทุกตำแหน่งอยู่ระหว่าง 1 ถึง 2,000,000
โดยไม่มีจุดปลายไหนซ้ำกัน เวลา 1 วินาที หน่วยความจำ 64 เมกะไบต์
N ≤ 500 และ M ≤ 500| Input | Output |
|---|---|
| 3 1 10 11 1 4 2 3 | 6 |
| 3 3 5 7 6 10 1999999 2000000 | 12 |
ใบ้
จุดปลายทั้งหมด 2N จุดตัดเส้นตรงออกเป็นช่วง 2N + 1 ช่วง
พอเดินจบช่วงหนึ่ง เราจะไปโผล่ที่ต้นของช่วงใดช่วงหนึ่งเสมอ และช่วงปลายทางนั้นถูกกำหนดไว้แน่นอนแล้ว
ไม่ขึ้นกับว่าเรามาจากไหน
แปลว่าเรากำลังเดินอยู่บนฟังก์ชันที่แต่ละช่วงชี้ไปช่วงเดียว ลองวาดลูกศรจากช่วงไปช่วงดู แล้วถามว่ารูปที่ได้หน้าตาเป็นยังไง และ "ช่วงที่เราไม่เคยเหยียบเลย" ไปอยู่ตรงไหนของรูปนั้น
ชุดแรกของโจทย์ มีเครื่องให้เพิ่มตัวเดียว
เส้นทางหลักตอนนี้ยาว 2 ก้าว และคุณเพิ่มเครื่องได้อีก 1 ตัว
เลือกว่าจะเอาเครื่องไปต่อวงไหน หรือเอาไปสร้างวงใหม่ จนกว่าเครื่องจะหมด
แต้มตอนนี้ 0 · เครื่องเหลือ 0 ตัว
ต่อวงที่ยาว c ใช้เครื่องหนึ่งตัวแล้วได้เพิ่ม c บวกสอง ส่วนการสร้างวงใหม่ใช้สองตัวได้เพิ่มสี่ และเครื่องเดี่ยว ๆ ได้เพิ่มหนึ่ง
ถ้ามีวงให้เลือกหลายวง ลองคิดก่อนว่าเรียงลำดับยังไงถึงจะคุ้มที่สุด แล้วเครื่องที่เหลือหลังต่อวงหมดแล้วควรทำอะไร
ที่มาของแนวคิดนี้
คำถามที่ทำให้ทุกอย่างคลี่ออกมีข้อเดียว คือ พอเดินจบช่วงหนึ่ง เราไปโผล่ที่ช่วงไหน และคำตอบขึ้นกับว่าเรามาจากไหนไหม พอคิดสักพักก็เห็นว่าไม่ขึ้นเลย มันขึ้นกับจุดปลายที่เราไปเหยียบอย่างเดียว
พอปลายทางถูกกำหนดไว้แน่นอนแล้ว สิ่งที่เรามีอยู่ในมือคือฟังก์ชันจากช่วงไปช่วง ซึ่งกราฟที่ทุกปม มีลูกศรออกเส้นเดียวแบบนี้เรียกว่ากราฟฟังก์ชัน (functional graph) และเพราะการจับคู่จุดปลาย เป็นแบบหนึ่งต่อหนึ่ง ฟังก์ชันนั้นจึงเป็นการสลับที่ ซึ่งมีรูปร่างที่รู้กันอยู่แล้วว่าต้องเป็น เส้นทางกับวงปิด ไม่มีอย่างอื่น ที่เหลือคือนับ
คำถามแบบเดียวกันนี้ใช้ซ้ำได้กับ โจทย์เกาะกับสะพาน ด้วย ถ้าปลายทางของแต่ละก้าวถูกกำหนดตายตัวเมื่อไร รูปของกราฟก็ถูกล็อกเมื่อนั้น
เรียงจุดปลายทั้ง 2N จุดตามตำแหน่ง แล้วตั้งชื่อช่วงว่า I₀ ถึง I₂ₙ
โดย I₀ คือช่วงจากจุดเริ่มถึงจุดปลายตัวแรก และ I₂ₙ คือช่วงจากจุดปลายตัวสุดท้ายถึงเส้นชัย
เดินจบช่วง Iⱼ แปลว่าไปเหยียบจุดปลายตัวที่ j+1 แล้วถูกโยนไปที่คู่ของมัน
จากนั้นเดินต่อในช่วงที่อยู่ทางตะวันออกของคู่นั้นทันที การจับคู่จุดปลายเป็นการสลับที่แบบหนึ่งต่อหนึ่ง
ลูกศรที่ได้จึงเป็นการสลับที่ของช่วง ซึ่งแตกออกเป็นเส้นทางเดียวจาก I₀ ไป
I₂ₙ บวกกับวงปิดอีกจำนวนหนึ่ง
คะแนนก่อนเพิ่มเครื่องคือจำนวนช่วงบนเส้นทางหลัก ลบหนึ่ง ส่วนวงปิดที่เหลือคือช่วงที่เราไม่มีวันไปถึง เพราะไม่มีลูกศรจากเส้นทางหลักชี้เข้าไปเลย
ปูพื้นไว้ให้แล้ว
กราฟฟังก์ชันคือกราฟที่ทุกปมมีลูกศรออกเส้นเดียว ห้ามมากกว่านั้นและห้ามไม่มี ข้อจำกัดข้อเดียวนี้บังคับรูปร่างของมันไว้หมด คือทุกส่วนต้องเป็นวงปิดหนึ่งวง ที่มีต้นไม้ห้อยเข้าหาวง เท่านั้น ในข้อนี้ลูกศรยังเป็นการจับคู่หนึ่งต่อหนึ่งอีกชั้น รูปจึงยุบลงเหลือเส้นทางกับวงล้วน ๆ ไม่มีต้นไม้ห้อย
ถ้าอ่านเรื่องนี้ครั้งแรก มีบทปูพื้นเรื่องกราฟฟังก์ชันอยู่ในคลังนี้แล้ว
มันสอนสองท่าที่ใช้ซ้ำได้ทุกข้อ คือปอกใบทิ้งเพื่อหาว่าใครอยู่บนวง และเดิน k ก้าวโดยไม่เดินจริง
| ตัวอย่าง | ช่วงบนเส้นทางหลัก | คะแนนตั้งต้น | ขนาดของวงที่เหลือ |
|---|---|---|---|
| ที่ 1 | 3 | 2 | 2, 1, 1 |
| ที่ 2 | 6 | 5 | 1 |
ตัวเลขทุกตัวมาจากการสร้างการสลับที่จริงแล้วเดินตามลูกศร ไม่ได้พิมพ์มือ
ตัวอย่างที่ 1 ยังไม่เพิ่มเครื่องเลยก็ได้ 2 คะแนน แต่โจทย์ตอบ 6 เพราะเพิ่มได้อีกหนึ่งเครื่อง
วางจุดปลายใหม่สองจุด จุดหนึ่งอยู่ในช่วง A อีกจุดอยู่ในช่วง B
ช่วงทั้งสองถูกผ่าออกเป็นสองท่อน และลูกศรถูกต่อใหม่ให้ท่อนหน้าของ A พาไปท่อนหลังของ B
และท่อนหน้าของ B พาไปท่อนหลังของ A
กรณีที่คุ้มที่สุดคือวางจุดหนึ่งไว้บนเส้นทางหลัก อีกจุดไว้ในวงที่ลอยอยู่
เมื่อนั้นวงทั้งวงจะถูกดูดเข้ามาต่อในเส้นทางหลัก เส้นทางได้ช่วงเพิ่ม 1 ช่อง (จากการผ่า A)
บวกกับ s + 1 ช่อง (จากวงขนาด s ที่ถูกผ่าอีกที) รวมเป็น s + 2 คะแนน
ระวัง
พอวงหมดแล้วยังเหลือเครื่องอยู่ อย่าเผลอคิดว่าเครื่องละ 2 คะแนน ถ้าวางจุดปลายทั้งสองไว้บนเส้นทางหลักทั้งคู่ เส้นทางจะขาดตรงกลาง ท่อนที่ขาดกลายเป็นวงใหม่ที่เข้าไม่ถึง ท่าที่ดีที่สุดคือยอมให้ขาดน้อยที่สุด คือแตกวงขนาด 1 ออกมา ได้แค่ +1 แล้วเครื่องถัดไปค่อยดูดวงขนาด 1 นั้นกลับเข้ามา ได้ +3
สรุปคือสองเครื่องได้ 4 คะแนน แต่เครื่องเดี่ยว ๆ ที่เหลือค้างได้แค่ 1 คะแนน ตรงนี้คือจุดที่โค้ดผมพลาดรอบแรก แล้วตัวตรวจสอบจับได้
พอค่าตอบแทนมาเป็นคู่แบบนี้ สิ่งเดียวที่ต้องรู้เกี่ยวกับเครื่องที่เหลือคือมันเป็นจำนวนคู่หรือคี่
ซึ่งการมองปัญหาด้วยคำถามคู่หรือคี่แบบนี้เรียกว่าการดูคู่คี่ (parity) เหลือเป็นคู่ก็จับคู่กันได้หมด
ได้เครื่องละ 2 คะแนนถ้วน เหลือเศษหนึ่งเครื่องก็หาคู่ไม่ได้ ต้องยอมรับ 1 คะแนน
บรรทัด (r / 2) * 4 + (r % 2) * 1 ในโค้ดคือประโยคนี้เขียนเป็นเลขตรง ๆ
ทบทวนพื้นฐาน · คู่คี่
คู่คี่ (parity) คือการสนใจแค่ว่าจำนวนหนึ่งหารสองลงตัวหรือไม่ โดยทิ้งค่าจริงไปเลย ฟังดูหยาบ แต่มันมีคุณสมบัติที่ใช้งานได้จริงคือ ทุกครั้งที่เราทำอะไรที่เปลี่ยนจำนวนไปทีละสอง คู่คี่จะไม่เปลี่ยน สิ่งที่ไม่เปลี่ยนแม้เราจะทำอะไรกับมันตั้งเท่าไร เรียกว่าค่าไม่แปรผัน (invariant) และมันตอบคำถามประเภท "ทำแบบนี้ไปเรื่อย ๆ จะไปถึงสภาพนั้นได้ไหม" ได้โดยไม่ต้องลองเลย
ในข้อนี้คู่คี่ตอบเรื่องเล็ก คือเครื่องที่เหลือจับคู่ได้ครบไหม แต่ท่าเดียวกันตัดสินเรื่องใหญ่ได้ด้วย เช่นในปริศนาสิบห้าช่อง คู่คี่ของการสลับที่บอกได้ว่ากระดานที่ได้มา แก้ได้หรือแก้ไม่ได้เลย และในโจทย์ตัดถนน คู่คี่ของความยาวเส้นทาง คือเงื่อนไขทั้งข้อ เจอโจทย์ที่ถามว่า "เป็นไปได้ไหม" ให้ลองถามหาคู่คี่ก่อนเสมอ
long long ans = base; // คะแนนก่อนเพิ่มเครื่อง
int used = 0;
for (size_t i = 0; i < cyc.size() && used < m; i++, used++)
ans += cyc[i] + 2; // ดูดวงขนาด s เข้ามาต่อ ได้เพิ่ม s + 2
long long r = m - used; // เครื่องที่เหลือ ตอนไม่มีวงให้ดูดแล้ว
ans += (r / 2) * 4 + (r % 2) * 1; // ต้องเสียเครื่องหนึ่งแตกวงก่อน อีกเครื่องค่อยดูดกลับ
และเพราะการดูดวงให้ s + 2 ซึ่งอย่างน้อยคือ 3 ต่อหนึ่งเครื่อง ในขณะที่การแตกแล้วดูดกลับให้เฉลี่ย 2
ต่อหนึ่งเครื่อง จึงต้องดูดวงให้หมดก่อนเสมอ และดูดวงใหญ่ก่อนวงเล็ก
กดถัดไปเพื่อใช้เครื่องทีละตัว
| เครื่องที่ | เอาไปทำอะไร | ได้เพิ่ม | รวม |
|---|---|---|---|
| 1 | ดูดวงขนาด 1 เข้ามาต่อ | 3 | 8 |
| 2 | ไม่มีวงเหลือแล้ว จึงต้องแตกวงขนาด 1 ออกมาก่อน | 1 | 9 |
| 3 | ดูดวงขนาด 1 ที่เพิ่งแตกไว้กลับเข้ามา | 3 | 12 |
สังเกตเครื่องที่ 2 ที่ได้แค่ 1 คะแนน มันดูเหมือนขาดทุน แต่มันคือค่าเปิดทางให้เครื่องที่ 3 ได้ 3 คะแนน ถ้ามีเครื่องแค่สองตัว คำตอบจะเป็น 9 ไม่ใช่ 12
เรื่องจริงที่เกิดขึ้นตามลำดับ
ผมนับได้ว่าการดูดวงขนาด s ให้ s + 2 โดยนับช่วงตรง ๆ ตรงนั้นมั่นใจ
แต่กรณีที่ไม่มีวงเหลือแล้ว ผมไม่ได้นับ ผมเดาว่าน่าจะ 2 ต่อเครื่องด้วยความสมมาตร
มันตอบตัวอย่างทั้งสองชุดถูก ผมเลยเดินหน้าต่อ
ตัวตรวจสอบตัวแรกที่ผมเขียนเป็นสคริปต์สั้น ๆ และมันโกหกผม เพราะผมตั้งพื้นที่ค้นหาไว้แคบเกินไป มันวางเครื่องได้ไม่ครบทุกความเป็นไปได้ ตัวเลขที่ได้จึงมั่วเป็นบางเคส ผมเสียเวลาไล่หาสาเหตุจากตัวเลขที่ผิดอยู่พักหนึ่ง ก่อนจะยอมเขียนตัวไล่วางทุกแบบใหม่เป็น C++ ซึ่งช้ากว่ามากแต่ครบจริง
พอได้ตัวเลขที่เชื่อได้ ผมถึงกล้าไล่ตารางทดลอง คือตรึงชุดเครื่องตั้งต้นไว้แล้วเพิ่มเครื่องทีละตัว ดูว่าคะแนนขยับเท่าไร ตัวเลขที่โผล่มาคือ 1 แล้ว 3 ไม่ใช่ 2 แล้ว 2 จากตรงนั้นค่อยย้อนกลับไปหาเหตุผลว่า ทำไมเครื่องที่ไม่มีวงให้ดูดถึงต้องเสียตัวหนึ่งไปกับการแตกวงก่อน
ลำดับที่ถูกคือวัดให้ได้ก่อน แล้วค่อยอธิบาย รอบนี้ผมทำสลับ คือเดาแล้วเชื่อ เพราะตัวอย่างในโจทย์ผ่าน
รอบแรกผมเขียนว่าเครื่องที่เหลือได้ตัวละ 2 คะแนน ซึ่งฟังดูสมเหตุสมผลมาก และมันตอบตัวอย่างในโจทย์ถูกทั้งสองชุด สิ่งที่จับได้คือโปรแกรมที่ไล่วางเครื่องทุกความเป็นไปได้แล้วจำลองการเดินจริง
มันช้ามาก ใช้ได้แค่กับเครื่องไม่กี่ตัวและเส้นตรงสั้น ๆ แต่มันไม่เชื่อทฤษฎีอะไรของผมเลย พอสุ่มเทียบ 200 ชุด มันชี้ทันทีว่าเคสที่มีวงเดียวแล้วเหลือเครื่องอีกตัว ผมตอบเกินจริงไปหนึ่ง
ข้อคิดที่อยากฝากไว้คือ การตอบตัวอย่างในโจทย์ถูกไม่ใช่หลักฐานอะไรเลย โดยเฉพาะกับข้อที่คำตอบเป็นสูตรโลภสั้น ๆ เพราะตัวอย่างในโจทย์มักถูกออกแบบมาให้เห็นภาพ ไม่ได้ออกแบบมาให้จับบัก
อินพุตมีเลขได้ถึงสี่ล้านตัว การอ่านด้วย scanf ทีละตัวช้าเกินลิมิตหนึ่งวินาที
โค้ดนี้จึงอ่านทั้งไฟล์เข้ามาก้อนเดียวแล้วแยกเลขเอง เวลาที่วัดได้ลดจาก 744 มิลลิวินาทีเหลือ
258 มิลลิวินาที
ส่วนการแปลงตำแหน่งเป็นหมายเลขจุดปลาย ใช้อาเรย์ขนาดเท่าตำแหน่งที่ไกลที่สุด เพราะพิกัดไม่เกินสองล้าน ตำแหน่งจุดจบจริง ๆ ที่โจทย์บอกว่าเป็น 2,000,001 ไม่มีผลกับคำตอบเลย มีผลแค่ว่าช่วงสุดท้ายชื่ออะไร
โครงสร้าง "แต่ละปมชี้ไปปมเดียว" ที่ข้อนี้ใช้มีชื่อเรียกและมีท่าประจำของมัน อ่านได้ที่บทปูพื้นฐาน กราฟที่ทุกปมมีทางออกทางเดียว
เมื่อการเดินถูกบังคับให้ไปทางเดียว ช่วงระหว่างจุดปลายจะกลายเป็นการสลับที่ คำตอบตั้งต้นคือความยาวของวงที่มีจุดเริ่ม และการเพิ่มเครื่องคือการดูดวงอื่นเข้ามาต่อ ยกเว้นตอนไม่มีวงเหลือ ที่เราต้องยอมจ่ายหนึ่งเครื่องแตกวงขึ้นมาเอง
ในหน้านี้