programming.in.th · ข้อ 2009
ทุกกลุ่มของเกาะคือวงหนึ่งวงที่มีต้นไม้ห้อย ปอกใบทิ้งเพื่อหาวงและทางยาวสุดในต้นไม้ แล้วคลี่วงเป็นสองรอบให้หน้าต่างเลื่อนทำงาน พร้อมท่า 40 คะแนนที่ตรงไปตรงมากว่า และบักที่ตัวอย่างในโจทย์ตอบถูกทั้งที่โค้ดลืมคิดทางในต้นไม้
สวนสาธารณะมีเกาะ N เกาะ จากเกาะ i มีสะพานหนึ่งสะพานทอดไปยังอีกเกาะหนึ่ง
ยาว L ทั้งสวนจึงมีสะพาน N สะพานพอดี สะพานเดินได้สองทาง
และระหว่างเกาะคู่ไหนก็มีเรือแล่นถึงกันได้
เราอยากให้ผลรวมความยาวสะพานที่เดินข้ามมากที่สุด ภายใต้กฎว่า
ให้กลุ่มหมายถึงชุดของเกาะที่เดินถึงกันได้ด้วยสะพาน กฎเรือแปลว่าเราข้ามไปกลุ่มอื่นได้ฟรี แต่กลับเข้ากลุ่มเดิมไม่ได้
เพราะพอเข้ากลุ่มไหนแล้ว ทุกเกาะในกลุ่มนั้นก็นับว่า "ไปถึงได้" หมด คำตอบจึงเป็น ผลรวมของเส้นทางที่ยาวที่สุดในแต่ละกลุ่ม
อินพุต / ขอบเขต / เอาต์พุต
N จากนั้น N บรรทัด บรรทัดที่ i
คือหมายเลขเกาะปลายทางกับความยาวของสะพานที่ออกจากเกาะ i 2 ≤ N ≤ 1,000,000, 1 ≤ L ≤ 100,000,000
เวลา 2.2 วินาที หน่วยความจำ 128 เมกะไบต์
N ไม่เกิน 4,000| Input | Output |
|---|---|
| 7 3 8 7 2 4 2 1 4 1 9 3 4 2 3 | 24 |
ระหว่างเกาะ 2 กับเกาะ 7 มีสะพานสองเส้น (ยาว 2 ที่ออกจากเกาะ 2 และยาว 3 ที่ออกจากเกาะ 7) นับเป็นคนละสะพาน
ใบ้
ทุกเกาะมีสะพานออกไปเส้นเดียว ลองวาดลูกศรจากเกาะ i ไปเกาะที่มันชี้ถึง
แล้วดูว่ารูปที่ได้หน้าตาเป็นยังไง เดินตามลูกศรจากเกาะไหนก็ตาม สุดท้ายจะไปจบที่ไหน
พอเห็นรูปแล้ว ลองแยกว่า "เส้นทางที่ยาวที่สุด" ในรูปแบบนั้นมีได้กี่ทรง
สี่เกาะกลุ่มเดียว พอให้ชินกับการเดินไม่ซ้ำเกาะ
เส้นทางตอนนี้ ยังไม่ได้เริ่มเดิน ยาว 0
กดเกาะเพื่อเริ่มเดิน แล้วกดเกาะถัดไปที่มีสะพานถึงกัน ห้ามเหยียบเกาะซ้ำ
รวมที่บันทึกไว้ 0 จากคำตอบเต็ม 0
คำตอบของโจทย์คือผลรวมของเส้นทางที่ยาวที่สุดของแต่ละกลุ่ม กลุ่มไหนที่ยังไม่ได้บันทึกก็ยังไม่ถูกนับ
ถ้าลองเดินหลายรอบแล้วจะเริ่มเห็นว่า เส้นทางที่ยาวที่สุดมีอยู่แค่สองทรง คือทรงที่ข้ามวง กับทรงที่จมอยู่ในกิ่งที่ห้อยลงมา
ก่อนจะเลือกวิธี ต้องรู้ก่อนว่าแผนที่หน้าตาเป็นยังไง ลองทำตามใบ้ คือวาดลูกศรจากแต่ละเกาะไปเกาะที่สะพานของมันชี้ถึง
ทุกเกาะมีลูกศรออกเส้นเดียว เดินตามลูกศรไปเรื่อย ๆ เกาะมีจำกัด สักวันต้องวนกลับมาเจอเกาะที่เคยผ่าน ทางที่วนนั้นคือวง (cycle อ่านว่า "ไซเคิล")
เกาะที่ไม่ได้อยู่บนวงก็เดินตามลูกศรไปจบที่วงเหมือนกัน มันจึงเป็นกิ่งที่ห้อยเข้าหาวง เรียกว่าต้นไม้ห้อย
กลุ่มที่มี k เกาะจะมีสะพาน k เส้นพอดี (สะพานของแต่ละเกาะอยู่ในกลุ่มของมันเอง) จึงมีวงได้แค่วงเดียว
จากรูป ทางที่ยาวที่สุดในกลุ่มหนึ่งมีได้แค่สองทรง
ในตัวอย่าง ทั้งสองกลุ่มชนะด้วยทรงข้ามวง ส่วนชุดที่ 4 ของเกมคือแผนที่ที่ทรงจมในต้นไม้ชนะ ถ้าเล่นชุดนั้นจนได้คำตอบเต็มแล้ว จะเห็นว่าทางยาวสุดของมันไม่แตะวงเลย
สะพานคู่ของเกาะ 2 กับ 7
สองเกาะนี้ชี้หากันเอง จึงเป็นวงยาว 2 ที่มีสะพานสองเส้น ถ้ายุบให้เหลือเส้นเดียวแล้วเก็บเส้นที่ยาวกว่าไว้ คำตอบยังเป็น 24 แต่ถ้าเก็บเส้นที่สั้นกว่าจะได้ 23 และถ้าลบทั้งคู่จนกลุ่มขาดจะได้ 21
รู้รูปแล้ว ไล่จากวิธีที่ตรงที่สุดก่อน โจทย์แบ่งคะแนนไว้สองชุด
| คะแนน | เงื่อนไขของชุด |
|---|---|
| 40 | N ไม่เกิน 4,000 |
| 60 | ขอบเขตเต็ม N ไม่เกิน 1,000,000 |
วิธีที่ 1
โตแบบทวีคูณ
ลองเดินทุกทางจากทุกเกาะ ใช้ได้แค่สวนไม่กี่เกาะ
ใช้เป็นตัวตรวจ
วิธีที่ 2
ราว c × ขนาดกลุ่ม แย่สุด N²
ตัดแล้วกลุ่มกลายเป็นต้นไม้ หาทางยาวสุดของต้นไม้ด้วยการค้นสองรอบ เอาค่าที่ดีที่สุดของทุกการตัด
ได้ 40 คะแนน
วิธีเต็ม
เชิงเส้น ราว 8N ก้าว
แยกคิดทีละทรง ทรงจมในต้นไม้ได้จากการปอกใบ ทรงข้ามวงได้จากหน้าต่างเลื่อนบนวงที่คลี่
ได้ครบ 100 คะแนน
วิธีที่ 2 ต้องใช้เรื่องเดียวจากรูปของแผนที่ คือทางที่ไม่ซ้ำเกาะไต่วงได้ทางเดียว มันจึงไม่ใช้สะพานบนวงอย่างน้อยหนึ่งเส้นเสมอ
งั้นลองตัดสะพานบนวงทิ้งทีละเส้น ตัดแล้วกลุ่มกลายเป็นต้นไม้ (tree) ทันที ทางที่ยาวที่สุดในต้นไม้มีชื่อว่าเส้นผ่านศูนย์กลาง (diameter อ่านว่า "ไดแอมิเทอร์")
เส้นผ่านศูนย์กลางของต้นไม้หาได้ด้วยการเดินทั่วต้นไม้สองรอบ วิธีและเหตุผลอยู่ในหัวข้อท้ายหน้า (อีกท่าของเส้นผ่านศูนย์กลาง) สำหรับตอนนี้รู้แค่ว่าแต่ละครั้งที่ตัด เราเดินทั่วกลุ่มสองรอบ
เรื่องจริงที่เกิดขึ้นตามลำดับ
ตอนเขียนท่านี้ ผมเขียนโค้ดที่ตัด "สะพานระหว่างเกาะคู่นี้" แทนที่จะตัด "สะพานหมายเลขนี้" ซึ่งฟังดูเหมือนกันจนกว่าจะเจอเกาะสองเกาะที่มีสะพานถึงกันสองเส้น
เคสแบบนั้นอยู่ในตัวอย่างของโจทย์เอง คือเกาะ 2 กับเกาะ 7 โค้ดจึงตัดทิ้งทีเดียวสองเส้น กลุ่มขาดออกจากกัน คำตอบกลายเป็น 21 แทนที่จะเป็น 24 อันนี้ตัวอย่างในโจทย์จับได้เอง
ตั้งแต่นั้นมา เมื่อกราฟมีเส้นซ้ำได้ ผมอ้างถึงเส้นด้วยหมายเลขเส้นเสมอ
ท่านี้ตัดวงละ c ครั้ง และทุกครั้งค้นทั้งกลุ่ม ต้นทุนจึงเป็น c คูณขนาดกลุ่ม แย่ที่สุดคือแผนที่ทั้งหมดเป็นวงเดียวยาว N ได้ N² ลองเทียบกับงบเวลาดู
คิดเป็นตัวเลข
เกินหลายพันเท่าแบบนี้ ปรับนิดหน่อยไม่พอแน่ เป้าคือต้องแตะแต่ละเกาะแค่ไม่กี่ครั้ง (เวลาเชิงเส้น) วิธีที่ 2 ช้าเพราะค้นทั้งกลุ่มใหม่ทุกครั้งที่ตัด วิธีเต็มจึงแยกคิดทีละทรงแทน ให้แต่ละทรงคิดรอบเดียวจบ
เริ่มจากทรงที่ง่ายกว่า ทางทรงจมในต้นไม้จะมีเกาะที่อยู่สูงสุด (ใกล้วงที่สุด) อยู่หนึ่งเกาะ จากเกาะนั้นทางแตกลงไปได้ไม่เกินสองกิ่ง ทางยาวสุดจึงคือการต่อสองกิ่งที่ลึกที่สุดเข้าด้วยกัน
ค่าที่เก็บไว้ที่ทุกเกาะ v
t1[v] ระยะไกลสุดจาก v ลงไปในต้นไม้ที่ห้อยเข้าหา v (กิ่งที่ลึกที่สุด)t2[v] กิ่งที่ลึกรองลงมา โดยต้องลงทางเกาะลูกคนละเกาะกับ t1[v]t1[v] + t2[v]ถ้าเกาะ u มีลูกศรชี้ไปเกาะ p เรียก p ว่าพ่อ และ u ว่าลูก (ลูกอยู่ไกลวงกว่า)
จะได้ t1 กับ t2 ของเกาะหนึ่ง ต้องรู้ค่าของลูกทุกตัวก่อน วิธีที่ทำได้ในรอบเดียวคือปอกใบ เหมือนปอกหัวหอมทีละชั้นจากนอกเข้าใน ใบ (leaf) ในที่นี้คือเกาะที่ไม่มีลูกศรชี้เข้าเลย
t1[u] + ความยาวสะพานของ u ขึ้นไปให้พ่อ (บวกสะพานเพราะกิ่งที่มองจากพ่อต้องเดินข้ามสะพานนั้นลงมาก่อน) ถ้ายาวกว่า t1 หรือ t2 ของพ่อก็แทนที่เกาะบนวงไม่เคยเข้าคิว เพราะมีเกาะบนวงอีกเกาะชี้เข้าเสมอ และเกาะนั้นไม่เคยถูกปอก ส่วนเกาะในต้นไม้ห้อยเข้าคิวครบทุกเกาะ ใบเข้าตั้งแต่แรก แล้วไล่ขึ้นมา พอลูกทุกตัวถูกปอก ตัวนับของพ่อก็ลดถึงศูนย์
เกาะที่เข้าคิวแล้ว ลูกของมันส่งค่ามาครบทุกตัว t1 กับ t2 จึงไม่เปลี่ยนอีก และ t2 มาจากลูกคนละตัวกับ t1 เองโดยอัตโนมัติ เพราะลูกแต่ละตัวส่งค่าขึ้นมาแค่ครั้งเดียว
ปอกตัวอย่างในโจทย์ทีละเกาะ
ชุดที่ 4 ของเกมลึกกว่า และเห็นตอนที่ t1 เดิมถอยไปเป็น t2
ที่มาของแนวคิดนี้
การปอกใบทำสองงานในรอบเดียว คือเก็บ t1 กับ t2 อย่างที่เพิ่งเล่า และหาวงไปด้วย ที่เลือกท่านี้ก็เพราะงานที่สอง
ท่าที่คนส่วนใหญ่คิดออกก่อน (ผมด้วย) คือเดินตามลูกศรไปเรื่อย ๆ แล้วทำเครื่องหมายไว้ จนกว่าจะเจอเกาะที่เคยเจอแล้ว
ปัญหาคือพอเจอเกาะซ้ำ ต้องแยกให้ออกว่ามันเป็นเกาะของรอบนี้ หรือของรอบก่อนที่เดินไปแล้ว ซึ่งต้องเพิ่มสถานะมาคุมอีกชั้น
การปอกเกาะที่ไม่มีใครชี้เข้ามาทิ้งไปเรื่อย ๆ ไม่ต้องเจอคำถามนั้นเลย สิ่งที่เหลือหลังปอกจนหมดคือวง
| เกาะ | อยู่บนวง | t1 | t2 | t1 + t2 |
|---|---|---|---|---|
| 1 | ใช่ | 9 | 0 | 9 |
| 2 | ใช่ | 0 | 0 | 0 |
| 3 | ใช่ | 4 | 0 | 4 |
| 4 | ใช่ | 0 | 0 | 0 |
| 5 | ไม่ | 0 | 0 | 0 |
| 6 | ไม่ | 0 | 0 | 0 |
| 7 | ใช่ | 0 | 0 | 0 |
สังเกตว่า t1 ของเกาะบนวงคือความลึกของต้นไม้ที่ห้อยจากเกาะนั้น ทรงข้ามวงต้องใช้ค่านี้ต่อ ท่านี้ใช้แค่คิวกับลูปธรรมดา ไม่มีการเรียกฟังก์ชันซ้อน ซึ่งสำคัญตอนต้นไม้ลึกเป็นล้านชั้น (ดูหัวข้อโค้ด)
ปอกเสร็จแล้ว เหลือแต่เกาะบนวง และแต่ละเกาะรู้แล้วว่าต้นไม้ที่ห้อยจากมันลึกเท่าไร ทีนี้ทรงข้ามวงเหลือแค่เลือกเกาะบนวงสองเกาะกับทิศที่จะไต่
ชื่อที่ใช้กับวงยาว c
d[i] ความลึกของต้นไม้ที่ห้อยจากเกาะตำแหน่ง i (คือ t1 ของเกาะนั้น)P[i] ระยะสะสม คือผลรวมความยาวสะพานบนวงจากตำแหน่ง 0 ไต่ตามลูกศรมาถึง iทางที่ลงต้นไม้ที่ตำแหน่ง i แล้วไต่ตามลูกศรไปถึงตำแหน่ง j ยาวเท่ากับ
ความยาวของทางทรงข้ามวง
d[i] + (P[j] − P[i]) + d[j]
ลองกับกลุ่มที่ 1 ของตัวอย่าง
ปัญหาแรกคือทางที่ไต่ข้ามจุดเริ่ม เช่นไต่จากตำแหน่ง 2 วนกลับไปตำแหน่ง 0 ค่า P[j] − P[i] จะติดลบ ทั้งที่ระยะจริงเป็นบวก
นึกภาพ
สร้อยข้อมือหนึ่งเส้น ถ้าตัดออกแล้ววางลงพื้นสองเส้นต่อกัน ทุกช่วงบนสร้อยที่สั้นกว่าหนึ่งรอบ ไม่ว่าจะเริ่มตรงไหน จะเห็นเป็นช่วงตรง ๆ ช่วงหนึ่งบนเส้นยาวนี้เสมอ
เราทำแบบเดียวกัน คือคลี่วงเป็นสองรอบ ได้แถวยาว 2c ตำแหน่ง k คือเกาะเดียวกับตำแหน่ง k mod c (เศษของ k หารด้วย c) และ P สะสมต่อไปเรื่อย ๆ ไม่วนกลับเป็นศูนย์
| ตำแหน่ง k | 0 | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|---|
| เกาะ | 1 | 3 | 4 | 1 | 3 | 4 |
| d | 9 | 4 | 0 | 9 | 4 | 0 |
| P | 0 | 8 | 10 | 14 | 22 | 24 |
ทุกทางทรงข้ามวงตอนนี้คือคู่ตำแหน่ง i < j บนแถวนี้ ที่ห่างกันไม่เกิน c − 1 เช่นไต่จากเกาะ 4 (ตำแหน่ง 2) ข้ามจุดเริ่มไปเกาะ 1 ก็คือคู่ตำแหน่ง 2 กับ 3 ได้ P[3] − P[2] = 4 เป็นบวกแล้ว
ระวัง
ระยะห่างต้องไม่เกิน c − 1 เพราะทางที่ไม่ซ้ำเกาะไต่วงได้มากที่สุด c − 1 สะพาน
ถ้าปล่อยถึง c โปรแกรมจะยอมให้ไต่ครบรอบแล้วกลับมาที่เดิม ซึ่งผิดกติกา และมันจะตอบเกินจริงแบบเงียบ ๆ
ไล่ทุกคู่ตรง ๆ ยังเป็น c² อยู่ดี และวงยาวได้ถึงล้าน ต้องตัดอีกชั้น โดยจับสูตรกลุ่มใหม่ แยกส่วนที่เป็นของ j ออกจากส่วนที่เป็นของ i
จับกลุ่มใหม่
(d[j] + P[j]) + (d[i] − P[i])
เมื่อเลือก j แล้ว ก้อนแรกเป็นค่าคงที่ เหลือแค่หาค่ามากสุดของ d[i] − P[i]
ในช่วง i = j − (c − 1) ถึง j − 1
ช่วงนี้ขยับไปทางขวาทีละช่องตาม j เรียกว่าหน้าต่างเลื่อน (sliding window อ่านว่า "สไลดิง วินโดว์")
ค่ามากสุดในหน้าต่างเลื่อนหาได้ด้วยคิวที่เข้าออกได้สองหัว เรียกว่า deque (อ่านว่า "เด็ค" ย่อจาก double-ended queue) อ่านเรื่องนี้แบบละเอียดได้ที่บทหน้าต่างเลื่อน
นึกภาพ
ทีมคัดตัวนักกีฬาที่สัญญามีอายุจำกัด ถ้ามีคนใหม่เข้ามาที่เก่งกว่าคนเก่า คนเก่าคนนั้นไม่มีวันได้ถูกเลือกอีกแล้ว เพราะคนใหม่ทั้งเก่งกว่าและอยู่ได้นานกว่า ไล่ออกได้เลย
คิวเก็บตำแหน่งเรียงจากเก่าไปใหม่ และเพราะทุกครั้งที่ใส่ตัวใหม่เราไล่ตัวที่ไม่มากกว่าออกจากท้ายก่อน ค่า d − P ในคิวจึงลดลงจากหัวไปท้ายเสมอ หัวคิวจึงเป็นค่ามากสุดของหน้าต่าง ทุกตำแหน่ง j ทำตามนี้
แต่ละตำแหน่งเข้าคิวครั้งเดียวและออกได้ครั้งเดียว ทั้งแถวยาว 2c จึงใช้เวลาเชิงเส้น ลองเดินดูกับกลุ่มที่ 1
กลุ่มที่ 1 คลี่เป็น 6 ตำแหน่ง หน้าต่างยาว 2
| k | เกาะ | d + P | d − P | คู่ที่ดีที่สุดที่จบที่ k | ค่า |
|---|---|---|---|---|---|
| 0 | 1 | 9 | 9 | ยังไม่มีคู่ | · |
| 1 | 3 | 12 | −4 | เกาะ 1 ไป 3 | 21 |
| 2 | 4 | 10 | −10 | เกาะ 1 ไป 4 | 19 |
| 3 | 1 | 23 | −5 | เกาะ 3 ไป 1 | 19 |
| 4 | 3 | 26 | −18 | เกาะ 1 ไป 3 | 21 |
| 5 | 4 | 24 | −24 | เกาะ 1 ไป 4 | 19 |
กลุ่มที่ 2 มีวงสองเกาะ (2 กับ 7) ไม่มีต้นไม้ห้อย หน้าต่างยาวแค่ 1 คู่ที่ดีที่สุดคือไต่สะพานยาว 3 ได้ 3
// ค่าของเส้นทางที่ลงต้นไม้ที่ i ไต่วงไปทางเดียวจนถึง j แล้วลงต้นไม้ที่ j คือ
// d[i] + d[j] + (P[j] - P[i]) = (d[j] + P[j]) + (d[i] - P[i])
// ก้อนหลังไม่ขึ้นกับ j เลย จึงใช้หน้าต่างเลื่อนเก็บค่ามากสุดของมันได้
deque<int> dq;
for (int j = 0; j < 2 * c; j++) {
while (!dq.empty() && dq.front() < j - (c - 1)) dq.pop_front(); // ไต่วงเกินรอบไม่ได้
if (!dq.empty())
local = max(local, d[j % c] + P[j] + d[dq.front() % c] - P[dq.front()]);
ll cur = d[j % c] - P[j];
while (!dq.empty() && d[dq.back() % c] - P[dq.back()] <= cur) dq.pop_back();
dq.push_back(j);
} เรื่องจริงที่เกิดขึ้นตามลำดับ
โค้ดรอบแรกของผมคำนวณทรงจมในต้นไม้ไว้เรียบร้อย เก็บใส่ตัวแปร best แล้วไม่ได้เอาไปใช้เลยสักบรรทัด
สิ่งที่เหลือไว้คือ (void)best; ไม่มี error ไม่มี warning
ตัวอย่างในโจทย์ตอบถูก 24 ตรงเป๊ะ เพราะทุกกลุ่มในตัวอย่างชนะด้วยทรงข้ามวงอยู่แล้ว ตัวสุ่มเทียบจับได้ที่แผนที่ 6 เกาะ โปรแกรมตอบ 18 ของจริงคือ 21
ค่าที่คำนวณถูกแล้วแต่ลืมเอาไปใช้ ไม่มีอะไรเตือนเลย ที่จับได้คือการสุ่มเทียบกับตัวไล่ทุกทาง
คำตอบของแต่ละกลุ่มคือค่าที่มากกว่าระหว่างสองทรง ทรงข้ามวงคิดทีละวงอยู่แล้ว แต่ t1 + t2 คิดไว้ที่ทุกเกาะ รวมเกาะในต้นไม้ห้อย จึงต้องรู้ก่อนว่าเกาะแต่ละเกาะอยู่กลุ่มไหน
คิวของการปอกใบช่วยตรงนี้ ในคิว ลูกมาก่อนพ่อเสมอ ไล่คิวย้อนกลับจึงได้พ่อก่อนลูก แต่ละเกาะก็แค่ลอกหมายเลขกลุ่มของเกาะที่มันชี้ถึง
| กลุ่ม | ทรงข้ามวง | ทรงจมในต้นไม้ | คำตอบของกลุ่ม |
|---|---|---|---|
| 1 | 21 | 9 | 21 |
| 2 | 3 | 0 | 3 |
ที่ N เป็นล้าน ทุกอย่างต้องเป็นอาเรย์แบน ๆ และห้ามเรียกฟังก์ชันตัวเองซ้อนกัน (recursion อ่านว่า "รีเคอร์ชัน") เพราะต้นไม้ที่ลึกล้านชั้นจะทำให้หน่วยความจำส่วนที่เก็บการเรียกซ้อน (stack) ล้น
คิวของการปอกใบเก็บเป็นเวกเตอร์เดียวโดยไม่ลบตัวที่หยิบไปแล้ว มันจึงใช้ได้สองต่อ ตอนเดินหน้ามันคือคิว และตอนไล่ย้อนกลับมันคือลำดับพ่อก่อนลูกที่ใช้แจกหมายเลขกลุ่ม
| ส่วนของโค้ด | อธิบายใน |
|---|---|
| อ่านอินพุต นับลูกศรชี้เข้า (indeg) | ตอนที่ 3 ทรงจมในต้นไม้ ขั้นแรกของการปอกใบ |
| ปอกใบทิ้งทีละชั้น เก็บ t1, t2 | ตอนที่ 3 ทรงจมในต้นไม้ |
| แยกวง เก็บ d, W และระยะสะสม P ยาว 2c | ตอนที่ 4 ทรงข้ามวง (คลี่วงเป็นสองรอบ) |
| หน้าต่างเลื่อน หาค่ามากสุดของ d[i] − P[i] | ตอนที่ 4 ทรงข้ามวง (เครื่องเดิน) |
| แจกหมายเลขกลุ่มลงต้นไม้ห้อย แล้วเอา t1 + t2 มาสู้ | ตอนที่ 5 รวมสองทรง |
| รวมคำตอบทุกกลุ่ม | กฎเรือในโจทย์ |
รวมทั้งหมดผมสุ่ม 1,500 ชุดเทียบเฉลยเต็มกับตัวไล่ทุกทาง และอีก 800 ชุดเทียบเฉลยเต็มกับเฉลยชุด 40 คะแนน ตรงกันทุกชุด
เวลาที่วัดได้บนเครื่องผม สวน 1,000,000 เกาะ แบบวงสั้นเยอะ ๆ ใช้ 235 มิลลิวินาที แบบวงเดียวยาวหนึ่งล้านใช้ 240 มิลลิวินาที จากลิมิต 2.2 วินาที
ทรงจมในต้นไม้หาเส้นผ่านศูนย์กลางด้วยการเก็บสองกิ่งที่ลึกที่สุด (DP บนต้นไม้) ซึ่งจำเป็นในข้อนี้ เพราะเราต้องใช้ความลึกรายเกาะไปคิดเรื่องวงต่อ
ถ้าโจทย์ถามแค่ตัวเลขเส้นผ่านศูนย์กลาง มีท่าที่เขียนสั้นกว่า และเป็นท่าที่โค้ดชุด 40 คะแนนใช้หลังตัดสะพานบนวง
บนต้นไม้ ระหว่างสองเกาะมีทางเดียว ลำดับที่เดินจึงไม่สำคัญ จะเดินลึกก่อนหรือกว้างก่อนก็ได้ระยะเท่ากัน
ถ้าสะพานไม่มีความยาว (นับเป็นจำนวนเส้น) ก็ใช้การค้นแบบกว้าง (BFS ย่อจาก breadth-first search อ่านว่า "เบรดท์เฟิสต์เสิร์ช" คือค้นทีละชั้นจากใกล้ไปไกล) แบบโค้ดข้างล่างได้
หลักที่ทำให้ท่านี้ถูก
ถ้าความยาวสะพานไม่ติดลบ เกาะที่ไกลที่สุดจากเกาะใดก็ได้ เป็นปลายข้างหนึ่งของเส้นผ่านศูนย์กลางเส้นใดเส้นหนึ่ง (อาจมีหลายเส้นที่ยาวเท่ากัน)
พอได้ปลายข้างหนึ่งแล้ว รอบที่สองก็แค่วัดจากปลายนั้นออกไปให้ไกลสุด บทพิสูจน์ต้องแยกกรณีว่าทางจากจุดเริ่มไป a ตัดกับเส้นผ่านศูนย์กลางหรือไม่ หน้านี้ขอข้ามไป และใช้ตัวตรวจข้างล่างยืนยันแทน
ระวัง หลักนี้ใช้ได้แค่บนต้นไม้
บนกราฟที่มีวง หลักนี้พัง และคำตอบจะผิดแบบเงียบ ๆ ในข้อนี้จึงใช้มันได้แค่หลังตัดสะพานบนวงทิ้งแล้ว (วิธีที่ 2) ส่วนเฉลยเต็มแยกเรื่องวงออกมาคิดเองตั้งแต่ต้น
// เส้นผ่าศูนย์กลางของต้นไม้ ด้วย BFS สองรอบ
// รอบแรกจากปมใดก็ได้ หาปมที่ไกลสุด รอบสองเริ่มจากปมนั้น ระยะที่ไกลสุดคือคำตอบ
// อินพุต: n แล้ว n-1 บรรทัด u v เอาต์พุต: ความยาวเส้นผ่าศูนย์กลาง (นับเป็นจำนวนเส้น)
#include <bits/stdc++.h>
using namespace std;
/** คืนคู่ (ปมที่ไกลสุด, ระยะถึงปมนั้น) เมื่อเริ่มจาก s */
pair<int, int> far(int s, const vector<vector<int>>& g) {
int n = (int)g.size();
vector<int> dist(n, -1);
dist[s] = 0;
vector<int> q{s};
for (int h = 0; h < (int)q.size(); h++)
for (int v : g[q[h]])
if (dist[v] == -1) {
dist[v] = dist[q[h]] + 1;
q.push_back(v);
}
int best = s;
for (int i = 0; i < n; i++) if (dist[i] > dist[best]) best = i;
return {best, dist[best]};
}
int main() {
int n;
if (scanf("%d", &n) != 1) return 0;
vector<vector<int>> g(n);
for (int i = 0; i + 1 < n; i++) {
int u, v;
scanf("%d %d", &u, &v);
g[u].push_back(v);
g[v].push_back(u);
}
if (n == 1) { printf("0\n"); return 0; }
int a = far(0, g).first; // ปมปลายทางหนึ่งของเส้นผ่าศูนย์กลาง
printf("%d\n", far(a, g).second);
return 0;
} ทุกกลุ่มคือวงหนึ่งวงที่มีต้นไม้ห้อย ทรงจมในต้นไม้ได้จากการปอกใบ ทรงข้ามวงได้จากหน้าต่างเลื่อนบนวงที่คลี่เป็นสองรอบ
กราฟที่ทุกปมมีลูกศรออกเส้นเดียวพอดีเรียกว่ากราฟฟังก์ชัน (functional graph) มีบทปูพื้นเรื่องกราฟฟังก์ชันและบทเรื่อง DP บนต้นไม้อยู่ในคลังนี้แล้ว
คำถามที่ยกไปใช้กับข้ออื่นได้คือ ลูกศรของแต่ละก้าวถูกกำหนดตายตัวหรือไม่ ถ้าตายตัว รูปของกราฟก็ถูกล็อกทันที และเราแยกกรณีได้โดยไม่ต้องค้นหา
ในหน้านี้