ปูพื้นฐาน
โจทย์ที่ขอ "ตัวที่ R ตามพจนานุกรม" กับโจทย์ที่ขอ "อันดับของตัวนี้" เป็นโค้ดชุดเดียวกันที่ต่างกันแค่ทิศทาง บทนี้ให้แม่พิมพ์นั้น พร้อมสองบักที่เกิดกับทุกคนอย่างน้อยครั้งหนึ่ง
โจทย์ตระกูลนี้ถามอยู่สองแบบ ซึ่งเป็นคำถามกลับด้านกัน
Rทั้งสองแบบมีกับดักเดียวกันคือ จำนวนของทั้งหมดโตแบบเลขชี้กำลัง การไล่สร้างทุกตัวแล้วเรียงจึงเป็นไปไม่ได้ทันทีที่ขนาดเกินสามสิบสี่สิบ
ท่ามาตรฐานที่แก้ทั้งสองแบบด้วยเครื่องมือชิ้นเดียวคือ นับก่อน แล้วค่อยเดิน ถ้าเรานับได้ว่า "ของที่ขึ้นต้นด้วยแบบนี้มีกี่ชิ้น" ทั้งสองคำถามจะกลายเป็นเรื่องง่ายทันที
สิ่งที่ต้องหาให้ได้คือฟังก์ชัน f(ตำแหน่ง, สถานะ) ที่ตอบว่า
ถ้ายืนอยู่ที่ตำแหน่งนี้ด้วยสถานะนี้ จะเติมส่วนที่เหลือได้กี่แบบ
ให้คิดถอยหลังจากท้ายมาหน้า เพราะการเติมส่วนที่เหลือไม่ขึ้นกับว่าเราเดินมายังไง ขึ้นกับสถานะปัจจุบันอย่างเดียว
งานที่ยากจริงของขั้นนี้ไม่ใช่การเขียนดีพี แต่คือการหาว่าอะไรคือสถานะที่เล็กที่สุดที่ยังพอ หลักที่ใช้ได้เสมอคือ เขียนเวอร์ชันที่จำทุกอย่างก่อนให้ถูก แล้วค่อยถามว่าข้อมูลชิ้นไหนไม่เคยถูกใช้จริง
เมื่อ f พร้อมแล้ว ทั้งสองคำถามใช้โครงเดียวกันเป๊ะ คือยืนที่ตำแหน่งแรกแล้วไล่ตัวเลือก
ตามลำดับพจนานุกรม
R ยังมากกว่าจำนวนนั้น ก็หัก R ลงแล้วข้ามไปตัวเลือกถัดไป ถ้าไม่มากกว่า ก็เลือกอันนี้
ระวัง
สองบักที่เกิดกับทุกคนอย่างน้อยครั้งหนึ่ง อันแรกคือเทียบผิดข้าง ต้องเป็น R ≤ cnt
ไม่ใช่ R < cnt ถ้าเขียนผิดคำตอบจะเลื่อนไปหนึ่งช่องเสมอ ซึ่งจับได้ยากเพราะมันยังดูสมเหตุสมผล
อันที่สองคือค่าล้น จำนวนของที่นับได้โตเร็วมาก ถ้าโจทย์บอกว่า R ไม่เกิน
สองล้านล้านล้าน ก็ต้องบวกแบบมีเพดาน ไม่งั้นตัวเลขจะพลิกเป็นลบแล้วการเทียบทุกครั้งหลังจากนั้นก็มั่วหมด
ยาวสี่หลัก ลองไล่ดูก่อนว่ามีกี่แบบ
ขอสตริงยาว 4 หลักที่ไม่มีเลขหนึ่งติดกัน อันดับที่ 5 จากทั้งหมด 8 แบบ
วางทีละหลัก ห้ามให้เลขหนึ่งอยู่ติดกัน
ตอนนี้ได้ ยังว่าง
เรียงตามพจนานุกรม เลขศูนย์มาก่อนเลขหนึ่ง
ตอนเลือกหลักแรก ถ้ารู้ว่าการวางศูนย์ไว้หน้าจะเหลือกี่แบบ คุณจะตัดสินใจได้ทันทีโดยไม่ต้องลองทีละแบบ
หาสตริงฐานสองยาว n ที่ไม่มีเลข 1 อยู่ติดกัน ตัวที่ R
ตามลำดับพจนานุกรม (โดย 0 มาก่อน 1) ถ้ามีไม่ถึง R ตัวให้ตอบ -1
| Input | Output |
|---|---|
| 6 9 | 010000 |
อ่านตัวอย่างนี้ยังไง
อินพุตมีสองเลขคือความยาวกับอันดับที่ขอ เอาต์พุตคือตัวสตริงเอง ไม่ใช่จำนวนแบบ
คำว่า "ตามลำดับพจนานุกรม" ในที่นี้แปลว่าเทียบทีละตัวจากซ้าย โดย 0 มาก่อน
1 เหมือนที่ a มาก่อน b
สตริงยาว 6 ที่ถูกกติกามีทั้งหมด 21 แบบ เรียงแล้วหยิบตัวที่ 9 ได้
010000 ซึ่งคือเอาต์พุต ที่โจทย์นี้ยากขึ้นตอนของจริงคือ n ใหญ่พอที่จะ
ทำให้ 21 กลายเป็นตัวเลขที่ไล่สร้างไม่ไหว
ใบ้
สถานะที่ต้องจำระหว่างเดินมีแค่อย่างเดียว คือตัวก่อนหน้าเป็นอะไร
เพราะกติกาพูดถึงแค่คู่ที่อยู่ติดกัน ลองเขียน f ออกมาแล้วดูว่าตัวเลขที่ได้คุ้นตาไหม
ที่มาของท่านี้ · สองบักที่เกิดกับทุกคน
ท่านี้เกิดจากการยอมรับว่าเราไล่สร้างทุกตัวไม่ไหว แต่นับว่ามีกี่ตัวที่ขึ้นต้นด้วยอะไรบางอย่างนั้นไหว พอนับได้ การหาตัวที่ R ก็กลายเป็นการเดินเลือกทีละหลัก โดยดูว่าโควตาของกิ่งนี้ครอบตัวที่ R ไว้หรือยัง
และมีสองบักที่เกิดกับทุกคนที่เขียนท่านี้ครั้งแรก อันแรกคือเทียบผิดข้าง
เขียน R < cnt แทนที่จะเป็น R ≤ cnt ซึ่งทำให้คำตอบเลื่อนไปหนึ่งช่องเสมอ
อันที่สองคือค่าล้น เพราะจำนวนตัวที่นับได้โตเร็วกว่าที่คิดมาก
ที่ทั้งสองอันจับยาก เพราะมันไม่พังแบบมีเสียง คำตอบที่เลื่อนไปหนึ่งช่องยังหน้าตาถูกต้องทุกอย่าง มันเป็นสตริงที่ถูกกติกา ยาวเท่าที่ควรจะเป็น และตอบเคสเล็ก ๆ ถูกได้บ่อย ๆ ด้วย ส่วนค่าล้นมักจะเงียบสนิทจนกว่า N จะใหญ่พอ ซึ่งไม่ใช่ขนาดที่เราทดสอบด้วยมือ
วิธีกันที่ได้ผลจริงมีสองอย่าง หนึ่งคือเทียบกับตัวไล่สร้างทุกตัวที่ N เล็ก ๆ ทุกค่าของ R เพราะการเลื่อนหนึ่งช่องจะโผล่ทันทีที่ขอบ สองคือคูณหาค่ามากที่สุดที่ตัวนับจะโตไปถึงก่อนเขียน แล้วเลือกชนิดข้อมูลจากเลขนั้น ไม่ใช่จากความเคยชิน
สถานะคือตัวก่อนหน้า มีสองค่า สูตรจึงสั้นมาก ถ้าตัวก่อนเป็น 0 ตัวนี้ใส่ได้ทั้งสองอย่าง
ถ้าตัวก่อนเป็น 1 ตัวนี้ใส่ได้แค่ 0
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
|---|---|---|---|---|---|---|---|---|---|
| 2 | 3 | 5 | 8 | 13 | 21 | 34 | 55 | 89 | 144 |
n เท่ากับ 90 จำนวนของมันเกินขอบเขตของ
จำนวนเต็ม 64 บิตไปแล้ว
| ตำแหน่ง | ใส่ 0 แล้วมีตามมากี่ตัว | ใส่ 1 แล้วมีตามมากี่ตัว | เลือก | R ที่เหลือ |
|---|---|---|---|---|
| 0 | 13 | 8 | 0 | 9 |
| 1 | 8 | 5 | 1 | 1 |
| 2 | 5 | 0 | 1 | |
| 3 | 3 | 2 | 0 | 1 |
| 4 | 2 | 1 | 0 | 1 |
| 5 | 1 | 1 | 0 | 1 |
1 ไม่ได้ เพราะตัวก่อนหน้าเป็น 1
ผลลัพธ์คือ 010000 ตรงกับการไล่สร้างทั้ง 21 ตัวแล้วนับเอา
// สตริงฐานสองยาว n ที่ไม่มีเลข 1 ติดกัน ตัวที่ R ตามลำดับพจนานุกรม
#include <bits/stdc++.h>
using namespace std;
typedef unsigned long long ull;
int main() {
int n; ull R;
scanf("%d %llu", &n, &R);
// f[i][last] = จำนวนวิธีเติมตำแหน่ง i..n-1 เมื่อตัวก่อนหน้าคือ last
vector<array<ull,2>> f(n + 1);
f[n][0] = f[n][1] = 1;
for (int i = n - 1; i >= 0; i--) {
f[i][0] = f[i + 1][0] + f[i + 1][1]; // ตัวก่อนเป็น 0 ใส่ได้ทั้ง 0 และ 1
f[i][1] = f[i + 1][0]; // ตัวก่อนเป็น 1 ใส่ได้แค่ 0
}
if (R > f[0][0]) { printf("-1\n"); return 0; }
string out; int last = 0;
for (int i = 0; i < n; i++) {
for (int b = 0; b <= 1; b++) { // ไล่ 0 ก่อน 1 ตามพจนานุกรม
if (b == 1 && last == 1) continue;
ull cnt = f[i + 1][b];
if (R <= cnt) { out.push_back('0' + b); last = b; break; }
R -= cnt;
}
}
printf("%s\n", out.c_str());
} ผมสุ่ม n กับ R จำนวน 800 ชุดเทียบกับตัวไล่สร้างทุกตัว ตรงกันหมด
ให้การเรียงสับเปลี่ยนของ 1 ถึง n มาหนึ่งชุด ตอบว่ามันอยู่อันดับที่เท่าไร
ตามลำดับพจนานุกรม โดยชุดแรกคืออันดับ 1
| Input | Output |
|---|---|
| 4 2 3 1 4 | 9 |
อ่านตัวอย่างนี้ยังไง
อินพุตคือ n แล้วตามด้วยการเรียงสับเปลี่ยนหนึ่งชุด เอาต์พุตคืออันดับ
ซึ่งเป็นตัวเลข ไม่ใช่ชุดตัวเลข และอันดับเริ่มนับที่ 1 ไม่ใช่ 0
ของ 4 ตัวเรียงได้ 24 ชุด ชุดที่โจทย์ให้มาอยู่อันดับที่ 9 ตารางข้างล่างคือชุดที่อยู่รอบ ๆ อันดับนั้น จะเห็นว่าลำดับพจนานุกรมคือการเทียบตัวหน้าก่อน ตัวหน้าเท่ากันแล้วค่อยดูตัวถัดไป
| อันดับ | การเรียงสับเปลี่ยน |
|---|---|
| 6 | 1 4 3 2 |
| 7 | 2 1 3 4 |
| 8 | 2 1 4 3 |
| 9 | 2 3 1 4 |
| 10 | 2 3 4 1 |
n ใหญ่จนไล่แบบนี้ไม่ได้ ท่าที่ใช้จริงคือเดินทีละตำแหน่งแล้วนับว่ามีชุดกี่ชุด
ที่ถูกข้ามไป ซึ่งคือภาพถัดไป
ใบ้
ข้อนี้เป็นคำถามกลับด้านของข้อแรก และคราวนี้การนับง่ายกว่ามาก เพราะเมื่อเราตรึงตัวหน้า ๆ ไว้แล้ว
ตัวที่เหลือจะเรียงยังไงก็ได้ทั้งหมด แล้วจำนวนวิธีเรียงของที่เหลือ k ชิ้นคือเท่าไร
ที่ตำแหน่งแรก ทุกชุดที่ขึ้นต้นด้วยเลขที่เล็กกว่า p[0] อยู่ก่อนหน้าชุดของเราทั้งหมด
และแต่ละตัวเลือกนั้นมีวิธีเรียงของที่เหลือ (n − 1)! แบบ ก็บวกเข้าไปตามจำนวนตัวเลือก
แล้วขยับไปตำแหน่งถัดไปโดยตัดเลขที่ใช้ไปแล้วออกจากกลุ่มที่นับ
// อันดับของการเรียงสับเปลี่ยนตามพจนานุกรม (สมาชิกคือ 1..n ไม่ซ้ำ)
#include <bits/stdc++.h>
using namespace std;
typedef unsigned long long ull;
int main() {
int n; scanf("%d", &n);
vector<int> p(n);
for (auto& v : p) scanf("%d", &v);
vector<ull> fact(n + 1, 1);
for (int i = 1; i <= n; i++) fact[i] = fact[i - 1] * i;
vector<char> used(n + 2, 0);
ull rank_ = 1;
for (int i = 0; i < n; i++) {
int smaller = 0; // ตัวที่ยังไม่ถูกใช้และเล็กกว่า p[i]
for (int v = 1; v < p[i]; v++) if (!used[v]) smaller++;
rank_ += (ull)smaller * fact[n - i - 1];
used[p[i]] = 1;
}
printf("%llu\n", rank_);
}
โค้ดนี้ไล่หา "จำนวนตัวที่ยังไม่ถูกใช้และเล็กกว่า" ด้วยลูปตรง ๆ ซึ่งเป็น n²
พอ n ใหญ่ก็เปลี่ยนไปใช้ต้นไม้เฟนวิกได้ทันทีโดยไม่ต้องเปลี่ยนตรรกะเลย นับหนึ่งตัวที่ยังอยู่
แล้วถามผลรวมของช่วง ซึ่งอยู่ใน บทผลรวมสะสมกับต้นไม้เฟนวิก
สิ่งที่ควรสังเกตคือโครงของโค้ดสองข้อนี้เหมือนกันทุกบรรทัด ต่างกันแค่ว่าข้อแรกหัก
R ลงจนถึงก้อนที่ใช่ ส่วนข้อสองบวกก้อนที่อยู่ก่อนหน้าเข้าไปในอันดับ
ผมสุ่มการเรียงสับเปลี่ยนขนาดเล็ก 600 ชุดเทียบกับการไล่ทุกชุดตามลำดับ ตรงกันหมด
ท่านับก่อนแล้วเดินในบทนี้ต้องการอย่างหนึ่งที่เราไม่ได้พูดถึงตรง ๆ คือ ของที่เรากำลังเรียงต้องประกอบขึ้นทีละชิ้นได้ สตริงประกอบทีละตัวอักษร การเรียงสับเปลี่ยนประกอบทีละตำแหน่ง เราจึงเดินลงไปทีละชั้นแล้วนับกิ่งได้
แต่มีโจทย์ "ขออันดับที่ k" อีกตระกูลที่ของไม่มีโครงแบบนั้นเลย เช่น
ผลบวกของทุกคู่ในอาเรย์ ที่เล็กเป็นอันดับที่ k ผลบวกไม่ได้ประกอบขึ้นทีละหลัก
มันเป็นเพียงตัวเลข และมีทั้งหมด n(n-1)/2 ตัว ซึ่งที่
n = 200,000 คือราวสองหมื่นล้านตัว สร้างทั้งกองไม่ได้
ท่าที่ใช้ตรงนี้คือค้นหาคำตอบแบบไบนารี คู่กับการนับ ซึ่งกลับทิศของคำถามทั้งหมด แทนที่จะถามว่า "อันดับที่ k คือค่าอะไร" ให้ถามว่า
ถ้าคำตอบคือ x จะมีของกี่ชิ้นที่ไม่เกิน x
คำถามใหม่นี้นับได้โดยไม่ต้องสร้างของ สำหรับผลบวกของคู่ พอเรียงอาเรย์แล้ว
การนับคู่ที่ผลบวกไม่เกิน x ทำได้ด้วยสองตัวชี้ในเวลาเชิงเส้น
ซึ่งเป็นท่าจากบทหน้าต่างเลื่อน ตรง ๆ
และเพราะจำนวนของที่ไม่เกิน x ไม่เคยลดลงเมื่อ x โตขึ้น
ฟังก์ชันนับจึงเป็นฟังก์ชันที่ค้นหาแบบไบนารีได้ เราจึงค้นหาค่า x ที่เล็กที่สุด
ซึ่งทำให้จำนวนของที่ไม่เกินมันถึง k พอดี ค่านั้นคือคำตอบ
ต้นทุนรวมเป็นการนับ log ครั้ง ครั้งละเชิงเส้น
// ค้นหาคำตอบแบบไบนารีคู่กับการนับ: หาผลบวก a[i]+a[j] (i<j) ที่เล็กเป็นอันดับที่ k
// ไม่ต้องสร้างผลบวกทั้ง n(n-1)/2 ตัวเลย
// อินพุต: n k แล้วอาเรย์ n ตัว เอาต์พุต: ค่าผลบวกอันดับที่ k (นับจาก 1)
#include <bits/stdc++.h>
using namespace std;
int main() {
int n; long long k;
if (scanf("%d %lld", &n, &k) != 2) return 0;
vector<long long> a(n);
for (int i = 0; i < n; i++) scanf("%lld", &a[i]);
sort(a.begin(), a.end());
// นับว่ามีผลบวกกี่คู่ที่ไม่เกิน x ด้วยสองตัวชี้ ซึ่งเป็น O(n) ต่อการนับหนึ่งครั้ง
auto countLE = [&](long long x) {
long long c = 0;
int j = n - 1;
for (int i = 0; i < n; i++) {
if (j <= i) j = i; // ไม่ให้ตัวชี้ขวาแซงมาซ้ายกว่าตัวชี้ซ้าย
while (j > i && a[i] + a[j] > x) j--;
if (j > i) c += j - i; // คู่ (i, i+1..j) ผ่านทั้งหมด
}
return c;
};
long long lo = a[0] + a[1], hi = a[n - 1] + a[n - 2];
while (lo < hi) {
long long mid = lo + (hi - lo) / 2; // ระวังการปัดของเลขลบ ใช้รูปนี้จึงปลอดภัย
if (countLE(mid) >= k) hi = mid;
else lo = mid + 1;
}
printf("%lld\n", lo);
return 0;
} เกณฑ์เลือกระหว่างสองท่าชัดเจน ถ้าของประกอบขึ้นทีละชิ้นได้และเราต้องการตัวของมันเอง ให้นับก่อนแล้วเดิน เพราะได้ของจริงออกมา ถ้าของเป็นเพียงค่าและเราต้องการแค่ตัวเลข ให้ค้นหาแบบไบนารีคู่กับการนับ คำถามที่ต้องตอบให้ได้ก่อนใช้ท่าหลังคือ เรานับของที่ไม่เกินค่าหนึ่งได้เร็วไหม โดยไม่ต้องสร้างมัน
ทั้งสองข้อชี้ไปที่ข้อสังเกตเดียวกัน คือส่วนที่ยากไม่เคยเป็นการเดิน การเดินเป็นแม่พิมพ์เดิมเสมอ ส่วนที่ยากคือหาสถานะที่ทำให้การนับเป็นไปได้
ถ้านับได้ว่า "ของที่ขึ้นต้นแบบนี้มีกี่ชิ้น" คำถามว่าตัวที่ R คืออะไร กับคำถามว่าตัวนี้อันดับเท่าไร ก็เป็นโค้ดชุดเดียวกันที่ต่างกันแค่ทิศทาง และงานจริงทั้งหมดอยู่ที่การหาสถานะให้เล็กพอจะนับไหว
ในหน้านี้