programming.in.th · ข้อ 2012

เครื่องเคลื่อนย้ายมวลสาร: ท่าโลภที่ตอบตัวอย่างถูกทั้งสองชุด แล้วยังผิดอยู่ดี

ข้อที่ควรอ่านถ้าเคยส่งแล้วตกทั้งที่ตัวอย่างผ่านหมด เพราะสูตรโลภที่ฟังดูสมเหตุสมผลที่สุดของข้อนี้ผิดตรงกรณีสุดท้ายพอดี และสิ่งเดียวที่จับได้คือตัวไล่วางทุกความเป็นไปได้แล้วจำลองจริง

★★★★☆ functional graphgreedyparity อ่าน 12 นาที 7 กันยายน 2026

โจทย์ · เดินไปทางตะวันออกเสมอ แต่โดนโยนกลับได้

เราเดินอยู่บนเส้นตรงจากตะวันตกไปตะวันออก เริ่มที่ตำแหน่ง 0 และต้องไปให้ถึงปลายทางที่ตำแหน่ง 2,000,001 กฎเดียวคือเดินไปทางตะวันออกเสมอ

บนเส้นนี้มีเครื่องเคลื่อนย้ายมวลสาร (teleporter) อยู่ N เครื่อง เครื่องหนึ่งมีจุดปลายสองจุด พอเราเดินไปเหยียบจุดปลายข้างหนึ่ง เราจะถูกโยนไปโผล่ที่จุดปลายอีกข้างทันที แล้วเดินต่อไปทางตะวันออกจากจุดที่โผล่ เราหลบจุดปลายที่ขวางอยู่ไม่ได้ และทุกครั้งที่ถูกโยนได้หนึ่งคะแนน

แกะคำศัพท์

teleporter อ่านว่า "เทเลพอร์เตอร์" แปลว่าเครื่องย้ายตัวข้ามที่ ประกอบจาก tele- ที่แปลว่าไกล (ตัวเดียวกับใน telephone และ television) กับ portare ในภาษาละตินที่แปลว่าขนหรือแบก

ก่อนออกเดินทาง เราวางเครื่องเพิ่มได้อีกไม่เกิน M เครื่อง จะวางจุดปลายไว้ที่ไหนก็ได้ (ตำแหน่งไม่ต้องเป็นจำนวนเต็มก็ได้) ขอแค่ไม่ซ้ำกับจุดปลายที่มีอยู่แล้ว งานของเราคือทำคะแนนให้ได้มากที่สุด

อินพุต / ขอบเขต / เอาต์พุต

EXAMPLE
InputOutput
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 ก้าวโดยไม่เดินจริง

เส้นทางหลัก เดินได้จริง I0 I1 I2 I3 I4 I5 I6 วงที่ลอยอยู่ ไปไม่ถึง
ตัวอย่างที่ 1 วาดจากลูกศรจริง จุดปลาย 6 จุดตัดเส้นออกเป็น 7 ช่วง ลูกศรเขียวด้านบนคือเส้นทางหลักที่เดินได้จริงจาก I0 ถึง I6 ผ่าน 3 ช่วง จึงได้ 2 คะแนน ส่วนลูกศรทองด้านล่างคือวงที่ลอยอยู่ 3 วง รวม 4 ช่วง ที่ไม่มีลูกศรจากเส้นทางหลักชี้เข้าไปเลย (วาดประกอบโดยผู้เขียน)
ทั้งสองตัวอย่าง · แยกช่วงออกเป็นเส้นทางกับวง
ตัวอย่างช่วงบนเส้นทางหลักคะแนนตั้งต้นขนาดของวงที่เหลือ
ที่ 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 ต่อหนึ่งเครื่อง จึงต้องดูดวงให้หมดก่อนเสมอ และดูดวงใหญ่ก่อนวงเล็ก

ตัวอย่างที่ 2 · เครื่องสามตัวถูกใช้ต่างกัน

กดถัดไปเพื่อใช้เครื่องทีละตัว

เริ่มที่ 5 คะแนน แล้วใช้เครื่องทีละตัว
เครื่องที่เอาไปทำอะไรได้เพิ่มรวม
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 ชุด มันชี้ทันทีว่าเคสที่มีวงเดียวแล้วเหลือเครื่องอีกตัว ผมตอบเกินจริงไปหนึ่ง

