programming.in.th · ข้อ 2043
นับคู่จุดที่ขึ้นทั้งสองแกนในกรอบสี่เหลี่ยม สองแสนกรอบภายในสี่วินาที เล่าตั้งแต่วิธีช้าที่สุดไปจนวิธีเต็ม มีเกมให้ลองเองก่อนเปิดเฉลย และเรื่องจริงของการไล่เวลาจาก 14.75 วินาทีเหลือ 3.66
ลองวาดประวัติศาสตร์ลงกระดาษ ให้เวลาเป็นแกนนอน ค่าของเหตุการณ์เป็นแกนตั้ง เหตุการณ์หนึ่งก็เป็นจุดหนึ่งจุด และยุคสมัยหนึ่งก็เป็นกรอบสี่เหลี่ยมหนึ่งกรอบ
เหตุการณ์มี N เหตุการณ์ เหตุการณ์ที่ i อยู่ที่จุด (i, Pi) โดย P เป็นการเรียงสับเปลี่ยนของ 1 ถึง N ไม่มีสองเหตุการณ์ไหนอยู่แถวหรือหลักเดียวกัน
คู่ i กับ j นับเป็นโศกนาฏกรรมเมื่อ i มาก่อน j และค่าของ i น้อยกว่าค่าของ j พูดง่าย ๆ คือจุดหนึ่งอยู่ล่างซ้ายของอีกจุด แต่ละยุคถามว่าในกรอบของยุคนั้นมีกี่คู่
อินพุต / ขอบเขต / เอาต์พุต
N M บรรทัดที่สองคือ P ทั้ง N ตัว แล้วอีก M บรรทัดคือกรอบของแต่ละยุค เขียนเรียงเป็น r1 r2 c1 c2N ≤ 100,000, M ≤ 200,000, เวลา 4 วินาที หน่วยความจำ 1024 เมกะไบต์| Input | Output |
|---|---|
| 9 9 9 8 7 6 2 4 5 3 1 4 9 3 6 2 9 1 8 3 8 2 4 3 9 2 7 2 8 1 6 1 9 1 9 1 3 5 7 2 3 3 3 6 6 6 6 | 1 4 2 4 4 4 0 0 0 |
อ่านตัวอย่างนี้ยังไง
เอาต์พุตคือจำนวนคู่ในกรอบ ไม่ใช่จำนวนเหตุการณ์ในกรอบ
ยุคที่ 1 กรอบคือ 4 9 3 6 คือดัชนี 4 ถึง 9 และค่า 3 ถึง 6
ในกรอบมี 4 เหตุการณ์ คือ (4, 6), (6, 4), (7, 5), (8, 3)
แต่คำตอบคือ 1 เพราะมีคู่เดียวที่ขึ้นทั้งสองแกน คือ (6, 7) (P6 = 4 น้อยกว่า P7 = 5) คู่อื่นค่าลดลงหมด
ยุคที่ 2 ตอบ 4 จากคู่ (5, 6), (5, 7), (5, 8), (6, 7) ส่วนสามยุคท้ายตอบ 0 เพราะกรอบแคบจนเหลือเหตุการณ์ไม่ถึงสองอัน
ตัวอย่างนี้ยังช่วยเช็กว่าอ่านโจทย์ถูก ถ้าเผลอนับคู่ที่ค่าลดลง ยุคที่ 1 จะได้ 5 ถ้าเผลอสลับช่วงดัชนีกับช่วงค่า จะได้ 0 ไม่ตรงกับ 1 ทั้งคู่
ใบ้
ถ้ากรอบเดียวก็ไล่ทุกคู่ในกรอบจบ แต่มี 200,000 กรอบ และแต่ละกรอบอาจกิน 100,000 เหตุการณ์ คูณกันแล้วเกินงบไปมหาศาล
ลองสังเกตอย่างหนึ่งก่อน คู่ที่เรานับมีเงื่อนไขติดตัวอยู่แล้วสองข้อคือ i มาก่อน j และค่าของ i น้อยกว่าค่าของ j แล้วกรอบมีขอบสี่ด้าน ขอบทั้งสี่ด้านนั้นบังคับปลายทั้งสองข้างจริง ๆ หรือเปล่า
กรอบกินทั้งกระดาน อุ่นเครื่องก่อน
กดจุดสองจุดเพื่อเลือกหนึ่งคู่
กรอบทองคือยุคที่ถาม วงที่ขอบทองคือเหตุการณ์ที่อยู่ในกรอบ กดสองจุดเพื่อเสนอหนึ่งคู่
เกมที่สองถามเรื่องที่เฉลยต้องใช้: ถ้าค่อย ๆ หดกรอบเข้ามาทีละด้าน คู่จะหลุดเพราะปลายไหน
มีคู่เดียวบนกระดาน หดขอบไหนก็ได้ แล้วดูว่าตอนคู่หลุด ปลายไหนออกนอกกรอบก่อน
คู่นี้อยู่ในกรอบครบทั้งสองปลาย ยังนับได้
| ขอบที่หด | ปลายล่าง i | ปลายบน j |
|---|---|---|
| ขอบซ้าย (r1) | · | · |
| ขอบขวา (r2) | · | · |
| ขอบล่าง (c1) | · | · |
| ขอบบน (c2) | · | · |
ลองหดครบสี่ขอบ ให้คู่หลุดด้วยแต่ละขอบอย่างน้อยครั้งหนึ่ง
ลองเล่นดูก่อนนะครับ พอเห็นว่าคู่แบบไหนถึงนับได้ และขอบไหนไล่ปลายไหนออก ค่อยไปดูเฉลย
ที่มาของแนวคิดนี้
ผมไม่ได้นึกท่าออกตั้งแต่แรก เริ่มจากวิธีที่ตรงที่สุดก่อน แล้วคูณดูว่าช้ากว่างบกี่เท่า ตัวเลขนั้นบอกเองว่าต้องไปหาท่าแบบไหน
ไล่จากช้าไปเร็ว มีสามขั้น ขั้นที่สองใช้ BIT (ต้นไม้เฟนวิก) ซึ่งเป็นตัวนับที่ตอบว่า "ที่จดไว้มีกี่ตัวที่ค่าไม่เกิน v" ได้ในราว log N ก้าว อธิบายเต็มอยู่ที่หน้าผลรวมสะสมกับต้นไม้เฟนวิก
วิธีที่ 1
ราว N² ต่อคำถาม
จับทุกคู่มาเช็กตรง ๆ ว่าขึ้นทั้งสองแกนไหม โค้ดนี้คือตัวตรวจที่หน้านี้ใช้เทียบคำตอบด้วย
ได้ชุด N, M ≤ 10
วิธีที่ 2
ราว (r2 − r1) × log N
เดินจาก r1 ไป r2 จดลง BIT เฉพาะตัวที่ค่าอยู่ใน [c1, c2] แต่ละตัวค่า v ถามว่าที่จดไว้มีกี่ตัวค่าอยู่ใน [c1, v − 1]
ได้ชุด N, M ≤ 5,000
วิธีเต็ม
ราว √N ต่อคำถาม (เศษบวก log)
หั่นเป็นก้อน (sqrt decomposition) ก้อนที่อยู่ในกรอบเต็ม ๆ ตอบจากตารางที่เตรียมไว้ เศษที่ขอบไล่เอง เล่าในตอนที่ 2 ถึง 5
ได้ครบ 100 คะแนน
วิธีที่ 2 ที่ขอบเขตเต็มคือ 200,000 × 100,000 × 17 ≈ 340,000,000,000 ก้าว เกินงบราว 850 เท่า ปรับนิดหน่อยไม่พอแน่
คิดกลับด้านจากงบแทน ถ้าคำถามละใช้แค่ √N ≈ 316 ก้าว ทั้งหมดรวมราว 63 ล้านก้าว ต่อให้คูณค่าคงที่เข้าไปอีกหลายเท่าก็ยังทัน เป้าจึงชัดว่าต้องตอบข้อละราว √N
| คะแนน | เงื่อนไขของชุด |
|---|---|
| 12 | N, M ไม่เกิน 10 |
| 12 | N, M ไม่เกิน 3000 ถึง 5000 (สามชั้น ชั้นละ 4%) |
| 16 | c1 = 1 และ c2 = N คือไม่มีขอบค่า (สี่ชั้น) |
| 12 | กรอบสองอันซ้อนกันสนิทหรือไม่แตะกันเลย (สามชั้น) |
| 24 | N ไม่เกิน 20000 ถึง 70000 (หกชั้น) |
| 12 | ทั้งลำดับมีคู่ที่ไม่เพิ่มขึ้นไม่เกิน 50 คู่ |
| 12 | ไม่มีเงื่อนไขเพิ่ม |
ก่อนลงมือหั่น มีเรื่องหนึ่งที่ทำให้งานหั่นง่ายลงครึ่งหนึ่ง จำเกมหดกรอบเมื่อกี้ได้ไหม
คู่หนึ่งมีสองจุด เรียกจุดที่มาก่อนว่าปลายล่าง (i) เพราะค่าน้อยกว่าด้วย จึงอยู่ล่างซ้าย อีกจุดคือปลายบน (j) อยู่บนขวา
เกมหดกรอบเมื่อกี้ให้ผลว่า ขอบซ้ายกับขอบล่างไล่ออกได้แต่ปลายล่าง ขอบขวากับขอบบนไล่ออกได้แต่ปลายบน ตอนนี้มาดูว่าทำไม "ทั้งสองจุดอยู่ในกรอบ" เขียนตรง ๆ ได้แปดเงื่อนไข แต่ครึ่งหนึ่งเป็นจริงเองอยู่แล้ว
| เงื่อนไข | ผล |
|---|---|
| i ≥ r1 | ต้องเช็ก |
| i ≤ r2 | ฟรี เพราะ i < j ≤ r2 |
| j ≥ r1 | ฟรี เพราะ j > i ≥ r1 |
| j ≤ r2 | ต้องเช็ก |
| Pi ≥ c1 | ต้องเช็ก |
| Pi ≤ c2 | ฟรี เพราะ Pi < Pj ≤ c2 |
| Pj ≥ c1 | ฟรี เพราะ Pj > Pi ≥ c1 |
| Pj ≤ c2 | ต้องเช็ก |
สี่ข้อที่เหลือแบ่งกันพอดี ไม่มีขอบไหนต้องดูสองปลายพร้อมกัน ตอนต่อ ๆ ไปจึงคิดทีละปลายได้
หั่นแกนดัชนีเป็นบล็อกละ B ตำแหน่ง โดยให้ B ราว √N (ที่ขอบเขตเต็มโค้ดใช้ B = 139) ช่วง [r1, r2] ของคำถามหนึ่งจะเห็นเป็นสามท่อนแบบในรูป
ต่อจากนี้จนจบตอนที่ 4 ใช้ตัวอย่างเดียวกันตลอด: 12 ตำแหน่ง บล็อกละ 3 (รูปบนใช้บล็อกละ 4 แค่ให้เห็นภาพ) ค่าเรียงตามตำแหน่งคือ 7, 11, 1, 2, 9, 5, 6, 3, 10, 12, 4, 8
เครื่องมือแรกคือตาราง F[v][b] = จำนวนตำแหน่งในบล็อก 0 ถึง b − 1 ที่ค่าไม่เกิน v
เป็นผลรวมสะสมทั้งตามค่าและตามบล็อก
| v | b = 0 | b = 1 | b = 2 | b = 3 | b = 4 |
|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 | 0 |
| 1 | 0 | 1 | 1 | 1 | 1 |
| 2 | 0 | 1 | 2 | 2 | 2 |
| 3 | 0 | 1 | 2 | 3 | 3 |
| 4 | 0 | 1 | 2 | 3 | 4 |
| 5 | 0 | 1 | 3 | 4 | 5 |
| 6 | 0 | 1 | 3 | 5 | 6 |
| 7 | 0 | 2 | 4 | 6 | 7 |
| 8 | 0 | 2 | 4 | 6 | 8 |
| 9 | 0 | 2 | 5 | 7 | 9 |
| 10 | 0 | 2 | 5 | 8 | 10 |
| 11 | 0 | 3 | 6 | 9 | 11 |
| 12 | 0 | 3 | 6 | 9 | 12 |
ลองอ่าน F
บล็อก 1 ถึง 2 มีกี่ตัวที่ค่าอยู่ใน [4, 10]
(F[10][3] − F[10][1]) − (F[3][3] − F[3][1])
= (8 − 2) − (3 − 1) = 4 ตัว คือค่า 9, 5, 6, 10
พอมี F แล้ว กลับมาที่คู่หนึ่งคู่ ปลายแต่ละข้างตกได้สามท่อน จัดกลุ่มแล้วเหลือสี่แบบ แต่ละแบบนับด้วยวิธีของตัวเอง
| ปลายสองข้างอยู่ตรงไหน | นับยังไง |
|---|---|
| เศษทั้งคู่ (ซ้ายกับซ้าย ขวากับขวา หรือซ้ายกับขวา) | ชิ้นเศษรวมไม่เกิน 2B ตัว แต่ละบล็อกเรียงค่าไว้ล่วงหน้าแล้ว จึงดึงเศษออกมาเรียงตามค่าได้เลย แล้วนับด้วย BIT แบบวิธีที่ 2 ราว B log B ก้าว |
| ข้างหนึ่งเป็นเศษ อีกข้างอยู่ในบล็อกเต็ม | ต่อชิ้นเศษหนึ่งชิ้น บวกลบสี่ช่องของ F ถ้าเศษซ้ายค่า v เป็นปลายล่าง นับตัวในบล็อกเต็มที่ค่าอยู่ใน [v + 1, c2] ถ้าเศษขวาค่า v เป็นปลายบน นับที่ค่าอยู่ใน [c1, v − 1] |
| ในบล็อกเต็มบล็อกเดียวกัน | อ่านตาราง f ของบล็อกนั้น (ข้างล่าง) ครั้งเดียวต่อบล็อก |
| ในบล็อกเต็มคนละบล็อก | ยากที่สุด แยกไปเล่าในตอนที่ 4 |
ลองจัดหมวดเองบนแถบบล็อกละ 4 แบบในรูป กดสองช่องในช่วง [r1, r2] เป็นปลายล่างกับปลายบนของคู่หนึ่ง
กดช่องแรก
เจอแล้ว 0 จาก 4 แบบ
ตาราง f ใช้กับคู่ในบล็อกเดียวกัน เรียงค่าในบล็อกแล้วให้อันดับ 1 ถึง B แล้วเตรียมตาราง B × B ไว้ว่า ถ้าปลายล่างอันดับไม่ต่ำกว่า u และปลายบนอันดับไม่เกิน v จะมีกี่คู่ แค่สองเงื่อนไขนี้พอ เพราะตอนที่ 2 ตัดเหลือปลายละข้อแล้ว
แถบค่า [c1, c2] แปลงเป็นช่วงอันดับด้วย F เพราะอันดับเรียงตามค่า
ตัวที่ค่าไม่เกิน c2 ในบล็อก b คืออันดับ 1 ถึง F[c2][b + 1] − F[c2][b] ตัวที่ค่าต่ำกว่า c1 คืออันดับ 1 ถึง F[c1 − 1][b + 1] − F[c1 − 1][b] ช่วงที่ต้องการคืออันดับระหว่างสองตัวเลขนี้
ทำไม B = 139 ไม่ใช่ √N = 316 พอดี
ต่อคำถามมีงานสองฝั่ง ฝั่งบล็อกเต็มราว N ÷ B ก้าว ฝั่งเศษราว B log B ก้าว บล็อกใหญ่ฝั่งแรกเบาลงแต่ฝั่งหลังหนักขึ้น
B = 316 รวมราว 2,941 ก้าว ส่วน B = 139 เหลือราว 720 + 990 = 1,710 ก้าว เพราะฝั่งเศษมี log ติดมา จุดคุ้มจึงเลื่อนมาทางบล็อกเล็ก ตาราง f ทุกบล็อกรวมกันใช้ราว N × B ≈ 13.9 ล้านช่อง ยังอยู่ในหน่วยความจำ
เหลือแบบสุดท้ายคือปลายทั้งสองอยู่ในบล็อกเต็มคนละบล็อก ถ้าจะเตรียมคำตอบไว้ล่วงหน้า ต้องจำตามบล็อกแรก บล็อกสุดท้าย ขอบค่าล่าง และขอบค่าบน เป็นตารางสี่มิติซึ่งใหญ่เกินจะเก็บ ต้องตัดมิติทิ้งให้ได้
วิธีตัดคือกวาดค่า (sweep อ่านว่า "สวีป" แปลว่ากวาด) ใส่เหตุการณ์ทีละตัวตามลำดับค่า 1, 2, 3, …
นึกภาพ
เรียกคนขึ้นเวทีทีละคนจากเตี้ยไปสูง ใครขึ้นมาทีหลังก็สูงกว่าทุกคนที่ยืนอยู่แล้วเสมอ เงื่อนไข "ค่าของ i น้อยกว่าค่าของ j" จึงเป็นจริงเองทุกครั้ง เหลือแค่ดูว่าใครยืนอยู่ทางซ้าย
ตอนใส่ตัวที่อยู่บล็อก bx มันเป็นปลายบนให้ทุกตัวที่ใส่แล้วในบล็อกก่อน bx และ ณ เวลา t ใส่ไปแค่ค่า 1 ถึง t ขอบค่าบน c2 จึงได้มาฟรี แค่อ่านผลตอน t = c2
สองแสนคำถามใช้การกวาดรอบเดียวร่วมกัน วิธีคือเก็บคำถามทั้งหมดไว้ก่อนแล้วค่อยตอบ (เรียกว่าทำแบบออฟไลน์, offline)
แต่ละคำถามนัดอ่านผลสองจังหวะ ตอน t = c2 ได้คู่ที่ค่าไม่เกิน c2 และตอน t = c1 − 1 เอาไว้หักคู่ที่ค่าต่ำกว่า c1 ออก (ทำไมต้องหัก ดูหลังเครื่องเดิน)
ระหว่างกวาดเก็บของไว้สองอย่าง
colTotal[bx] จำนวนคู่ที่ปลายบนอยู่บล็อก bx และปลายล่างอยู่บล็อกใดก็ได้ที่มาก่อนE[b1][bx] จำนวนคู่ที่ปลายบนอยู่บล็อก bx และปลายล่างอยู่บล็อกที่มาก่อน b1 คือพวกที่หลุดขอบซ้ายของคำถามใส่ค่าหนึ่งตัวต้องอัปเดตคอลัมน์ bx ของ E ทุกแถว ราว N ÷ B ช่อง ทั้งการกวาดจึงราว 72,000,000 ครั้ง (สังเกตด้วยว่า colTotal[bx] เท่ากับ E[bx][bx] เสมอ โค้ดเก็บแยกไว้ให้อ่านง่าย)
อ่านคำตอบของบล็อกเต็ม b1 ถึง b2
บวก colTotal[bx] − E[b1][bx] ไล่ bx = b1 ถึง b2 (อ่านแถวเดียวของ E ติดกันรวดเดียว)
ลองเดินดูกับตัวอย่าง 12 ตำแหน่ง ค่าเรียงตามตำแหน่งคือ 7, 11, 1, 2, 9, 5, 6, 3, 10, 12, 4, 8 คำถามมีบล็อกเต็มคือ 1 ถึง 2 แถบค่า [4, 10]
ใส่ค่า 1 ถึง 12 ทีละตัว บล็อกละ 3 ตำแหน่ง
| บล็อก | 0 | 1 (เต็ม) | 2 (เต็ม) | 3 | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| ตำแหน่ง | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
| ค่าที่ใส่แล้ว | · | · | · | · | · | · | · | · | · | · | · | · |
| บล็อก 0 | บล็อก 1 | บล็อก 2 | บล็อก 3 | |
|---|---|---|---|---|
| ใส่แล้ว | 0 | 0 | 0 | 0 |
| colTotal | 0 | 0 | 0 | 0 |
| E[1] | 0 | 0 | 0 | |
| E[2] | 0 | 0 | ||
| E[3] | 0 |
ช่องว่างในแถว E คือช่องที่คำถามไม่เคยอ่าน แถว E[0] ไม่ได้วาดเพราะเป็นศูนย์ตลอด
ผลที่อ่านได้ตอน t = 10 คือ 6 คู่ ซึ่งยังรวมคู่ที่ค่าต่ำกว่าขอบล่าง 4 อยู่
ขอบล่างแก้ด้วยการแบ่งค่าเป็นสองกอง กอง A คือค่าที่น้อยกว่า c1 กอง S คือค่าใน [c1, c2] คู่ที่ค่าไม่เกิน c2 จึงมีสามพวก: ใน A ทั้งคู่, ใน S ทั้งคู่, และข้ามกอง เราต้องการแค่พวกที่สอง
สูตรตัดขอบค่าล่าง
คู่ที่อยู่ใน S ทั้งคู่ = (ผลที่อ่านตอน t = c2) − (ผลที่อ่านตอน t = c1 − 1) − Cross(A, S)
ผลที่อ่านตอน t = c1 − 1 คือพวกใน A ทั้งคู่ ส่วน Cross(A, S) คือพวกข้ามกอง
Cross นับง่าย เพราะค่าทุกตัวใน A น้อยกว่าทุกตัวใน S ปลายล่างของคู่ข้ามกองจึงมาจาก A เสมอ
เหลือดูแค่ตำแหน่ง: ตัวใน S ที่บล็อก b จับคู่กับตัวใน A ทุกตัวในบล็อกเต็มก่อนหน้า (นับเฉพาะบล็อก b1 ถึง b − 1 เพราะปลายล่างต้องอยู่ในช่วงของคำถามด้วย) อ่านจำนวนจาก F ได้หมด
ในตัวอย่าง ตัวใน S ของบล็อก 2 (ค่า 6, 10) จับคู่กับตัวใน A ที่อยู่บล็อกก่อนหน้า (ค่า 2) ได้ 2 × 1 = 2 คู่ จึงได้ Cross = 2 คำตอบของแบบนี้คือ 6 − 1 − 2 = 3 คู่ ตรงกับการไล่นับตรง ๆ ทีละคู่
เรื่องจริงที่เกิดขึ้นตามลำดับ
ตัวแรกที่ให้คำตอบถูกทุกเคสใช้เวลา 14.75 วินาที จากลิมิต 4 วินาที คือช้ากว่าที่ต้องการเกือบสี่เท่า ตอนนั้นผมคิดว่าคงต้องรื้ออัลกอริทึม
ที่แก้จริงมีอย่างเดียวที่แตะอัลกอริทึม: เวอร์ชันแรกหาช่วงอันดับของแต่ละบล็อกด้วย binary search บนค่าที่เรียงไว้ ทุกบล็อกทุกคำถาม พอเปลี่ยนไปอ่านจาก F ตามที่เล่าในตอนที่ 3 log ตัวนั้นก็หายไป ที่เหลือทั้งหมดคือการวางข้อมูลในหน่วยความจำ
แคชคืออะไร
แคช (cache อ่านว่า "แคช" มาจากคำฝรั่งเศส cacher แปลว่าซ่อน) คือหน่วยความจำก้อนเล็กที่เร็วมาก ติดอยู่กับ CPU เทียบได้กับโต๊ะทำงาน ส่วน RAM คือโกดังหลังร้าน
ทุกครั้งที่ไปโกดัง CPU ยกของมาทั้งถาด ถาดละ 64 ไบต์ (int 16 ตัวที่อยู่ติดกัน) ของชิ้นถัดไปอยู่ถาดเดิมก็หยิบจากโต๊ะได้เลย ถ้าไม่อยู่ต้องเดินไปโกดังใหม่ ช้ากว่าเป็นสิบ ๆ เท่า
ตาราง F ผมเก็บเป็น F[บล็อก][ค่า] ตามที่นึกออกก่อน แต่ลูปที่รันบ่อยที่สุดไล่ทีละบล็อกโดยตรึงค่าไว้
การอ่านสองครั้งติดกันจึงห่างกัน 391 กิโลไบต์ ไม่เคยอยู่ถาดเดียวกัน
พอสลับเป็น F[ค่า][บล็อก] ลูปเดียวกันก็อ่านของที่วางติดกัน
| แก้อะไร | วินาที |
|---|---|
| ตัวแรกที่ให้คำตอบถูก | 14.75 |
| ตัด binary search ต่อบล็อกต่อคำถาม ใช้ตาราง F แทน | 8.14 |
| สลับแกนตาราง F จาก F[บล็อก][ค่า] เป็น F[ค่า][บล็อก] | 3.88 |
| สลับแกนตาราง E กับตาราง f แล้วจูนขนาดบล็อกใหม่ | 3.66 |
เรื่องที่ผมไม่ได้คาดไว้คือขนาดบล็อกที่ดีที่สุดย้ายที่หลังแก้แคช ก่อนแก้ B = 240 ชนะ B = 120 พอแก้เสร็จกลับตรงข้าม B = 139 เร็วที่สุด ส่วน B = 80 ช้ากว่าราวหกสิบเปอร์เซ็นต์
บล็อกใหญ่เสียเปรียบเพราะตาราง f ของแต่ละบล็อกมีขนาด B × B ต้องเล็กพอให้ค้างอยู่บนโต๊ะได้ ส่วนบล็อกเล็กเกินไปก็มีจำนวนบล็อกเยอะ งานฝั่งบล็อกเต็มต่อคำถามกับการอัปเดต E จึงเพิ่มตาม ค่า B ที่จูนไว้ตอนข้อมูลยังวางผิดแกนจึงใช้ต่อไม่ได้
ที่เหลือเป็นของเล็ก การอ่านอินพุตราวเก้าแสนจำนวนด้วย scanf กินไปราวหนึ่งในเจ็ดวินาที
เปลี่ยนเป็นอ่านทั้งไฟล์ทีเดียวแล้วแกะเองก็ได้คืนมา
โค้ดเต็มแบ่งเป็นหกช่วง แต่ละช่วงมีคอมเมนต์หัวเป็นชื่อเดียวกับคอลัมน์แรก อ่านคู่กับเฉลยตอนที่ระบุได้เลย
| ช่วงในโค้ด | ทำอะไร | อ่านคู่กับ |
|---|---|---|
| อ่านอินพุต | อ่านทั้งไฟล์ทีเดียวแล้วแกะตัวเลขเอง | ตอนที่ 5 |
| ตาราง F | นับว่าบล็อกก่อนหน้ามีกี่ตัวที่ค่าไม่เกิน v เก็บแบบ F[ค่า][บล็อก] | ตอนที่ 3, 5 |
| ต่อบล็อก: เรียงค่า, อันดับ, ตาราง f | เตรียมคำตอบของคู่ในบล็อกเดียวกัน | ตอนที่ 3 |
| อ่านคำถาม | เก็บกรอบของทุกคำถามไว้ก่อน เพราะการกวาดต้องรู้ล่วงหน้าว่าจะหยุดอ่านที่ค่าไหน | ตอนที่ 4 |
| หมวด "บล็อกเต็มคนละบล็อก": กวาดค่าจากน้อยไปมาก | กวาดค่า เก็บ colTotal กับ E แล้วอ่านคำตอบตอน t = c2 และ c1 − 1 | ตอนที่ 4 |
| ประกอบคำตอบของแต่ละคำถาม (ก) ถึง (ง) | รวมสี่แบบของคู่ และหักส่วน Cross | ตอนที่ 2, 3, 4 |
| รูปแบบคำถาม | วินาที | เมกะไบต์ |
|---|---|---|
| กรอบสุ่มทั้งสองแกน | 3.66 | 365 |
| กรอบเต็มระนาบทุกคำถาม | 1.7 | 359 |
| กรอบยาวเกือบเต็มแกนดัชนี | 3.59 | 363 |
| กรอบสั้น | 0.77 | 363 |
คำว่าตรวจแล้วในหน้านี้หมายถึงตรวจกับตัวตรวจของเราเองและตัวอย่างในโจทย์ ยังไม่ได้ส่งเข้าเครื่องตรวจของ programming.in.th และเวลาที่วัดเป็นของเครื่องที่ใช้เขียน ไม่ใช่ของเครื่องตรวจ ตัวเลข 92 เปอร์เซ็นต์จึงเป็นตัวเลขที่ควรระวัง
สามข้อนี้ตอบได้จากสิ่งที่อ่านมา ลองคิดก่อนแล้วค่อยเปิดคำตอบ
ข้อ 1. ถ้าโจทย์เปลี่ยนเป็นนับคู่ที่ค่าลดลง (i มาก่อน j แต่ค่าของ i มากกว่า) ต้องแก้โค้ดตรงไหนบ้าง
ข้อ 2. ตาราง E เก็บแค่ "ปลายล่างอยู่ก่อนบล็อก b1" ทำไมไม่ต้องมีอีกตารางสำหรับ "ปลายบนอยู่หลังบล็อก b2"
ข้อ 3. ถ้าเลือก B = N (บล็อกเดียว) หรือ B = 1 (ตัวละบล็อก) จะเกิดอะไรขึ้น
ก่อนออกแบบโครงสร้างข้อมูล ให้เขียนเงื่อนไขทั้งหมดของสิ่งที่กำลังนับออกมาเรียงกันก่อน แล้วตัดข้อที่เป็นจริงอยู่แล้วทิ้ง ข้อนี้ดูเหมือนมีแปดเงื่อนไข พอตัดแล้วเหลือสี่ และสี่ข้อนั้นแยกกันคนละปลายพอดี
เมื่อเงื่อนไขหนึ่งเกะกะ ลองเรียงข้อมูลตามมันแล้วใส่ทีละตัว ของที่ใส่ทีหลังชนะของที่ใส่ก่อนเสมอ เงื่อนไขนั้นจึงหายไปเอง แบบที่การกวาดค่าทำกับเงื่อนไขค่าของ i น้อยกว่าค่าของ j
และเมื่อโปรแกรมช้ากว่าที่คาดสองถึงสี่เท่า ให้สงสัยการวางข้อมูลก่อนสงสัยอัลกอริทึม ลูปที่วิ่งไปตามแกนหนึ่งแต่ตารางเก็บเรียงตามอีกแกนหนึ่ง ทำให้ช้าต่างกันได้หลายเท่าตัว ทั้งที่จำนวนคำสั่งเท่าเดิม
ตัดเงื่อนไขที่ฟรีทิ้งจนขอบกรอบผูกปลายคู่ละข้าง หั่นแกนดัชนีเป็นบล็อกละราว √N แล้วนับคู่ข้ามบล็อกด้วยการใส่ค่าทีละตัวจากน้อยไปมาก
ในหน้านี้