programming.in.th · ข้อ 2011
ข้อที่สอนวิธีแปลงเงื่อนไขซึ่งดูเหมือนต้องตรวจทุกช่วง ให้เป็นเงื่อนไขเดียวที่ตรวจได้ระหว่างเดิน แล้วต่อด้วยการบีบสถานะจนเหลือหกแบบ พร้อมเวอร์ชันที่ไม่ต้องบีบสำหรับคนที่ยังมองไม่ออก
กษัตริย์รามเสสที่สองสั่งปลูกสวนเป็นแนวยาวจากพระราชวังลุกซอร์ไปวิหารคาร์นัก มีต้นไม้ N ต้นเรียงกัน
ปลูกได้แค่สองชนิดคือต้นบัว แทนด้วย L และต้นกก แทนด้วย P
สวนจะเรียกว่า สมดุล เมื่อทุกช่วงติดกันของสวน มีจำนวนบัวกับจำนวนกกต่างกัน ไม่เกินสองต้น ย้ำว่าทุกช่วง ไม่ใช่แค่ทั้งสวน
เอาสวนสมดุลทุกแบบของความยาวหนึ่ง ๆ มาเรียงตามพจนานุกรม (L มาก่อน P)
แล้วให้เลข 1, 2, 3 ไปเรื่อย ๆ งานของเราคือรับสวนสมดุลมาหนึ่งแบบ แล้วตอบว่ามันเป็นแบบที่เท่าไร
โดยตอบเป็นเศษเหลือจากการหารด้วย M
อินพุต / ขอบเขต / เอาต์พุต
N บรรทัดที่ 2 คือ M บรรทัดที่ 3 คือสตริงยาว N ตัวที่เป็นสวนสมดุล1 ≤ N ≤ 1,000,000, 7 ≤ M ≤ 10,000,000 เวลา 1.5 วินาที หน่วยความจำ 64 เมกะไบต์MN ไม่เกิน 40| Input | Output |
|---|---|
| 5 7 PLPPL | 5 |
| 12 10000 LPLLPLPPLPLL | 39 |
ความยาว 5 มีสวนสมดุลทั้งหมด 14 แบบ เรียงตามพจนานุกรมได้
LLPLP, LLPPL, LPLLP, LPLPL, LPLPP, LPPLL, LPPLP, PLLPL, PLLPP, PLPLL, PLPLP, PLPPL, PPLLP, PPLPL ตัว PLPPL อยู่อันดับที่ 12
และ 12 หารเอาเศษด้วย 7 ได้ 5
ใบ้
"ทุกช่วงติดกัน" ฟังดูเหมือนต้องตรวจ N² ช่วง แต่ลองแทนบัวด้วย +1 กกด้วย −1
แล้วเขียนผลรวมสะสมออกมาดู จำนวนบัวลบจำนวนกกของช่วง (i, j] ก็คือผลต่างของผลรวมสะสมสองจุด
แล้วเงื่อนไขที่ว่าทุกช่วงต่างกันไม่เกินสอง จะกลายเป็นเงื่อนไขอะไรที่พูดถึงผลรวมสะสมทั้งเส้นทีเดียว
สวนสั้น ๆ ห้าต้น ลองไล่ให้ชินกับรางก่อน
ปลูกสวนยาว 5 ต้น ให้เป็นสวนสมดุลอันดับที่ 6 จากทั้งหมด 14 แบบ โดยเรียงตามพจนานุกรม บัว (L) มาก่อน กก (P)
กดปลูกทีละต้น เส้นจะขยับขึ้นเมื่อปลูกบัว และลงเมื่อปลูกกก
สวนตอนนี้ ยังว่าง · ความกว้างของราง 0
รางกว้างเกินสองเมื่อไร สวนนั้นไม่สมดุลทันที และไม่ว่าจะปลูกต่อยังไงก็แก้ไม่ได้แล้ว
ลองสังเกตว่าตอนกำลังเลือกต้นถัดไป สิ่งที่ต้องรู้จริง ๆ มีแค่กี่อย่าง แล้วมันคือตำแหน่งของเส้น หรือระยะห่างจากขอบราง
ที่มาของแนวคิดนี้
เงื่อนไขในโจทย์พูดถึงทุกช่วงที่ติดกัน ซึ่งอ่านครั้งแรกแล้วเหมือนบังคับให้ต้องตรวจ N² ช่วง และถ้าต้องตรวจทีละช่วงจริง ก็ไม่มีทางเดินไปพร้อมกับการนับอันดับได้เลย ผมเลยลองเขียนเงื่อนไขนั้นใหม่ในรูปผลรวมสะสม พอเขียนออกมา เงื่อนไขของทุกช่วงพร้อมกันก็ยุบเหลือประโยคเดียวคือ ค่ามากสุดลบค่าน้อยสุดของผลรวมสะสมต้องไม่เกินสอง ซึ่งตรวจได้ระหว่างเดินทีละต้น
ท่าที่ผมทำต่อและอยากให้ลองทำตาม คือเขียนเวอร์ชันที่จำทุกอย่างให้ถูกก่อน จำค่ามากสุด ค่าน้อยสุด และค่าปัจจุบันของผลรวมสะสมไปตรง ๆ ไม่ต้องฉลาด เวอร์ชันนั้นเขียนง่ายและถูกแน่ และมันกลายเป็นเฉลยชุดทดสอบย่อย 40 คะแนนพอดี
แล้วค่อยถามคำถามเดียวกับมันว่า ข้อมูลชิ้นไหนที่จำไว้ แต่ไม่เคยถูกใช้จริง คำตอบคือตำแหน่งสัมบูรณ์ของผลรวมสะสม เพราะทุกการตัดสินใจใช้แค่ระยะห่างระหว่างค่าปัจจุบันกับสองขอบเท่านั้น พอตัดตำแหน่งสัมบูรณ์ทิ้ง สถานะทั้งหมดก็เหลือแค่หกแบบ และผมสุ่ม 300 ชุดเทียบสองเวอร์ชัน ตรงกันหมด
บทเรียนที่ยกไปข้ออื่นได้คือ วิธีบีบสถานะที่ปลอดภัยที่สุดไม่ใช่การนั่งคิดสถานะที่เล็กที่สุดตั้งแต่แรก แต่คือเขียนตัวที่จำเยอะเกินไปให้ถูกก่อน แล้วไล่ตัดสิ่งที่ไม่มีใครอ่าน
ให้ S[k] คือผลรวมสะสมของ k ต้นแรก จำนวนบัวลบจำนวนกกในช่วง
(i, j] ก็คือ S[j] − S[i] ตรง ๆ เงื่อนไขว่าทุกช่วงต้องต่างกันไม่เกินสอง
จึงแปลว่า |S[j] − S[i]| ≤ 2 สำหรับทุกคู่ ซึ่งพูดสั้น ๆ ได้ว่า
ค่ามากที่สุดของผลรวมสะสม ลบค่าน้อยที่สุด ต้องไม่เกินสอง
พูดอีกแบบคือ เส้นผลรวมสะสมต้องเดินอยู่ในรางกว้างสองหน่วยตลอดทั้งเส้น เงื่อนไขที่มองแวบแรกเหมือนต้องตรวจทุกช่วง กลายเป็นเงื่อนไขเดียวที่ตรวจได้ระหว่างเดิน
ทีนี้ตอนเราเดินจากซ้ายไปขวา ข้อมูลที่ต้องจำมีแค่สองอย่างคือตอนนี้อยู่สูงจากพื้นรางเท่าไร
กับรางกว้างเท่าไรแล้ว ตำแหน่งจริงของผลรวมสะสมไม่สำคัญเลย เพราะเงื่อนไขสนใจแต่ระยะสัมพัทธ์
ตั้ง a เป็นผลรวมสะสมลบค่าน้อยสุดที่เคยเจอ และ b เป็นค่ามากสุดลบค่าน้อยสุด
จะได้ 0 ≤ a ≤ b ≤ 2 ซึ่งมีแค่ 6 แบบ
| สถานะ (a, b) | ความหมาย | ปลูกบัว ไปไหน | ปลูกกก ไปไหน |
|---|---|---|---|
(0, 0) | สูงจากพื้นราง 0 รางกว้าง 0 | (1, 1) | (0, 1) |
(0, 1) | สูงจากพื้นราง 0 รางกว้าง 1 | (1, 1) | (0, 2) |
(1, 1) | สูงจากพื้นราง 1 รางกว้าง 1 | (2, 2) | (0, 1) |
(0, 2) | สูงจากพื้นราง 0 รางกว้าง 2 | (1, 2) | ตัน |
(1, 2) | สูงจากพื้นราง 1 รางกว้าง 2 | (2, 2) | (0, 2) |
(2, 2) | สูงจากพื้นราง 2 รางกว้าง 2 | ตัน | (1, 2) |
// สถานะคือ (a, b) โดย a = ผลรวมสะสม ลบ ค่าน้อยสุดที่เคยเจอ
// b = ค่ามากสุดที่เคยเจอ ลบ ค่าน้อยสุดที่เคยเจอ
// เงื่อนไขสมดุลคือ b <= 2 เสมอ จึงมีสถานะที่ใช้ได้แค่หกแบบ
static inline int stepState(int s, int c) {
int a = SA[s], b = SB[s];
if (c == 0) { // ปลูกต้นบัว ผลรวมสะสมบวกหนึ่ง
int na = a + 1, nb = b;
if (na > nb) nb = na; // ดันเพดานขึ้น
if (nb > 2) return -1; // ช่วงกว้างเกินสอง สวนนี้ไม่สมดุลแล้ว
return SID[na][nb];
}
int na = a - 1, nb = b; // ปลูกต้นกก ผลรวมสะสมลบหนึ่ง
if (na < 0) { na = 0; nb = b + 1; } // พื้นต่ำลง ระยะห่างจากพื้นถึงเพดานจึงกว้างขึ้น
if (nb > 2) return -1;
return SID[na][nb];
}
พอนับได้แล้ว การหาอันดับก็เป็นท่ามาตรฐาน คำนวณ g[i][t] ไว้ก่อนว่า
ถ้ายืนที่ตำแหน่ง i ด้วยสถานะ t จะเติมส่วนที่เหลือได้กี่แบบ
คิดถอยหลังจากท้ายมาหน้าในเวลาเชิงเส้น
แล้วเดินตามสตริงที่โจทย์ให้มา ทุกตำแหน่งที่โจทย์เลือก P แปลว่าสวนทุกแบบที่เลือก
L ตรงนี้ (โดยที่ข้างหน้าเหมือนกันทุกตัว) อยู่ก่อนหน้าสวนของเรา
ก็บวกจำนวนนั้นเข้าไปในอันดับ พอเดินจบก็บวกหนึ่งเพราะอันดับเริ่มที่หนึ่ง
ความยาว 5 มีสวนสมดุล 14 แบบ ตรงกับที่ไล่นับตรง ๆ ได้ 14 แบบพอดี
| ต้นที่ | โจทย์ปลูก | สถานะก่อนปลูก | ถ้าปลูกบัวแทน จะมีกี่แบบ | อันดับสะสม |
|---|---|---|---|---|
| 1 | P | (0, 0) | 7 | 8 |
| 2 | L | (0, 1) | 8 | |
| 3 | P | (1, 1) | 2 | 10 |
| 4 | P | (0, 1) | 2 | 12 |
| 5 | L | (0, 2) | 12 |
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 2 | 4 | 6 | 10 | 14 | 22 | 30 | 46 | 62 | 94 | 126 | 190 |
ชุดทดสอบย่อยให้ N ไม่เกิน 40 ซึ่งแปลว่าเราไม่ต้องมองออกว่าสถานะยุบเหลือ 6 แบบ
แค่จำสถานะดิบ ๆ ทั้งชุดคือ (ตำแหน่ง, ผลรวมสะสม, ค่าน้อยสุดที่เคยเจอ, ค่ามากสุดที่เคยเจอ)
ใส่ไว้ใน map ก็พอ
ที่มันรอดได้เพราะเงื่อนไขสมดุลตัดสถานะทิ้งเองอยู่แล้ว สถานะที่ไปถึงได้จริงมีไม่มาก ท่านี้ตรงกับวิธีที่คนส่วนใหญ่คิดออกก่อน และมันก็เป็นตัวตรวจสอบชั้นดี สำหรับเวอร์ชันบีบสถานะที่เขียนทีหลัง ผมสุ่มสวนสมดุลเล็ก ๆ 300 ชุดมาเทียบสองโปรแกรม ตรงกันหมด
บทเรียนคือ ลำดับการคิดที่ปลอดภัยคือเขียนเวอร์ชันจำสถานะดิบให้ถูกก่อน แล้วค่อยถามว่า "ในสถานะทั้งหมดนี้ มีข้อมูลส่วนไหนที่ไม่เคยถูกใช้จริง" คำตอบตรงนั้นคือทางบีบ ที่นี่คำตอบคือตำแหน่งสัมบูรณ์ของผลรวมสะสมไม่เคยถูกใช้ ใช้แต่ระยะสัมพัทธ์
| ทาง | ขอบเขตที่มันไหว | งานคร่าว ๆ |
|---|---|---|
| จำสถานะดิบไว้ใน map | N ไม่เกิน 40 | สถานะที่ไปถึงได้ นับด้วยการรันจริง |
| บีบสถานะเหลือ 5 แบบ | N ถึง 1,000,000 | 5,000,000 ครั้ง |
ตาราง g มีขนาด (N + 1) × 6 ซึ่งที่ N เต็มขอบเขตคือ
6,000,006 ช่อง เก็บเป็น long long ก็ราว
46 เมกะไบต์ ยังอยู่ในลิมิต 64 เมกะไบต์
ถ้าอยากประหยัดกว่านี้เก็บเป็น int ได้ เพราะทุกค่าถูกหารเอาเศษด้วย M ที่ไม่เกินสิบล้านอยู่แล้ว
เวลาที่วัดได้บนเครื่องผม สวนยาว 1,000,000 ต้นที่สุ่มให้สมดุล ใช้เวลา 88 มิลลิวินาที จากลิมิต 1.5 วินาที
ท่านับก่อนแล้วค่อยเดินหาอันดับนี้ใช้ซ้ำได้กับโจทย์อีกหลายข้อ อ่านแบบเต็ม ๆ ได้ที่บทปูพื้นฐาน นับก่อน แล้วค่อยเดินไปหาตัวที่ต้องการ
เงื่อนไขที่พูดถึงทุกช่วงติดกัน มักแปลเป็นเงื่อนไขบนผลรวมสะสมได้ และพอแปลแล้วสิ่งที่ต้องจำก็เหลือแค่ ระยะสัมพัทธ์ ไม่ใช่ตำแหน่งจริง ที่นี่มันยุบเหลือ 6 สถานะ ซึ่งเล็กพอจะเดินผ่านสวนล้านต้นได้ในรอบเดียว
ในหน้านี้