ปูพื้นฐาน
ท่าที่ใช้ได้ทุกครั้งที่ผลของก้าวถัดไปขึ้นกับของไม่กี่ชิ้นล่าสุด บทนี้ให้ข้อสอบสองข้อที่ใช้ตัดสินว่าสิ่งที่จะจำนั้นเป็นสถานะที่ถูกต้องไหม แล้วจบด้วยการนับจำนวนสถานะ ซึ่งเป็นตัวชี้ขาดว่าท่านี้รอดหรือตาย พร้อมโจทย์ฝึก 3 ข้อ
โจทย์ตัวอย่างสั้นที่สุดที่ทำให้เห็นปัญหานี้คือ ให้ของเรียงมาเป็นแถว เลือกกี่ชิ้นก็ได้ ได้คะแนนเท่าค่าของชิ้นที่เลือก มีกติกาข้อเดียวคือห้ามเลือกของสามชิ้นที่ติดกัน ให้คะแนนรวมมากที่สุด
ท่ามาตรฐานของ DP คือให้ dp[i] เป็นคะแนนดีที่สุดเมื่อดูของ i ชิ้นแรก แล้วเดินไปข้างหน้าทีละชิ้น
ลองใช้กับแถวสั้น ๆ แค่สามชิ้นคือ 5 5 100 ดู
ดูของสองชิ้นแรกก่อน คะแนนดีที่สุดคือเลือกทั้งคู่ ได้ 10 ถ้าเราจดไว้แค่เลขนี้แล้วเดินต่อ พอถึงชิ้นที่สามซึ่งมีค่า 100 เราจะเลือกมันไม่ได้ เพราะนั่นจะเป็นการเลือกสามชิ้นติดกัน คำตอบที่ได้คือ 10
แต่คำตอบจริงของแถวนี้คือ 105 ซึ่งได้จากการเลือกชิ้นที่ 1 กับ 3 นั่นคือยอมทิ้งชิ้นที่สอง ทั้งที่มันทำให้คะแนนของคำนำหน้าลดลงจาก 10 เหลือ 5
ตรงนี้คือหัวใจของทั้งบท
ทางที่คะแนนน้อยกว่าในตอนนี้ กลับเป็นทางที่พาไปสู่คำตอบที่ดีกว่าในตอนจบ เพราะมันทิ้งสภาพ ที่ดีกว่าไว้ให้ก้าวถัดไป ตราบใดที่ตารางเก็บได้แค่ตัวเลขเดียวต่อหนึ่งคำนำหน้า มันจะโยนทางที่ถูกทิ้งไปทุกครั้ง โดยไม่มีทางรู้ตัวเลย
ของสี่ชิ้น พอให้เห็นกติกาว่าห้ามติดกันสามชิ้น
กติกาข้อเดียว ห้ามเลือกของสามชิ้นที่ติดกัน
กดเลือกของที่จะเก็บ ห้ามเลือกสามชิ้นที่ติดกัน
แต้มตอนนี้ 0 · เลือกติดกันยาวสุด 0 ชิ้น
ตัวเลขทุกตัวเป็นบวก ดังนั้นการไม่เลือกเลยไม่เคยเป็นทางที่ดี
ตอนตัดสินใจของชิ้นถัดไป ลองสังเกตว่าคุณต้องมองย้อนกลับไปกี่ชิ้น แล้วนั่นแหละคือขนาดของสิ่งที่ต้องจำ
สิ่งที่ตารางขาดคือ "ตอนนี้เลือกติดกันมาแล้วกี่ชิ้น" ถ้าเพิ่มค่านี้เข้าไปเป็นดัชนีที่สองของตาราง ทางที่คะแนนน้อยกว่า แต่สภาพดีกว่าก็จะไม่ถูกโยนทิ้ง เพราะมันไปอยู่คนละช่องกัน
ค่านี้เป็นได้แค่ 0, 1 หรือ 2 เท่านั้น (ถ้าถึง 3 ก็ผิดกติกาไปแล้ว) ตารางจึงกว้างขึ้นแค่สามเท่า ไม่ใช่โตแบบทวีคูณตามความยาวแถว และนี่คือรูปแบบที่บทนี้จะพูดถึงตลอด คือสถานะที่เป็นความจำล่าสุด ของสิ่งที่เพิ่งเกิดขึ้น ไม่ใช่บันทึกของอดีตทั้งหมด
การยุบอดีตทั้งกองให้เหลือค่าเล็ก ๆ ค่าเดียวแบบนี้เรียกว่าการบีบสถานะ (state compression) ชื่อมันตรงกับสิ่งที่เกิดขึ้นเลย คืออดีตถูกบีบจนเหลือเท่าที่จำเป็น ระวังคำนี้ไว้อย่างหนึ่ง การบีบสถานะไม่ได้แปลว่าย่อข้อมูลให้เล็กลงเฉย ๆ มันคือการทิ้งสิ่งที่ไม่มีผลต่ออนาคต ซึ่งต้องพิสูจน์ได้ว่าทิ้งแล้วไม่เสียคำตอบ สองข้อสอบข้างล่างนี้คือวิธีพิสูจน์
สองข้อสอบที่ของชิ้นหนึ่งต้องผ่าน ถึงจะเรียกว่าสถานะได้
ของที่ตกข้อใดข้อหนึ่งใช้เป็นสถานะไม่ได้ ส่วนของที่ผ่านทั้งสองข้อยังต้องผ่านด่านที่สามอีกหนึ่งด่าน คือมันมีได้กี่แบบ ซึ่งเป็นด่านที่ตัดสินว่าท่านี้รอดหรือตาย
สมการจึงมีแค่สองบรรทัด อ่านจากมุมของการผลัก คือยืนที่สถานะเดิมแล้วส่งค่าออกไปยังปลายทาง
บรรทัดบนคือข้ามของชิ้นนี้ ช่วงที่เลือกติดกันขาดทันที ไม่ว่าก่อนหน้าจะค้างมากี่ชิ้นก็ตกมาที่ศูนย์เหมือนกันหมด บรรทัดล่างคือเก็บของชิ้นนี้ ช่วงยาวขึ้นหนึ่ง ซึ่งทำได้เฉพาะตอนที่ค้างอยู่ไม่ถึงสองชิ้น
เอาแถว 5 5 100 1 8 9 มาเดินจริง แต่ละแถวของตารางคือหลังตัดสินใจของชิ้นนั้นเสร็จแล้ว
ช่องที่เป็นจุดคือสถานะที่ยังไปไม่ถึง
กดถัดไปเพื่อดูตารางโตขึ้นทีละแถว
| หลังชิ้นที่ | ค้าง 0 ชิ้น | ค้าง 1 ชิ้น | ค้าง 2 ชิ้น |
|---|---|---|---|
| ก่อนเริ่ม | 0 | · | · |
| 1. ค่า 5 | 0 | 5 | · |
| 2. ค่า 5 | 5 | 5 | 10 |
| 3. ค่า 100 | 10 | 105 | 105 |
| 4. ค่า 1 | 105 | 11 | 106 |
| 5. ค่า 8 | 106 | 113 | 19 |
| 6. ค่า 9 | 113 | 115 | 122 |
แผนที่ดีที่สุดคือเก็บชิ้นที่ 1, 3, 5, 6 รวม 122 คะแนน
สังเกตแถวที่เขียนว่า 2. ค่า 5 สองช่องซ้ายยังเก็บ 5 ไว้
ทั้งที่ช่องขวาสุดของแถวเดียวกันมี 10 ซึ่งมากกว่า ถ้าตารางมีคอลัมน์เดียว
เลข 10 จะกลืนที่เหลือไปหมดตั้งแต่ตรงนั้น แล้วของชิ้นที่ 3 ที่มีค่า 100
ก็จะเก็บไม่ได้ ทั้งที่แถวถัดมาบอกชัดว่าเลข 5 คือตัวที่พาไปถึง 105
สองข้อสอบข้างบนบอกว่าอะไรใช้เป็นสถานะได้ แต่ไม่ได้บอกว่ามันคุ้ม
เพราะของที่ผ่านสองข้อนั้นเสมอมีอยู่อย่างหนึ่ง คือ "จำอดีตทั้งหมดไว้เลย" ซึ่งพอไหมก็พอ ปิดตัวเองไหมก็ปิด
แต่มันมีได้ถึง 2^n แบบ ซึ่งที่ n = 100,000 คือจำนวนที่เขียนลงกระดาษยังไม่ไหว
เวลาความจำที่ต้องเก็บมีหน้าตาเป็นหน้าต่างของค่าไม่กี่ค่าล่าสุด จำนวนสถานะคิดตรง ๆ ได้จากสองตัวเลข คือค่าหนึ่งช่องเป็นได้กี่แบบ กับหน้าต่างกว้างกี่ช่อง
| ที่ไหน | จำอะไร | ค่าต่อช่อง | กว้าง | มีกี่แบบ |
|---|---|---|---|---|
| ตัวอย่างประจำบท | เลือกติดกันมาแล้วกี่ชิ้น | 3 | 1 | 3 |
| ฝึกข้อ 2 | สองช่องล่าสุดสีเหมือนกันไหม | 2 | 1 | 2 |
| ฝึกข้อ 3 | ตัวอักษรสองตัวล่าสุด | 3 | 2 | 9 |
| ข้อ Miners | คู่ล่าสุดของสองเหมือง | 4 | 4 | 256 |
| ถ้าเผลอจำทั้งอดีต | ตัดสินใจไปแล้วยังไงบ้างทุกชิ้น | 2 | n | 2^n |
คิดก่อนอ่านต่อ
แถวที่สองของตารางน่าสนใจกว่าที่เห็น ในโจทย์ทาสีนั้นค่าหนึ่งช่องคือสี ซึ่งมีได้เป็นล้านแบบ แต่ช่องขวาสุดกลับเขียนว่ามีสถานะแค่สองแบบ ลองเดาก่อนว่าเป็นไปได้ยังไง คำถามที่จะพาไปถึงคำตอบคือ กติกาของโจทย์นั้นถามถึงค่าของช่องก่อนหน้า หรือถามถึงความสัมพันธ์ ระหว่างช่องก่อนหน้ากับช่องนี้กันแน่
ตารางไม่ต้องเก็บทั้งผืน เพราะสมการอ้างถึงแถวก่อนหน้าแค่แถวเดียว เก็บสองแถวสลับกันไปมาก็พอ หน่วยความจำจึงเป็นค่าคงที่ ไม่ขึ้นกับความยาวแถวเลย
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;
scanf("%d", &n);
vector<long long> a(n);
for (auto& x : a) scanf("%lld", &x);
const long long NEG = LLONG_MIN / 4; // เผื่อที่ให้บวกค่าเข้าไปโดยไม่ล้น
vector<long long> cur(3, NEG), nxt(3);
cur[0] = 0; // ยังไม่เลือกอะไร ค้างศูนย์ชิ้น
for (int i = 0; i < n; i++) {
fill(nxt.begin(), nxt.end(), NEG); // ต้องล้างก่อนทุกรอบ เพราะเราเขียนแบบผลัก
for (int j = 0; j < 3; j++) {
if (cur[j] == NEG) continue; // สถานะนี้ยังไปไม่ถึง
nxt[0] = max(nxt[0], cur[j]); // ข้ามชิ้นนี้ ช่วงที่เลือกติดกันขาด
if (j < 2) nxt[j + 1] = max(nxt[j + 1], cur[j] + a[i]); // เก็บชิ้นนี้ ช่วงยาวขึ้นหนึ่ง
}
cur = nxt;
}
printf("%lld\n", *max_element(cur.begin(), cur.end()));
return 0;
}
สองบรรทัดที่คนพลาดบ่อยที่สุดอยู่ในนี้ทั้งคู่ อย่างแรกคือ fill ที่ล้างแถวปลายทางก่อนทุกรอบ
เพราะเราเขียนแบบผลัก ปลายทางบางช่องอาจไม่มีใครส่งค่าเข้ามาเลยในรอบนี้ ถ้าไม่ล้างก็จะเหลือค่าเก่าจากรอบก่อนค้างอยู่
แล้วกลายเป็นสถานะผีที่ไม่มีทางเดินไปถึงได้จริง
อย่างที่สองคือค่า NEG ที่ใช้แทน "สถานะนี้ยังไปไม่ถึง" ซึ่งต้องแยกจากคะแนน 0 ให้ขาด เพราะ 0
เป็นคะแนนที่ถูกต้องได้ (ตอนยังไม่เลือกอะไรเลย) และต้องหารสี่ทิ้งไว้ด้วย เพราะเราจะเอาค่าไปบวกกับ a[i]
ถ้าใช้ LLONG_MIN ตรง ๆ แล้วบวกเลขบวกเข้าไป มันจะล้นกลับไปเป็นค่าบวกมหาศาลแล้วชนะทุกอย่างในตาราง
สามข้อนี้ตอบคนละคำถาม ข้อแรกถามว่า "ท่านี้ย้ายไปกติกาที่กลับด้านกันได้ไหม" ข้อสองถามว่า "ถ้าค่าหนึ่งช่องมีได้เป็นล้านแบบจะทำยังไง" ข้อสามถามว่า "แล้วเมื่อไรที่ต้องจำค่าจริง ๆ" ลองคิดเองก่อนแล้วค่อยแตะดูเฉลยครับ
โจทย์กำหนด
ของเรียงมาเป็นแถว n ≤ 200 000 ชิ้น ชิ้นที่ i มีราคา c[i] ซึ่งเป็นบวก
คุณต้องซื้อบางชิ้น โดยมีกติกาว่าห้ามเว้นสามชิ้นติดกัน คือในทุกช่วงยาวสามชิ้นต้องมีของที่ซื้ออย่างน้อยหนึ่งชิ้น
ให้จ่ายน้อยที่สุด
| Input | Output |
|---|---|
| 8 7 2 9 4 1 8 3 6 | 6 |
อ่านตัวอย่างนี้ยังไง
อินพุตคือจำนวนชิ้นแล้วตามด้วยราคาของแต่ละชิ้นเรียงตามแถว เอาต์พุตคือเงินที่จ่าย ไม่ใช่จำนวนชิ้นที่ซื้อ ราคาทุกชิ้นเป็นบวก การซื้อเพิ่มจึงไม่มีวันถูกลง งานคือซื้อให้น้อยที่สุดเท่าที่กติกายอม
กติกา "ห้ามเว้นสามชิ้นติดกัน" อ่านง่ายที่สุดโดยไล่หน้าต่างยาวสามชิ้นทุกหน้าต่าง ทุกหน้าต่างต้องมีของที่ซื้ออย่างน้อยหนึ่งชิ้น ชุดนี้จ่ายน้อยที่สุดได้ 6 โดยซื้อชิ้นที่ 2, 5, 7
ท่าที่คนคิดออกก่อนคือ "เว้นให้ครบสองชิ้นเสมอแล้วซื้อชิ้นที่สาม" ซึ่งชุดนี้จ่าย 17 แพงกว่าคำตอบจริง เพราะมันไม่สนใจว่าชิ้นที่มันบังคับให้ซื้อจะแพงแค่ไหน
คำใบ้
กติกาข้อนี้เป็นภาพสะท้อนในกระจกของโจทย์ประจำบทพอดี ลองถามว่าสิ่งที่ต้องนับตอนนี้คืออะไร แล้วเปลี่ยนอะไรบ้างในสมการสองบรรทัดข้างบน
ที่มาของท่านี้
คำถามที่ยากของบทนี้ไม่ใช่ "จะเขียนสมการยังไง" แต่คือ "จะจำอะไร" และมันมีวิธีหาคำตอบที่ตรงไปตรงมากว่าการนั่งเดา คือไปอ่านที่ตัวกติกา แล้วดูว่ามันตรวจอะไรบ้าง กติกาข้อนี้พูดว่า "ห้ามเว้นสามชิ้นติดกัน" มันจึงตรวจแค่จำนวนชิ้นที่เว้นติดกันมาแล้ว สิ่งนั้นคือสิ่งเดียวที่อนาคตต้องรู้จากอดีต ที่เหลือทิ้งได้หมด
ท่านี้ให้ผลชัดที่สุดในโจทย์ฝึกข้อสอง กติกาบอกว่าห้ามสามช่องติดกันเป็นสีเดียวกัน
ถ้าอ่านแบบผ่าน ๆ จะรู้สึกว่าต้องจำสีของช่องล่าสุด ซึ่งแปลว่าตารางมี k ช่อง
และที่ k ถึงพันล้าน ตารางนั้นสร้างไม่ได้เลย แต่พออ่านที่กติกาจริง ๆ มันไม่เคยถามว่าสีอะไร
มันถามแค่ว่าเหมือนกันหรือเปล่า สถานะจึงยุบจากพันล้านเหลือสองแบบ
และเพราะการยุบแบบนี้ฟังดูดีเกินจริง ผมไม่เชื่อมันจากการให้เหตุผลอย่างเดียว
ผมเขียนตัวไล่ทาสีทุกวิธีจริง ๆ แล้วเทียบกับสูตรสองสถานะ ทุกค่าของ n ตั้งแต่ 1 ถึง 8
คู่กับทุกค่าของ k ตั้งแต่ 1 ถึง 4 ตรงกันหมดทุกช่อง รวมกรณีขอบอย่าง k = 1
ที่ทาได้แบบเดียวจนกระทั่ง n ถึงสาม แล้วคำตอบกลายเป็นศูนย์
บทเรียนที่ยกไปข้ออื่นได้คือ ก่อนตั้งสถานะ ให้เขียนกติกาออกมาเป็นประโยคแล้วขีดเส้นใต้คำที่มันตรวจจริง คำที่ไม่ถูกขีดเส้นใต้ ไม่ต้องจำ
สิ่งที่ต้องจำเปลี่ยนจาก "เลือกติดกันมาแล้วกี่ชิ้น" เป็น "เว้นติดกันมาแล้วกี่ชิ้น" ซึ่งยังเป็น 0, 1, 2 เหมือนเดิม ส่วนสมการก็สลับที่กัน การเลือกกลายเป็นสิ่งที่ตัดช่วงให้ขาดแล้วกลับไปที่ศูนย์ และการเว้น คือสิ่งที่ทำให้ช่วงยาวขึ้น
จุดที่ต้องคิดเพิ่มคือค่าตั้งต้นต้องเป็นค่ามาก ไม่ใช่ค่าน้อย เพราะเราหาค่าต่ำสุด และเงื่อนไข "ห้ามเว้นสามติดกัน" ที่ปลายแถวก็จบเองโดยไม่ต้องเขียนอะไรเพิ่ม เพราะสถานะที่เว้นครบสามชิ้นไม่เคยถูกสร้างขึ้นมาตั้งแต่แรก
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;
scanf("%d", &n);
vector<long long> c(n);
for (auto& x : c) scanf("%lld", &x);
const long long INF = LLONG_MAX / 4;
vector<long long> cur(3, INF), nxt(3);
cur[0] = 0; // ยังไม่เว้นติดกันเลย
for (int i = 0; i < n; i++) {
fill(nxt.begin(), nxt.end(), INF);
for (int j = 0; j < 3; j++) {
if (cur[j] == INF) continue;
nxt[0] = min(nxt[0], cur[j] + c[i]); // เลือก ช่วงที่เว้นขาดทันที
if (j < 2) nxt[j + 1] = min(nxt[j + 1], cur[j]); // เว้น ช่วงยาวขึ้นหนึ่ง
}
cur = nxt;
}
printf("%lld\n", *min_element(cur.begin(), cur.end()));
return 0;
} สุ่มเทียบกับตัวลองทุกชุด 500 แถว ตรงกันทั้งหมด
โจทย์กำหนด
มีช่องเรียงกัน n ช่อง ทาสีได้ k สี ห้ามให้สามช่องที่ติดกันเป็นสีเดียวกัน
นับจำนวนวิธีทาทั้งหมด เอาเศษจากการหารด้วย 10^9 + 7 โดย n ≤ 10^18 และ
k ≤ 10^9
| Input | Output |
|---|---|
| 4 3 | 66 |
อ่านตัวอย่างนี้ยังไง
อินพุตมีแค่สองเลขคือจำนวนช่องกับจำนวนสี ไม่มีอาเรย์อะไรเลย เอาต์พุตคือจำนวนวิธีทา ที่ถูกกติกา ตัวอย่างนี้เล็กพอที่จะไล่ด้วยมือได้ ทาได้ทั้งหมด 3 ยกกำลัง 4 เท่ากับ 81 แบบ ในนั้นถูกกติกา 66 แบบ ที่เหลือ 15 แบบมีสามช่องติดกันเป็นสีเดียว เช่น แดง แดง แดง แดง
ที่ต้องระวังคือ n ของจริงใหญ่ถึงสิบยกกำลังสิบแปด แปลว่าห้ามวนทีละช่องด้วยซ้ำ
ตัวอย่างเล็ก ๆ นี้มีไว้ให้เห็นว่าโจทย์นับอะไร ไม่ได้มีไว้ให้เห็นวิธีทำ
คำใบ้
ตอบคำถามในกล่อง "คิดก่อนอ่านต่อ" ข้างบนก่อน กติกาบอกว่าห้ามสามช่องเป็นสีเดียวกัน มันไม่ได้สนใจเลยว่าสีนั้นคือสีอะไร
ถ้าจำ "ช่องล่าสุดเป็นสีอะไร" เราจะได้ตาราง k ช่อง ซึ่งที่ k ระดับพันล้านคือสร้างไม่ไหว
แต่ลองใช้ข้อสอบสองข้อกับของที่เล็กกว่านั้น คือ "สองช่องล่าสุดสีเหมือนกันไหม" ซึ่งมีแค่สองแบบ
k − 1 ทาง) ถ้าสองช่องล่าสุดสีเหมือนกัน ช่องถัดไปต้องทาสีอื่นเท่านั้น
(k − 1 ทาง) ทั้งหมดนี้ไม่ต้องรู้เลยว่าสีนั้นชื่ออะไร
จำนวนสถานะจึงหล่นจาก k เหลือ 2 และเพราะแต่ละก้าวเป็นแค่การคูณกับบวก โค้ดจึงเป็นลูปเดียว
ไม่ต้องมีตารางเลย
#include <bits/stdc++.h>
using namespace std;
const long long MOD = 1000000007;
int main() {
long long n, k;
scanf("%lld %lld", &n, &k);
long long k1 = (k - 1) % MOD;
// สถานะไม่ใช่ "ช่องล่าสุดเป็นสีอะไร" ซึ่งมี k แบบ
// แต่เป็น "สองช่องล่าสุดสีเหมือนกันไหม" ซึ่งมีสองแบบ ไม่ว่า k จะใหญ่แค่ไหน
long long diff = k % MOD, same = 0; // ช่องแรกนับรวมไว้ฝั่ง diff
for (long long i = 2; i <= n; i++) {
long long nd = (diff + same) % MOD * k1 % MOD; // ทาสีต่างจากช่องก่อนหน้า
long long ns = diff; // ทาซ้ำได้ ก็ต่อเมื่อคู่ก่อนหน้าไม่ซ้ำ
diff = nd;
same = ns;
}
printf("%lld\n", (diff + same) % MOD);
return 0;
}
สุ่มเทียบกับตัวไล่ทุกการทาสี 200 เทส ที่ n ≤ 8 และ k ≤ 4 ตรงกันทั้งหมด
รวมกรณี k = 1 ซึ่งตอบ 0 ทันทีที่ n ≥ 3
ระวัง
n ระดับ 10^18 แปลว่าลูปที่วน n รอบก็ยังช้าเกินไปอยู่ดี
โค้ดข้างบนตอบได้ถึงราว n หลักร้อยล้าน ถ้าโจทย์ดัน n ไปไกลกว่านั้น
ท่าถัดไปคือเขียนการก้าวหนึ่งขั้นเป็นเมทริกซ์ 2 คูณ 2 แล้วยกกำลังด้วยการยกกำลังเร็ว
ซึ่งเป็นไปได้ก็เพราะเราบีบสถานะเหลือสองแบบไว้ก่อนแล้ว
โจทย์กำหนด
นับสตริงยาว n ที่สร้างจากตัวอักษร a, b, c
โดยห้ามมีตัวอักษรเดียวกันสามตัวติดกัน และห้ามมีคำว่า abc โผล่เป็นสามตัวติดกัน
เอาเศษจากการหารด้วย 10^9 + 7
| Input | Output |
|---|---|
| 4 | 60 |
อ่านตัวอย่างนี้ยังไง
อินพุตคือความยาวอย่างเดียว เอาต์พุตคือจำนวนสตริงที่ถูกกติกาทั้งสองข้อพร้อมกัน
สตริงยาว 4 สร้างได้ 81 แบบ ถ้าตัดเฉพาะแบบที่มีตัวเดียวกันสามตัวติดกัน
จะเหลือ 66 แบบ แล้วในนั้นยังมีอีก 6 แบบที่มีคำว่า
abc โผล่อยู่ ต้องตัดทิ้งอีกชั้น เหลือ 60 แบบ
ตรงนี้คือจุดที่ข้อนี้ต่างจากข้อ 2 กติกาข้อแรกสนใจแค่ว่า "เหมือนตัวก่อนหน้าไหม"
แต่กติกาข้อสองสนใจว่าตัวก่อนหน้าคือตัวอะไรจริง ๆ เพราะ abc
เป็นสามตัวที่ต่างกันหมด สถานะแบบเดิมจึงมองไม่เห็น
คำใบ้
ลองใช้ท่าของฝึกข้อ 2 ดูก่อน คือจำแค่ว่าสองตัวล่าสุดเหมือนกันไหม แล้วดูว่ามันตกข้อสอบข้อไหน คำตอบอยู่ที่กติกาข้อที่สองของโจทย์นี้
ท่าของข้อ 2 ตกข้อสอบข้อหนึ่ง ทันที กติกา "ห้ามมี abc" ถามถึงค่าจริง ๆ ของสองตัวล่าสุด
ไม่ใช่แค่ความสัมพันธ์ ถ้าเรารู้แค่ว่าสองตัวล่าสุด "ต่างกัน" เราตอบไม่ได้ว่าเติม c ต่อท้ายได้หรือเปล่า
เพราะมันขึ้นกับว่าสองตัวนั้นคือ ab หรือ ba หรือ ac
สถานะจึงต้องเป็นคู่ตัวอักษรสองตัวล่าสุดจริง ๆ ซึ่งมี 3 คูณ 3 เท่ากับ 9 แบบ ยังเล็กมาก
อัดเป็นเลขตัวเดียวด้วย p × 3 + q โดยให้ q เป็นตัวล่าสุดเสมอ เวลาเติมตัวใหม่
สถานะปลายทางคือ q × 3 + r ซึ่งอ่านออกทันทีว่า "ตัวเก่าหลุดออกไปหนึ่งตัว ตัวใหม่เข้ามาหนึ่งตัว"
#include <bits/stdc++.h>
using namespace std;
const long long MOD = 1000000007;
int main() {
long long n;
scanf("%lld", &n);
if (n == 1) { printf("3\n"); return 0; } // ยังไม่มีคู่ให้จำ ตอบตรง ๆ
// สถานะคือคู่ตัวอักษรสองตัวล่าสุด อัดเป็น p*3+q โดย q คือตัวล่าสุด
vector<long long> cur(9, 0), nxt(9);
for (int p = 0; p < 3; p++)
for (int q = 0; q < 3; q++) cur[p * 3 + q] = 1; // ความยาว 2 ทุกคู่ยังไม่ผิดกติกา
for (long long i = 3; i <= n; i++) {
fill(nxt.begin(), nxt.end(), 0);
for (int p = 0; p < 3; p++)
for (int q = 0; q < 3; q++) {
long long v = cur[p * 3 + q];
if (!v) continue;
for (int r = 0; r < 3; r++) {
if (p == q && q == r) continue; // สามตัวติดกันเหมือนกัน
if (p == 0 && q == 1 && r == 2) continue; // abc
nxt[q * 3 + r] = (nxt[q * 3 + r] + v) % MOD;
}
}
cur = nxt;
}
long long s = 0;
for (int i = 0; i < 9; i++) s = (s + cur[i]) % MOD;
printf("%lld\n", s);
return 0;
}
ผมเทียบกับตัวไล่ทุกสตริงครบทุกค่าของ n ตั้งแต่ 1 ถึง 12 ไม่ใช่แค่สุ่ม
บวกกับสุ่มอีก 120 เทส ตรงกันทั้งหมด ที่ต้องไล่ให้ครบเพราะกรณี n = 1 กับ n = 2
ยังไม่มีคู่ให้จำ ต้องดักแยกไว้ ซึ่งเป็นจุดที่โค้ดพลาดกันบ่อยที่สุดของข้อนี้
ทั้งบทนี้บีบอดีตให้เหลือตัวเลขตัวเดียว คือจำนวนที่เลือกติดกันมาแล้ว ซึ่งเป็นไปได้เพราะสิ่งที่อนาคตต้องรู้มีแค่นั้น แต่มีโจทย์อีกตระกูลที่สิ่งที่ต้องจำ เป็นแถวหนึ่งแถว ไม่ใช่ตัวเลขตัวเดียว และท่าเดิมก็ไม่พอทันที
ตัวอย่างที่เห็นภาพที่สุดคือ นับวิธีปูโดมิโนขนาด 1 คูณ 2 ให้เต็มตาราง n แถว
m คอลัมน์ เดินทีละคอลัมน์แล้วถามว่า "ต้องจำอะไรจากคอลัมน์ก่อนหน้า"
คำตอบคือ ช่องไหนของคอลัมน์นี้ถูกโดมิโนแนวนอนจากคอลัมน์ก่อนกินไปแล้ว
ซึ่งเป็นข้อมูล n บิต ไม่ใช่ตัวเลขตัวเดียว
ท่าที่รับมือคือเก็บ n บิตนั้นเป็นจำนวนเต็มหนึ่งตัว โดยให้บิตที่
i แทนแถวที่ i จำนวนเต็มตัวนั้นเรียกว่าหน้ากากบิต (bitmask)
และ DP ที่ใช้มันเป็นสถานะเรียกว่า DP บนหน้ากากบิต รูปที่เดินทีละช่อง
ไม่ใช่ทีละคอลัมน์เต็ม เรียกเฉพาะลงไปอีกว่า broken profile
สองข้อสอบของบทนี้ยังใช้ได้เหมือนเดิม ข้อพอไหม รู้หน้ากากแล้วคิดทางเลือกของช่องถัดไป ได้ครบไหม ตอบว่าได้ เพราะช่องที่ถูกกินไปแล้วห้ามวางซ้ำ ที่เหลือวางได้ ข้อปิดตัวเองไหม รู้หน้ากากเดิมกับสิ่งที่เพิ่งวาง แล้วคำนวณหน้ากากใหม่ได้ไหม ตอบว่าได้ ด้วยการเปิดปิดบิตตรง ๆ
ส่วนด่านที่สามของบทนี้ คือสถานะมีได้กี่แบบ กลายเป็นเรื่องคอขวดตรงนี้พอดี
หน้ากาก n บิตมีได้ 2ⁿ แบบ ต้นทุนจึงเป็น O(m · n · 2ⁿ)
ซึ่งใช้ได้เฉพาะเมื่อ ด้านหนึ่งของตารางแคบ โค้ดข้างล่างจึงสลับให้ n
เป็นด้านที่แคบกว่าเสมอก่อนเริ่ม เพราะนั่นคือความต่างระหว่างทำได้กับทำไม่ได้
// DP บนหน้ากากบิต (broken profile): นับวิธีปูโดมิโน 1x2 ให้เต็มตาราง n แถว m คอลัมน์
// สถานะคือ "หน้ากากของช่องที่ยื่นล้ำมาจากคอลัมน์ก่อนหน้า" ซึ่งเป็นการบีบสถานะแบบเดียวกับบทนี้
// แต่สิ่งที่บีบไม่ใช่ตัวเลขเดียว มันคือแถวหนึ่งแถวที่เก็บเป็นบิต
// อินพุต: n m เอาต์พุต: จำนวนวิธี หารเอาเศษด้วย 1e9+7
#include <bits/stdc++.h>
using namespace std;
const long long MOD = 1000000007LL;
int main() {
int n, m;
if (scanf("%d %d", &n, &m) != 2) return 0;
if (n > m) swap(n, m); // ให้ n เป็นด้านที่แคบกว่า หน้ากากจึงเล็กที่สุด
int full = 1 << n;
vector<long long> cur(full, 0), nxt(full, 0);
cur[0] = 1;
for (int col = 0; col < m; col++) {
// เดินทีละคอลัมน์ ภายในคอลัมน์เดินทีละแถว
for (int row = 0; row < n; row++) {
fill(nxt.begin(), nxt.end(), 0);
for (int mask = 0; mask < full; mask++) {
long long v = cur[mask];
if (v == 0) continue;
if (mask >> row & 1) {
// ช่องนี้ถูกโดมิโนแนวนอนจากคอลัมน์ก่อนกินไปแล้ว ปิดบิตทิ้ง
nxt[mask ^ (1 << row)] = (nxt[mask ^ (1 << row)] + v) % MOD;
} else {
// วางแนวนอน ยื่นไปคอลัมน์ถัดไป
nxt[mask | (1 << row)] = (nxt[mask | (1 << row)] + v) % MOD;
// วางแนวตั้ง กินช่องนี้กับช่องล่างในคอลัมน์เดียวกัน
if (row + 1 < n && !(mask >> (row + 1) & 1))
nxt[mask | (1 << (row + 1))] = (nxt[mask | (1 << (row + 1))] + v) % MOD;
}
}
cur.swap(nxt);
}
}
printf("%lld\n", cur[0] % MOD);
return 0;
} คำถามที่ยกไปใช้กับข้ออื่นได้คือ สิ่งที่อนาคตต้องรู้ เป็นตัวเลขตัวเดียวหรือเป็นรูปร่าง ถ้าเป็นตัวเลขตัวเดียว ใช้ท่าในบทนี้ ถ้าเป็นรูปร่างที่แคบพอจะเขียนเป็นบิตได้ ให้เขียนเป็นหน้ากากบิต และถ้ามันกว้างเกินจะเขียนเป็นบิต ให้กลับไปดูว่าโจทย์ให้ขอบเขตแคบ ๆ ไว้ที่ไหนหรือไม่ เพราะขอบเขตนั้นมักเป็นคำใบ้ว่าอะไรที่ควรกลายเป็นหน้ากาก
เมื่อทางที่คะแนนน้อยกว่าตอนนี้พาไปคำตอบที่ดีกว่าตอนจบได้ แปลว่าตารางขาดสภาพไปหนึ่งมิติ ให้ถามว่าอนาคตต้องรู้อะไรจากอดีตบ้าง ตัดที่เหลือทิ้งให้หมด แล้วนับดูว่าสิ่งที่เหลือมีได้กี่แบบ ถ้าจำนวนนั้นไม่โตตามความยาวอินพุต ข้อนั้นก็จบแล้ว
ข้อที่ใช้ของในบทนี้เต็ม ๆ คือ Miners ซึ่งความจำคือคู่ล่าสุดของสองเหมืองรวมเป็น 256 แบบ ส่วน หยิบหนังสือ เป็นญาติที่ความจำไม่ใช่หน้าต่างของค่าล่าสุด แต่เป็นตัวนับที่ค้างอยู่ และบท DP บนช่วง คือกรณีที่ความจำแบบหน้าต่างช่วยไม่ได้เลย จนต้องเปลี่ยนสถานะทั้งใบ
ในหน้านี้