ดูโค้ดตัวตรวจสอบ (ไล่วางทุกแบบแล้วจำลอง)
brute_tele.cpp
#include <bits/stdc++.h>
using namespace std;
int LIM;
int simulate(vector<pair<int,int>>& ps){
    static int par[400]; for(int i=0;i<=LIM;i++) par[i]=-1;
    for(auto&[a,b]:ps){ par[a]=b; par[b]=a; }
    int pos=0,sc=0;
    while(true){
        int nx=-1; for(int p=pos+1;p<=LIM;p++) if(par[p]>=0){ nx=p; break; }
        if(nx<0) return sc;
        pos=par[nx]; sc++; if(sc>3000) return -1;
    }
}
int main(int argc,char**argv){
    int n,m; scanf("%d %d %d",&n,&m,&LIM);
    vector<pair<int,int>> base(n);
    set<int> used;
    for(auto&p:base){ scanf("%d %d",&p.first,&p.second); used.insert(p.first); used.insert(p.second); }
    vector<int> freeP; for(int i=1;i<=LIM;i++) if(!used.count(i)) freeP.push_back(i);
    int F=(int)freeP.size(), need=2*m, best=0;
    vector<int> idx(need);
    function<void(int,int)> choose=[&](int start,int k){
        if(k==need){
            vector<int> sel; for(int i=0;i<need;i++) sel.push_back(freeP[idx[i]]);
            sort(sel.begin(),sel.end());
            do{
                vector<pair<int,int>> ps=base;
                for(int i=0;i<m;i++){ int a=sel[2*i],b=sel[2*i+1]; if(a>b) swap(a,b); ps.push_back({a,b}); }
                best=max(best,simulate(ps));
            } while(next_permutation(sel.begin(),sel.end()));
            return;
        }
        for(int i=start;i<F;i++){ idx[k]=i; choose(i+1,k+1); }
    };
    if(m==0) best=simulate(base); else choose(0,0);
    printf("%d\n",best);
}

ข้อคิดที่อยากฝากไว้คือ การตอบตัวอย่างในโจทย์ถูกไม่ใช่หลักฐานอะไรเลย โดยเฉพาะกับข้อที่คำตอบเป็นสูตรโลภสั้น ๆ เพราะตัวอย่างในโจทย์มักถูกออกแบบมาให้เห็นภาพ ไม่ได้ออกแบบมาให้จับบัก

โค้ด C++

อินพุตมีเลขได้ถึงสี่ล้านตัว การอ่านด้วย scanf ทีละตัวช้าเกินลิมิตหนึ่งวินาที โค้ดนี้จึงอ่านทั้งไฟล์เข้ามาก้อนเดียวแล้วแยกเลขเอง เวลาที่วัดได้ลดจาก 744 มิลลิวินาทีเหลือ 258 มิลลิวินาที

ส่วนการแปลงตำแหน่งเป็นหมายเลขจุดปลาย ใช้อาเรย์ขนาดเท่าตำแหน่งที่ไกลที่สุด เพราะพิกัดไม่เกินสองล้าน ตำแหน่งจุดจบจริง ๆ ที่โจทย์บอกว่าเป็น 2,000,001 ไม่มีผลกับคำตอบเลย มีผลแค่ว่าช่วงสุดท้ายชื่ออะไร

ดูโค้ดเต็ม
tele.cpp
#include <bits/stdc++.h>
using namespace std;
// อ่านเลขเองทีละไบต์ อินพุตมีสองล้านตัวเลข scanf ช้าเกินไปสำหรับลิมิตหนึ่งวินาที
static char ibuf[1<<25]; static size_t ipos=0, ilen=0;
static inline int rd(){
    while(ipos<ilen && (ibuf[ipos]<'0'||ibuf[ipos]>'9')) ipos++;
    int v=0; while(ipos<ilen && ibuf[ipos]>='0' && ibuf[ipos]<='9') v=v*10+(ibuf[ipos++]-'0');
    return v;
}
int main(){
    ilen=fread(ibuf,1,sizeof(ibuf),stdin);
    int n=rd(), m=rd();
    vector<int> W(n),E(n); int hi=0;
    for(int i=0;i<n;i++){ W[i]=rd(); E[i]=rd(); hi=max(hi,E[i]); }
    const int LIM=hi+2;                            // ใช้แค่ถึงปลายที่ไกลสุด ตำแหน่งจุดจบไม่มีผลกับคำตอบ
    vector<int> partnerAt(LIM,-1);                 // partnerAt[p] = ตำแหน่งปลายอีกข้าง
    for(int i=0;i<n;i++){ partnerAt[W[i]]=E[i]; partnerAt[E[i]]=W[i]; }
    // เรียงตำแหน่งปลายทั้งหมด แล้วให้เลข 1..2n  ช่วง I_j อยู่ทางตะวันออกของปลายที่ j (I_0 คือจากจุดเริ่ม)
    vector<int> pts; pts.reserve(2*n);
    for(int i=0;i<n;i++){ pts.push_back(W[i]); pts.push_back(E[i]); }
    sort(pts.begin(),pts.end());
    int P=(int)pts.size();
    vector<int> id(LIM,0);
    for(int i=0;i<P;i++) id[pts[i]]=i+1;           // ปลายตัวที่ i+1
    // nxt[j] = ช่วงถัดไปหลังจากเดินจบช่วง j   (j = 0..P-1)
    vector<int> nxt(P+1,-1);
    for(int j=0;j<P;j++) nxt[j]=id[ partnerAt[ pts[j] ] ];
    // เดินจาก I_0 ไปจนถึง I_P นับจำนวนช่วง
    vector<char> vis(P+1,0);
    long long base=0; int cur=0;
    while(cur!=P){ vis[cur]=1; cur=nxt[cur]; base++; }
    vis[P]=1;
    // วงที่เหลือ
    vector<long long> cyc;
    for(int j=0;j<P;j++) if(!vis[j]){ long long len=0; int u=j; while(!vis[u]){ vis[u]=1; u=nxt[u]; len++; } cyc.push_back(len); }
    sort(cyc.rbegin(),cyc.rend());
    long long ans=base;                            // base = จำนวนครั้งที่ถูกย้ายก่อนเพิ่มเครื่อง
    int used=0;
    for(size_t i=0;i<cyc.size() && used<m; i++,used++) ans+=cyc[i]+2;   // ดูดวงเข้ามาต่อในเส้นทาง
    long long r=m-used;                            // เครื่องที่เหลือเมื่อไม่มีวงให้ดูดแล้ว
    ans += (r/2)*4 + (r%2)*1;                      // ต้องเสียหนึ่งเครื่องแตกวงก่อน อีกเครื่องค่อยดูดกลับ
    printf("%lld\n",ans);
}
ดูโค้ดที่ผมเขียนผิดรอบแรก และเคสที่เล็กที่สุดที่มันแตกต่าง

