1. 程式人生 > 遊戲資訊 >新卡訊息(究極寶玉神)

新卡訊息(究極寶玉神)

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\) 排序,所以左段中的 \(a\) 一定小於等於右段中的 \(a\)。那麼利用雙指標可以 \(O(N)\) 求出:對於右段的每個數,左段中有多少個數 \(b\) 不大於它。
是不是感覺思路很像求逆序對?實際上逆序對也是二維偏序問題,而且其中一維(下標)已經有序。
另外,關於兩個元素相等的情況,如上的演算法顯然會漏算。解決的方法也非常簡單,只需將相等的元素合併,做上標記,更新答案時特殊處理即可。

三維偏序

如果真正理解了二維偏序,三維偏序也很好解決。套上一個樹狀陣列即可。
將所有元素按 \(a\) 為第一關鍵字,\(b\) 為第二關鍵字,\(c\) 為第三關鍵字排序。再在排序好的陣列上進行以 \(b\)

為關鍵字的歸併排序。在歸併的過程中維護\(c\) 為“下標”,“權值”為出現次數的樹狀陣列,並利用其性質更新答案。
可以自己驗證其正確性。

模板題
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分治的思想還是挺簡單的。