n 個村莊(n ≤ 50,000)排成一線,相鄰村莊有隧道相連。m 個事件(m ≤ 50,000):
D x:第 x 個村莊被摧毀(隧道斷開)Q x:查詢第 x 個村莊所在連通塊大小(含自身)R:恢復最近一次被摧毀的村莊(棧式撤銷)輸出每個 Q 操作的答案。樣例中村莊 3,6,5 依次摧毀,查詢 4 輸出 1(孤立),查詢 5 輸出 0(已摧毀),恢復後查詢 4 輸出 2,再恢復查詢 4 輸出 4。
村莊連通塊由「被摧毀村莊」分割。若村莊 x 未被摧毀,其所在連通塊邊界為:
兩端虛擬邊界:0 和 n+1 視為永久「被摧毀」。
| 事件 | 平衡樹操作 |
|---|---|
| D x | 插入 x;壓入棧記錄撤銷順序 |
| R | 棧頂彈出 y,刪除 y |
| Q x | 若 x 在樹中 → 輸出 0;否則查前驱 p、後繼 s,輸出 s - p - 1 |
const int MAXN = 50005; int ch[MAXN][2], fa[MAXN], val[MAXN], sz[MAXN], rnd[MAXN]; bool destroyed[MAXN]; stackhistory; int root = 0, tot = 0; // 旋轉、Splay、查找第 k 大、前驱/後繼、插入、刪除 // ... 標準 Splay 實現 ... int get_pre(int x) { // 最大的 < x int cur = root, ans = 0; while (cur) { if (val[cur] < x) { ans = cur; cur = ch[cur][1]; } else cur = ch[cur][0]; } return ans ? val[ans] : 0; // 0 為左虛擬邊界 } int get_suc(int x) { // 最小的 > x int cur = root, ans = 0; while (cur) { if (val[cur] > x) { ans = cur; cur = ch[cur][0]; } else cur = ch[cur][1]; } return ans ? val[ans] : n+1; // n+1 為右虛擬邊界 } int main() { n = read(); m = read(); // 初始化:插入虛擬邊界 0 和 n+1 insert(root, 0); insert(root, n+1); while (m--) { char op = read_op(); if (op == 'D') { int x = read(); destroyed[x] = true; history.push(x); insert(root, x); } else if (op == 'Q') { int x = read(); if (destroyed[x]) printf("0\n"); else { int p = get_pre(x), s = get_suc(x); printf("%d\n", s - p - 1); } } else { // 'R' int x = history.top(); history.pop(); destroyed[x] = false; del(root, x); } } }
本題在線操作要求,Splay/Treap/紅黑樹均可,Splay 代碼量較大但均攤 O(log n) 穩定通過。
scanf("%d%d", &n, &m) 後用 getchar() 讀操作字符,跳過空白fa、sz,pushup 別漏本題是「動態區間連通性查詢」的經典轉化:維護分割點有序集合,查詢轉化為前驱後繼問題。Splay 树提供統一的區間操作框架,掌握此模式可解決動態區間第 k 大、區間翻轉、區間刪除/插入等大類問題。