ทั้งไฟล์เหมือนโค้ดข้างบนทุกบรรทัด ต่างกันแค่บรรทัดสุดท้ายที่คิดแต้มของเครื่องที่เหลือ

tele_wrong.cpp · เฉพาะท่อนที่คิดแต้มเครื่องที่เหลือ
long long r = m - used;                  // เครื่องที่เหลือ ตอนไม่มีวงให้ดูดแล้ว
ans += r * 2;                            // <-- บรรทัดที่ผิด เดาว่าเครื่องละ 2 ด้วยความสมมาตร
                                         // ของจริงคือ (r / 2) * 4 + (r % 2) * 1

เคสที่เล็กที่สุดที่สองเวอร์ชันตอบไม่ตรงกันคือเครื่องตั้งต้นเครื่องเดียว แล้วให้เพิ่มได้อีกสองเครื่อง

INPUT
1
2
1 2

เครื่องตั้งต้นตัวเดียวตัดเส้นออกเป็นสามช่วง เดินได้ 1 คะแนน และเหลือวงขนาด 1 อยู่หนึ่งวง เครื่องที่เพิ่มตัวแรกดูดวงนั้นเข้ามาได้ 1 + 2 = 3 รวมเป็น 4 จากนั้นไม่มีวงเหลือแล้ว เครื่องตัวที่สองจึงได้แค่ 1 คะแนน คำตอบจริงคือ 5 แต่โค้ดที่คิดว่าเครื่องละ 2 ตอบ 6

ตัวเลข 5 นี้ยืนยันด้วยตัวไล่วางทุกแบบข้างบนด้วย ไม่ได้มาจากสูตรเดียวกับที่กำลังตรวจ ส่วนตัวอย่างในโจทย์ทั้งสองชุดใช้เครื่องหมดไปกับการดูดวงพอดี เครื่องที่เหลือจึงเป็นศูนย์ทั้งคู่ ทั้งสองเวอร์ชันเลยตอบ 6 กับ 12 เท่ากัน และผมไม่มีทางรู้จากตรงนั้นเลยว่าพลาด

โครงสร้าง "แต่ละปมชี้ไปปมเดียว" ที่ข้อนี้ใช้มีชื่อเรียกและมีท่าประจำของมัน อ่านได้ที่บทปูพื้นฐาน กราฟที่ทุกปมมีทางออกทางเดียว

สรุปบรรทัดเดียว

เมื่อการเดินถูกบังคับให้ไปทางเดียว ช่วงระหว่างจุดปลายจะกลายเป็นการสลับที่ คำตอบตั้งต้นคือความยาวของวงที่มีจุดเริ่ม และการเพิ่มเครื่องคือการดูดวงอื่นเข้ามาต่อ ยกเว้นตอนไม่มีวงเหลือ ที่เราต้องยอมจ่ายหนึ่งเครื่องแตกวงขึ้นมาเอง

แหล่งที่มา

  1. โจทย์ Teleporters บน programming.in.th ข้อ 2012 programming.in.th/tasks/2012 (สืบค้น 6 กันยายน 2026)
  2. ต้นทางของโจทย์คือ 20th International Olympiad in Informatics ที่กรุงไคโร ประเทศอียิปต์ วันแข่งที่ 2