新卡訊息(究極寶玉神)
阿新 • • 發佈:2022-04-01
CDQ分治
用於解決偏序問題。
《演算法競賽進階指南》中,稱CDQ分治為“基於時間的分治演算法”,其實是偏序問題的一種特殊形式。
二維偏序
在學習線段樹和樹狀陣列時,已經可以利用排序+資料結構 \(O(N\log{N})\) 解決二維偏序問題。同樣,CDQ分治也行。
有 \(N\) 個元素,每個元素有 \(a,b,c\) 三個屬性。求對於每個元素 \(i\),滿足 \(a_j≤a_i\) 且 \(b_j≤b_i\) 且 \(j≠i\) 的 \(j\) 的數量。
先將所有元素按 \(a\) 為第一關鍵字,\(b\) 為第二關鍵字排序。再在排序好的陣列上進行以 \(b\) 為關鍵字的歸併排序,並在歸併時進行統計。
具體來說,將陣列分成兩段,對兩段分別進行歸併排序。此步驟結束後,要求每段中 \(b\)
是不是感覺思路很像求逆序對?實際上逆序對也是二維偏序問題,而且其中一維(下標)已經有序。
另外,關於兩個元素相等的情況,如上的演算法顯然會漏算。解決的方法也非常簡單,只需將相等的元素合併,做上標記,更新答案時特殊處理即可。
三維偏序
如果真正理解了二維偏序,三維偏序也很好解決。套上一個樹狀陣列即可。
將所有元素按 \(a\) 為第一關鍵字,\(b\) 為第二關鍵字,\(c\) 為第三關鍵字排序。再在排序好的陣列上進行以 \(b\)
可以自己驗證其正確性。
模板題
Code
#include<cstdio> #include<algorithm> using namespace std; const int N = 1e5 + 5, M = 2e5 + 5; int n, m, t[M], ans[N]; struct Node { int a, b, c, cnt, res; bool operator <(const Node &o) const { return a != o.a ? a < o.a : (b != o.b ? b < o.b : c < o.c); } bool operator ==(const Node &o) const { return a == o.a && b == o.b && c == o.c; } } x[N], tmp[N]; int read() { int x = 0; char c = getchar(); while (c < '0' || c > '9') c = getchar(); while (c >= '0' && c <= '9') {x = (x << 3) + (x << 1) + (c ^ 48); c = getchar();} return x; } void Init() { sort(x + 1, x + n + 1); int j = 0; for (int i = 1; i <= n; i++) if (j && x[i] == x[j]) x[j].cnt++; else x[++j] = x[i]; n = j; } void Add(int x, int v) { for (; x <= m; x += x & -x) t[x] += v; } int Ask(int x) { int res = 0; for (; x; x -= x & -x) res += t[x]; return res; } void CDQ(int l, int r) { if (l == r) return ; int mid = l + r >> 1; CDQ(l, mid); CDQ(mid + 1, r); int i = l, j = mid + 1, k = l; while (i <= mid && j <= r) if (x[i].b <= x[j].b) { Add(x[i].c, x[i].cnt); tmp[k++] = x[i++]; } else { x[j].res += Ask(x[j].c); tmp[k++] = x[j++]; } while (i <= mid) { Add(x[i].c, x[i].cnt); tmp[k++] = x[i++]; } while (j <= r) { x[j].res += Ask(x[j].c); tmp[k++] = x[j++]; } for (i = l; i <= mid; i++) Add(x[i].c, -x[i].cnt); //memset很慢,減回去更快 for (i = l; i <= r; i++) x[i] = tmp[i]; } int main() { int s; s = n = read(); m = read(); for (int i = 1; i <= n; i++) x[i] = (Node){read(), read(), read(), 1}; Init(); CDQ(1, n); for (int i = 1; i <= n; i++) ans[x[i].res + x[i].cnt - 1] += x[i].cnt; for (int i = 0; i < s; i++) printf("%d\n", ans[i]); return 0; }
基於時間的分治演算法
將時間(時間戳)看作一個維度。對查詢操作產生影響的修改操作一定在其之前,可以發現有偏序的性質。那麼許多線上問題可以轉化為離線,利用CDQ分治求解。
例:樹狀陣列模板
先將區間求和轉化為求字首和。加入時間維度,可以發現問題變成了二維偏序,分別為“時間”和“下標”。另外,其實此題還有一個隱藏的維度 \(k\),表示操作種類。設修改為0,查詢為1,則可以抽象成更具普適性的三維偏序。然而此題 \(k\) 只有兩種取值,無需再維護樹狀陣列,只需樸素地判斷即可。做法同二維偏序。
點選檢視程式碼
#include<cstdio>
using namespace std;
const int N = 1e5 + 5;
int n, m, ans[N], q;
struct Node {
int k, p, v, id;
bool operator <(const Node &oth) const {
return p != oth.p ? p < oth.p : k < oth.k;
}
} a[3 * N], tmp[3 * N];
int read() {
int x = 0, f = 1; char c = getchar();
while (c < '0' || c > '9') {if (c == '-') f = -1; c = getchar();}
while (c >= '0' && c <= '9') {x = (x << 3) + (x << 1) + (c ^ 48); c = getchar();}
return x * f;
}
void CDQ(int l, int r) {
if (l == r) return ;
int mid = l + r >> 1;
CDQ(l, mid); CDQ(mid + 1, r);
int sum = 0, i = l, j = mid + 1, tot = l;
while (i <= mid && j <= r)
if (a[i] < a[j]) {
if (a[i].k == 1) sum += a[i].v;
tmp[tot++] = a[i++];
}
else {
if (a[j].k == 2) ans[a[j].id] += sum * a[j].v;
tmp[tot++] = a[j++];
}
while (i <= mid) tmp[tot++] = a[i++];
while (j <= r) {
if (a[j].k == 2) ans[a[j].id] += sum * a[j].v;
tmp[tot++] = a[j++];
}
for (i = l; i <= r; i++) a[i] = tmp[i];
}
int main() {
n = read(); m = read();
for (int i = 1; i <= n; i++) a[i] = (Node){1, i, read()};
for (int i = 1; i <= m; i++) {
int opt = read(), x = read(), y = read();
if (opt == 1) a[++n] = (Node){1, x, y};
else {
a[++n] = (Node){2, y, 1, ++q};
a[++n] = (Node){2, x - 1, -1, q};
}
}
CDQ(1, n);
for (int i = 1; i <= q; i++) printf("%d\n", ans[i]);
return 0;
}
同樣,可以再加一維,變成四維(三維)偏序。例:莫基亞
點選檢視程式碼
#include<cstdio>
#include<algorithm>
using namespace std;
const int N = 2e5 + 5, M = 5e5 + 5;
int n, m, t[M], idx, ans[N];
struct Node {
int k, x, y, t, v, id;
bool operator <(const Node &o) const {
return t != o.t ? t < o.t : x != o.x ? x < o.x : y != o.y ? y < o.y : k < o.k;
}
} q[N << 2], tmp[N << 2];
int read() {
int x = 0; char c = getchar();
while (c < '0' || c > '9') c = getchar();
while (c >= '0' && c <= '9') {x = (x << 3) + (x << 1) + (c ^ 48); c = getchar();}
return x;
}
void Add(int x, int v) {
for (; x <= m; x += x & -x) t[x] += v;
}
int Ask(int x) {
int res = 0;
for (; x; x -= x & -x) res += t[x];
return res;
}
void CDQ(int l, int r) {
if (l == r) return ;
int mid = l + r >> 1;
CDQ(l, mid); CDQ(mid + 1, r);
int i = l, j = mid + 1, k = l;
while (i <= mid && j <= r)
if (q[i].x <= q[j].x) {
if (!q[i].k) Add(q[i].y, q[i].v);
tmp[k++] = q[i++];
}
else {
if (q[j].k) ans[q[j].id] += Ask(q[j].y) * q[j].v;
tmp[k++] = q[j++];
}
while (i <= mid) {
if (!q[i].k) Add(q[i].y, q[i].v);
tmp[k++] = q[i++];
}
while (j <= r) {
if (q[j].k) ans[q[j].id] += Ask(q[j].y) * q[j].v;
tmp[k++] = q[j++];
}
for (i = l; i <= mid; i++)
if (!q[i].k) Add(q[i].y, -q[i].v);
for (i = l; i <= r; i++) q[i] = tmp[i];
}
int main() {
int opt;
m = read();
while (scanf("%d", &opt), opt != 3) {
if (opt == 1) q[++n] = (Node){0, read(), read(), n, read()};
else {
int x1 = read(), y1 = read(), x2 = read(), y2 = read();
q[++n] = (Node){1, x2, y2, n, 1, ++idx};
q[++n] = (Node){1, x2, y1 - 1, n, -1, idx};
q[++n] = (Node){1, x1 - 1, y2, n, -1, idx};
q[++n] = (Node){1, x1 - 1, y1 - 1, n, 1, idx};
}
}
sort(q + 1, q + n + 1);
CDQ(1, n);
for (int i = 1; i <= idx; i++) printf("%d\n", ans[i]);
return 0;
}
CDQ分治的思想還是挺簡單的。