เฉลยโจทย์แข่ง

ICPC 2026 ไทย รอบย่อย: สรุปโจทย์ + เฉลยแบบเข้าใจง่าย

รวมโจทย์ 7 ข้อ (เพียงบางส่วน) จากรอบคัดเลือกย่อย ICPC 2026 Thailand ชุดวันอาทิตย์ที่ 19 ก.ค. ที่ผมนั่งไล่แกะทีละข้อ ตั้งใจเล่าแบบ "เพื่อนเล่าให้ฟัง" ไม่ต้องอ่านโจทย์ต้นฉบับก็ตามได้ แต่ละข้อจะให้ คำใบ้และไอเดียก่อน แล้วเก็บ เฉลยโค้ดไว้ในกล่องพับ (กดเองเมื่อพร้อม) จะได้ลองคิดก่อนแอบดู

เกี่ยวกับงานนี้

งานนี้คือ ICPC 2026 Thailand รอบย่อย (sub-regional) สนามภาคเหนือ จัดที่ภาควิชาวิทยาการคอมพิวเตอร์ มหาวิทยาลัยเชียงใหม่ โจทย์เป็นชุดกลางที่ใช้แข่งพร้อมกันหลายสนาม (มช. เป็นเจ้าภาพสนามภาคเหนือ ไม่ใช่ผู้ออกโจทย์) เส้นทางแยกเป็น 2 สาย สายในประเทศ คือรอบย่อยแล้วไปต่อ รอบระดับประเทศ (National Contest) คือ ICPC Thailand National Contest 2026 ที่ปีนี้เจ้าภาพคือภาควิชาวิทยาการคอมพิวเตอร์ คณะวิทยาศาสตร์ มหาวิทยาลัยเกษตรศาสตร์ (มก.) จัด 5-6 ก.ย. 2026 ที่กรุงเทพฯ ส่วน สายไป World Finals เป็นคนละสาย ต้องไปผ่าน รอบภูมิภาค (Regional) ที่ทีมไปสมัครแข่งกับสนามต่างประเทศที่จัดประจำเอง แล้วจึงไปรอบชิงแชมป์เอเชียแปซิฟิก (เข้าใจว่าไทยขาดการจัด regional ในบ้านมาหลายปี กำลังค่อย ๆ ฟื้น) การแข่งรอบย่อยกระจายเป็น 3 วัน (เสาร์-อาทิตย์ที่ผ่านมา และอาทิตย์หน้า) โจทย์แต่ละวันคนละชุด บทความนี้หยิบมาเฉพาะ 7 ข้อจากชุดวันอาทิตย์ที่ 19 กรกฎาคม จึงเป็นเพียงบางส่วน แล้วเขียนเป็น editorial ฉบับเล่าง่ายหลังงาน ไล่แกะทีละข้อให้คนทั่วไปอ่านตามได้โดยไม่ต้องมีพื้นการแข่งมาก่อน

งานนี้อยู่ตรงไหนของเส้นทาง ICPC (แยกเป็น 2 สาย) สายในประเทศ (ที่เราอยู่) สายไป World Finals (คนละสาย) รอบย่อย (Sub-regional) สนามภาคเหนือ = ม.ช. · 19 ก.ค. อยู่ตรงนี้ รอบระดับประเทศ (National) ม.เกษตรศาสตร์ (มก.) · 5-6 ก.ย. (สุดสายในประเทศตอนนี้) รอบภูมิภาค (Regional) สนามต่างประเทศที่จัดประจำ · ทีมไปสมัครเอง ICPC Asia Pacific Championship รอบชิงแชมป์เอเชียแปซิฟิก (super-regional) ICPC World Finals
สองสายแยกกัน: (ซ้าย) สายในประเทศ รอบย่อย → รอบระดับประเทศ ที่เราแข่งอยู่ตอนนี้ · (ขวา) สายสู่ World Finals ที่ต้องผ่านรอบภูมิภาค (regional) โดยทีมไปสมัครสนามต่างประเทศที่จัดประจำเอง → รอบชิงแชมป์เอเชียแปซิฟิก → World Finals (diagram วาดประกอบโดยผู้เขียน)

หมายเหตุ: เส้นทางแยกเป็น 2 สาย สายในประเทศคือ "รอบย่อย" (sub-regional) กระจายตามภาค งานที่ ม.ช. คือสนามภาคเหนือ ผ่านแล้วไปต่อ "รอบระดับประเทศ" (National Contest) คือ ICPC Thailand National Contest 2026 ที่ ม.เกษตรศาสตร์ (มก.) จัด 5-6 ก.ย. ที่กรุงเทพฯ ส่วนการจะไป World Finals เป็นคนละสาย ต้องไปลุ้นผ่าน "รอบภูมิภาค" (regional) โดยทีมไปสมัครแข่งกับสนามต่างประเทศที่เขาจัดประจำเอง แล้วจึงไปรอบชิงแชมป์เอเชียแปซิฟิก เท่าที่ทราบไทยเว้นการจัด regional ในบ้านมาหลายปีและกำลังค่อย ๆ ฟื้นกลับมา ทีมที่อยากไปจึงต้องไปสมัครแข่งที่สนามต่างประเทศเอง


แกะคำศัพท์

ICPC อ่านว่า "ไอ-ซี-พี-ซี" ย่อจาก International Collegiate Programming Contest แปลว่า "การแข่งเขียนโปรแกรมระดับมหาวิทยาลัยนานาชาติ" เป็นรายการแข่งเขียนโปรแกรมแก้โจทย์อัลกอริทึมที่เก่าแก่และใหญ่ที่สุดในโลก ทีมละ 3 คน ใช้คอมเครื่องเดียว

Editorial อ่านว่า "เอดิทอเรียล" แปลตรงตัวว่า "บทบรรณาธิการ" แต่ในวงการแข่งโปรแกรมมันคือ "เฉลยอย่างเป็นทางการ" ที่อธิบายวิธีคิดของแต่ละข้อ บทความนี้ก็คือ editorial ฉบับเล่าง่ายของผมเองครับ


วิธีรันโค้ดเฉลยในบทความนี้ (อ่านก่อนถ้าเอาโค้ดไปลอง)

โค้ดเฉลยทุกข้ออ่านอินพุตผ่าน stdin ด้วย sys.stdin.read() คืออ่านอินพุตทั้งก้อนรวดเดียว เป็นวิธี มาตรฐานของการแข่งโปรแกรม เพราะเร็วและรับหลายบรรทัดได้สบาย โปรแกรมจะอ่านไปเรื่อย ๆ จนกว่าจะเจอสัญญาณ "จบอินพุต" (EOF ย่อจาก End Of File) แล้วจึงคำนวณและพิมพ์คำตอบ

เพราะแบบนี้ ถ้าเอาไปกดรันใน IDLE แล้วพิมพ์อินพุตเองมันจะค้าง ไม่ใช่โค้ดพัง แต่ IDLE ส่งสัญญาณ EOF ไม่ได้เหมือน terminal จริง โปรแกรมเลยรออินพุตที่ไม่มีวันจบ วิธีที่ถูกคือรันผ่าน terminal (เช่น PowerShell) แล้วป้อนอินพุตให้มัน

วิธีที่ง่ายสุด ป้อนทุกเลขในบรรทัดเดียวแล้ว pipe เข้าไป (เพราะ .split() ตัดทั้งเว้นวรรคและขึ้นบรรทัดใหม่เหมือนกัน จึงไม่ต้องจัดบรรทัด):

PowerShell · pipe อินพุตเข้าตรง ๆ
echo "7 1 2 1 1 3 2 2 4 1 2 5 1 4 6 2 5 7 7" | python solution.py

ถ้าอยากคงอินพุตเป็นหลายบรรทัดให้อ่านง่าย (เหมือนหน้าตาโจทย์จริง) ใช้ here-string ของ PowerShell (@" ... "@) แล้ว pipe เข้าไป:

