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
คำตอบของโจทย์คือผลรวมของเส้นทางที่ยาวที่สุดของแต่ละกลุ่ม กลุ่มไหนที่ยังไม่ได้บันทึกก็ยังไม่ถูกนับ
ถ้าลองเดินหลายรอบแล้วจะเริ่มเห็นว่า เส้นทางที่ยาวที่สุดมีอยู่แค่สองทรง คือทรงที่ข้ามวง กับทรงที่จมอยู่ในกิ่งที่ห้อยลงมา
เรื่องจริงที่เกิดขึ้นตามลำดับ
ก่อนอื่น เรื่องที่มาของท่าปอกใบ ท่าที่คนส่วนใหญ่คิดออกก่อน (ผมด้วย) คือเดินตามลูกศรไปเรื่อย ๆ แล้วทำเครื่องหมายไว้ จนกว่าจะเจอปมที่เคยเจอแล้ว ปัญหาคือพอเจอปมซ้ำ ต้องแยกให้ออกว่ามันเป็นปมของรอบนี้ หรือของรอบก่อนที่เดินไปแล้ว ซึ่งต้องเพิ่มสถานะมาคุมอีกชั้น การปอกปมที่ไม่มีใครชี้เข้ามาทิ้งไปเรื่อย ๆ ไม่ต้องเจอคำถามนั้นเลย สิ่งที่เหลือหลังปอกจนหมดคือวงพอดี
ทีนี้เรื่องที่ผิดจริง โค้ดรอบแรกของผมคำนวณเส้นทางที่จมอยู่ในต้นไม้ห้อยเอาไว้เรียบร้อย
เก็บใส่ตัวแปร best แล้วไม่ได้เอาไปใช้เลยสักบรรทัด สิ่งที่เหลือไว้คือ (void)best;
โปรแกรมคิดแต่เส้นทางที่วิ่งข้ามวง ไม่มี error ไม่มี warning
และตัวอย่างในโจทย์ตอบถูก คือ 24 ตรงเป๊ะ เพราะตัวอย่างของโจทย์บังเอิญไม่ได้กดดันกรณีนั้น
ตัวสุ่มเทียบจับได้ที่ n = 6 โปรแกรมตอบ 18 ของจริงคือ 21 เพราะเส้นทางที่ยาวที่สุดของเคสนั้นคือ 5-4-6-3-1
ซึ่งอยู่ในต้นไม้ห้อยล้วน ๆ ไม่แตะวงเลย ทางแก้คือแจกหมายเลขกลุ่มลงต้นไม้ที่ห้อยอยู่ให้ครบก่อน
แล้วค่อยเอาเส้นผ่านศูนย์กลางของทุกปมมาสู้กับคำตอบของกลุ่ม
รวมทั้งหมดผมสุ่ม 1,500 ชุดเทียบเฉลยเต็มกับตัวไล่ทุกทาง และอีก 800 ชุดเทียบเฉลยเต็มกับเฉลยชุดทดสอบย่อย
บทเรียนที่ยกไปข้ออื่นได้คือ โค้ดที่คำนวณของถูกแล้วลืมเอาไปใช้ เป็นบักที่เงียบที่สุดแบบหนึ่ง คอมไพเลอร์ไม่ช่วย ตัวอย่างในโจทย์ไม่ช่วย มีแต่การสุ่มเทียบเท่านั้นที่จับได้
กราฟที่ทุกปมมีลูกศรออกเส้นเดียวมีรูปร่างตายตัวมาก เดินตามลูกศรไปเรื่อย ๆ จากปมไหนก็ตาม จำนวนปมมีจำกัด เราจึงต้องวนกลับมาที่เดิมสักวัน แปลว่าทุกกลุ่มมีวงพอดีหนึ่งวง และสิ่งที่ไม่ได้อยู่บนวง ก็คือต้นไม้ที่ห้อยลงมาจากปมบนวง รูปนี้บางคนเรียกว่ารูปตัวโรว์ (ρ) ตามหน้าตาของมัน
เมื่อรูปเป็นแบบนี้ เส้นทางที่ยาวที่สุดในกลุ่มหนึ่งมีได้แค่สองทรง
แทนที่จะค้นหาวงก่อน ให้ปอกใบทิ้งก่อน นับว่าแต่ละปมมีลูกศรชี้เข้ากี่เส้น ปมที่ไม่มีใครชี้เข้าเลยคือใบ ตัดทิ้งแล้วลดตัวนับของปมที่มันชี้ไป ทำซ้ำจนไม่มีใบเหลือ สิ่งที่เหลือคือปมบนวงเป๊ะ ๆ และระหว่างปอกเราก็เก็บสองกิ่งที่ลึกที่สุดของทุกปมไปด้วยเลย ท่านี้ไม่ใช้การเรียกซ้ำ จึงไม่ล้นสแต็กแม้ต้นไม้จะลึกเป็นล้านชั้น
ให้ d[i] คือความลึกของต้นไม้ที่ห้อยอยู่ที่ปมบนวงตัวที่ i และ P[i]
คือระยะสะสมรอบวง ค่าของเส้นทางที่ลงต้นไม้ที่ i ไต่ไปถึง j แล้วลงต้นไม้ที่ j คือ
d[i] + d[j] + (P[j] − P[i])
จับกลุ่มใหม่เป็น (d[j] + P[j]) + (d[i] − P[i]) จะเห็นว่าก้อนหลังไม่ขึ้นกับ j เลย
ดังนั้นเดิน j ไปข้างหน้า แล้วเก็บค่ามากสุดของก้อนหลังในหน้าต่างที่ยาวไม่เกิน c − 1
ก็จบในเวลาเชิงเส้น ส่วนการคลี่วงออกเป็นสองรอบ เป็นวิธีจัดการกับการไต่ที่วนข้ามจุดเริ่ม
// ค่าของเส้นทางที่ลงต้นไม้ที่ 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);
} ระวัง
หน้าต่างต้องยาวไม่เกิน c − 1 ไม่ใช่ c เพราะเส้นทางที่ไม่ซ้ำเกาะไต่วงได้มากที่สุด
c − 1 เส้น ถ้าปล่อยเป็น c โปรแกรมจะยอมให้ไต่ครบรอบแล้วกลับมาที่เดิม
ซึ่งผิดกติกาแต่ไม่มีอะไรฟ้อง มันแค่ตอบเกินจริงเงียบ ๆ
กลุ่มที่ 1 กดถัดไปเพื่อไล่เกาะบนวงทีละเกาะ
| ลำดับบนวง | เกาะ | ต้นไม้ที่ห้อยลึก d | สะพานไปตัวถัดไป | ระยะสะสม P |
|---|---|---|---|---|
| 0 | 1 | 9 | 8 | 0 |
| 1 | 3 | 4 | 2 | 8 |
| 2 | 4 | 0 | 4 | 10 |
กลุ่มที่ 2 กดถัดไปเพื่อไล่เกาะบนวงทีละเกาะ
| ลำดับบนวง | เกาะ | ต้นไม้ที่ห้อยลึก d | สะพานไปตัวถัดไป | ระยะสะสม P |
|---|---|---|---|---|
| 0 | 2 | 0 | 2 | 0 |
| 1 | 7 | 0 | 3 | 2 |
คู่ที่ดีที่สุดของแต่ละกลุ่มคือลงต้นไม้ที่เกาะ 1 ไป 3 และ 7 ไป 2 ส่วนเส้นทางที่จมอยู่ในต้นไม้ล้วน ๆ ยาวที่สุด 9 และ 0 คำตอบของกลุ่มคือค่าที่มากกว่าในสองอันนั้น
รวมทุกกลุ่มได้ 21 + 3 = 24 ตรงกับคำตอบในโจทย์
เรื่องจริงที่เกิดขึ้นตามลำดับ
มุมนี้มีบักของมันเองที่ผมเจอตอนเขียน ตอนตัดเส้นบนวงเพื่อให้กลุ่มกลายเป็นต้นไม้ ผมเขียนโค้ดที่ตัด "เส้นระหว่างเกาะคู่นี้" แทนที่จะตัด "สะพานหมายเลขนี้" ซึ่งฟังดูเหมือนกันจนกว่าจะเจอเกาะสองเกาะที่มีสะพานถึงกันสองเส้น
และเคสแบบนั้นไม่ได้หายาก มันอยู่ในตัวอย่างของโจทย์เอง คือเกาะ 2 กับเกาะ 7 โค้ดจึงตัดทิ้งทีเดียวสองเส้น กลุ่มขาดออกจากกัน คำตอบกลายเป็น 21 แทนที่จะเป็น 24 อันนี้โชคดีที่ตัวอย่างในโจทย์จับได้เอง ต่างจากบักในเฉลยเต็มที่ตัวอย่างในโจทย์ผ่านฉลุย
บทเรียนที่ยกไปข้ออื่นได้คือ เมื่อกราฟมีเส้นซ้ำได้ ให้เก็บและอ้างถึงเส้นด้วยหมายเลขเส้นเสมอ ไม่ใช่ด้วยคู่ปลายทั้งสอง
ถ้ายังไม่อยากคิดเรื่องหน้าต่างเลื่อน มีท่าที่ตรงไปตรงมากว่ามาก และได้ 40 คะแนน สังเกตว่าเส้นทางที่ไม่ซ้ำเกาะจะไต่วงได้ทางเดียว แปลว่ามันต้องไม่ใช้เส้นบนวงอย่างน้อยหนึ่งเส้น
งั้นก็ลองตัดเส้นบนวงออกทีละเส้น พอตัดแล้วกลุ่มนั้นกลายเป็นต้นไม้ทันที และเส้นทางที่ยาวที่สุดในต้นไม้คือเส้นผ่านศูนย์กลาง ซึ่งหาได้ด้วยการค้นสองรอบคือ ค้นจากปมไหนก็ได้เพื่อหาปมที่ไกลที่สุด แล้วค้นจากปมนั้นอีกรอบ
ต้นทุนคือความยาววงคูณขนาดกลุ่ม ซึ่งแย่ที่สุดคือ N² ตาย ๆ ที่ N เป็นล้าน
แต่ที่สี่พันมันสบายมาก และเหมือนเดิม มันเป็นตัวตรวจสอบให้เวอร์ชันเร็ว
ผมสุ่มสวนเล็ก ๆ 800 ชุดเทียบสองโปรแกรม ตรงกันหมด
บักที่เวอร์ชันนี้ทำให้ผมเจอตอนเขียนคือ ตอนแรกผมตัด "เส้นระหว่างเกาะคู่นี้" แทนที่จะตัด "สะพานหมายเลขนี้" พอเจอเกาะสองเกาะที่มีสะพานถึงกันสองเส้น มันเลยตัดทิ้งทีเดียวสองเส้น กลุ่มขาดออกจากกัน คำตอบจึงต่ำเกินจริง อาการนี้ไม่โผล่ในตัวอย่างเล็ก ๆ ที่คิดเองด้วยมือเลย
ที่ N เป็นล้าน ทุกอย่างต้องเป็นอาเรย์แบน ๆ และห้ามเรียกซ้ำ
ตัวปอกใบใช้คิวที่เก็บเป็นเวกเตอร์เดียวโดยไม่ป็อปทิ้ง ซึ่งได้ประโยชน์สองต่อ คือมันเป็นคิวไปในตัว
และลำดับในเวกเตอร์นั้นเองคือลำดับที่ลูกมาก่อนพ่อ ซึ่งพอไล่ย้อนกลับก็ได้ลำดับที่พ่อมาก่อนลูก
เอาไว้แจกหมายเลขกลุ่มลงต้นไม้ที่ห้อยอยู่ได้พอดี
เวลาที่วัดได้บนเครื่องผม สวน 1,000,000 เกาะ ทั้งแบบที่วงสั้นเยอะ ๆ และแบบที่เป็นวงเดียวยาวหนึ่งล้าน ใช้เวลา 606 มิลลิวินาที เท่ากันทั้งสองแบบ จากลิมิต 2.2 วินาที
ท่าปอกใบกับการแยกวงในกราฟแบบนี้ใช้ซ้ำได้กับอีกหลายข้อ อ่านเวอร์ชันเต็มได้ที่ กราฟที่ทุกปมมีทางออกทางเดียว ส่วนเรื่องเส้นผ่านศูนย์กลางของต้นไม้อยู่ใน ดีพีบนต้นไม้
ค่าที่เฉลยนี้หาในต้นไม้แต่ละต้น คือทางที่ยาวที่สุดระหว่างสองปมใด ๆ ในต้นไม้นั้น มีชื่อว่าเส้นผ่าศูนย์กลางของต้นไม้ (tree diameter) และมีท่ามาตรฐานสองท่า เฉลยข้างบนใช้ท่าแรก คือ DP เก็บความลึกที่ลึกที่สุดสองอันดับแรกของลูก แล้วต่อกันที่ปมนั้น ซึ่งจำเป็นที่นี่เพราะเราต้องใช้ค่าความลึกรายปมไปคิดเรื่องวงต่อ
แต่ถ้าโจทย์ถามแค่ตัวเลขเส้นผ่าศูนย์กลาง อย่างเดียว มีท่าที่สั้นกว่าครึ่ง
BFS สองรอบ รอบแรกเริ่มจากปมใดก็ได้ หาปมที่ไกลที่สุดเรียกว่า a
รอบสองเริ่มจาก a ระยะที่ไกลที่สุดที่ได้คือคำตอบ
ที่มันถูกเพราะข้ออ้างข้อเดียว ปมที่ไกลที่สุดจากปมใดก็ได้ ต้องเป็นปลายข้างหนึ่งของ เส้นผ่าศูนย์กลางเสมอ เหตุผลคือถ้ามันไม่ใช่ปลายของเส้นผ่าศูนย์กลาง เราจะสร้างทางที่ยาวกว่า เส้นผ่าศูนย์กลางได้จากมัน ซึ่งขัดกับคำว่ายาวที่สุด พอรู้ปลายข้างหนึ่งแล้ว รอบที่สองก็แค่วัดจากปลายนั้นออกไปให้ไกลสุด
ระวัง ข้ออ้างนี้ใช้ได้แค่บนต้นไม้
ท่า BFS สองรอบใช้กับกราฟทั่วไปไม่ได้ มันพังทันทีเมื่อกราฟมีวง ซึ่งเป็นเหตุผลว่าทำไมในโจทย์นี้เราใช้มันได้เฉพาะกับต้นไม้ที่ห้อยจากวง ไม่ใช่กับทั้งกลุ่ม ถ้าเผลอเอาไปใช้กับกลุ่มทั้งกลุ่มที่มีวงอยู่ คำตอบจะผิดแบบเงียบ ๆ และนี่คือเหตุผลที่เฉลยหลักต้องแยกเรื่องวงออกมาคิดเองตั้งแต่ต้น
// เส้นผ่าศูนย์กลางของต้นไม้ ด้วย 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) กราฟที่ทุกปมมีลูกศรออก เส้นเดียวพอดี ข้อจำกัดข้อเดียวนั้นบังคับรูปร่างไว้หมด คือทุกกลุ่มต้องเป็นวงหนึ่งวงที่มีต้นไม้ห้อยเข้าหาวง ซึ่งเป็นเหตุผลที่เราแยกกรณีได้แค่สองแบบตลอดทั้งเฉลย
ส่วนตัวเลขที่เราหาในต้นไม้แต่ละต้น คือทางที่ยาวที่สุดระหว่างสองปมใด ๆ ในต้นไม้นั้น มีชื่อว่า เส้นผ่าศูนย์กลางของต้นไม้ (tree diameter) รู้ชื่อนี้ไว้คุ้มมาก เพราะมันมีท่ามาตรฐานสองท่า ท่าที่เฉลยนี้ใช้คือ DP เก็บความลึกที่ลึกที่สุดสองอันดับแรกของลูก แล้วเอามาต่อกันที่ปมนั้น อีกท่าที่สั้นกว่าคือไล่กว้างสองรอบ รอบแรกจากปมใดก็ได้เพื่อหาปมที่ไกลสุด รอบสองเริ่มจากปมนั้น ระยะที่ได้คือคำตอบ ทั้งสองท่าใช้เวลาเชิงเส้นเท่ากัน
มีบทปูพื้นเรื่องกราฟฟังก์ชันและบทเรื่อง DP บนต้นไม้ อยู่ในคลังนี้แล้ว และคำถามที่ยกไปใช้ได้คือ ลูกศรของแต่ละก้าวถูกกำหนดตายตัวหรือไม่ ถ้าตายตัว รูปของกราฟก็ถูกล็อกทันที และเรานับได้เลยโดยไม่ต้องค้นหา
เมื่อทุกปมมีทางออกทางเดียว กลุ่มหนึ่งกลุ่มคือวงหนึ่งวงที่มีต้นไม้ห้อย เส้นทางที่ยาวที่สุดจึงมีแค่สองทรง และทรงที่ข้ามวงยุบเป็นการหาค่ามากสุดในหน้าต่างเลื่อน หลังจากคลี่วงออกเป็นสองรอบแล้ว
ในหน้านี้