programming.in.th · ข้อ 2005
ข้อที่สอนให้อ่านขอบเขตแปลก ๆ ของโจทย์ให้ออกว่ามันกำลังใบ้เฉลยอยู่ แล้วต่อด้วยการยุบเงื่อนไขทั้งข้อเหลือสองกฎ ซึ่งกฎหนึ่งพิสูจน์ยากพอที่จะต้องมีตัวตรวจสอบที่ไม่เชื่อทฤษฎีอะไรเลย
เมอโกกับสลาฟโกซ้อมปั่นจักรยานด้วยกัน คนที่ปั่นตามได้เปรียบเพราะคนนำช่วยบังลม ทั้งคู่จึงสลับกันนำทุกครั้งที่ถึงเมืองใหม่ เพื่อไม่ให้ใครได้เปรียบ ทั้งคู่จึงเลือกซ้อมบนเส้นทางที่ผ่านถนนเป็นจำนวนคู่
ในประเทศมีเมือง N เมือง ถนน M สาย ในจำนวนนั้นมี N − 1 สายที่
ราดยาง และเชื่อมทุกเมืองถึงกันพอดี นั่นคือถนนราดยางประกอบกันเป็นต้นไม้
ส่วนที่เหลือเป็นถนนที่ไม่ได้ราดยาง แต่ละเส้นมีค่าใช้จ่ายในการปิด
เส้นทางซ้อมคือวงปิดที่ออกจากเมืองหนึ่งแล้ววนกลับมาที่เดิม โดยไม่ซ้ำเมืองและไม่ซ้ำถนน คู่แข่งอยากขัดขวาง จึงจะปิดถนนที่ไม่ได้ราดยางบางเส้น (ปิดถนนราดยางไม่ได้) ให้ไม่เหลือเส้นทางซ้อมที่ยาวเป็นเลขคู่เลย งานของเราคือหาค่าใช้จ่ายรวมที่ต่ำที่สุด
อินพุต / ขอบเขต / เอาต์พุต
N และ M จากนั้น M บรรทัด
แต่ละบรรทัดคือ A B C ถ้า C = 0 คือถนนราดยาง ถ้า C > 0
คือถนนไม่ราดยางที่ปิดได้ด้วยค่าใช้จ่าย C 2 ≤ N ≤ 1000, N − 1 ≤ M ≤ 5000,
C ≤ 10,000 และทุกเมืองมีถนนต่ออยู่รวมไม่เกิน 10 สาย
เวลา 0.3 วินาที หน่วยความจำ 64 เมกะไบต์
ขอบเขตข้อสุดท้ายที่ว่าดีกรีไม่เกิน 10 ดูเหมือนของแถม แต่มันคือกุญแจของทั้งข้อ โจทย์ที่ใส่ขอบเขตแปลก ๆ แบบนี้มาให้ มักกำลังบอกใบ้ว่าเฉลยต้องใช้มันตรง ๆ
| Input | Output |
|---|---|
| 5 8 2 1 0 3 2 0 4 3 0 5 4 0 1 3 2 3 5 2 2 4 5 2 5 1 | 5 |
สิ่งที่เราเลือกคือเซตของถนนไม่ราดยางที่จะปิด ตัวอย่างในโจทย์มีถนนแบบนั้น 4 สาย จึงมีเซตให้ลองแค่ 16 แบบ ไล่ดูทีละแบบได้สบาย แต่ขอบเขตจริงเปิดให้ถนนไม่ราดยาง มีได้ถึง 4,001 สาย จำนวนเซตคือสองยกกำลัง 4,001
ด่านที่สองหนักกว่าด่านแรก สมมติมีคนยื่นเซตหนึ่งมาให้แล้วบอกให้ตอบว่าเซตนี้ใช้ได้ไหม เงื่อนไขของโจทย์พูดถึงทุกวงปิดที่ยังเหลืออยู่ในกราฟ กราฟหนึ่งกราฟมีวงปิดได้มากกว่า จำนวนถนนของมันเยอะ ตัวอย่างเล็ก ๆ ในโจทย์มีถนนแค่ 8 สาย แต่มีวงจรอย่างง่ายอยู่ 12 วง แค่การตรวจคำตอบหนึ่งข้อก็แพงแล้ว ยังไม่นับว่าต้องตรวจกี่ข้อ
ทางออกจึงต้องมาจากการยุบเงื่อนไขทั้งข้อให้เหลือสิ่งที่ตัดสินได้ระหว่างเดินบนต้นไม้ โดยไม่ต้องไปแตะวงปิดสักวงเดียว และตรงนั้นเองที่ขอบเขตดีกรีไม่เกิน 10 ซึ่งดูเหมือนของแถม จะได้ใช้งานจริง
ใบ้
ถนนราดยางเป็นต้นไม้ ดังนั้นถนนไม่ราดยางหนึ่งเส้นที่เชื่อม A กับ B
จะสร้างวงขึ้นมาพอดีหนึ่งวง คือเส้นทางบนต้นไม้จาก A ไป B บวกกับตัวมันเอง
ลองถามสองคำถามนี้ดู วงที่เกิดจากถนนเส้นเดียวมีความยาวเท่าไร และถ้าเราเก็บถนนไม่ราดยางไว้สองเส้น ที่เส้นทางบนต้นไม้ของมันทับกัน จะเกิดวงอะไรขึ้นอีกบ้างที่เราไม่ได้ตั้งใจ
ห้าเมืองเดียวกับที่บทความใช้
กดปิดถนนที่ไม่ราดยางได้ตามใจ แล้วกดตรวจว่าเหลือเส้นทางซ้อมที่ยุติธรรมไหม
จ่ายไปแล้ว 0 · ถูกที่สุดคือ 0
เส้นทางซ้อมที่ยุติธรรมคือวงจรที่เดินแล้วกลับมาที่เดิม โดยผ่านถนนเป็นจำนวนคู่
ตัวตรวจของเกมนี้ไม่ได้ใช้กฎย่อของเฉลยเลย มันไล่หาวงจรทุกวงจริง ๆ ถ้าเดาแล้วผิด แปลว่ากฎในหัวยังไม่ตรงกับของจริง
ที่มาของแนวคิดนี้
สิ่งที่ผมจับได้ก่อนเป็นอย่างแรก ไม่ใช่กฎสองข้อ แต่เป็นบรรทัดหนึ่งในขอบเขตที่ดูเหมือนของแถม คือ ทุกเมืองมีถนนต่ออยู่ไม่เกินสิบสาย ขอบเขตเล็ก ๆ ที่แปะมาเฉพาะที่แบบนี้แทบไม่เคยเป็นของแถม มันมักกำลังใบ้ว่าเฉลยต้องเอาไปใช้ตรง ๆ และเลขสิบกับการเลือกลูกบางตัวรวมกัน มีคำตอบอยู่แบบเดียวคือ หน้ากากบิต
พอรู้ว่าจะจ่ายเป็นหน้ากากบิตบนลูกของแต่ละปม โจทย์ก็เปลี่ยนจาก "จะตัดถนนเส้นไหน" เป็น "แต่ละปมจะอนุญาตให้ลูกตัวไหนต่อขึ้นมาถึงตัวเองบ้าง" ซึ่งเป็นคำถามที่มีขนาดจำกัดและตอบทีละปมได้
ส่วนกฎสองข้อที่ทำให้ยุบมาถึงตรงนี้ได้ ผมไม่กล้าเชื่อจากการนั่งพิสูจน์อย่างเดียว เรื่องนั้นเล่าต่อในหัวข้อถัดไป
บทเรียนที่ยกไปข้ออื่นได้คือ ขอบเขตที่แปลกและเล็กผิดที่ผิดทาง ให้ถือว่าเป็นคำใบ้ของเฉลยไว้ก่อน แล้วถามว่าเลขตัวนั้นทำให้อะไรที่ปกติแพงเกินไป กลายเป็นถูกลง
ถนนไม่ราดยางเส้นหนึ่งที่มีเส้นทางบนต้นไม้ยาว len สร้างวงยาว len + 1
วงนั้นเป็นเลขคู่เมื่อ len เป็นเลขคี่ ดังนั้น
กฎที่ 1 ถนนที่เส้นทางบนต้นไม้ยาวเป็นเลขคี่ ต้องถูกปิดเสมอ ไม่มีทางเลือก
ส่วนเส้นที่เส้นทางยาวเป็นเลขคู่ จะสร้างวงยาวเป็นเลขคี่ ซึ่งเก็บไว้ได้ แต่ถ้าเราเก็บไว้สองเส้นที่เส้นทาง บนต้นไม้ใช้ถนนราดยางร่วมกันอย่างน้อยหนึ่งเส้น เราจะได้วงคี่สองวงที่มีส่วนซ้อนกัน เอาสองวงมารวมกันแล้วตัดส่วนที่ซ้อนออก จะได้วงใหม่ที่ยาว คี่ + คี่ − 2 × ส่วนซ้อน ซึ่งเป็นเลขคู่เสมอ
กฎที่ 2 ถนนที่เก็บไว้ ต้องมีเส้นทางบนต้นไม้ที่ไม่ทับกันเลยสักเส้น
ปัญหาจึงกลายเป็น เลือกชุดของถนนที่เส้นทางบนต้นไม้ยาวเป็นเลขคู่ โดยเส้นทางไม่ทับกัน ให้น้ำหนักรวมมากที่สุด แล้วคำตอบคือค่าใช้จ่ายทั้งหมดลบน้ำหนักที่เก็บได้
| ถนน | ค่าปิด | เส้นทางบนต้นไม้ | ยาว | ตัดสิน |
|---|---|---|---|---|
1 ถึง 3 | 2 | 1 - 2 - 3 | 2 | เก็บได้ ถ้าไม่ทับใคร |
3 ถึง 5 | 2 | 3 - 4 - 5 | 2 | เก็บได้ ถ้าไม่ทับใคร |
2 ถึง 4 | 5 | 2 - 3 - 4 | 2 | เก็บได้ ถ้าไม่ทับใคร |
2 ถึง 5 | 1 | 2 - 3 - 4 - 5 | 3 | ต้องปิดเสมอ (วงยาวคู่) |
| ชุดที่เลือก | ถนนราดยางที่ถูกใช้ | น้ำหนักรวม |
|---|---|---|
1 ถึง 3 เส้นเดียว | 1-2, 2-3 | 2 |
3 ถึง 5 เส้นเดียว | 3-4, 4-5 | 2 |
2 ถึง 4 เส้นเดียว | 2-3, 3-4 | 5 |
| 2 ถึง 4 | 2-3, 3-4 | 5 |
ตรงนี้แหละที่ขอบเขต "ดีกรีไม่เกิน 10" เข้ามามีบทบาท ปักรากต้นไม้ไว้ที่เมือง 1
แล้วคิดดีพีจากใบขึ้นไปหาราก นิยาม dp[v][mask] ว่าคือน้ำหนักมากที่สุดที่เก็บได้ในต้นไม้ย่อยของ
v โดยที่ถนนราดยางที่ต่อ v กับลูกในเซต mask ถูกจองไปแล้วโดยเส้นทางของบรรพบุรุษ
ลูกของแต่ละปมมีไม่เกิน 10 คน หน้ากากจึงมีไม่เกิน 1024 แบบ ซึ่งเล็กมาก ถนนไม่ราดยางแต่ละเส้นถูกคิดที่ปมร่วมที่ต่ำที่สุดของปลายทั้งสอง เพราะเส้นทางของมันขึ้นสูงสุดแค่นั้น
// g[mask] = น้ำหนักมากที่สุดในต้นไม้ย่อยของ v เมื่อลูกในเซต mask ถูกจองไปแล้ว
for (int mk = 0; mk < full; mk++) {
long long s = sum; // ค่าเริ่มต้นคือปล่อยลูกทุกคนทำงานของตัวเอง
for (int j = 0; j < k; j++) if (mk >> j & 1) s -= d0[j];
g[mk] = s;
}
// ไล่จากเซตใหญ่ไปเซตเล็ก เพราะ g[mk] อ้างถึง g[mk | se] ซึ่งมีบิตมากกว่าเสมอ
for (int mk = full - 1; mk >= 0; mk--)
for (auto& c : cs) // cs = ถนนไม่ราดยางที่มีจุดร่วมสูงสุดอยู่ที่ v
if (!(c.se & mk)) g[mk] = max(g[mk], c.val + g[mk | c.se]);
ค่าของการเลือกถนนเส้นหนึ่งคือน้ำหนักของมัน บวกกับผลรวมของ dp ของทุกปมที่เส้นทางเดินผ่าน
โดยแต่ละปมนั้นถูกจองลูกที่เส้นทางเพิ่งไต่ขึ้นมา ส่วนการไล่หน้ากากจากใหญ่ไปเล็กเป็นเรื่องจำเป็น
เพราะสูตรอ้างถึง g[mask | se] ซึ่งมีบิตมากกว่าเสมอ
กฎสองข้อข้างบนฟังดูสมเหตุสมผล แต่ "สมเหตุสมผล" กับ "ถูก" เป็นคนละเรื่อง โดยเฉพาะข้อความว่า ถ้าเส้นทางไม่ทับกันเลย ก็รับประกันว่าไม่มีวงคู่โผล่มา ซึ่งเป็นทิศทางที่พิสูจน์ยากกว่าอีกทิศเยอะ
ผมจึงเขียนตัวตรวจสอบที่ไม่เชื่อทฤษฎีอะไรเลย มันไล่ทุกวิธีเลือกว่าจะเก็บถนนเส้นไหนไว้บ้าง แล้วไล่หาวงจรอย่างง่ายทุกวงในกราฟที่ได้ ถ้าเจอวงยาวเป็นเลขคู่แม้แต่วงเดียวก็ตัดทิ้ง ที่เหลือค่อยเอาน้ำหนักมากที่สุด
สุ่มต้นไม้เล็ก ๆ ไม่เกินเจ็ดเมือง พร้อมถนนไม่ราดยางสุ่มอีกไม่เกินห้าเส้น จำนวน 500 ชุด แล้วเทียบกับโค้ดจริง ตรงกันหมด นี่เป็นครั้งเดียวที่ผมกล้าพูดว่ากฎสองข้อนั้นถูก เพราะตัวตรวจสอบไม่ได้ใช้กฎนั้นเลยสักนิด
ที่ N ไม่เกิน 1000 และ M ไม่เกิน 5000 การหาปมร่วมที่ต่ำที่สุดไม่ต้องใช้ท่าหรู
แค่ไต่ขึ้นตามพ่อทีละชั้นก็พอ และการหาผลรวม dp ตลอดเส้นทางก็ไล่ทีละปมได้
รวมแล้วเป็น M × N ซึ่งคือ 5,000,000 ครั้ง
ส่วนดีพีบนหน้ากากเป็น 2^ดีกรี คูณจำนวนถนนที่มีปมร่วมอยู่ที่ปมนั้น
รวมทั้งต้นไม้แล้วไม่เกิน 5,120,000 ครั้ง ทั้งสองก้อนอยู่ในลิมิต 0.3 วินาทีสบาย ๆ
เวลาที่วัดได้บนเครื่องผม ต้นไม้ 1000 เมืองที่ทุกปมมีลูกเก้าคน พร้อมถนนไม่ราดยาง 5000 เส้น ใช้เวลา 51 มิลลิวินาที จากลิมิต 0.3 วินาที
พื้นฐานสองอย่างที่ข้อนี้ยืนอยู่บนมันคือ ดีพีบนต้นไม้ กับการเก็บเซตไว้ในบิต ซึ่งมีตัวอย่างเต็ม ๆ ใน ดีพีที่สถานะคือความจำล่าสุด
สองกฎในเฉลยเป็นข้อสรุปเรื่องคู่คี่ (parity) ทั้งคู่ คือเราไม่ได้สนใจว่าเส้นทางยาวเท่าไร สนใจแค่ว่ามันยาวเป็นจำนวนคู่หรือคี่ พอถามแค่นั้น ถนนทั้งกราฟก็ถูกจัดออกเป็นสองพวกทันที และเงื่อนไขทั้งข้อยุบลงเหลือกฎสั้น ๆ สองข้อ
ท่านี้เป็นญาติกับการระบายสองสี (bipartite coloring) คือถ้าเราไล่ระบายสีปมสลับกันสองสีตามเส้นเชื่อม แล้วระบายได้สำเร็จโดยไม่มีเส้นไหนเชื่อมสีเดียวกัน กราฟนั้นไม่มีวงคี่เลย ซึ่งเป็นข้อความเดียวกับกฎในเฉลยนี้ พูดด้วยคำอีกชุด รู้ความเชื่อมโยงนี้ไว้จะอ่านโจทย์กราฟที่พูดเรื่องคู่คี่ออกเร็วขึ้นมาก มีคำอธิบายเรื่องคู่คี่อยู่ในโจทย์เครื่องเคลื่อนย้าย
อีกท่าที่ข้อนี้ใช้คือการบีบสถานะ (state compression) ตอนไล่ต้นไม้ เราไม่เก็บทุกอย่างของกิ่ง เก็บแค่ค่าที่พ่อต้องใช้ ซึ่งมีบทปูพื้นอยู่ในคลังนี้แล้ว
ต้นไม้ทำให้ถนนนอกต้นไม้หนึ่งเส้นเท่ากับวงหนึ่งวง ความเป็นคู่เป็นคี่จึงตัดเส้นครึ่งหนึ่งทิ้งไปเลย และเงื่อนไขที่เหลือคือเส้นทางห้ามทับกัน ซึ่งพอดีกับดีพีบนต้นไม้ที่จำว่าลูกคนไหนถูกจองไปแล้ว
ในหน้านี้