PowerShell · here-string หลายบรรทัด
@"
7
1 2 1
1 3 2
2 4 1
2 5 1
4 6 2
5 7 7
"@ | python solution.py

หรือเก็บอินพุตไว้ในไฟล์ in.txt แล้ว redirect เข้า (เหมือนที่ระบบตัดสินออนไลน์ทำ):

PowerShell · ป้อนอินพุตจากไฟล์
python solution.py < in.txt

ทั้งสองแบบต้องมี Python ติดตั้งในเครื่องก่อน และเปลี่ยน solution.py เป็นชื่อไฟล์ที่บันทึกโค้ดไว้ ตัวอย่างข้างบนใช้ชุดทดสอบของข้อ G (คำตอบคือ 4)


A · Holy Circle Talisman: ไม้หมุนเป็นวง

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

โจทย์กำหนด · อินพุต / ขอบเขต / เอาต์พุต

คำใบ้

ปักเข็มแบ่งไม้เป็นสองแขนยาว a กับ b (โดย a + b = L) พอหมุนครบรอบ แขนแต่ละข้างกวาดเป็นวงกลม รวมกันได้ แผ่นวงกลมเต็ม รัศมี = แขนที่ยาวกว่า → อยากให้พื้นที่น้อยสุด ต้องทำให้แขนยาวสุด "สั้นที่สุด" จะปักตรงไหนดี?

ลองเอง: กดที่เข็มสีทองแล้วลากไปปักบนไม้ได้เลย (หรือกดปุ่มด้านล่าง) แล้วดูว่าวงที่หมุนออกมามีพื้นที่เท่าไหร่ ลองเทียบดูว่าปักตรงไหนได้วงเล็กสุด
แขนซ้าย a = 2 แขนขวา b = 8 พื้นที่ = 64π

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

เข็ม (กลาง) L/2 L/2 รัศมี = L/2 หมุนครบรอบ → แผ่นวงกลมเต็ม พื้นที่ = π·(L/2)2
ปักกลางไม้แล้วหมุน ได้วงกลมรัศมี L/2 เล็กที่สุด (diagram วาดประกอบโดยผู้เขียน)

ปักตรงกลางพอดี (a = b = L/2) พื้นที่จึงน้อยสุด = (L/2)2·π ตามรูปแบบเอาต์พุตข้างบน เราตอบแค่ k = (L/2)2 และมาถึงตรงนี้จะเห็นว่ากฎ "L เป็นเลขคู่" มีไว้ทำไม เพราะมันทำให้ L/2 ลงตัว k เลยออกมาเป็นจำนวนเต็มเป๊ะ

EXAMPLE
INPUT (L)OUTPUT (k)
44
1025
50062500

* ตัวอย่างสมมติเพื่อประกอบความเข้าใจ

