ปูพื้นฐาน
DSU ต่อเส้นได้ในพริบตาแต่ตัดทิ้งไม่ได้เลย ถ้ารู้คำสั่งทั้งหมดล่วงหน้า ให้มองแต่ละเส้นเป็นช่วงอายุบนแกนเวลา หั่นลงต้นไม้ช่วง แล้วเดินลงไปโดยใส่ตอนเข้าปมและย้อนตอนออก ใช้ได้กับทุกโครงสร้างที่ใส่ของเร็วและย้อนได้ ไม่ใช่แค่ DSU มีเกมหั่นช่วงอายุ ตัวเดินที่เห็นสแต็กทำงานทีละจังหวะ ตัวอย่างที่เล็กที่สุดที่ทำให้การบีบทางเดินตอบผิด และโจทย์ฝึก 10 ข้อ
เมืองหนึ่งมีถนนที่เปิดและปิดซ่อมสลับกันทุกวัน วันนี้ถนนเลียบคลองปิด พรุ่งนี้สะพานใหม่เปิด มะรืนสะพานเก่าปิดซ่อม หลังทุกการเปลี่ยนแปลงมีคนถามว่า ตอนนี้เมืองแยกเป็นกี่ส่วนที่ขับรถถึงกันไม่ได้
ถ้าถนนมีแต่เปิด นี่คือบท DSUตรงตัว เปิดถนนหนึ่งเส้นก็ unite ครั้งเดียว
ปัญหาอยู่ที่วันที่ถนนปิด DSU ไม่มีคำสั่งแยกก๊วน และจะแยกก็ไม่รู้ด้วยซ้ำว่าก๊วนขาดจริงไหม
เพราะอาจมีทางอ้อมอยู่ ส่วนการนับใหม่ทั้งเมืองทุกครั้ง ถ้าเมืองมี 10⁵ จุด ถนน 10⁵
เส้น และการเปลี่ยนแปลง 10⁵ ครั้ง ก็เป็นราว 2·10¹⁰ ก้าว
บทนี้แก้เรื่องนั้นได้ ภายใต้เงื่อนไขเดียว คือรู้การเปลี่ยนแปลงทั้งหมดล่วงหน้า และตอบทีเดียวตอนจบได้ โจทย์แข่งส่วนใหญ่เป็นแบบนี้ เพราะ input ถูกอ่านจนหมดก่อนที่โปรแกรมจะต้องพิมพ์อะไรออกมา
แกะคำศัพท์
โจทย์ที่ให้ตอบหลังอ่านครบได้เรียกว่า offline อ่านว่า "ออฟไลน์" ส่วนโจทย์ที่ต้องตอบคำถามเดิมให้เสร็จก่อนถึงจะได้เห็นคำถามถัดไปเรียกว่า online สองคำนี้ยืมมาจากความหมายเดิมตรง ๆ แบบออนไลน์คือต้องตอบสดขณะที่ของไหลเข้ามา แบบออฟไลน์คือเอางานกลับไปทำเงียบ ๆ แล้วค่อยส่งทีเดียว ท่าในบทนี้ใช้ได้เฉพาะแบบออฟไลน์
บท DSU มีข้อหนึ่งที่มีแต่การฉีกเส้นใย (Anansi's Cobweb) ข้อนั้นแก้ด้วยการอ่านเหตุการณ์จากท้ายมาหน้า พอกลับทิศ การลบทุกครั้งกลายเป็นการเพิ่ม ซึ่ง DSU ถนัด
ท่านั้นพังทันทีเมื่อมีทั้งเพิ่มและลบปนกัน เพราะกลับทิศแล้วการเพิ่มก็กลายเป็นการลบ ปัญหาเดิมแค่ย้ายไปอยู่อีกฝั่ง ต้องหาทางที่ไม่ต้องลบเลยไม่ว่าจะเดินทิศไหน
ลองแยกสองคำนี้ออกจากกัน ลบคือถอดของชิ้นไหนก็ได้ที่อยู่ข้างใน ส่วนย้อนกลับคือถอดของชิ้นที่ใส่ล่าสุดเท่านั้น เหมือนจานที่ซ้อนบนชั้น หยิบได้แค่ใบบนสุด หรือปุ่ม Ctrl+Z ที่ย้อนได้ตามลำดับที่ทำไป ข้ามไปย้อนขั้นที่สามจากท้ายโดยไม่ย้อนสองขั้นหลังไม่ได้
DSU ลบไม่ได้ แต่ย้อนกลับได้ ถ้าแลกกับการยอมทิ้งกลบีบทางเดิน การรวมหนึ่งครั้งแก้ของแค่สองช่อง
คือ par[b] ของหัวฝั่งที่ไปขึ้นกับอีกฝั่ง กับ sz[a] ของหัวที่รับไว้ จดตัว b ลงสแต็ก
ตอนย้อนก็หยิบ b ออกมา อ่านแม่จาก par[b] คืนขนาด แล้วตั้ง par[b] = b
แกะคำศัพท์
การย้อนแบบนี้เรียกว่า rollback อ่านว่า "โรลแบ็ก" แปลตรงตัวว่า "ม้วนกลับ" คำเดียวกับที่ฐานข้อมูลใช้เมื่อธุรกรรมล้มกลางทาง แล้วต้องคืนทุกอย่างให้เหมือนก่อนเริ่ม และในบทนี้ก็หมายถึงแบบนั้นจริง ๆ คืนให้เหมือนก่อนเริ่ม ไม่ใช่ถอดชิ้นที่เลือกเอง
เหลือคำถามเดียว ถ้าเครื่องมือย้อนได้แค่ตามลำดับ จะจัดลำดับการใส่เส้นยังไงให้ทุกครั้งที่เส้นหนึ่งต้องหายไป มันคือเส้นที่ใส่ล่าสุดพอดี
เปลี่ยนมุมมองจาก "เหตุการณ์ทีละครั้ง" เป็น "เส้นทีละเส้น" เส้นแต่ละเส้นเกิดตอนถูกเพิ่มและตายตอนถูกลบ
ระหว่างนั้นมันมีชีวิตอยู่ต่อเนื่อง เรียกช่วงนี้ว่าช่วงอายุของเส้น (lifetime) บนแกนเวลาที่นับจุดเวลาที่ต้องตอบ
เส้นที่ถูกลบตอนเหตุการณ์ที่ i อยู่ถึงจุดเวลา i - 1 ส่วนเส้นที่ไม่ถูกลบเลยอยู่จนจุดสุดท้าย
ตัวอย่างที่ใช้ทั้งบทมี 4 จุด เส้นตั้งต้น 1-2, 1-4, 2-3 แล้วมีสองเหตุการณ์ คือเพิ่ม 1-3 แล้วลบ 1-2 จุดเวลาจึงมี 3 จุด คือก่อนเหตุการณ์แรก หลังเหตุการณ์ที่หนึ่ง และหลังเหตุการณ์ที่สอง
ถ้าจับเส้นทุกเส้นที่มีช่วงอายุคลุมทั้งแกนใส่ไว้ก่อน ก็ไม่ต้องย้อนมันเลย ส่วนเส้นที่อยู่แค่ครึ่งหน้า ใส่ตอนเริ่มครึ่งหน้า ย้อนตอนจบครึ่งหน้า แล้วแบ่งครึ่งต่อไปเรื่อย ๆ การแบ่งครึ่งซ้ำ ๆ แบบนี้ก็คือ ต้นไม้ช่วงบนแกนเวลานั่นเอง
แถวล่างสุดคือจุดเวลา 16 จุด ปมแต่ละปมของต้นไม้คลุมช่วงของจุดเวลาที่อยู่ใต้มัน คลิกเลือกปมให้คลุมช่วงอายุที่แถบสีทองบอกพอดี ไม่ขาดไม่เกิน และห้ามซ้อนกัน โดยใช้ปมให้น้อยที่สุด ยิ่งใช้ปมน้อย ยิ่งต้องใส่เส้นนั้นน้อยครั้ง
ช่วงอายุยาวแปดจุดเวลา
ช่วงอายุ [8, 15] คลิกปมเพื่อเลือกหรือยกเลิก
เลือกปมที่คลุมช่วงสีทองให้พอดี
ยังไม่ได้เลือกปม
แถวบนสุดคือราก คลุมทั้งแกน แถวล่างสุดคือใบ คลุมจุดเวลาเดียว ปมที่เลือกแล้วเป็นสีเขียว
ลองเทียบชุด ข กับชุด ก ช่วงที่สั้นกว่าไม่ได้แปลว่าใช้ปมน้อยกว่า สิ่งที่ตัดสินคือขอบของช่วงไปตรงกับขอบของปมหรือเปล่า
ถ้ามีกฎที่หั่นช่วงไหนก็ได้ด้วยปมน้อยสุดโดยไม่ต้องลอง มันหน้าตาเป็นยังไง ลองคิดก่อนแล้วค่อยเลื่อนลงไป
กฎคือการอัปเดตช่วงของต้นไม้ช่วงทุกตัวอักษร เริ่มที่ราก ถ้าช่วงของปมอยู่ในช่วงอายุทั้งปม ก็วางเส้นไว้ที่ปมนั้นแล้วหยุด
ถ้าไม่ทับกันเลยก็หยุด ที่เหลือก็ลงไปถามลูกทั้งสอง ในบทนี้ฟังก์ชันนี้ชื่อ addRange ช่วงหนึ่งช่วงถูกวางไม่เกินชั้นละสองปม
จึงไม่เกินราว 2 log₂ q ปม
สิ่งที่ทำให้วิธีนี้ถูกคือประโยคเดียว เส้นที่อยู่บนปมใดปมหนึ่ง มีชีวิตตลอดทุกจุดเวลาที่อยู่ใต้ปมนั้น
เพราะช่วงของปมอยู่ในช่วงอายุของเส้นทั้งช่วง กลับกัน สำหรับใบ t ใดก็ตาม เส้นที่มีชีวิตที่ t
ถูกวางไว้บนปมใดปมหนึ่งบนทางจากรากลงมาถึงใบ t พอดีหนึ่งปม
เส้นที่ต้องใส่เพื่อตอบใบ t จึงคือเส้นบนทางจากรากถึงใบนั้นพอดี ไม่ขาดไม่เกิน
ตอนนี้ลำดับที่ต้องการก็มาเอง เดินต้นไม้แบบ DFS จากราก เข้าปมไหนก็ unite ทุกเส้นบนปมนั้น
ถึงใบก็อ่านคำตอบ แล้วตอนออกจากปมก็ rollback จนสแต็กสั้นเท่าตอนที่เข้ามา
เส้นที่ต้องหายไปตอนออกจากปม คือเส้นของปมนั้นเอง ซึ่งเป็นเส้นที่ใส่ล่าสุด เพราะเส้นของปมลูกทุกปมถูกย้อนไปก่อนแล้วตอนออกจากลูก
ตัวเดินข้างล่างใช้ตัวอย่างเดิม ต้นไม้บน 3 จุดเวลามี 5 ปม ของบนแต่ละปมคือ ปม 1[0,2]: 1-4, 2-3 · ปม 2[0,1]: 1-2 · ปม 3t=2: 1-3 · ปม 4t=0: ว่าง · ปม 5t=1: 1-3
dfs บนต้นไม้ช่วง พร้อม rollback
| จุด | 1 | 2 | 3 | 4 |
|---|---|---|---|---|
| par | 1 | 2 | 3 | 4 |
| sz | 1 | 1 | 1 | 1 |
| stk | [ ] | |||
| ปม | ยังไม่เข้า | |||
| คำตอบ | · · · | |||
ตรงขั้นที่เส้น 1-3 ถูกใส่ที่ใบ t=1 ให้สังเกตว่ามันไม่ได้รวมอะไรเลย เพราะ 1 กับ 3 ต่อกันผ่าน 1-2 กับ 2-3 อยู่แล้ว แต่เส้นเดียวกันนี้ถูกวางอีกครั้งที่ใบ t=2 และคราวนี้มันรวมจริง เพราะ 1-2 ถูกย้อนออกไปก่อนแล้วตอนออกจากปม [0,1] เส้นเดียวกันทำงานต่างกันในสองปม ตามว่าตอนนั้นมีเส้นอื่นอยู่ด้วยหรือเปล่า
สแต็กของเราจดแค่การรวม ส่วนการบีบทางเดินแก้ par ของทุกคนบนทางเดินโดยไม่มีใครจด
พอย้อนการรวมกลับ ช่องที่ถูกบีบยังชี้ไปที่หัวคนที่ไม่ใช่หัวของมันแล้ว ผลคือคำตอบผิด ไม่ใช่แค่ช้า
ตัวอย่างของบทนี้เล็กพอที่จะเห็นเรื่องนี้ครบ มันคืออินพุตที่เล็กที่สุดที่ทำให้รุ่นบีบทางเดินตอบผิด
ซึ่งหาได้จากการไล่ทุกกรณีเรียงตามจำนวนเหตุการณ์ แล้วจำนวนจุด แล้วจำนวนเส้นตั้งต้น
ในรุ่นที่บีบ ตอนตอบใบ t=1 การเรียก find(3) บีบ par[3] ให้ชี้ข้ามไปที่หัวทันที
แล้วไม่มีอะไรลงสแต็ก พอออกจากปม [0,1] และถอด 2 ออก 3 ก็ยังชี้ค้างไปที่ 1 อยู่ ใบ t=2 จึงเห็น 1 กับ 3 อยู่ก๊วนเดียวกันแล้วไม่รวม
| รุ่น | t = 0 | t = 1 | t = 2 |
|---|---|---|---|
| เดินกราฟใหม่ทุกจุดเวลา | 1 | 1 | 1 |
| ต้นไม้ + rollback | 1 | 1 | 1 |
| ต้นไม้ + rollback + บีบทางเดิน | 1 | 1 | 2 |
ต่อให้จดทุกช่องที่ถูกบีบลงสแต็กด้วย ก็ยังเสียประโยชน์ของมันอยู่ดี บีบทางเดินเร็วเพราะงานที่จ่ายไปครั้งหนึ่งช่วยครั้งถัดไป ซึ่งเป็นการนับแบบเฉลี่ย (amortized) พอย้อนกลับได้ ทางเดินยาว ๆ ถูกคืนกลับมาครบ แล้วครั้งถัดไปก็ต้องเดินใหม่ทั้งทาง หน้า cp-algorithms สรุปเรื่องนี้ไว้ประโยคเดียวว่าการย้อนกลับทำลายการนับแบบเฉลี่ย
ทางออกคือกลับไปใช้รวมตามขนาดอย่างเดียว ซึ่งบท DSUปูไว้แล้วว่าให้ความลึกไม่เกิน log₂ n กับทุกครั้ง ไม่ต้องเฉลี่ย ย้อนกลับกี่ครั้งขอบนี้ก็ยังจริง เพราะมันเป็นเรื่องของรูปต้นไม้ ไม่ใช่เรื่องของประวัติ
โค้ดนี้คือคำตอบของโจทย์ฝึกข้อแรก และเป็นแม่แบบของทุกข้อที่เหลือ ชื่อทุกตัวเหมือนบท DSU เพิ่มแค่ stk
กับ rollback ส่วน find ไม่บีบทางเดินด้วยเหตุผลของหัวข้อที่แล้ว
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 100005, MAXQ = 100005; // จุดเวลามี k+1 จุด
int n, m, k, Q, comps;
int par[MAXN], sz[MAXN];
vector<int> stk; // ลูกที่ถูกย้ายไปขึ้นกับแม่ เรียงตามลำดับที่รวม
vector<pair<int,int>> node[4 * MAXQ];
int ans[MAXQ];
// ไม่บีบทางเดิน เพราะการบีบแก้ par หลายช่องที่สแต็กไม่ได้จดไว้ ย้อนกลับไม่ได้
int find(int x) {
while (par[x] != x) x = par[x];
return x;
}
// รวมตามขนาดอย่างเดียว ความลึกจึงไม่เกิน log2(n) และ find เป็น O(log n) จริงทุกครั้ง
bool unite(int a, int b) {
a = find(a), b = find(b);
if (a == b) return false;
if (sz[a] < sz[b]) swap(a, b);
par[b] = a;
sz[a] += sz[b];
stk.push_back(b);
comps--;
return true;
}
// การรวมหนึ่งครั้งแก้แค่ par[b] กับ sz[a] และ a ยังเป็น par[b] อยู่ จึงถอดคืนได้จากลูกตัวเดียว
void rollback(int keep) {
while ((int)stk.size() > keep) {
int b = stk.back(); stk.pop_back();
sz[par[b]] -= sz[b];
par[b] = b;
comps++;
}
}
// แตกช่วงอายุ [ql, qr] ลงปมที่คลุมพอดี แบบเดียวกับการอัปเดตช่วงของ segment tree
void addRange(int v, int l, int r, int ql, int qr, pair<int,int> item) {
if (qr < l || r < ql) return;
if (ql <= l && r <= qr) { node[v].push_back(item); return; }
int mid = (l + r) / 2;
addRange(2 * v, l, mid, ql, qr, item);
addRange(2 * v + 1, mid + 1, r, ql, qr, item);
}
// เส้นบนปม v มีชีวิตตลอดช่วง [l, r] จึงใส่ครั้งเดียวแล้วใช้ได้กับทุกใบข้างใต้
void dfs(int v, int l, int r) {
int keep = stk.size();
for (auto [a, b] : node[v]) unite(a, b);
if (l == r) ans[l] = comps;
else {
int mid = (l + r) / 2;
dfs(2 * v, l, mid);
dfs(2 * v + 1, mid + 1, r);
}
rollback(keep);
}
int main() {
ios::sync_with_stdio(false); cin.tie(nullptr);
cin >> n >> m >> k;
Q = k + 1; // จุดเวลา 0 คือก่อนเหตุการณ์แรก จุด i คือหลังเหตุการณ์ที่ i
map<pair<int,int>, int> born; // เส้นที่ยังอยู่ -> จุดเวลาที่เกิด
for (int i = 0; i < m; i++) {
int a, b; cin >> a >> b;
born[{min(a, b), max(a, b)}] = 0;
}
for (int i = 1; i <= k; i++) {
int t, a, b; cin >> t >> a >> b;
pair<int,int> e = {min(a, b), max(a, b)}; // ลบอาจเขียนสลับข้างกับตอนเพิ่ม
if (t == 1) born[e] = i;
else {
addRange(1, 0, Q - 1, born[e], i - 1, e); // ถูกลบที่ i จึงอยู่ถึง i-1
born.erase(e);
}
}
for (auto [e, s] : born) addRange(1, 0, Q - 1, s, Q - 1, e);
for (int i = 1; i <= n; i++) par[i] = i, sz[i] = 1;
comps = n;
dfs(1, 0, Q - 1);
for (int i = 0; i < Q; i++) cout << ans[i] << (i + 1 < Q ? ' ' : '\n');
return 0;
}
นับต้นทุนจากข้างในออกมา find หนึ่งครั้งเดินไม่เกิน log₂ n ชั้น เส้นแต่ละเส้นถูกวางไม่เกินราว 2 log₂ q ปม และทุกครั้งที่วางก็ถูก unite หนึ่งครั้งตอนเข้าปมนั้น รวมเป็น O((m + k) · log q · log n) ถ้าเขียนแบบทั่วไป โครงสร้างที่เพิ่มของได้ในเวลา T(n) และย้อนได้
จะรองรับการลบแบบออฟไลน์ได้ในเวลา O(T(n) · log q) ต่อหนึ่งช่วงอายุ ซึ่งคือชื่อของหน้าต้นฉบับ
สิ่งที่แม่แบบนี้ไม่สนเลย
โครงต้นไม้บนแกนเวลาไม่รู้ว่าข้างในเป็น DSU มันต้องการแค่สองอย่าง คือใส่ของได้ และย้อนการใส่ล่าสุดได้ โจทย์ฝึกครึ่งหลังของบทนี้เปลี่ยนของข้างในเป็นอย่างอื่นทั้งหมด ทั้งกระเป๋าเป้ (knapsack) ฐานของ xor และ bitset และบางข้อใช้แค่ครึ่งเดียวของท่านี้
สิบข้อนี้เรียงเป็นสามกลุ่ม ข้อ 1 ถึง 5 ใช้ท่าเต็มกับ DSU แต่ละข้อเปลี่ยนสิ่งที่ DSU ต้องจำ หรือเปลี่ยนวิธีได้ช่วงอายุมา ข้อ 6 กับ 7 เปลี่ยนของข้างในเป็นอย่างอื่นที่ไม่ใช่ DSU เลย ข้อ 8 กับ 9 ใช้แค่ครึ่งหลังของท่า คือย้อนกลับโดยไม่มีต้นไม้ช่วง และข้อ 10 รวมทุกอย่างเข้าด้วยกัน สามข้อในนี้มาจากหน้าต้นฉบับของ cp-algorithms ส่วนที่เหลือผมเลือกเพิ่มให้ครบมุม
กราฟมี n จุด เส้นตั้งต้น m เส้น แล้วมีเหตุการณ์ k ครั้ง แต่ละครั้งสร้างเส้นใหม่หรือตัดเส้นเดิมทิ้ง
ให้บอกจำนวนก๊วนก่อนเหตุการณ์แรก และหลังเหตุการณ์ทุกครั้ง
อินพุต / ขอบเขต / เอาต์พุต
n m k เส้นตั้งต้น m เส้น แล้วเหตุการณ์ t a b โดย t = 1 คือสร้าง t = 2 คือตัดn, m, k ≤ 10⁵ สร้างเฉพาะคู่ที่ยังไม่มีเส้น ตัดเฉพาะเส้นที่มีอยู่ เวลา 1 วินาทีk + 1 ตัว| Input | Output |
|---|---|
| 5 3 3 1 4 2 3 3 5 1 2 5 2 3 5 1 1 2 | 2 2 2 1 |
อ่านตัวอย่างนี้ยังไง
ตัวเลขแรกของเอาต์พุตคือก่อนเหตุการณ์แรก ตอนนั้นมีก๊วน {1, 4} กับ {2, 3, 5} จึงได้ 2 เหตุการณ์แรกเพิ่ม 2-5 ซึ่งอยู่ก๊วนเดียวกันอยู่แล้ว เหตุการณ์ที่สองตัด 3-5 แต่ 5 ยังถึง 3 ผ่าน 2 ได้ ก๊วนจึงไม่ขาด เหตุการณ์สุดท้ายเพิ่ม 1-2 สองก๊วนรวมเป็นหนึ่ง
| เส้น | เกิด | อยู่ถึง |
|---|---|---|
| 3-5 | 0 | 1 |
| 1-2 | 3 | 3 |
| 1-4 | 0 | 3 |
| 2-3 | 0 | 3 |
| 2-5 | 1 | 3 |
ใบ้
ข้อนี้คือโจทย์ของหัวข้อข้างบนทุกประการ ที่ต้องระวังมีเรื่องเดียว คือเส้นถูกระบุด้วยคู่จุด โจทย์ไม่ได้สัญญาว่าตอนตัดจะเขียนคู่จุดเรียงเหมือนตอนสร้าง จะจับคู่ "สร้าง" กับ "ตัด" ของเส้นเดียวกันยังไง
ที่มาของแนวคิดนี้
ผมรู้จักท่านี้อยู่แล้วก่อนเปิดโจทย์ สิ่งที่ทำจริงคือเช็กว่าตัวเลขบังคับให้ใช้มันหรือเปล่า
นับใหม่ทุกจุดเวลาคือ 10⁵ ครั้ง ครั้งละ 2·10⁵ ได้ 2·10¹⁰ ส่วนย้อนเวลาอย่างเดียวใช้ไม่ได้
เพราะมีทั้งสร้างและตัดปนกัน ที่เหลือคือเงื่อนไขที่ทำให้ท่านี้ใช้ได้ คือโจทย์ให้ input ทั้งหมดก่อนต้องตอบ
ประมาณต้นทุนแบบหยาบที่สุด เส้นไม่เกิน 2·10⁵ ช่วง ช่วงละไม่เกินราว 34 ปม ปมละ find สองครั้ง ครั้งละไม่เกิน 17 ชั้น
ได้ราว 2·10⁸ ครั้งของการเดินขึ้นหนึ่งชั้นในกรณีแย่สุด ซึ่งพอสำหรับหนึ่งวินาที วัดจริงช้าสุด 0.2 วินาที
บทเรียนคือ ท่าที่มีสอง log คูณกันดูน่ากลัวบนกระดาษ ให้คูณตัวเลขจริงก่อนตัดสิน
เก็บ map จากคู่ (min, max) ไปยังจุดเวลาที่เส้นนั้นเกิด เจอการตัดก็ดึงออกมาปิดช่วงแล้ววางลงต้นไม้
อ่านครบแล้ว เส้นที่ยังเหลือใน map คือเส้นที่อยู่ถึงจุดสุดท้าย คู่จุดต้องเรียงเองทุกครั้ง
เพราะถ้าตอนสร้างเขียน 2 1 แต่ตอนตัดเขียน 1 2 แล้วเราใช้คู่ตามที่อ่านมาเป็นกุญแจ
จะหาเส้นไม่เจอ แล้วเส้นนั้นจะถูกนับว่าไม่เคยตาย
โค้ดของข้อนี้คือแม่แบบในหัวข้อ "แม่แบบ กับต้นทุนของมัน" ข้างบนทุกบรรทัด
ตรวจกับตัวที่เดินกราฟใหม่ทุกจุดเวลา 20,000 รอบ สุ่มสลับข้างของคู่จุดทั้งตอนสร้างและตอนตัด ตรงกันทุกรอบ
เคสใหญ่สามแบบที่ n, m, k = 10⁵ ใช้ไม่เกิน 0.2 วินาที
กราฟเริ่มว่าง มี N จุด แล้วมีคำสั่ง K ข้อ + u v เพิ่มเส้น - u v ลบเส้น
และ ? ถามจำนวนก๊วน ณ ตอนนั้น โจทย์นี้คือตัวอย่างหลักในหน้าต้นฉบับของ cp-algorithms
อินพุต / ขอบเขต / เอาต์พุต
N K แล้วคำสั่งบรรทัดละข้อ อ่านจากไฟล์ connect.in เขียนลง connect.outN, K ≤ 3·10⁵ เวลา 3 วินาที? พิมพ์จำนวนก๊วนหนึ่งบรรทัด| Input | Output |
|---|---|
| 5 11 ? + 1 2 + 2 3 + 3 4 + 4 5 + 5 1 ? - 2 3 ? - 4 5 ? | 5 1 1 2 |
อ่านตัวอย่างนี้ยังไง
บรรทัดแรกคือจำนวนจุดกับจำนวนคำสั่ง แล้วอีก 11 บรรทัดคือคำสั่งเรียงตามเวลา
ในนั้นมี ? อยู่ 4 ข้อ เอาต์พุตจึงมี 4 บรรทัด บรรทัดละหนึ่งจำนวนก๊วน
ไม่ใช่จำนวนเส้นหรือจำนวนคำสั่ง และจุดที่ยังไม่มีเส้นเลยก็นับเป็นก๊วนของตัวเอง
คำถามแรกมาก่อนที่จะมีเส้นสักเส้น ทุกจุดจึงอยู่คนเดียว ได้ 5 ห้าคำสั่งถัดมาต่อ 1-2-3-4-5 แล้ววนกลับมาที่ 1 เป็นวงเดียว คำถามที่สองจึงได้ 1 พอลบ 2-3 วงก็แค่คลายเป็นเส้นยาว ทุกจุดยังถึงกันหมด ได้ 1 เท่าเดิม ลบ 4-5 อีกเส้นถึงจะขาดออกเป็นสองท่อน ได้ 2
| คำสั่ง | คำถามที่ | ก๊วน |
|---|---|---|
| ? | 0 | 5 |
| + 1 2 | ||
| + 2 3 | ||
| + 3 4 | ||
| + 4 5 | ||
| + 5 1 | ||
| ? | 1 | 1 |
| - 2 3 | ||
| ? | 2 | 1 |
| - 4 5 | ||
| ? | 3 | 2 |
ใบ้
ต่างจากข้อ 1 ตรงที่ไม่ต้องตอบทุกจุดเวลา ตอบเฉพาะตอนเจอ ? ถ้าให้ใบของต้นไม้เป็นทุกคำสั่ง ต้นไม้ยาว K เสมอ
แม้ทั้งไฟล์จะมีคำถามข้อเดียว แกนเวลาควรนับอะไร
ที่มาของแนวคิดนี้
ข้อนี้ทำหลังข้อ 1 จึงเริ่มจากโค้ดของข้อนั้นทั้งก้อน แล้วถามว่าอะไรต่าง ได้สี่ข้อ กราฟเริ่มว่าง ตอบเฉพาะตอนถูกถาม ต้องจับคู่เพิ่มกับลบเอง และอ่านเขียนไฟล์ ข้อที่เปลี่ยนความคิดจริงมีข้อเดียวคือข้อที่สอง
บทเรียนคือ ท่าเดิมไม่ต้องแก้ ที่ต้องคิดใหม่คือแกนเวลาคืออะไร เลือกแกนให้ตรงกับจุดที่ต้องตอบ
ให้แกนเวลาเป็น "คำถามลำดับที่ 0, 1, 2, ..." ตัวนับ Q คือจำนวน ? ที่อ่านมาแล้ว
เส้นที่เกิดตอน Q = c เห็นได้ตั้งแต่คำถามที่ c และเส้นที่ตายตอน Q = d เห็นได้ถึงคำถามที่ d - 1
ของที่ได้ตามมาเองคือ เส้นที่เกิดแล้วตายโดยไม่มีคำถามคั่นจะได้ช่วง [c, c - 1] ซึ่งว่าง ข้ามได้เลย
ต้องระวังกรณีที่ไม่มี ? เลย ถ้าเรียก dfs(1, 0, -1) การแบ่งครึ่ง (0 + (-1)) / 2 ได้ 0
แล้วโค้ดจะเรียกตัวเองไม่รู้จบ จึงหยุดตั้งแต่ก่อนสร้างต้นไม้
กราฟ n จุด เริ่มไม่มีเส้น คำสั่งแต่ละข้อให้คู่จุด x y ถ้ามีเส้นนี้อยู่แล้วให้ลบ ถ้ายังไม่มีให้เพิ่ม
หลังทุกคำสั่งให้ตอบว่ากราฟตอนนั้นระบายสองสีได้หรือเปล่า คือไม่มีเส้นไหนเชื่อมจุดสีเดียวกัน
อินพุต / ขอบเขต / เอาต์พุต
n q แล้ว q คู่ x y โดย x < yn, q ≤ 10⁵ เวลา 6 วินาทีYES หรือ NO หลังทุกคำสั่ง| Input | Output |
|---|---|
| 3 5 2 3 1 3 1 2 1 2 1 2 | YES YES NO YES NO |
อ่านตัวอย่างนี้ยังไง
บรรทัดแรกคือจำนวนจุด 3 จุด กับจำนวนคำสั่ง 5 ข้อ แล้วอีก 5 บรรทัดคือคู่จุดของแต่ละคำสั่ง คู่เดิมที่ส่งมาซ้ำหมายถึงสั่งลบเส้นนั้นออก ไม่ใช่ใส่ซ้ำเข้าไปอีกเส้น เอาต์พุตมีหนึ่งบรรทัดต่อหนึ่งคำสั่ง ตอบว่ากราฟทั้งกราฟ ณ ตอนนั้นยังระบายสองสีโดยไม่ให้ปลายเส้นไหนสีเดียวกันได้อยู่ไหม
คำสั่งแรกใส่ 2-3 คำสั่งที่สองใส่ 1-3 ได้เส้นยาวสามจุด ระบายสลับสีได้สบาย ทั้งคู่จึงตอบ YES คำสั่งที่สามใส่ 1-2 ปิดเป็นสามเหลี่ยม พอไล่สลับสีรอบวงจะกลับมาชนสีเดิม ได้ NO คำสั่งที่สี่ส่ง 1-2 มาอีกครั้ง เส้นนั้นมีอยู่แล้วจึงถูกลบ กลับไปเป็นเส้นยาว ได้ YES แล้วคำสั่งที่ห้าใส่กลับเข้ามา เป็นสามเหลี่ยมอีกรอบ ได้ NO
ใบ้
คำสั่งแบบสลับเปิดปิดแปลงเป็นช่วงอายุได้ตรง ๆ ส่วน DSU ต้องรู้มากกว่าว่าใครอยู่ก๊วนไหน มันต้องรู้สีด้วย ลองนึกถึงข้อ Parity ในบท DSU แล้วถามต่อว่า ถ้าที่ปมหนึ่งเจอวงคี่แล้ว ใบข้างใต้ปมนั้นยังต้องลงไปดูอีกไหม
ที่มาของแนวคิดนี้
ระบายสองสีใหม่ทุกคำสั่งคือ 10⁵ ครั้ง ครั้งละราว 2·10⁵ ต่อให้เวลาหกวินาทีก็ไม่พอ
โครงต้นไม้บนแกนเวลาไม่ต้องแก้เลย เปลี่ยนแค่ของที่ DSU ต้องจำ ซึ่งคือท่าเดียวกับข้อ Parity ของบท DSU
ระหว่างเขียนมีบรรทัดเดียวที่ผมเอาออก รุ่นแรกของ rollback ตั้ง d[b] = 0 คืนด้วย
ทั้งที่ find หยุดที่หัวโดยไม่อ่าน d ของหัว และ unite เขียน d ทับทุกครั้งที่หัวถูกย้าย
บรรทัดนั้นจึงไม่มีผลกับอะไรเลย ลบแล้วรันตัวตรวจซ้ำก็ผ่านเหมือนเดิม บทเรียนคือ ของที่ไม่มีใครอ่านอีก ไม่ต้องจดและไม่ต้องคืน
d[x] บอกว่า x ต่างสีกับแม่หรือเปล่า สีของ x เทียบหัวคือ xor ของ d ตลอดทางขึ้น
ตอนรวมสองหัว ตั้ง d ของหัวที่ไปขึ้นกับอีกฝั่งให้ปลายทั้งสองของเส้นต่างสีกัน คือ ca xor cb xor 1
ข้อนี้ unite คืน false เมื่อเส้นปิดวงคี่ ไม่ใช่เมื่อไม่ได้รวม
เพราะสิ่งที่ DFS ต้องรู้คือกราฟยังระบายสองสีได้ไหม
และเพราะเส้นบนปมหนึ่งมีชีวิตครบทุกใบข้างใต้ ถ้าใส่แล้วเจอวงคี่ ทุกใบข้างใต้ก็มีวงคี่นั้นด้วย
ไม่ต้องลงไปต่อ ใบที่ไม่ถูกเยี่ยมคือใบที่ตอบ NO ซึ่งเป็นค่าเริ่มต้นของอาร์เรย์คำตอบอยู่แล้ว
มีเซตของจุดบนระนาบ ถ้ามีสามมุมของสี่เหลี่ยมที่ด้านขนานแกน มุมที่สี่จะงอกขึ้นมาเอง งอกไปเรื่อย ๆ จนไม่มีมุมไหนงอกได้อีก เซตที่ได้เรียกว่าส่วนขยาย แต่ละคำถามให้จุดมาหนึ่งจุด ถ้าอยู่ในเซตแล้วให้เอาออก ถ้ายังไม่อยู่ให้ใส่ แล้วตอบขนาดของส่วนขยาย
อินพุต / ขอบเขต / เอาต์พุต
q แล้วจุด x y อีก q บรรทัดq, x, y ≤ 3·10⁵ เวลา 3.5 วินาที| Input | Output |
|---|---|
| 7 1 1 1 2 2 1 2 2 1 2 1 3 2 1 | 1 2 4 4 4 6 3 |
อ่านตัวอย่างนี้ยังไง
บรรทัดแรกคือจำนวนคำถาม 7 ข้อ แล้วอีก 7 บรรทัดคือพิกัดของจุดที่ถูกสลับในคำถามนั้น จุดที่ยังไม่อยู่ในเซตจะถูกใส่เข้าไป จุดที่อยู่แล้วจะถูกเอาออก ตัวเลขที่ต้องพิมพ์คือขนาดของส่วนขยาย คือจำนวนจุดหลังปล่อยให้มุมที่สี่งอกจนหมด ไม่ใช่จำนวนจุดที่เราใส่เข้าไปเอง
สองคำถามแรกใส่ (1, 1) กับ (1, 2) ทั้งคู่อยู่แถวเดียวกัน ไม่มีมุมไหนงอกได้ ได้ 1 กับ 2 คำถามที่สามใส่ (2, 1) ครบสามมุมพอดี (2, 2) จึงงอกขึ้นมาเอง ได้ 4 คำถามที่สี่ใส่ (2, 2) ซึ่งงอกอยู่แล้ว ขนาดจึงเท่าเดิม และคำถามที่ห้าเอา (1, 2) ออก ก็ยังเหลือสามมุมที่ทำให้มันงอกกลับมา ได้ 4 เท่ากันอีก คำถามที่หกใส่ (1, 3) เปิดหลักใหม่ ทั้งสองแถวเต็มเป็นตาราง 2 คูณ 3 ได้ 6 แล้วคำถามสุดท้ายเอา (2, 1) ออก แถวบนกับแถวล่างไม่เหลือหลักที่ใช้ร่วมกัน จึงไม่งอกอะไรเลย เหลือ 3
ใบ้
กฎงอกไม่สนค่าพิกัดเลย สนแค่ว่าจุดไหนแชร์แถวหรือแชร์หลักกัน ลองมองแถวกับหลักเป็นจุดของกราฟ แล้วมองแต่ละจุดในเซตเป็นเส้นเชื่อมแถวกับหลัก สามมุมของสี่เหลี่ยมกลายเป็นอะไรในกราฟนั้น
ที่มาของแนวคิดนี้
สามจุด (x1,y1) (x1,y2) (x2,y1) คือทางเดิน x2 → y1 → x1 → y2 ในกราฟแถวกับหลัก
และจุดที่งอกคือเส้นลัด x2 → y2 พอเห็นเป็นทางเดิน ก็เดาต่อได้ว่าก๊วนที่มี a แถวกับ b หลัก
จะงอกจนเต็มเป็น a × b จุดพอดี คำตอบคือผลรวมของ a × b ทุกก๊วน
ผมไม่เชื่อข้อสรุปนี้จนกว่าตัวตรวจจะยืนยัน ตัวตรวจจึงไม่รู้เรื่องแถว หลัก หรือก๊วนเลย มันวนหาสามมุมตามนิยามแล้วเติมจุดจนนิ่ง ถ้าข้อสรุปผิด สองโปรแกรมจะตอบไม่ตรงกัน ตัวอย่างบนหน้านี้ก็ถูกตรวจด้วยวิธีเดียวกันตอนสร้างหน้า บทเรียนคือ ข้อสรุปที่ทำให้โจทย์ง่ายลงมาก ๆ ต้องมีตัวตรวจที่ไม่เชื่อข้อสรุปนั้น
ถ้ามีแต่ใส่จุด นี่คือ DSU ธรรมดาที่หัวจำ cx (จำนวนแถว) กับ cy (จำนวนหลัก) และเก็บผลรวมไว้ในตัวแปรเดียว
ตอนรวม ลบผลคูณของสองก้อนเก่าออก บวกผลคูณของก้อนใหม่เข้า การเอาจุดออกคือส่วนที่ต้องใช้ท่าของบทนี้ จุดแต่ละจุดเข้าออกได้หลายรอบ แต่ละรอบเป็นช่วงอายุแยกกัน
จุดเดียวที่ต่างจากแม่แบบคือ rollback ต้องคืนผลรวมด้วย ลำดับที่ถูกคือลบผลคูณของก้อนรวมออกก่อน
แล้วค่อยหัก cx, cy ของลูกออกจากแม่ แล้วบวกผลคูณของทั้งสองก้อนกลับเข้าไป
ถ้าสลับลำดับ จะลบผลคูณของก้อนที่ถูกหักไปแล้วแทนก้อนรวม
เมืองหนึ่งมี n คน ทุกวันมีข่าวว่าใครพบใคร ไวรัสมีระยะฟักตัว k วัน ใครที่สัมผัสกันทางตรงหรือทางอ้อมภายใน k วันล่าสุด นับเป็นกลุ่มเสี่ยงเดียวกัน คำสั่ง 1 x y คือวันนี้ x พบ y
2 z ถามว่าตอนนี้ z อยู่กลุ่มเดียวกับกี่คน และ 3 คือขึ้นวันใหม่
อินพุต / ขอบเขต / เอาต์พุต
n q k แล้วคำสั่ง q บรรทัดn ≤ 10⁵, q ≤ 5·10⁵, k ≤ 10⁵ เวลา 5 วินาที| Input | Output |
|---|---|
| 5 12 1 1 1 2 1 1 3 1 3 4 2 4 2 5 3 2 1 1 1 2 1 3 2 2 1 3 2 1 | 4 1 1 3 1 |
อ่านตัวอย่างนี้ยังไง
บรรทัดแรกคือจำนวนคน จำนวนคำสั่ง และระยะฟักตัว ตัวอย่างนี้ระยะฟักตัวเป็น 1 การพบกันจึงมีผลอยู่แค่วันที่พบ พอขึ้นวันใหม่ก็หมดอายุทันที บรรทัดที่เหลือคือคำสั่งเรียงตามเวลา เอาต์พุตมีหนึ่งบรรทัดต่อคำสั่งแบบ 2 ตัวเลขที่พิมพ์คือจำนวนคนในกลุ่มโดยนับตัวคนที่ถูกถามด้วย ไม่ใช่จำนวนครั้งที่พบกัน
วันแรกมีพบกัน 1-2, 1-3 และ 3-4 ถามถึงคนที่ 4 จึงไล่ต่อไปถึง 3, 1 และ 2 ได้ 4 ส่วนคนที่ 5 ยังไม่พบใครเลย เหลือตัวเองคนเดียว ได้ 1 พอเจอคำสั่ง 3 วันใหม่เริ่ม การพบของเมื่อวานหมดอายุทั้งหมด ถามคนที่ 1 จึงเหลือ 1 จากนั้นวันใหม่มีพบกัน 1-2 กับ 3-2 คนที่ 1 จึงถึงคนที่ 2 และ 3 ได้ 3 แล้วขึ้นวันใหม่อีกครั้ง ทุกอย่างหมดอายุ เหลือ 1
| พบกัน | วันที่ | คำสั่งที่ | นับอยู่ถึงคำสั่งที่ |
|---|---|---|---|
| 1-2 | 0 | 0 | 5 |
| 1-3 | 0 | 1 | 5 |
| 3-4 | 0 | 2 | 5 |
| 1-2 | 1 | 7 | 10 |
| 3-2 | 1 | 8 | 10 |
k = 1 การพบกันจึงนับอยู่แค่วันเดียว หมดอายุตอนเริ่มวันถัดไป คำถาม 2 1 หลังขึ้นวันใหม่จึงได้ 1
เพราะทุกคนที่ 1 เคยพบหมดอายุไปแล้ว
ใบ้
การพบกันแต่ละครั้งคือเส้นที่หายไปเอง แต่เงื่อนไขหมดอายุพูดเป็น "วัน" ในขณะที่แกนเวลาที่ต้องตอบคือ "คำสั่ง" จะแปลงวันหมดอายุเป็นเลขคำสั่งได้ตั้งแต่ตอนอ่านหรือเปล่า
ส่วนที่ยากที่สุดคือการอ่านว่านับวันยังไง
ตัวโจทย์ไม่ได้บอกตรง ๆ ว่าการพบวันนี้นับอยู่ถึงวันไหน มีแค่โน้ตหนึ่งบรรทัดกับตัวอย่างสามชุด ผมตั้งสมมติฐานว่า
การพบในวันที่ d นับอยู่ตลอดวัน d ถึง d + k - 1 แล้วเช็กกับตัวอย่าง ผ่านทั้งสามชุด
เพื่อให้แน่ใจว่าตัวอย่างล็อกการตีความไว้จริง ไม่ใช่ผ่านเพราะบังเอิญ ผมลองขยับช่วงอายุยาวขึ้นหนึ่งวันและสั้นลงหนึ่งวัน
ทั้งสองแบบตอบตัวอย่างผิดทุกชุด ตัวอย่างที่ 1 กับ 2 ต่างกันแค่ k และคู่นี้พอจะจับการคลาดหนึ่งวันได้ทั้งสองทิศ
บทเรียนคือ ถ้ากฎนับวันกำกวม ให้ลองขยับหนึ่งวันทั้งสองทิศแล้วดูว่าตัวอย่างล้มไหม
ระหว่างอ่าน จด dayStart[d] คือคำสั่งแรกของวันที่ d ไว้ การพบในวันที่ d ที่คำสั่งที่ i
จึงมีช่วงอายุ [i, dayStart[d + k] - 1] ถ้าวันที่ d + k ไม่มีจริงก็ยาวถึงคำสั่งสุดท้าย ที่เหลือคือแม่แบบทุกบรรทัด
ถึงใบที่เป็นคำถามก็ตอบ sz[find(z)]
มีอีกทางที่รู้จักกัน เพราะการพบเรียงตามวัน การหมดอายุจึงเป็นแบบเข้าก่อนออกก่อน และมีเทคนิคเฉพาะสำหรับ DSU แบบคิว แต่ท่าทั่วไปของบทนี้ก็เร็วพออยู่แล้ว ผมจึงไม่ได้เขียนทางนั้น
อาร์เรย์ยาว n เริ่มเป็นศูนย์ มีคำสั่ง q ข้อ ข้อหนึ่งคือบวก x ให้ทุกช่องตั้งแต่ l ถึง r
เลือกคำสั่งมาทำเป็นชุดใดก็ได้ แล้วดูค่าสูงสุดของอาร์เรย์ ให้หาว่าจำนวน y ตัวไหนบ้างในช่วง 1 ถึง n
ที่เป็นค่าสูงสุดนั้นได้
อินพุต / ขอบเขต / เอาต์พุต
n q แล้วคำสั่ง l r xn, q ≤ 10⁴, x ≤ n เวลา 2 วินาทีy ที่เป็นไปได้ แล้วค่าเหล่านั้นเรียงจากน้อยไปมาก| Input | Output |
|---|---|
| 4 3 1 3 1 2 4 2 3 4 4 | 4 1 2 3 4 |
อ่านตัวอย่างนี้ยังไง
บรรทัดแรกคือความยาวอาร์เรย์กับจำนวนคำสั่ง อีก 3 บรรทัดคือคำสั่ง บรรทัดแรกสุดหมายถึงบวก 1 ให้ช่อง 1 ถึง 3 เอาต์พุตบรรทัดแรกคือจำนวนค่าที่เป็นไปได้ บรรทัดที่สองคือตัวค่าเหล่านั้น ค่าที่นับคือค่าสูงสุดของอาร์เรย์ทั้งเส้นหลังเลือกชุดคำสั่งชุดหนึ่ง ไม่ใช่จำนวนคำสั่งที่เลือกหรือตำแหน่งช่องที่มันเกิด
จุดที่สะดุดง่ายคือโจทย์นับเฉพาะค่าในช่วง 1 ถึง 4 เท่านั้น ค่าที่โผล่เกินนี้มีจริงแต่ไม่ถูกนับ เลือกคำสั่งแรกอย่างเดียว อาร์เรย์เป็น 1 1 1 0 สูงสุด 1 เลือกคำสั่งที่สองอย่างเดียวได้ 0 2 2 2 สูงสุด 2 เลือกสองคำสั่งแรกพร้อมกัน ช่อง 2 กับ 3 โดนทั้งคู่ ได้ 1 3 3 2 สูงสุด 3 และเลือกคำสั่งที่สามอย่างเดียวได้ 0 0 4 4 สูงสุด 4 ครบสี่ค่าพอดี ส่วนชุดที่เหลือ เช่น คำสั่งที่หนึ่งกับที่สามพร้อมกัน ได้ 1 1 5 4 สูงสุด 5 ซึ่งเกิน 4 จึงตกไป
ใบ้
ถ้าค่าสูงสุดเป็น y และเกิดที่ช่อง p คำสั่งที่ไม่คลุม p มีประโยชน์อะไรไหม
ถ้าไม่มี คำถามทั้งข้อก็เหลือแค่เรื่องของช่องเดียว แล้วช่องที่อยู่ติดกันใช้งานร่วมกันได้แค่ไหน
ที่มาของแนวคิดนี้
ทางแรกคือทำ subset sum ด้วย bitset ใหม่ที่ทุกช่อง ได้ราว n · q · n / 64 ≈ 1.6·10¹⁰ ช้าเกินหลายร้อยเท่า
สิ่งที่ซ้ำคือช่องที่อยู่ติดกันถูกคลุมด้วยคำสั่งชุดเกือบเดียวกัน ทางที่สองคือกวาดจากซ้ายไปขวา คำสั่งไหนเริ่มก็ใส่ คำสั่งไหนจบก็ถอด
ติดตรงการถอด เพราะ reach |= reach << x ย้อนไม่ได้ OR ไม่จำว่าบิตเปิดมาจากคำสั่งไหน
สองทางนี้บอกรูปของคำตอบว่า ต้องแบ่งกันใช้ส่วนที่ร่วมกัน และต้องไม่ถอดเลย ต้นไม้ช่วงให้ทั้งสองอย่าง ส่วนโครงที่ใช้ ผมรู้จักอยู่ก่อนแล้ว บทเรียนคือ ของที่ย้อนไม่ได้เลยก็ยังใช้ท่านี้ได้ ถ้าไม่ต้องย้อน
ข้อสังเกตแรก ถ้าเลือกเฉพาะคำสั่งที่คลุมช่อง p ช่องอื่นได้แค่บางตัวในชุดนั้น เพราะ x เป็นบวกทุกตัว
จึงไม่มีช่องไหนเกินค่าที่ p ค่า y จึงเป็นไปได้ก็ต่อเมื่อมีสักช่องที่ทำผลรวม y ได้จากคำสั่งที่คลุมมัน
ข้อนี้ใช้ครึ่งแรกของท่าเท่านั้น แกนของต้นไม้เป็นตำแหน่ง ไม่ใช่เวลา แจกคำสั่งลงปมด้วย addRange
แล้ว DFS ด้วย bitset หนึ่งแถวต่อความลึก ลูกทำงานบนสำเนา แม่ยังถืออันเดิมไว้ การ "ย้อน" จึงเกิดเองโดยไม่ต้องถอดอะไร
ต้นไม้บนหนึ่งหมื่นตำแหน่งลึกไม่เกิน 14 ชั้น จึงใช้ bitset แค่ 16 แถว
พิพิธภัณฑ์มีของจัดแสดง แต่ละชิ้นมีราคาและมวล มีเหตุการณ์สามแบบ ของใหม่เข้าตู้ ของเลข x ถูกเก็บเข้าห้องนิรภัย
และคำถามว่า ถ้าขโมยของที่จัดแสดงอยู่ได้มวลรวมไม่เกิน m จะได้ราคาสูงสุดเท่าไร ต้องตอบทุก m ตั้งแต่ 1 ถึง k
แล้วรวมเป็นตัวเลขตัวเดียวด้วยสูตร hash ของโจทย์
อินพุต / ขอบเขต / เอาต์พุต
n k ของตั้งต้น n ชิ้น แล้ว q เหตุการณ์ 1 v w, 2 x หรือ 3n ≤ 5000, k ≤ 1000, q ≤ 30,000 ของใหม่ไม่เกินหนึ่งหมื่นชิ้น เวลา 2 วินาทีs(m) · p^(m-1) มอดุโล 10⁹ + 7 โดย p = 10⁷ + 19| Input | Output |
|---|---|
| 3 10 30 4 60 6 5 1 9 3 1 42 5 1 20 3 3 2 2 2 4 3 1 40 6 3 | 556674384 168191145 947033915 181541912 |
อ่านตัวอย่างนี้ยังไง
บรรทัดแรกคือจำนวนของตั้งต้นกับมวลสูงสุดที่สนใจ อีกสามบรรทัดคือราคากับมวลของของแต่ละชิ้น ถัดมาคือจำนวนเหตุการณ์ แล้วตัวเหตุการณ์ เอาต์พุตมีหนึ่งบรรทัดต่อเหตุการณ์แบบ 3 ตัวอย่างนี้จึงมี 4 บรรทัด แต่ละบรรทัดคือค่า hash ที่ยุบคำตอบของทุกมวลตั้งแต่ 1 ถึง 10 ให้เหลือตัวเลขเดียว ไม่ใช่ราคาสูงสุดของมวลใดมวลหนึ่ง ตัวเลขพวกนี้จึงเทียบกันตรง ๆ ไม่ได้ ใช้ดูได้อย่างเดียวว่าตรงกับเฉลยไหม
คำถามแรกถามตอนที่ตู้มีของสามชิ้นแรก จากนั้นของเลข 4 กับ 5 เข้าตู้ แล้วถามเป็นคำถามที่สอง ต่อมาของเลข 2 กับ 4 ถูกเก็บเข้าห้องนิรภัย คำถามที่สามจึงเหลือของเลข 1, 3 และ 5 แล้วของเลข 6 เข้าตู้ก่อนคำถามสุดท้าย ตู้ตอนนั้นมีของสี่ชิ้น จากทั้งหมด 6 ชิ้นที่เคยผ่านเข้ามา
| เลข | ราคา | มวล | เห็นตั้งแต่คำถาม | ถึงคำถาม |
|---|---|---|---|---|
| 1 | 30 | 4 | 0 | 3 |
| 2 | 60 | 6 | 0 | 1 |
| 3 | 5 | 1 | 0 | 3 |
| 4 | 42 | 5 | 1 | 1 |
| 5 | 20 | 3 | 1 | 3 |
| 6 | 40 | 6 | 3 | 3 |
ใบ้
ใส่ของหนึ่งชิ้นลง knapsack ใช้ O(k) แต่ถอดออกไม่ได้ เพราะ max ไม่จำว่าได้ค่ามาจากทางไหน
ถ้าจะใช้ท่าของบทนี้ ต้องย้อนกลับได้ การใส่ของหนึ่งชิ้นแก้ได้ถึง k ช่อง จะย้อนด้วยอะไรถึงจะคุ้ม
เรื่องจริงที่เกิดขึ้นตามลำดับ
รุ่นแรกใช้ -1 แทน "ของชิ้นนี้ยังไม่ถูกเก็บ" แล้วตอนเก็บของก็เขียนคำถามสุดท้ายที่ยังเห็นมัน ซึ่งคือ T - 1
ปัญหาคือถ้าของถูกเก็บก่อนมีคำถามแรก T - 1 ก็เท่ากับ -1 พอดี ของที่ถูกเก็บไปแล้วจึงถูกนับว่าอยู่ตลอดไป
รุ่นนั้นผ่านตัวอย่างที่ 1 เพราะไม่มีของชิ้นไหนถูกเก็บก่อนคำถามแรก แต่ตัวอย่างที่ 2 ของโจทย์เองก็ล้ม มันตอบ 516052772 แทน 0 ตัวตรวจจับได้ตั้งแต่รอบแรก อินพุตที่เล็กที่สุดที่ทำให้ผิด หาได้จากการไล่ทุกกรณีเรียงตามจำนวนเหตุการณ์ คือของหนึ่งชิ้น เก็บมันทิ้ง แล้วถาม รุ่นผิดตอบ 1 ที่ถูกคือ 0
แก้ด้วยค่าคงที่ ALIVE = INT_MAX บทเรียนคือ ค่าพิเศษที่แทน "ไม่มี" ต้องอยู่นอกช่วงของค่าจริงทั้งหมด
-1 ดูเหมือนอยู่นอกช่วงเลขคำถาม จนกว่าจะเจอกรณีที่ยังไม่มีคำถามเลย
ย้อนด้วยสำเนา ไม่ใช่สแต็ก การใส่ของหนึ่งชิ้นแก้ได้ถึง k + 1 ช่อง จดทุกช่องลงสแต็กก็ได้ แต่งานเท่ากับคัดลอกทั้งแถวอยู่แล้ว
เก็บอาร์เรย์ dp หนึ่งแถวต่อความลึก ลูกทำงานบนแถวของตัวเอง แม่ยังถืออันเดิม ต้นไม้บนสามหมื่นใบลึก 15 ชั้น ใช้ไม่ถึง 140 KB
และเลือกแกนเวลาเป็นคำถาม เหมือนข้อ 2 ของบทนี้ ทุกใบจึงเป็นคำถามจริง ค่า s(m) สูงได้ราว 1.5·10¹⁰
ต้องมอดก่อนคูณกับกำลังของ p ไม่งั้น long long ล้น
กราฟมีน้ำหนักต่อกันครบ ต้นไม้แผ่ทั่วที่เบาที่สุด (ย่อว่า MST) อาจมีได้หลายต้น แต่ละคำถามให้ชุดของเส้นมา ให้ตอบว่ามี MST สักต้นที่มีเส้นในชุดนั้นครบทุกเส้นไหม
อินพุต / ขอบเขต / เอาต์พุต
n m เส้น u v w แล้ว q คำถาม แต่ละคำถามคือ k กับเลขเส้น k ตัวn, m, q ≤ 5·10⁵ ผลรวมของ k ทุกคำถามไม่เกิน 5·10⁵ เวลา 2 วินาทีYES หรือ NO ต่อคำถาม| Input | Output |
|---|---|
| 5 7 1 2 2 1 3 2 2 3 1 2 4 1 3 4 1 3 5 2 4 5 2 4 2 3 4 3 3 4 5 2 1 7 2 1 2 | YES NO YES NO |
อ่านตัวอย่างนี้ยังไง
บรรทัดแรกคือจำนวนจุดกับจำนวนเส้น จากนั้น 7 บรรทัดคือเส้นละบรรทัด (ปลายสองข้างกับน้ำหนัก)
เส้นบรรทัดแรกคือเส้นหมายเลข 1 ไล่ลงไปตามลำดับ แล้วบอกจำนวนคำถาม ตามด้วยคำถามละบรรทัด
ขึ้นต้นด้วยจำนวนเส้นในชุด แล้วตามด้วยเลขเส้น คำถามหนึ่งข้อถามว่ามีต้นไม้แผ่ทั่วที่เบาที่สุดสักต้นไหม
ที่มีเส้นในชุดนั้นอยู่ครบทุกเส้นพร้อมกัน เอาต์พุตจึงเป็น YES หรือ NO ของแต่ละคำถาม
ไม่ใช่น้ำหนักของต้นไม้หรือจำนวนต้นที่ทำได้
กราฟนี้มีต้นไม้แผ่ทั่วที่เบาที่สุดอยู่ 12 ต้น ทุกต้นหนัก 6 เท่ากัน คำถามแรกขอเส้น 3 กับ 4 ซึ่งมีต้นที่รับไว้ได้ทั้งคู่ จึงตอบ YES คำถามที่สองขอเส้น 3, 4, 5 ครบทั้งสาม แต่สามเส้นนี้คือสามเหลี่ยม 2-3-4 ใส่พร้อมกันแล้วเป็นวง จึงตอบ NO คำถามที่สามขอเส้น 1 กับ 7 ซึ่งอยู่คนละมุมของกราฟ เส้นหนึ่งลากจุด 1 เข้ามา อีกเส้นลากจุด 5 เข้ามา ต้นเดียวรับได้ทั้งคู่ จึงตอบ YES ส่วนคำถามสุดท้ายขอเส้น 1 กับ 2 ซึ่งหนัก 2 ทั้งคู่และต่างก็ลากจุด 1 เข้ามาเหมือนกัน ต้นเดียวต้องการแค่เส้นเดียวพอ รับทั้งคู่เมื่อไหร่ก็หนักเกิน 6 จึงตอบ NO
ใบ้
คิดทีละเส้นไม่ได้ ตัวอย่างข้อสองหักความคิดนั้นอยู่แล้ว ลองนึกว่า Kruskal เดินถึงน้ำหนัก w ตอนนั้นก๊วนที่เกิดจากเส้นที่เบากว่า w
ขึ้นกับว่าเลือกเส้นไหนมาก่อนหรือเปล่า แล้วถ้าต้องลองเส้นของคำถามหนึ่งบนก๊วนแบบนั้น จะคืนก๊วนให้คำถามถัดไปยังไง
สองทางที่ผมลองก่อน แล้วทิ้ง
ทางแรกคือบังคับเส้นของคำถามเข้าไปก่อนแล้วรัน Kruskal ต่อจนได้ต้นไม้ แล้วเทียบน้ำหนัก ถูกแต่ช้า Kruskal หนึ่งรอบแตะห้าแสนเส้น
คูณห้าแสนคำถามเป็น 2.5·10¹¹ ทางที่สองคือหาว่าเส้นแต่ละเส้นอยู่ใน MST ได้ไหม ซึ่งคำถามที่สองของตัวอย่างหักทิ้งทันที
เส้น 3, 4, 5 อยู่ใน MST ได้ทุกเส้น แต่อยู่พร้อมกันไม่ได้
ข้อสรุปที่ใช้ ผมเชื่อจากการคิดแต่ไม่ได้พิสูจน์ละเอียด ตัวตรวจจึงไม่ใช้มันเลย มันไล่ทุกชุดเส้น n - 1 เส้น
เก็บต้นไม้แผ่ทั่วที่เบาที่สุดทั้งหมด แล้วดูตรง ๆ ว่ามีต้นไหนมีเส้นของคำถามครบ บทเรียนคือ rollback ไม่ได้มีไว้คู่กับต้นไม้ช่วงอย่างเดียว
เมื่อไรที่ต้องลองของบนสถานะร่วมแล้วคืนสถานะให้คนถัดไป ก็ใช้ได้ทันที
ตอน Kruskal เดินถึงน้ำหนัก w ก๊วนที่เกิดจากเส้นที่เบากว่า w เป็นแบบเดียวกันเสมอ ไม่ว่าจะเลือกเส้นไหนมาก่อน
ชุดเส้นน้ำหนัก w ของคำถามหนึ่งจะอยู่ใน MST ด้วยกันได้ ก็ต่อเมื่อบนก๊วนแบบนั้นมันไม่สร้างวง
แตกทุกคำถามเป็นกลุ่มย่อยตามน้ำหนัก เรียงทุกกลุ่มตามน้ำหนัก แล้วเดิน Kruskal ไปพร้อมกัน
ก่อนลองกลุ่มน้ำหนัก w รวมถาวรทุกเส้นที่เบากว่า w จด keep ลองเส้นของกลุ่ม แล้ว rollback(keep)
การรวมถาวรของ Kruskal อยู่ใต้ keep เสมอจึงไม่ถูกย้อนไปด้วย ข้อนี้จึงใช้ครึ่งหลังของท่าโดยไม่มีต้นไม้ช่วงเลย
เพราะการย้อนมีแค่ชั้นเดียว
ตู้หนังสือ n ชั้น ชั้นละ m ช่อง คำสั่งมีสี่แบบ วางหนังสือ หยิบหนังสือ กลับด้านทั้งชั้น
และ 4 k ทำให้ตู้กลับไปเหมือนหลังคำสั่งที่ k หลังทุกคำสั่งให้ตอบว่าตู้มีหนังสือกี่เล่ม
อินพุต / ขอบเขต / เอาต์พุต
n m q แล้วคำสั่ง 1 i j, 2 i j, 3 i หรือ 4 kn, m ≤ 1000, q ≤ 10⁵ เวลา 2 วินาที| Input | Output |
|---|---|
| 4 2 6 3 2 2 2 2 3 3 3 2 2 2 2 3 2 | 2 1 3 3 2 4 |
อ่านตัวอย่างนี้ยังไง
บรรทัดแรก 4 2 6 คือตู้ 4 ชั้น ชั้นละ 2 ช่อง และมีคำสั่ง 6 ข้อ บรรทัดที่เหลือคือคำสั่งข้อละบรรทัด เอาต์พุตมีบรรทัดละหนึ่งจำนวน คือจำนวนหนังสือของทั้งตู้หลังคำสั่งนั้น ไม่ใช่จำนวนหนังสือของชั้นที่เพิ่งถูกแตะ
เริ่มจากตู้ว่าง คำสั่ง 3 2 กลับด้านชั้น 2 ทั้งชั้น ชั้นนั้นจึงเต็มสองเล่ม ได้ 2
คำสั่ง 2 2 2 หยิบเล่มที่ช่อง 2 ของชั้น 2 ออก เหลือ 1 คำสั่ง 3 3
กลับด้านชั้น 3 เพิ่มมาอีกสองเล่ม รวม 3 คำสั่ง 3 2 กลับด้านชั้น 2 อีกครั้ง
ชั้นนั้นยังมีเล่มเดียวเท่าเดิม แค่ย้ายช่อง ยอดจึงค้างที่ 3 สองคำสั่งท้ายหยิบเล่มนั้นออก
แล้วกลับด้านชั้น 2 ได้ 2 กับ 4
ตัวอย่างนี้ไม่มีคำสั่งแบบ 4 (ย้อนกลับไปสภาพหลังคำสั่งก่อนหน้าข้อใดข้อหนึ่ง) เลยสักข้อ รูปข้างล่างจึงวาดจากตัวอย่างอีกชุดของโจทย์ที่มีคำสั่งนั้น
ใบ้
เก็บตู้ทั้งตู้ทุกเวอร์ชันคือล้านช่องคูณแสนเวอร์ชัน ลองดูรูปของเวอร์ชันแทน ทุกเวอร์ชันเกิดจากเวอร์ชันเดียวก่อนหน้าด้วยคำสั่งเดียว นั่นคือต้นไม้ แล้วถ้าเดินต้นไม้นี้ด้วยท่าเข้าปมก็ทำ ออกจากปมก็ย้อน ตู้ในหน่วยความจำจะเป็นเวอร์ชันไหนตอนอยู่ที่ปมไหน
ถูกตั้งแต่แรก แต่พังบนเครื่องที่สแต็กเล็ก
รุ่นแรกเดินต้นไม้เวอร์ชันด้วย dfs ที่เรียกตัวเอง ผ่านตัวอย่างและตัวตรวจทุกรอบ แต่ต้นไม้เวอร์ชันลึกได้ถึงแสนชั้น
เมื่อไม่มีคำสั่งแบบ 4 เลย บนเครื่องที่ผมใช้ ถ้าคอมไพล์แบบปกติ รุ่นนั้นไม่พิมพ์อะไรออกมาเลยบนเคสแบบนี้
ต้องสั่งขยายสแต็กตอนลิงก์ถึงจะรันได้ ผมไม่ได้ส่งขึ้นระบบตรวจของ Codeforces จึงไม่รู้ว่าที่นั่นสแต็กพอหรือไม่
นี่คือเรื่องเดียวกับข้อ Knight Tournament ในบท DSU ความลึกที่ไม่มีใครคุม โค้ดบนหน้านี้จึงเดินต้นไม้ด้วยสแต็กของตัวเอง ผลลัพธ์ตรงกับรุ่นเรียกตัวเองที่ขยายสแต็กแล้วทุกบรรทัดบนเคสใหญ่ทั้งสี่แบบ
คำสั่ง v ต่อจากเวอร์ชัน v - 1 ส่วน 4 k ต่อจากเวอร์ชัน k แล้วไม่ทำอะไร
เวอร์ชันทั้งหมดจึงเป็นต้นไม้ที่มีตู้ว่างเป็นราก เดินจากราก เข้าปมก็ทำคำสั่งของปมนั้นแล้วอ่านคำตอบ ออกจากปมก็ย้อนคำสั่งนั้น
นี่คือครึ่งหลังของท่าเดียวกัน บนต้นไม้ที่โจทย์ให้มา แทนต้นไม้ช่วงที่เราสร้างเอง
คำสั่งทุกแบบย้อนได้ด้วยการทำซ้ำอีกครั้ง แต่ต้องย้อนเฉพาะคำสั่งที่เปลี่ยนตู้จริง วางบนช่องที่มีหนังสือแล้วหรือหยิบจากช่องว่างไม่เปลี่ยนอะไร
จึงเก็บ changed ของแต่ละเวอร์ชันไว้ ส่วนการกลับด้านทั้งชั้นเก็บเป็นธงหนึ่งตัวต่อชั้น ช่องจะมีหนังสือจริงเมื่อค่าในช่องไม่เท่ากับธงของชั้น
การกลับด้านจึงเหลือ O(1)
กราฟต่อกันเป็นก้อนเดียว ความยาวของทางเดินคือ xor ของน้ำหนักทุกเส้นที่เดินผ่าน เดินซ้ำเส้นก็ xor ซ้ำ
คำสั่งมีสามแบบ เพิ่มเส้น ลบเส้น (ลบแล้วกราฟยังต่อกัน) และถามความยาวที่สั้นที่สุดจาก x ไป y
อินพุต / ขอบเขต / เอาต์พุต
n m เส้น x y d แล้ว q คำสั่ง 1 x y d, 2 x y หรือ 3 x yn, m, q ≤ 2·10⁵, d < 2³⁰ เวลา 3.5 วินาที| Input | Output |
|---|---|
| 5 5 1 2 3 2 3 4 3 4 5 4 5 6 1 5 1 5 3 1 5 1 1 3 1 3 1 5 2 1 5 3 1 5 | 1 1 2 |
อ่านตัวอย่างนี้ยังไง
บรรทัดแรกคือจำนวนจุดกับจำนวนเส้นตั้งต้น ตามด้วยเส้นละบรรทัด (ปลายสองข้างกับน้ำหนัก) แล้วบอกจำนวนคำสั่ง และคำสั่งข้อละบรรทัด เอาต์พุตออกเฉพาะคำสั่งแบบ 3 ตัวอย่างนี้มีคำสั่ง 5 ข้อ แต่มีคำตอบแค่ 3 บรรทัด ตัวเลขที่ตอบคือค่า xor ของน้ำหนักตลอดทางเดิน ไม่ใช่จำนวนเส้นที่เดินผ่าน
คำสั่งแรกถามจาก 1 ไป 5 ตอนนั้นเส้น 1-5 น้ำหนัก 1 ยังอยู่ เดินเส้นเดียวได้ 1 คำสั่งที่สองเพิ่มเส้น 1-3 น้ำหนัก 1 เข้ามา ถามซ้ำก็ยังได้ 1 เพราะทางลัดเดิมยังไม่หายไปไหน คำสั่งที่สี่ลบเส้น 1-5 ทิ้ง คำถามสุดท้ายจึงต้องอ้อมไปทาง 1-3-4-5 ได้ 1 xor 5 xor 6 เท่ากับ 2
ใบ้
ถ้าเดินไปวนวงหนึ่งแล้วเดินกลับทางเดิม ขาไปกับขากลับหักล้างกัน เหลือแค่ค่าของวง ทางเดินไหนก็ได้จาก x ไป y จึงเท่ากับทางใดทางหนึ่งบนต้นไม้ xor กับวงบางชุด บนกราฟที่ไม่เปลี่ยน โจทย์นี้ใช้ของสองอย่าง แล้วเมื่อกราฟเปลี่ยนได้ ของสองอย่างนั้นย้อนกลับได้หรือเปล่า
ที่มาของแนวคิดนี้
ข้อนี้ประกอบจากสองท่าที่รู้จักอยู่แล้ว ไม่ได้ค้นพบระหว่างทำ ท่าแรกคือเวอร์ชันกราฟคงที่ คำตอบคือ xor ของระยะจากรากถึงสองจุด แล้วลดให้เล็กที่สุดด้วยฐานของ xor (linear basis) ของค่าวงทั้งหมด ท่าที่สองคือทั้งบทนี้ งานจริงคือเลือกว่าแต่ละส่วนย้อนด้วยวิธีไหน
ตัวตรวจต้องไม่ใช้ฐาน xor ไม่งั้นมันก็เชื่อสิ่งเดียวกับตัวจริง จึงให้ BFS บนสถานะ (จุด, xor ที่สะสม) ซึ่งคือนิยามของโจทย์ตรง ๆ
ใช้ได้เมื่อน้ำหนักเล็ก บิตสูงตรวจแยกด้วยการคูณน้ำหนักทุกเส้นด้วย 2²⁷ บทเรียนคือ โครงสร้างหลายตัวในโปรแกรมเดียวย้อนกลับคนละวิธีได้
ขอแค่ทุกตัวถูกย้อนที่จุดเดียวกัน คือตอนออกจากปม
DSU เก็บ d[x] คือ xor จาก x ถึงแม่ ท่าเดียวกับข้อ 3 แต่เป็นตัวเลขเต็มแทนบิตเดียว ตอนรวม ตั้ง d
ของหัวที่ไปขึ้นกับอีกฝั่งให้ทางในต้นไม้จาก x ไป y มีค่าเท่ากับน้ำหนักเส้นจริงพอดี ถ้าสองจุดอยู่ก๊วนเดียวกันแล้ว เส้นนี้ไม่ต่อก๊วนแต่ปิดวง
ค่าของวงคือ dx xor dy xor w ส่งเข้าฐาน
DSU ย้อนด้วยสแต็กเพราะแต่ละการรวมแก้ไม่กี่ช่องในอาร์เรย์ขนาด n ส่วนฐานมีแค่ 30 ช่อง จึงย้อนด้วยสำเนาหนึ่งแถวต่อความลึก
แบบเดียวกับ knapsack ในข้อ 7 สองวิธีอยู่ในโปรแกรมเดียวกันได้โดยไม่ต้องรักษาลำดับให้ตรงกัน
ถ้ารู้เหตุการณ์ทั้งหมดล่วงหน้า เปลี่ยนการเพิ่มและลบเป็นช่วงอายุ วางแต่ละช่วงลงต้นไม้บนแกนเวลา แล้วเดินต้นไม้โดยใส่ตอนเข้าปมและย้อนตอนออก เครื่องมือข้างในต้องย้อนได้ ซึ่งสำหรับ DSU แปลว่ารวมตามขนาดอย่างเดียว ห้ามบีบทางเดิน
ทั้งบทไม่มีการลบเกิดขึ้นเลยสักครั้ง เราแค่จัดลำดับการใส่ใหม่ จนทุกครั้งที่ของชิ้นหนึ่งต้องหายไป มันคือชิ้นที่ใส่ล่าสุดพอดี
addRange ในหน้านี้