เฉลยโค้ด + คำอธิบาย
python
import sys
L = int(sys.stdin.read())
# ปักเข็มกลางไม้ พื้นที่น้อยสุด = (L/2)^2 * pi
# โจทย์ให้ตอบเป็นหน่วยของ pi เลยตอบแค่ k = (L//2)**2
print((L // 2) ** 2)
  • อ่าน L เข้ามาแล้วปริ้นต์ (L//2)**2 ตรง ๆ
  • // คือหารปัดลง ส่วน ** 2 คือยกกำลังสอง เพราะ L เป็นเลขคู่ หารสองจึงลงตัวเสมอ
  • Big-O: O(1) คำนวณจากสูตรตรง ๆ ไม่มีลูปเลย

B · Painting: ปั๊มแสตมป์ทำภาพ

การบ้านวิชาศิลปะ: ทำภาพให้ได้มากที่สุด แต่ครูรับเฉพาะภาพที่มีของครบเป๊ะ ๆ ต่อ 1 ภาพ คือ วงกลม 3 รูป, สามเหลี่ยม 1 รูป, สี่เหลี่ยม 2 รูป, เส้น 4 เส้น (ขาดหรือเกินแม้แต่ชิ้นเดียวไม่รับ) เราขี้เกียจเลยใช้แสตมป์ปั๊ม แต่แสตมป์แต่ละแบบใช้ได้จำกัด: วงกลม A ครั้ง, สามเหลี่ยม B ครั้ง, สี่เหลี่ยม C ครั้ง, เส้น D ครั้ง แล้วถามว่าทำภาพครบชุดได้สูงสุดกี่ภาพ

โจทย์กำหนด · อินพุต / ขอบเขต / เอาต์พุต

คำใบ้

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

หลายคนงงว่าทำไมปั๊มวงกลมไป 1 อันแล้วยังไม่นับเป็น "ภาพ" คำตอบคือ 1 ภาพที่ครูรับต้องเป็นชุดครบพอดี คือ วงกลม 3 รูป + สามเหลี่ยม 1 รูป + สี่เหลี่ยม 2 รูป + เส้น 4 เส้น วงกลมเดี่ยว ๆ จึงเป็นแค่ชิ้นส่วน ยังไม่ใช่ภาพ และถ้าปั๊มเกินโควตาของภาพก็ไม่รับเช่นกัน (ต้องเป๊ะทั้งไม่ขาดไม่เกิน) ลองปั๊มเองด้านล่างดู

ลองเอง: กดปั๊มแสตมป์ให้ได้ "1 ภาพที่ครูรับ" · ครูรับเฉพาะภาพที่ของครบเป๊ะ ขาดก็ไม่รับ เกินก็ไม่รับ

ในเมื่อ 1 ภาพกินวงกลม 3 รูป, สามเหลี่ยม 1 รูป, สี่เหลี่ยม 2 รูป, เส้น 4 เส้น จำนวนภาพที่แต่ละแสตมป์ "จ่ายไหว" คือ A÷3, B÷1, C÷2, D÷4 (ปัดลง) ตัวที่น้อยที่สุดคือคอขวด:

สูตร

คำตอบ = min(A÷3, B÷1, C÷2, D÷4) (หารปัดลงทั้งหมด)

EXAMPLE
INPUT (A B C D)OUTPUT
3 1 2 41
10 20 30 403

ตัวอย่างจากโจทย์ · แถวสอง: min(10÷3, 20÷1, 30÷2, 40÷4) = min(3, 20, 15, 10) = 3 (วงกลมคือคอขวด)

เฉลยโค้ด + คำอธิบาย
python
import sys
a, b, c, d = map(int, sys.stdin.read().split())
# หารปัดลง (// ใน Python) แล้วเอาตัวที่น้อยสุด = คอขวด
print(min(a // 3, b // 1, c // 2, d // 4))
  • // คือ "หารปัดลง" (integer division) เช่น 10 // 3 = 3
  • ค่าถึง 1018 แต่ Python ใช้ int ใหญ่ไม่จำกัดอยู่แล้ว ไม่ต้องกลัวล้น
  • Big-O: O(1) แค่หา min ของ 4 ค่า

C · Addition: เพิ่มกำลังทีละเมือง

มีค่าพลังงาน Fₙ สำหรับ N เมือง โดย F₀ = 1, F₁ = 3 และ Fₓ = 3·F₍ₓ₋₁₎ − 2·F₍ₓ₋₂₎ เครื่องเพิ่มกำลังได้ทีละเมือง ค่าใช้จ่ายก้าว x→x+1 คือ F₍ₓ₊₁₎ − Fₓ และมีเครื่องปั่นไฟผลิตได้ K หน่วยต่อก้าว (ต้องเหลือสำรอง ≥ 1) แล้วถามว่าไปได้สูงสุดกี่เมือง

โจทย์กำหนด · อินพุต / เอาต์พุต

คำใบ้

สูตร recurrence ที่ดูยุ่งนี้ ลองแทนค่า F₀, F₁, F₂, F₃… ดูสัก 4–5 ตัว แล้วจะสังเกตเห็นแพตเทิร์น มันคือลำดับที่ "โตเท่าตัว" ซ่อนอยู่

แก้ recurrence ได้เป็น Fx = 2(x+1) − 1 ดังนั้นค่าใช้จ่ายก้าว x→x+1 = 2(x+1) (คือ 2, 4, 8, 16, …)

ไล่ทีละก้าว ค่าใช้จ่ายเป็นเลขยกกำลังสองที่โตเท่าตัวทุกครั้ง:

  • 0→1 เมือง ใช้ 2
  • 1→2 เมือง ใช้ 4
  • 2→3 เมือง ใช้ 8 …

เพราะต้องเหลือสำรอง 1 หน่วย พลังงานที่ใช้จ่ายจริงต่อก้าวจึงเหลือแค่ K−1 และแต่ละก้าวต้องจ่ายไหวทีละก้าว ไม่ใช่รวมกัน เช่น K = 6 ใช้จ่ายจริงได้ก้าวละไม่เกิน 5 ก้าว 0→1 (ใช้ 2) กับ 1→2 (ใช้ 4) ผ่าน แต่ 2→3 ใช้ 8 เกิน 5 เลยหยุด ได้ 2 เมือง ส่วน K = 2 ใช้จ่ายจริงได้แค่ 1 ก้าวแรกก็ใช้ 2 เกินแล้ว จึงได้ 0 เมือง

EXAMPLE
INPUT (K)OUTPUT
20
62
1006

* K=2 ให้ผลเป็น 0 ตามที่โจทย์ระบุ ส่วน K=100 ใช้จ่ายจริงได้ 99 ก้าวแพงสุดคือ 26=64 ยังไหว แต่ 27=128 เกิน จึงได้ 6 เมือง

จุดต้องระวัง

คำว่า "ต้องเหลือสำรอง ≥ 1" ทำให้พลังงานที่ใช้ได้จริงต่อก้าวคือ K−1 ไม่ใช่ K เต็ม ถ้าลืมจุดนี้เคสขอบอย่าง K=2 จะพลาด (ที่ถูกคือ 0 ไม่ใช่ 1)

เฉลยโค้ด + คำอธิบาย
python
import sys
K = int(sys.stdin.read())
# ก้าว x->x+1 ใช้พลังงาน 2**(x+1) และต้องเหลือสำรอง 1 หน่วย
# ก้าวจะทำได้เมื่อ 2**(x+1) <= K-1 (เดินสะสมจนก้าวถัดไปจ่ายไม่ไหว)
towns = 0
while (1 << (towns + 1)) <= K - 1:
    towns += 1
print(towns)
  • 1 << (towns + 1) คือ 2**(towns+1) (เลื่อนบิต เร็วกว่ายกกำลัง)
  • เดินเพิ่มเมืองไปเรื่อย ๆ ตราบใดที่ก้าวถัดไปยังจ่ายไหวภายใต้ K−1
  • Big-O: O(log K) ลูปเพิ่มทีละเมือง แต่ 2towns โตเท่าตัวทุกรอบ จำนวนรอบจึงราว log₂K

D · Diamond Glyph: เพชรบนกำแพงหิน

กำแพงสลักด้วย < และ > เต็มไปหมด ตัวอักษรพวกนี้จริง ๆ คือ "ขีดเฉียง" ที่ประกอบกันเป็นเพชรซ้อน ๆ (glyph) โจทย์ถามว่ามี glyph ทั้งหมดกี่อัน โดย glyph tier k คือเพชร (ring) ขนาด 1, 2, …, k ซ้อนศูนย์กลางเดียวกันครบทุกวง

แกะคำศัพท์

glyph อ่านว่า "กลิฟ" แปลว่า "รูปอักขระหรือสัญลักษณ์ที่แกะสลัก" มาจากภาษากรีก glyphē ที่แปลว่า "การแกะสลัก" ในวงการฟอนต์ glyph หมายถึงรูปหน้าตาของตัวอักษรหนึ่งตัว โจทย์นี้ยืมคำมาเรียกลายเพชรซ้อนที่สลักบนกำแพง

โจทย์กำหนด · อินพุต / ขอบเขต / เอาต์พุต

คำใบ้ที่ยากที่สุดของข้อนี้

1 ช่องมีขีดเฉียง 2 ขีด (ครึ่งบน + ครึ่งล่าง) ไม่ใช่ขีดเดียว! ตัว > กับ < ต่างกันแค่ "วางขีด / กับ \ สลับครึ่งบน-ล่างกัน" (ในเฉลยมีภาพประกอบให้เห็นชัด)

1 ช่อง สูง 2 หน่วย · ครึ่งบน (yt คู่) + ครึ่งล่าง (yt คี่) \ / คือตัว > / \ คือตัว < ครึ่งบน ครึ่งล่าง
ตัว > = บน \ / ล่าง / ส่วน < = บน / / ล่าง \ วางขีดสลับครึ่งกัน (diagram วาดประกอบโดยผู้เขียน)

พลิกกลับกัน: ถ้าเจอขีดหนึ่งขีด อยากรู้ว่ามันมาจากตัวไหน ต้องดู 2 อย่าง คือ ชนิดขีด (/ หรือ \) และ อยู่ครึ่งไหน (บน/ล่าง) ตามตารางนี้ (ตรงนี้คือหัวใจของการตรวจแต่ละขีด ที่เดี๋ยวจะเขียนเป็นฟังก์ชันในเฉลยโค้ด):

ขีดที่เจอครึ่งบน (yt คู่)ครึ่งล่าง (yt คี่)
/<>
\><

แนวคิดที่ 1 (คิดตรง ๆ): ไล่ซ้อนจนวงขาด. เพชรเล็กสุด (ring ขนาด 1) เกิดจาก <> ต่อกันในแถวเดียว (หรือบล็อก 2×2 แนวตั้ง) วิธีที่นึกออกก่อนตอนแข่งคือไปยืนทุกจุดศูนย์กลาง แล้วลองซ้อน ring ขนาด 1, 2, 3, … ต่อกันไปเรื่อย ๆ จนวงแรกที่ขาด นับว่าซ้อนต่อเนื่องได้กี่ชั้น (เรียกว่า T) ถ้าได้ถึง T ก็จะมี glyph tier 1..T = T อัน ที่จุดนั้น ยังไม่ต้องรู้ว่า T ลึกได้แค่ไหน เพราะ N, M ≤ 70 ยังไงก็ทัน

สูตรรวม (แนวคิดที่ 1)

คำตอบ = ผลรวม T ของทุกจุดศูนย์กลาง โดย T = จำนวน ring ต่อเนื่องเริ่มจากขนาด 1 ที่มีครบ

การเช็ก ring ขนาด s ก็คือไล่ 4 ด้านของเพชร ด้านละ s ขีด แต่ละขีดแปลงเป็นช่องบนกริดแล้วเทียบตามตารางด้านบน ถ้าครบทุกขีดคือ ring มีจริง ภาพนี้คือ 4 ด้านที่โค้ดเดินเก็บทีละขีด:

ศูนย์กลาง (X, Y) A / B \ C / D \ 4 ด้าน ด้านละ s ขีด วน i = 0 … s−1 เก็บทีละขีด
ring คือเพชรสี่ด้าน โค้ดเดินเลาะขอบทีละด้านด้วยดัชนี i (diagram วาดประกอบโดยผู้เขียน)

ลองกับตัวอย่างจริงของโจทย์ (กริด 4×5) กดปุ่มด้านล่างดูทีละสเต็ป จะเห็นเลยว่าตัว </> แต่ละตัวแตกเป็นขีดเฉียงยังไง แล้วขีดพวกนั้นประกอบกันเป็นเพชร tier 1 ห้าอัน และซ้อนต่อเป็น tier 2 อีกหนึ่ง รวม 5 + 1 = 6:

<><<> <<>>> >><<< >><><

สเต็ป 1/4

สไลด์โชว์: ตัวอักษร → ขีดเฉียง → เพชร tier 1 (5 อัน เขียว) → เพชร tier 2 (1 อัน เหลืองประ) = 6 (diagram วาดประกอบโดยผู้เขียน)

ตรงนี้มีข้อสังเกตที่พลิกทั้งข้อ: ในตัวอย่างข้างบน ซ้อนได้แค่ tier 2 ไม่ใช่เรื่องบังเอิญ คือมันซ้อนเกิน 2 ไม่ได้เลยไม่ว่ากริดจะเป็นยังไง ลองถามว่า ถ้าอยากได้ glyph tier 3 (ต้องมี ring 1, 2, 3 ซ้อนศูนย์กลางเดียวกันครบ) จะเป็นไปได้ไหม?

ข้อสังเกต: ทำไม T ไม่มีทางเกิน 2

กุญแจอยู่ที่ 1 ช่องเป็นตัวอักษรได้ตัวเดียว และตัวนั้นบังคับ "ขีดครึ่งบน" กับ "ขีดครึ่งล่าง" ให้เป็น ขีดตรงข้ามกันเสมอ (> = บน \/ล่าง /, < = บน //ล่าง \) ไม่มีตัวไหนที่ทั้งสองครึ่งเป็นขีดชนิดเดียวกัน ทีนี้ถ้าลองวางกรอบ ring 2 กับ ring 3 ให้ศูนย์กลางเดียวกันแล้วไล่พิกัดจริง จะเจอ 4 ช่องที่ทั้งสองวงพาดผ่านพร้อมกัน โดย ring 2 ไปลงครึ่งหนึ่งของช่อง ส่วน ring 3 ไปลงอีกครึ่ง แล้วทั้งคู่ดันสั่ง "ขีดชนิดเดียวกัน" (เช่น \ ทั้งคู่) คนละครึ่ง ซึ่งเป็นตัวอักษรที่ไม่มีจริง ring 2 กับ ring 3 จึงอยู่ศูนย์กลางเดียวกันไม่ได้เด็ดขาด พอ ring 2 มี ring 3 ก็ถูกตัดทิ้งอัตโนมัติ

ring 2 + ring 3 สั่งช่องเดียวกัน แต่ตัวอักษรที่มีจริงมีแค่ 2 ตัว ring 3 ขอครึ่งบน \ ring 2 ขอครึ่งล่าง \ บน \ · ล่าง \ → ไม่มีตัวอักษรแบบนี้ ≠ บน \ · ล่าง / คือตัว > บน / · ล่าง \ คือตัว <
ตัวอักษรจริงมีครึ่งบน-ล่างเป็นขีดตรงข้ามเสมอ แต่ ring 2 กับ ring 3 ที่ศูนย์กลางเดียวกันสั่งให้ทั้งสองครึ่งเป็นขีดชนิดเดียวกัน จึงซ้อนพร้อมกันไม่ได้ tier เลยตันที่ 2 (diagram วาดประกอบโดยผู้เขียน)

โน้ต: "วาดเพชรเดี่ยว ๆ ขนาด 3" กับ "ซ้อนถึง tier 3" คนละเรื่อง

เพชรเดี่ยว ๆ ขนาด 3 วาดด้วย <> ได้อยู่ (กรอบ 12 ขีดปิดสนิทไม่ชนกัน) สิ่งที่เป็นไปไม่ได้คือการซ้อน ring 3 เข้ากับ ring 2 ที่จุดเดียวกัน แต่เพราะ glyph tier 3 บังคับว่าต้องมี ring 1, 2, 3 ครบ เพชรเดี่ยวขนาด 3 ที่ไม่มีวงเล็กซ้อนอยู่จึงนับเป็น glyph ไม่ได้ (T เริ่มนับจาก 1 พอขาดวงในก็หยุด) ผลคือ ทุกจุดให้ T ได้แค่ 0, 1, หรือ 2 เท่านั้น

แนวคิดที่ 2 (รู้ทันแล้วก็หยุดที่ 2). พอรู้ว่า T ≤ 2 เสมอ ก็ไม่ต้องไล่ซ้อนแบบไม่รู้จบอีก แค่เช็กทุกจุดว่ามี ring 1 ไหม (มี = +1) แล้วมี ring 2 ต่อไหม (มี = +1 อีก) จบ ไม่ต้องแตะ ring 3 ขึ้นไปเลย โค้ดสั้นลงและ Big-O แน่นอนขึ้น (เทียบกันในกล่องเฉลยด้านล่าง)

EXAMPLE
INPUTOUTPUT
4 5
<><<>
<<>>>
>><<<
>><><
6

* ตัวอย่างจากโจทย์ · มีเพชรเล็ก (ring ขนาด 1) 5 จุด และหนึ่งในนั้นซ้อนได้ถึง ring ขนาด 2 → 5 + 1 = 6

เฉลยโค้ด (2 โซลูชัน) + คำอธิบาย

โซลูชัน 1 (ไล่ซ้อนจนวงขาด, คิดตรง ๆ ตอนแข่ง) ทุกจุดศูนย์กลางลองซ้อน ring ขนาด 1, 2, 3, … จนวงแรกที่ขาด แล้วบวกจำนวนชั้นที่ซ้อนได้

python
import sys
data = sys.stdin.read().split()
N, M = int(data[0]), int(data[1])
grid = [data[2 + i] for i in range(N)]

# ขีด 1 ขีดตรงกับตัวอักษรบนกริดไหม (ใช้ตารางเทียบด้านบน)
def ok_seg(x0, yt, kind):
    if not (0 <= x0 < M and 0 <= yt < 2 * N):   # หลุดขอบ = ไม่มีขีด
        return False
    r = yt // 2                      # yt คือครึ่งแถว แถวจริง = yt // 2
    if yt % 2 == 0:                  # yt คู่ = ครึ่งบน
        ch = '>' if kind == '\\' else '<'
    else:                            # yt คี่ = ครึ่งล่าง
        ch = '>' if kind == '/' else '<'
    return grid[r][x0] == ch

# ring ขนาด s รอบศูนย์กลาง (X,Y) มีครบทุกขีดไหม (4 ด้าน ด้านละ s ขีด)
def ring_ok(X, Y, s):
    for i in range(s):
        segs = ((X-s+i, Y-i-1, '/'), (X+i, Y-s+i, '\\'),        # ด้าน A, B
                (X+s-i-1, Y+i, '/'), (X-i-1, Y+s-i-1, '\\'))    # ด้าน C, D
        for (x0, yt, kind) in segs:
            if not ok_seg(x0, yt, kind):
                return False
    return True

ans = 0
for X in range(M + 1):
    for Y in range(2 * N + 1):       # Y ยาว 2N เพราะ 1 ช่อง = 2 ครึ่ง
        s = 1
        while ring_ok(X, Y, s):      # ซ้อนไปเรื่อย ๆ จนวงขาด
            s += 1
        ans += s - 1                 # ซ้อนได้ (s-1) ชั้น = glyph (s-1) อัน
print(ans)
  • ทำไม Y ถึงวิ่งถึง 2N? เพราะ 1 ช่องมี 2 ขีด (บน/ล่าง) เลยให้ความสูงช่องละ 2 หน่วย โดย yt คู่ = ครึ่งบน, คี่ = ครึ่งล่าง (ดู diagram + ตารางด้านบน)
  • ตัวแปร segs คืออะไร? คือ 4 ขีดของ ring (ด้านละ 1 ขีดต่อรอบ i) ตำแหน่งมาจากการเดินเลาะ 4 ด้านตามภาพเพชรด้านบน
  • ans += s - 1: ถ้าที่จุดนี้ซ้อน ring ได้ถึงขนาด s−1 (ขนาด s วงแรกที่ขาด) ก็แปลว่ามี glyph tier 1 ถึง s−1 = รวม s−1 อัน
  • ทำไมต้องมีลูป i ในเมื่อ s ก็ลูปนอกอยู่แล้ว? เพราะ s กับ i วัดคนละอย่าง: s คือ ขนาดของวง (รัศมี) ส่วน i คือ เดินไล่ตรวจทีละขีดไปตามด้านของวงนั้น วงขนาด s มี 4 ด้าน ด้านละ s ขีด รวม 4·s ขีด ถ้าตัด i ออกจะตรวจแค่ขีดตรงมุม ไม่ตรวจกลางด้าน วงที่ "แหว่งกลางด้าน" จะหลุดผ่าน นับเพชรเกิน คำตอบพัง ลองกดสไลด์ดูว่าพอ s โตขึ้น i ต้องตรวจกี่ขีด:

s = 1

จุดทองคือ 4 มุมของวง จุดเขียวคือขีดที่ลูป i ไล่ตรวจ วงยิ่งใหญ่ (s มาก) จำนวนขีดที่ต้องตรวจยิ่งเพิ่มเป็น 4·s (diagram วาดประกอบโดยผู้เขียน)
  • Big-O (โซลูชัน 1): จุดศูนย์กลางมี O(N·M) จุด ถ้ายังไม่รู้ว่าซ้อนลึกได้แค่ไหน ต้องเผื่อกรณีแย่สุดว่าแต่ละจุดไล่ ring ได้ถึง min(N,M) ชั้น ชั้น s ตรวจ 4·s ขีด รวมเป็น O(N·M·min(N,M)²) ตัวเลขดูน่ากลัวแต่ N,M ≤ 70 ก็ยังทันสบาย โค้ดนี้เลยผ่านทั้งที่ "ไม่รู้" ว่าจริง ๆ ring ขาดที่ชั้น 3 เสมอ (มันพึ่งให้ข้อมูลตัดวงให้เอง)

โซลูชัน 2 (เขียนใหม่จากศูนย์ เพราะรู้ตั้งแต่แรกว่าเพดานคือ tier 2) ถ้าเดินเข้าห้องแข่งโดยรู้อยู่แล้วว่า glyph มีได้แค่ ring 1 (2 ลาย) กับ ring 2 (1 ลาย) ก็ไม่ต้องมีเครื่อง ring_ok ทั่วไปให้ยุ่งเลย วนทุกช่องแล้ว "จับลายตัวอักษร" ตรง ๆ 3 ลาย แล้วนับรวม: <> แนวนอน (ring 1), บล็อก 2×2 ที่เป็น >< ซ้อนสองแถว (ring 1 อีกลาย), และแม่แบบ 3 แถวของ tier 2 ที่พิสูจน์ไว้ข้างบน จบในโปรแกรมเดียวที่สั้นกว่าครึ่ง

python
import sys
data = sys.stdin.read().split()
N, M = int(data[0]), int(data[1])
g = [data[2 + i] for i in range(N)]

def at(r, c):                 # นอกกริด = ช่องว่าง (ไม่แมตช์ลายไหนเลย)
    return g[r][c] if 0 <= r < N and 0 <= c < M else '.'

ans = 0
for r in range(N):
    for c in range(M):
        # ---- glyph tier 1 : ring ขนาด 1 มี 2 ลาย ----
        # ลายนอน  <>   (เพชรเล็กคั่นระหว่างสองช่อง)
        if at(r, c) == '<' and at(r, c+1) == '>':
            ans += 1
        # ลายตั้ง  ><    (บล็อก 2x2)
        #          ><
        if at(r, c) == '>' and at(r, c+1) == '<' and at(r+1, c) == '>' and at(r+1, c+1) == '<':
            ans += 1
        # ---- glyph tier 2 : มีลายเดียว ยึด (r,c) ที่ '<' ซ้ายสุดของแถวกลาง ----
        #   . > < .
        #   < < > >
        #   . > < .
        if (at(r, c) == '<' and at(r, c+1) == '<' and at(r, c+2) == '>' and at(r, c+3) == '>'
                and at(r-1, c+1) == '>' and at(r-1, c+2) == '<'
                and at(r+1, c+1) == '>' and at(r+1, c+2) == '<'):
            ans += 1
print(ans)
  • ทำไมนับแค่นี้ถึงครบ: ทุก <> แนวนอน = glyph tier 1 หนึ่งอัน (รวมวงในของ tier 2 ที่ก็เป็น <> อยู่แล้ว), บล็อกตั้ง = tier 1 อีกลาย, ส่วนแม่แบบ tier 2 คือ glyph วงนอกที่บวกเพิ่ม จุดที่เป็น tier 2 จึงถูกนับ 2 พอดี (วงใน 1 + วงนอก 1) เท่ากับที่โซลูชัน 1 บวก T = 2
  • Big-O (โซลูชัน 2): O(N·M) เป๊ะ ๆ วนทุกช่องครั้งเดียว แต่ละช่องเทียบตัวอักษรจำนวนคงที่ (อย่างมากราว 10 ช่อง) ไม่มีลูปตามขนาดวง ไม่มี min(N,M) โผล่ในสูตรอีก เพดานนี้การันตีด้วยการพิสูจน์ ไม่ได้พึ่งว่าอินพุตจะตัดวงให้เองเหมือนโซลูชัน 1
  • สองแบบให้ผลตรงกัน: ผมลองไล่เทียบผลสองโค้ดกับกริดสุ่มหลายพันแบบ (และกริดเล็กแบบครบทุกความเป็นไปได้) แล้วตรงกันทุกอัน โซลูชัน 1 เขียนไวกว่าเพราะไม่ต้องคิดอะไรมาก ส่วนโซลูชัน 2 คือเวอร์ชัน "รู้ของจริง" ที่เขียนพอดีกับที่โจทย์เป็น

E · Binary String Creation: สร้างสตริงวงกลม

ให้สตริงเลขฐานสอง A ยาว N ต้องสร้าง สตริงวงกลม B ที่สั้นที่สุด พร้อมลำดับการเดินซ้าย/ขวาบนวง เพื่อให้เดินเก็บตัวอักษรมาต่อกันได้เท่ากับ A

โจทย์กำหนด · อินพุต / ขอบเขต / เอาต์พุต

คำใบ้

มอง B เป็น "บล็อกของเลขเหมือนกันสลับกันไป" แล้วสังเกตว่าบล็อกขนาด 2 ช่อง (เช่น 11) เด้งซ้าย-ขวาไปมา อ่านเลขซ้ำได้ไม่จำกัด → run ยาวแค่ไหนก็ใช้แค่ 2 ช่องพอ!

เพราะมีแค่สองสัญลักษณ์ 0/1 จึงใช้แค่ 2 บล็อกก็พอเสมอ → ความยาว M น้อยสุดมีแค่ 1–4:

  • A ตัวเดียวล้วน (เช่น 1111) → M = 1
  • M = s₀ + s₁ เมื่อมีทั้ง 0 และ 1 โดย
  • s₁ = 2 ถ้ามี 11 ใน A ไม่งั้น = 1
  • s₀ = 2 ถ้ามี 00 ใน A ไม่งั้น = 1

เช่น A = 1101 (มี 11 ไม่มี 00) → M = 2+1 = 3

EXAMPLE
INPUTOUTPUT
4
1101
3
101
LLR

ตัวอย่างจากโจทย์ · มีหลายคำตอบที่ถูกได้ (เช่นโค้ดข้างล่างอาจได้ B=110 เดิน RRR ซึ่งก็ถูก)

เฉลยโค้ด + คำอธิบาย
python
import sys
data = sys.stdin.read().split()
n = int(data[0]); A = data[1]
if '0' not in A or '1' not in A:            # มีสัญลักษณ์เดียวล้วน
    print(1); print(A[0]); print('R' * (n - 1))
else:
    s1 = 2 if '11' in A else 1
    s0 = 2 if '00' in A else 1
    # วางบล็อกให้ช่องแรก (index 0) ตรงกับ A[0]
    B = ('1'*s1 + '0'*s0) if A[0] == '1' else ('0'*s0 + '1'*s1)
    M = len(B); pos = 0; moves = []
    for t in range(1, n):
        r, l = (pos + 1) % M, (pos - 1) % M  # เพื่อนบ้านขวา/ซ้าย (วนได้)
        if B[r] == A[t]: pos = r; moves.append('R')  # เดินโลภ ไปฝั่งที่บิตตรง
        else:            pos = l; moves.append('L')
    print(M); print(B); print(''.join(moves))
  • ทำไมเดินโลภแล้วไม่มีทางตัน? เพราะบล็อกใหญ่สุดแค่ 2 ช่อง จากช่องไหนก็ตามบนวง เพื่อนบ้านจะมีบิตที่เราต้องการเสมอ (ช่องเดี่ยว ๆ ตัวถัดไปก็เป็นอีกบิตพอดีตามนิยาม s)
  • (pos + 1) % M / (pos - 1) % M คือ "เดินขวา/ซ้ายบนวง" ตัว % ทำให้วนกลับหัวท้ายได้
  • โจทย์บอก "ตอบอันไหนก็ได้ถ้ามีหลายคำตอบ" เลยเลือก R ก่อนเมื่อทั้งสองฝั่งตรงได้ ไม่ผิด
  • Big-O: O(N) สร้างบล็อกยาวไม่เกิน 4 แล้วเดินเก็บ move ทีละก้าว N−1 ครั้ง

F · Quantum Counter: ตัวนับควอนตัม

เครื่องนับ alpha เริ่มที่ A, omega เริ่มที่ B ทุกวินาที alpha ลด 1 และ omega เพิ่ม 1 เกิด "wormhole" เมื่อ alpha หารด้วย omega ลงตัว (นับตอนเริ่มต้นด้วย) หยุดเมื่อตัวใดตัวหนึ่งเป็น 0 แล้วถามว่าเกิด wormhole กี่ครั้ง

โจทย์กำหนด · อินพุต / ขอบเขต / เอาต์พุต

คำใบ้

ที่เวลา t: alpha = A − t, omega = B + t ลองตั้งตัวแปรใหม่ d = B + t (ค่า omega) แล้วเขียน A − t ในรูปของ d ดู แล้วเงื่อนไขหารลงตัวจะยุบง่ายขึ้นเยอะ

มาไล่ดูทีละวินาทีกันก่อน วินาทีแรกสุด (ยังไม่ขยับอะไร) alpha = A และ omega = B เช็คเลยว่า omega หาร alpha ลงตัวไหม ถ้าลงตัวก็นับเป็น wormhole หนึ่งครั้ง จากนั้นเวลาเดินไป 1 วินาที alpha ลดเหลือ A−1 ส่วน omega เพิ่มเป็น B+1 แล้วเช็คเงื่อนไขเดิมอีกครั้ง ทำซ้ำแบบนี้ไปเรื่อย ๆ ค่า alpha ไล่ลงทีละ 1 เป็น A, A−1, A−2, … ส่วน omega ไล่ขึ้นทีละ 1 เป็น B, B+1, B+2, …

แล้วมันจบเมื่อไหร่? โจทย์บอกให้หยุดเมื่อตัวใดตัวหนึ่งเป็น 0 ในที่นี้ตัวที่ค่อย ๆ ลดคือ alpha มันจึงเป็นตัวที่แตะ 0 ก่อน alpha เริ่มที่ A แล้วลดวินาทีละ 1 ดังนั้นวินาทีสุดท้ายที่ alpha ยังเหลือ (alpha = 1) คือหลังเวลาผ่านไป A−1 วินาที พอถัดไปอีกวินาที alpha = 0 พอดี เกมจบ ทีนี้ถ้าเราตั้งชื่อ "จำนวนวินาทีที่ผ่านไป" ว่า t โดยนับวินาทีแรกสุดเป็น t = 0 ก็จะเห็นเองว่า t วิ่งตั้งแต่ 0 ไปจนถึง A−1 (รวม A วินาที) พอดี นี่แหละคือที่มาของช่วงเวลา t ที่เราจะใช้ไล่ในข้อนี้ ไม่ได้ตั้งมาลอย ๆ

ทีนี้ตั้ง d = omega = B + t พอ t ไล่จาก 0 ถึง A−1 ค่า d ก็ กวาดครบทุกจำนวนเต็มตั้งแต่ B ถึง B + (A−1) = S − 1 พอดี (ทีละค่า ไม่ซ้ำ ไม่ขาด เมื่อ S = A + B) และเพราะ alpha = A − t = (A + B) − d = S − d เงื่อนไข "d หาร alpha" จึงเท่ากับ "d หาร S − d" ในเมื่อ d หารตัวมันเองอยู่แล้ว ก็เหลือแค่ d หาร S

หัวใจของข้อนี้

เงื่อนไขหารลงตัวที่ดูยุ่ง ยุบเหลือแค่: d เป็นตัวหารของ S = A + B ที่อยู่ในช่วง [B, S−1] · คำตอบ = จำนวนตัวหารของ S ในช่วงนั้น

ตัวอย่าง A=13, B=5, S=18 · d = B + t กวาดจาก 5 ถึง 17 (t = 0…12) 5 t=0 6 t=1 7 t=2 8 t=3 9 t=4 10 t=5 11 t=6 12 t=7 13 t=8 14 t=9 15 t=10 16 t=11 17 t=12 จุดเขียว = d ที่หาร S=18 ลงตัว (6 และ 9) → 2 wormhole
เวลา t เดินไปข้างหน้า ค่า d = B+t เลื่อนไปทางขวาทีละก้าว ครบทุกค่าใน [B, S−1] เราแค่ถามว่าค่าไหน "หาร S ลงตัว" (diagram วาดประกอบโดยผู้เขียน)

ตารางไล่ทีละวินาทีของตัวอย่างเดียวกัน (A=13, B=5, S=18) แถวเขียวคือวินาทีที่เกิด wormhole:

talpha = A−tomega = d = B+td หาร S=18 ?
0135·
1126หารลงตัว → wormhole
2117·
3108·
499หารลงตัว → wormhole
5810·
6711·
7612·
8513·
9414·
10315·
11216·
12117·

alpha ไล่ลง 13→1, omega (=d) ไล่ขึ้น 5→17 ครบช่วง [5,17] มีแค่ d=6 กับ d=9 ที่หาร 18 ลงตัว จึงได้ 2

EXAMPLE
INPUT (A B)OUTPUT
13 52
138 610

ตัวอย่างจากโจทย์ · แถวแรก: S=18 ตัวหาร {1,2,3,6,9,18} ที่อยู่ในช่วง [5,17] คือ 6 กับ 9 → 2

เฉลยโค้ด + คำอธิบาย
python
import sys
A, B = map(int, sys.stdin.read().split())
S = A + B
lo, hi = B, S - 1
cnt, i = 0, 1
while i * i <= S:            # วนหาตัวหารถึงแค่รากของ S ก็พอ
    if S % i == 0:
        j = S // i           # ตัวหารมาเป็นคู่ (i, S//i)
        if lo <= i <= hi: cnt += 1
        if j != i and lo <= j <= hi: cnt += 1   # กันนับซ้ำตอน S เป็นกำลังสองพอดี
    i += 1
print(cnt)
  • ทำไมวนแค่ถึง √S? ตัวหารมักมาเป็นคู่ i กับ S÷i โดยตัวหนึ่งจะ ≤ √S เสมอ เจอ i ก็ได้ S//i ฟรี → S ถึง 2×1012 แต่ √S แค่ ~1.4 ล้าน ทันเวลา
  • j != i กันเคส S เป็นกำลังสองพอดี (เช่น 144 = 12×12) ไม่ให้นับตัวกลางซ้ำ
  • Big-O: O(√S) วนหาตัวหารถึงแค่รากของ S (มากสุดราว 1.4 ล้านรอบ)

G · Painting the NFTree: ต้นไม้ทาสี

มีต้นไม้ราก 1 แต่ละเส้นมี "สีที่อยากได้" หนึ่งครั้งการทา (operation) เลือกสี c เลือกจุด v แล้วไล่ทาเส้นลงลูก ๆ ที่ยังไม่ทาลงไปเรื่อย ๆ ด้วยสี c เดียวกัน พูดง่าย ๆ คือ ทา "ก้อนเส้นที่ต่อกันเป็นก้อนเดียว สีเดียวกัน" ได้ทีเดียว ถามหาจำนวนครั้งที่น้อยที่สุด

โจทย์กำหนด · อินพุต / ขอบเขต / เอาต์พุต

คำใบ้

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

1 2 1 1 2 7 123 4567 สี 1 (แดง): เส้น 1-2, 2-4, 2-5 ติดกัน = 1 ก้อน สี 2 (น้ำเงิน): เส้น 1-3 กับ 4-6 แยกกัน = 2 ก้อน สี 7 (ทอง): เส้น 5-7 = 1 ก้อน → รวม 4 ก้อน
ต้นไม้จากเทสต์เคสแรก (N=7) เลขบนเส้นคือ "สี" ของเส้น (คอลัมน์ที่ 3 ของอินพุต) สังเกตว่าสี 2 (น้ำเงิน) โผล่ 2 ที่ที่ไม่ติดกัน เลยนับเป็น 2 ก้อน รวมทั้งหมด 4 ก้อน (diagram วาดประกอบโดยผู้เขียน)

แทนที่จะไล่ union หาก้อน มีสูตรลัดสวย ๆ: ในต้นไม้ ก้อนสีเดียวที่มี k เส้นจะมี k+1 จุด พอจัดพีชคณิตด้วยการนับสองทางแบบกฎจับมือ (พิสูจน์เต็ม ๆ ในกล่องด้านล่าง) มันยุบเหลือ:

สูตรลัด

คำตอบ = (ผลรวมจำนวนสีที่ต่างกันรอบทุกจุด) − (N − 1)

ทำไมสูตรลัดถึงถูก (พิสูจน์ด้วยการนับสองทาง / กฎจับมือ)

ตั้งหลักก่อน: 2 นิยามที่จะใช้

นับสองทาง (double counting) อ่านว่า "ดับเบิล เคาน์ติ้ง" คือเทคนิคพิสูจน์ว่า "ของกองเดียวกัน" ถ้านับด้วยวิธีต่างกันสองแบบ ผลรวมต้องเท่ากันเสมอ (เพราะมันคือกองเดิม) เราเลยใช้มันจับสองสูตรที่หน้าตาต่างกันมายืนยันว่าเท่ากันได้

กฎจับมือ (handshake lemma) คือกรณีคลาสสิกของนับสองทางในกราฟ: ผลรวมดีกรี (จำนวนเส้นที่ต่อกับแต่ละจุด) ของทุกจุด = 2 × จำนวนเส้น เขียนเป็น Σv deg(v) = 2·E เหตุผลคือเส้น 1 เส้นมีปลาย 2 ข้าง ถูกนับที่จุดปลายทั้งสองข้าง ชื่อมาจากภาพงานเลี้ยง: นับ "จำนวนมือที่ถูกจับ" ได้ 2 เท่าของ "จำนวนครั้งที่จับมือกัน" พอดี

จุดยากของข้อนี้: เราจะไม่ได้เอากฎจับมือมาใช้ตรง ๆ (ที่นับ "ปลายเส้น") แต่ยืมวิธีคิดนับสองทางมาปรับ ให้ไปนับ "คู่ (จุด, สี)" แทน แล้วนับสองทาง (ทีละสี เทียบกับ ทีละจุด) ให้ได้ค่าเดียวกัน นั่นแหละคือกุญแจที่พลิกจำนวนก้อน ซึ่งปกติต้องไล่หาด้วย union-find ให้กลายเป็นสูตรปิดสั้น ๆ นี่คือที่มาทีละสเต็ป

ตั้งต้น: ต้นไม้มี N จุด, N−1 เส้น ทุกเส้นมีสีเดียว "ก้อน" คือกลุ่มเส้นสีเดียวกันที่ติดกัน คำตอบ = จำนวนก้อนรวมทุกสี

สเต็ป 1 · คิดทีละสี: หยิบเฉพาะเส้นสี c มากองรวมกัน เพราะต้นไม้ไม่มีวงจร (cycle) เส้นชุดย่อยก็ไม่มีวงจร จึงเป็น "ป่า" (forest) และในป่า ก้อนที่มี e เส้นจะมี e+1 จุดเสมอ ให้ Vc = จำนวนจุดที่มีเส้นสี c แตะ และ ec = จำนวนเส้นสี c จะได้ จำนวนก้อนสี c = Vc − ec (เพราะรวมทุกก้อน: Σ(e+1) = Σe + จำนวนก้อน = จำนวนจุดทั้งหมดที่สีนั้นแตะ)

จำนวนก้อน (ต่อ 1 สี) = จำนวนจุดที่แตะ (V) − จำนวนเส้น (e) 123 4567 สี 1: V=4 − e=3 = 1 ก้อน (ต่อกันหมด) 123 4567 สี 2: V=4 − e=2 = 2 ก้อน (แยกกัน)
หยิบมาทีละสี: จุดที่สีนั้นแตะคือ Vc เส้นของสีนั้นคือ ec · สีแดงต่อกันหมดเป็นก้อนเดียว (4−3=1) สีน้ำเงินขาดเป็นสองหย่อม (4−2=2) · V เท่ากันแต่เส้นต่างกัน จำนวนก้อนเลยต่างกัน (diagram วาดประกอบโดยผู้เขียน)

ทำไม "บวกเข้าแล้วตัดออก" ถึงได้จำนวนก้อน (หลักเดียวกับ inclusion-exclusion): ถ้าเดินนับทีละจุดว่ารอบตัวมีกี่สี แล้วเอามาบวกรวมกันทุกจุด ก้อนที่กินหลายจุดจะโดนนับซ้ำ นับหนึ่งครั้งต่อจุดที่มันแตะ ก้อนขนาด k จุดจึงถูกนับ k ครั้ง ทั้งที่ควรได้แค่ 1 ส่วนที่นับเกินคือ k−1 พอดี และ k−1 ก็คือจำนวนเส้นในก้อนนั้น (ป่า: k จุดต่อกันใช้ k−1 เส้น) เพราะฉะนั้น "บวกทุกจุดเข้า แล้วลบจำนวนเส้นทั้งหมด (N−1) ออก" = หักส่วนที่นับเกินทิ้งพอดีเป๊ะ เหลือ 1 ต่อก้อน

ก้อนสีแดง (จุด 1-2-4-5) ถูกนับซ้ำตอนไล่ทีละจุด −1 −1 −1 1245 +1+1+1+1 นับทีละจุด: +1 × 4 = 4 หักเส้นในก้อน: −1 × 3 เหลือ = 1 ก้อน
ก้อน 4 จุดถูกนับ 4 ครั้ง (จุดละครั้ง) แต่จริง ๆ คือก้อนเดียว · ส่วนเกิน 3 = จำนวนเส้นในก้อนพอดี ลบเส้นออกจึงเหลือ 1 นี่คือเหตุผลที่สูตรต้อง "ลบ N−1" (diagram วาดประกอบโดยผู้เขียน)

สเต็ป 2 · บวกทุกสี: คำตอบ = Σc(Vc − ec) = (Σc Vc) − (Σc ec)

สเต็ป 3 · พจน์ขวาง่าย: Σc ec = N−1 เพราะทุกเส้นถูกนับที่สีของมันเพียงสีเดียว รวมทุกสีก็คือจำนวนเส้นทั้งหมด

สเต็ป 4 · หัวใจ นับสองทาง: Σc Vc คือการนับ "คู่ (จุด, สี) ที่จุดนั้นมีเส้นสีนั้นแตะ" โดยไล่ทีละสี แต่ของกองเดียวกันนี้ ถ้าไล่ทีละจุดแทน จะได้ Σv distinct(v) เมื่อ distinct(v) = จำนวนสีต่างกันรอบจุด v ของกองเดียวกันนับสองทางย่อมเท่ากัน จึงได้ Σc Vc = Σv distinct(v)

(นี่คือหลักเดียวกับกฎจับมือคลาสสิก Σv deg(v) = 2·E ที่นับ "ปลายเส้น" สองทาง เพียงแต่เรานับคู่ (จุด, สี) แทนปลายเส้น)

รวมร่าง: คำตอบ = Σv distinct(v) − (N−1) ตรงกับสูตรลัดเป๊ะ

ตรวจกับตัวอย่าง (เทสต์แรก N=7): เส้น 1-2, 2-4, 2-5 เป็นสี 1, เส้น 1-3 กับ 4-6 เป็นสี 2, เส้น 5-7 เป็นสี 7

สี \ จุด1234567รวมแถว = Vc
สี 1●●●●4
สี 2●●●●4
สี 7●●2
รวมคอลัมน์ = distinct(v)211221110
ช่อง ● = จุดนั้นมีเส้นสีนั้นแตะ · นับทีละแถว (แต่ละสีแตะกี่จุด = Vc) ได้ 4+4+2 · นับทีละคอลัมน์ (แต่ละจุดมีกี่สี = distinct) ได้ 2+1+1+2+2+1+1 · สองทางรวมได้ 10 เท่ากัน แล้วลบจำนวนเส้น N−1 = 6 เหลือ 4 ก้อน (diagram วาดประกอบโดยผู้เขียน)

ทีนี้สรุปทั้งสามสีในตารางเดียว (คอลัมน์ V กับ e อ่านจากรูปเทียบสีด้านบนได้เลย):

สีจุดที่แตะ (Vc)เส้น (ec)ก้อน = V − e
สี 1 (แดง)431
สี 2 (น้ำเงิน)422
สี 7 (ทอง)211
รวมทุกสี1064

คำตอบ = 4 ก้อน · ลองแทนสูตรลัดดู: Σ distinct(v) − (N−1) = 10 − 6 = 4 ตรงกัน โดยเลข 10 คือผลรวมคอลัมน์ V (เท่ากับ Σ distinct จากรูปนับสองทาง) และ 6 คือผลรวมคอลัมน์ e (เท่ากับ N−1 = จำนวนเส้นทั้งหมด)

EXAMPLE
INPUTOUTPUT
7
1 2 1
1 3 2
2 4 1
2 5 1
4 6 2
5 7 7
4
5
1 2 1
1 3 1
2 4 1
3 5 1
1

* ตัวอย่างจากโจทย์ · เทสต์แรกตรงกับรูปด้านบน (สี 2 มี 2 ก้อนแยกกัน จึงได้ 4) ส่วนเทสต์สองทุกเส้นเป็นสีเดียวกันและเชื่อมกันทั้งต้น เลยรวมเป็นก้อนเดียว = 1

เฉลยโค้ด + คำอธิบาย
python
import sys
data = sys.stdin.read().split()
n = int(data[0]); idx = 1
colors = [set() for _ in range(n + 1)]   # colors[v] = เซตสีของเส้นที่ต่อกับจุด v
for _ in range(n - 1):
    u, v, c = int(data[idx]), int(data[idx+1]), int(data[idx+2]); idx += 3
    colors[u].add(c); colors[v].add(c)
# ผลรวมจำนวนสีต่างกันของทุกจุด ลบด้วยจำนวนเส้น (n-1)
print(sum(len(s) for s in colors) - (n - 1))
  • ใช้ set() ต่อจุด เพื่อเก็บ "สีที่ต่างกัน" สีซ้ำที่จุดเดียวกันจะถูกยุบอัตโนมัติ
  • len(colors[v]) คือจำนวนสีต่างที่จุด v พอบวกทุกจุดแล้วลบจำนวนเส้น ก็ได้จำนวนก้อนพอดี (ดูวิธีพิสูจน์แบบละเอียดในกล่อง "ทำไมสูตรลัดถึงถูก" ด้านบน)
  • Big-O: O(N) อ่าน N−1 เส้น ใส่ set ต่อจุด (เฉลี่ยต่อครั้ง O(1)) แล้วบวก len ของทุกจุด
  • ทำไม .add() ถึงเป็น O(1) ไม่ใช่ O(log N)? เพราะ set ของ Python เป็น hash table ไม่ใช่ tree มันคำนวณตำแหน่งของค่าจากการ hash แล้วโดดไปวางเลย ไม่ต้องไล่เทียบกับสมาชิกตัวอื่น (ถ้าเป็น set แบบ tree อย่าง std::set ใน C++ หรือ TreeSet ใน Java จะเป็น O(log N) ต่อครั้ง ทำให้รวมเป็น O(N log N)) เอกสารทางการของ Python ระบุว่าการเช็ก/เพิ่มสมาชิกใน set เป็น O(1) โดยเฉลี่ย (กรณีเลวร้ายสุดถ้า hash ชนกันหมดจะเป็น O(N) ต่อครั้ง แต่แทบไม่เกิดกับข้อมูลจำนวนเต็มทั่วไป) ดู Python Wiki: TimeComplexity

ปิดท้าย

สังเกตแพตเทิร์นร่วมของโจทย์ชุดนี้ไหมครับ บางข้อเป็นข้อแจกแต้มไว้อุ่นเครื่อง (เช่น A แค่แทนสูตรพื้นที่วงกลม, B แค่หาคอขวด) แต่ข้อที่ดูโหด เกือบทุกข้อ "ก้าวแรก" คือการ ทำสูตรที่ดูยุ่งให้ยุบเป็นของง่าย: recurrence กลายเป็นเลขยกกำลัง (C), เพชรที่ดูเหมือนซ้อนได้ไม่รู้จบกลายเป็นแค่เช็กสองชั้น (D), สตริงวงกลมกลายเป็นบล็อกไม่กี่ตัว (E), เงื่อนไขหารลงตัวกลายเป็นนับตัวหาร (F), การทาสีกลายเป็นนับก้อน (G) พอมองเห็นโครงสร้างที่ซ่อนอยู่ โค้ดที่เหลือก็สั้นนิดเดียว นั่นแหละคือเสน่ห์ของโจทย์แข่งครับ


ขอบคุณทีมออกโจทย์ที่ออกโจทย์ดี ๆ ให้ได้ฝึกคิด และมีเรื่องสนุก ๆ มาเล่าให้ฟังครับ อ้างอิงจาก:

  1. ชุดโจทย์: รอบคัดเลือกย่อย ICPC 2026 Thailand (ชุดโจทย์กลาง จัดแข่งพร้อมกันหลายสนาม) · เล่มที่อ้างอิงคือของสนามภาคเหนือ: The 2026 ICPC Thailand Northern Region Contest (Problem Set) โดยเจ้าภาพสนาม ภาควิชาวิทยาการคอมพิวเตอร์ คณะวิทยาศาสตร์ มหาวิทยาลัยเชียงใหม่
  2. ทีมออกโจทย์ (ตามที่ระบุในหน้าปกชุดโจทย์): Atittarn Buathep, Borworntat Dendumrongkul, Jessada Thutkawkorapin, Kittipat Pongarunotai, Mattanyu Tangngekkee, Natapong Sriwatanasakdi, Supakorn Kijwattanachai
  3. เนื้อหาในบทความ: เรียบเรียงและเฉลยด้วยคำของผู้เขียนเอง ส่วน diagram ทั้งหมดวาดขึ้นใหม่ประกอบบทความ