Chương 6·Bài 60·Nâng cao

CDQ Divide and Conquer

Đếm cặp (j,i)(j, i) thỏa ba bất đẳng thức cùng lúc là khó: sắp theo một tọa độ, Fenwick lo được tọa độ thứ hai, còn tọa độ thứ ba thì không. CDQ chia đôi theo tọa độ đầu tiên: mọi cặp hoặc nằm gọn trong một nửa (đệ quy xử lý), hoặc có jj ở nửa trái và ii ở nửa phải. Với các cặp vắt qua, tọa độ đầu đã tự thỏa, chỉ còn bài hai chiều giải bằng ghép theo tọa độ thứ hai và Fenwick trên tọa độ thứ ba. Tọa độ đầu thường là thời gian, biến bài có cập nhật thành bài tĩnh, ngoại tuyến.

Điểm bị trội ba chiều

Sắp các điểm theo (a,b,c)(a, b, c). Hàm cdq(l, r):

  1. Gọi cdq(l, mid) và cdq(mid + 1, r); sau đó mỗi nửa đã được sắp theo bb (ghép như sắp xếp trộn).
  2. Ghép hai nửa theo bb. Gặp phần tử trái thì cộng vào Fenwick tại vị trí cc; gặp phần tử phải thì hỏi tổng tiền tố tới cc của nó. Mọi phần tử trái có aa không lớn hơn mọi phần tử phải, nên điều kiện còn lại đúng là bj≤bib_j \le b_i và cj≤cic_j \le c_i.
  3. Xóa các phần tử trái khỏi Fenwick bằng cách cộng ngược, không xóa cả mảng.

Mỗi tầng tốn O(nlog⁡k)O(n \log k), có log⁡n\log n tầng: O(nlog⁡nlog⁡k)O(n \log n \log k).

Điểm trùng nhau. Hai điểm bằng nhau ở cả ba tọa độ trội lẫn nhau, nhưng CDQ chỉ đếm từ trái sang phải. Gộp các điểm trùng thành một phần tử có trọng số cnt, cộng cnt vào Fenwick; kết quả của một nhóm là số đếm được cộng cnt - 1 (các bản sao khác trong nhóm). Sau khi gộp, nếu aj=aia_j = a_i với jj trái, ii phải thì (bj,cj)(b_j, c_j) đứng trước (bi,ci)(b_i, c_i) theo thứ tự từ điển, nên phần tử phải không thể trội phần tử trái và không cặp nào bị bỏ sót.

Bản hai chiều cho dễ thấy: điểm đã sắp theo a, mỗi lần ghép nửa trái (xanh dương) đóng góp cho nửa phải (xanh lá).

Hỏi nhanh

Vì sao phải gộp các điểm trùng hoàn toàn trước khi chạy CDQ?

Ví dụPhổ Thống Trị Ba Chiều

Đề. n≤105n \le 10^5 điểm (ai,bi,ci)(a_i, b_i, c_i) với tọa độ tới k≤2⋅105k \le 2 \cdot 10^5. Với mỗi ii, fif_i là số j≠ij \ne i có cả ba tọa độ không lớn hơn. In số điểm có f=df = d với mỗi d=0..n−1d = 0..n-1.

Lời giải. Sắp các điểm theo (a,b,c)(a, b, c) và gộp các điểm trùng nhau thành một bản kèm số bản sao cnt. Khi đó nếu jj có cả ba tọa độ không lớn hơn ii và hai điểm khác nhau, thì jj đứng trước ii theo thứ tự từ điển, nên chỉ cần đếm các điểm đứng trước. CDQ chia đôi dãy: cặp nằm trong cùng một nửa do đệ quy lo; cặp (jj ở nửa trái, ii ở nửa phải) đã chắc aj≤aia_j \le a_i, chỉ còn so bb và cc: quét theo bb, Fenwick theo cc.

Bước 1: gộp điểm trùng. Sau khi sắp, điểm giống hệt điểm trước thì tăng cnt, ngược lại mở một bản mới. Không gộp thì hai bản sao của cùng một điểm có thể rơi vào hai nửa, và bản đứng sau sẽ không được tính cho bản đứng trước.

C++
for (int i = 1; i <= n; i++) {
    if (m && tmp[i].a == p[m].a && tmp[i].b == p[m].b && tmp[i].c == p[m].c) p[m].cnt++;
    else p[++m] = tmp[i], p[m].cnt = 1, p[m].res = 0;
}

Bước 2: ghép hai nửa. Gọi hai nửa trước; khi trả về, mỗi nửa đã được sắp lại theo bb (nhờ bước trộn ở cuối hàm), nên quét được như trộn hai dãy. Với mỗi j ở nửa phải, đưa vào Fenwick mọi i ở nửa trái có bi≤bjb_i \le b_j, mỗi điểm cộng cnt của nó tại vị trí cc. get(p[j].c) là số điểm trái có bb và cc đều không lớn hơn.

C++
int i = l, t = l;
for (int j = mid + 1; j <= r; j++) {
    while (i <= mid && p[i].b <= p[j].b) add(p[i].c, p[i].cnt), tmp[t++] = p[i++];
    p[j].res += get(p[j].c);
    tmp[t++] = p[j];
}

Bước 3: hoàn tác và trộn. Trừ lại đúng các điểm đã cộng, không xóa cả mảng Fenwick. tmp là kết quả trộn hai nửa theo bb, chép lại vào p[l..r].

C++
for (int x = l; x < i; x++) add(p[x].c, -p[x].cnt);
while (i <= mid) tmp[t++] = p[i++];
for (int x = l; x <= r; x++) p[x] = tmp[x];

Bước 4: kết quả. Một điểm có res điểm khác nằm dưới nó, cộng cnt - 1 bản sao của chính nó. Cả nhóm bản sao có cùng ff, nên cộng cnt vào ans tại chỉ số đó.

C++
for (int i = 1; i <= m; i++) ans[p[i].res + p[i].cnt - 1] += p[i].cnt;
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

const int N = 1e5 + 5, K = 2e5 + 5;
struct P {
    int a, b, c, cnt, res;
};
int n, k, m, bit[K], ans[N];
P p[N], tmp[N];

bool cmp(P x, P y) {
    if (x.a != y.a) return x.a < y.a;
    if (x.b != y.b) return x.b < y.b;
    return x.c < y.c;
}

void add(int i, int v) {
    for (; i <= k; i += i & -i) bit[i] += v;
}

int get(int i) {
    int s = 0;
    for (; i; i -= i & -i) s += bit[i];
    return s;
}

void cdq(int l, int r) {
    if (l == r) return;
    int mid = (l + r) / 2;
    cdq(l, mid);
    cdq(mid + 1, r);
    int i = l, t = l;
    for (int j = mid + 1; j <= r; j++) {
        while (i <= mid && p[i].b <= p[j].b) add(p[i].c, p[i].cnt), tmp[t++] = p[i++];
        p[j].res += get(p[j].c);
        tmp[t++] = p[j];
    }
    for (int x = l; x < i; x++) add(p[x].c, -p[x].cnt);
    while (i <= mid) tmp[t++] = p[i++];
    for (int x = l; x <= r; x++) p[x] = tmp[x];
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n >> k;
    for (int i = 1; i <= n; i++) cin >> tmp[i].a >> tmp[i].b >> tmp[i].c;
    sort(tmp + 1, tmp + n + 1, cmp);
    for (int i = 1; i <= n; i++) {
        if (m && tmp[i].a == p[m].a && tmp[i].b == p[m].b && tmp[i].c == p[m].c) p[m].cnt++;
        else p[++m] = tmp[i], p[m].cnt = 1, p[m].res = 0;
    }
    cdq(1, m);
    for (int i = 1; i <= m; i++) ans[p[i].res + p[i].cnt - 1] += p[i].cnt;
    for (int d = 0; d < n; d++) cout << ans[d] << '\n';
}
5 3
1 1 1
1 1 1
2 2 2
2 1 3
3 3 3

Mỗi tầng đệ quy O(nlog⁡k)O(n \log k), có log⁡n\log n tầng: O(nlog⁡nlog⁡k)O(n \log n \log k).

Thời gian làm chiều thứ nhất

Bài có cập nhật và truy vấn xen kẽ: truy vấn chỉ chịu ảnh hưởng của các cập nhật xảy ra trước. Coi chỉ số thao tác là tọa độ thứ nhất; dãy thao tác vốn đã sắp theo nó. Ở mỗi lần ghép, cập nhật nửa trái tác động lên truy vấn nửa phải, và bài còn lại là bài tĩnh: điểm có trọng số, hỏi tổng trong vùng.

Truy vấn hình chữ nhật tách thành bốn truy vấn góc (x,y)(x, y) có dấu theo bao hàm loại trừ, mỗi góc hỏi tổng các điểm có x′≤xx' \le x, y′≤yy' \le y. Khi ghép theo xx, cập nhật cùng xx phải đứng trước truy vấn. Fenwick theo yy có thể lớn tới cạnh bảng mà không cần nén, miễn mỗi lần chỉ hoàn tác những ô vừa cộng.

Ví dụTổng Trên Bảng Động

Đề. Bảng w×ww \times w, w≤2⋅106w \le 2 \cdot 10^6. Tới 1.6⋅1051.6 \cdot 10^5 lệnh cộng giá trị vào một ô và 10410^4 lệnh hỏi tổng hình chữ nhật, xen kẽ theo thời gian, kết thúc bằng lệnh 3.

Lời giải. Tổng hình chữ nhật [x1,x2]×[y1,y2][x_1, x_2] \times [y_1, y_2] bằng tổng bốn góc có dấu: S(x2,y2)−S(x1−1,y2)−S(x2,y1−1)+S(x1−1,y1−1)S(x_2, y_2) - S(x_1 - 1, y_2) - S(x_2, y_1 - 1) + S(x_1 - 1, y_1 - 1), với S(x,y)S(x, y) là tổng các ô có tọa độ không lớn hơn. Mỗi lệnh hỏi thành bốn truy vấn góc. Dãy thao tác đã sắp theo thời gian, chính là chiều thứ nhất của CDQ: lệnh cộng ở nửa trái tác động lên truy vấn ở nửa phải. Trong một lần ghép, bài còn lại là bài tĩnh hai chiều: quét theo xx, hỏi Fenwick theo yy.

Bước 1: tách lệnh hỏi thành bốn góc. type = 0 là lệnh cộng v vào ô (x,y)(x, y); type = 1 là một góc của truy vấn id, với v là dấu ±1\pm 1.

C++
q++;
op[++m] = {x2, y2, 1, q, 1};
op[++m] = {x1 - 1, y2, 1, q, -1};
op[++m] = {x2, y1 - 1, 1, q, -1};
op[++m] = {x1 - 1, y1 - 1, 1, q, 1};

Bước 2: ghép. Khung như ví dụ trước. Chỉ lệnh cộng được thêm vào Fenwick (và được hoàn tác); góc hỏi chỉ đọc. Điều kiện op[i].x <= op[j].x lấy cả xx bằng nhau, đúng nghĩa “không lớn hơn” của SS.

C++
for (int j = mid + 1; j <= r; j++) {
    while (i <= mid && op[i].x <= op[j].x) {
        if (op[i].type == 0) add(op[i].y, op[i].v);
        tmp[t++] = op[i++];
    }
    if (op[j].type == 1) ans[op[j].id] += op[j].v * get(op[j].y);
    tmp[t++] = op[j];
}

Góc có x1−1=0x_1 - 1 = 0 hay y1−1=0y_1 - 1 = 0: không lệnh cộng nào có x≤0x \le 0, và get(0) trả 0, nên góc ấy đóng góp 0 như mong muốn. Fenwick dài ww tới 2⋅1062 \cdot 10^6 mà không cần nén, vì mỗi lần ghép chỉ hoàn tác đúng các ô đã cộng.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

const int M = 2e5 + 5, W = 2e6 + 5;
struct Op {
    int x, y, type, id;
    ll v;
};
int w, m, q;
ll bit[W], ans[M];
Op op[M], tmp[M];

void add(int i, ll v) {
    for (; i <= w; i += i & -i) bit[i] += v;
}

ll get(int i) {
    ll s = 0;
    for (; i > 0; i -= i & -i) s += bit[i];
    return s;
}

void cdq(int l, int r) {
    if (l == r) return;
    int mid = (l + r) / 2;
    cdq(l, mid);
    cdq(mid + 1, r);
    int i = l, t = l;
    for (int j = mid + 1; j <= r; j++) {
        while (i <= mid && op[i].x <= op[j].x) {
            if (op[i].type == 0) add(op[i].y, op[i].v);
            tmp[t++] = op[i++];
        }
        if (op[j].type == 1) ans[op[j].id] += op[j].v * get(op[j].y);
        tmp[t++] = op[j];
    }
    for (int x = l; x < i; x++)
        if (op[x].type == 0) add(op[x].y, -op[x].v);
    while (i <= mid) tmp[t++] = op[i++];
    for (int x = l; x <= r; x++) op[x] = tmp[x];
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    int type;
    cin >> type >> w;
    while (cin >> type && type != 3) {
        if (type == 1) {
            int x, y;
            ll a;
            cin >> x >> y >> a;
            op[++m] = {x, y, 0, 0, a};
        } else {
            int x1, y1, x2, y2;
            cin >> x1 >> y1 >> x2 >> y2;
            q++;
            op[++m] = {x2, y2, 1, q, 1};
            op[++m] = {x1 - 1, y2, 1, q, -1};
            op[++m] = {x2, y1 - 1, 1, q, -1};
            op[++m] = {x1 - 1, y1 - 1, 1, q, 1};
        }
    }
    if (m) cdq(1, m);
    for (int i = 1; i <= q; i++) cout << ans[i] << '\n';
}
0 5
1 2 2 3
1 4 3 5
2 1 1 3 3
1 3 3 2
2 2 2 4 3
3

O(mlog⁡mlog⁡w)O(m \log m \log w) với mm là tổng số lệnh và góc.

Đếm theo thời gian xóa

Bài xóa dần phần tử và hỏi số nghịch thế trước mỗi lần xóa: đáp án trước lần xóa thứ kk bằng tổng nghịch thế ban đầu trừ đi số nghịch thế đã mất ở k−1k - 1 lần xóa đầu. Lần xóa phần tử ee làm mất các cặp nghịch thế giữa ee và những phần tử còn lại lúc đó, tức những phần tử bị xóa sau ee hoặc không bao giờ bị xóa.

Vậy mỗi phần tử có ba tọa độ: thời điểm xóa tt (không bị xóa thì t=m+1t = m + 1), vị trí pp, giá trị vv. Sắp theo tt giảm dần, với mỗi ee đếm các phần tử đứng trước (bị xóa muộn hơn) có p<pe,v>vep < p_e, v > v_e hoặc p>pe,v<vep > p_e, v < v_e. Hai trường hợp là hai lượt quét trong cùng một lần ghép: quét pp tăng cho trường hợp đầu, pp giảm cho trường hợp sau.

Ví dụNghịch Thế Khi Xóa

Đề. Hoán vị độ dài n≤105n \le 10^5, xóa lần lượt m≤5⋅104m \le 5 \cdot 10^4 giá trị. In số nghịch thế ngay trước mỗi lần xóa.

Lời giải. Gọi tet_e là thời điểm xóa phần tử ee (m+1m + 1 nếu không bị xóa). Lần xóa ee làm mất đúng các cặp nghịch thế giữa ee và những phần tử còn lại lúc đó, tức các phần tử có t>tet > t_e. Sắp phần tử theo tt giảm dần, đếm cho mỗi ee số phần tử đứng trước nó tạo nghịch thế với nó: vị trí nhỏ hơn mà giá trị lớn hơn, hoặc vị trí lớn hơn mà giá trị nhỏ hơn. Đó là bài đếm cặp ba chiều (t,p,v)(t, p, v), giải bằng CDQ như ví dụ đầu.

Bước 1: nghịch thế ban đầu. Fenwick đếm total, rồi trừ lại để Fenwick về 0. where[v] là vị trí của giá trị vv, dùng để ghi tim cho phần tử bị xóa. Sau đó id sắp theo thời điểm xóa giảm dần; phần tử không bị xóa đứng đầu, và chúng không bao giờ cần lost.

C++
total += i - 1 - get(a[i]);
add(a[i], 1);

Bước 2: lượt thứ nhất của ghép. Hai nửa đã sắp theo vị trí. Quét j ở nửa phải theo vị trí tăng, đưa vào Fenwick các phần tử trái có vị trí nhỏ hơn. (i - l) - get(a[id[j]]) là số phần tử đó có giá trị lớn hơn.

C++
for (int j = mid + 1; j <= r; j++) {
    while (i <= mid && pos[id[i]] < pos[id[j]]) add(a[id[i]], 1), i++;
    lost[id[j]] += (i - l) - get(a[id[j]]);
}

Bước 3: lượt thứ hai. Quét ngược, đưa vào các phần tử trái có vị trí lớn hơn; get(a[id[j]] - 1) đếm những phần tử có giá trị nhỏ hơn. Sau mỗi lượt hoàn tác Fenwick, rồi merge hai nửa theo vị trí để tầng trên quét được.

C++
for (int j = r; j > mid; j--) {
    while (i >= l && pos[id[i]] > pos[id[j]]) add(a[id[i]], 1), i--;
    lost[id[j]] += get(a[id[j]] - 1);
}

Bước 4: in. Trước lần xóa thứ kk in total, rồi trừ lost của phần tử vừa xóa.

C++
for (int k = 1; k <= m; k++) {
    cout << total << '\n';
    total -= lost[where[del[k]]];
}
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

const int N = 1e5 + 5;
int n, m, a[N], pos[N], tim[N], id[N], tmp[N], bit[N];
ll lost[N];

void add(int i, int v) {
    for (; i <= n; i += i & -i) bit[i] += v;
}

int get(int i) {
    int s = 0;
    for (; i; i -= i & -i) s += bit[i];
    return s;
}

bool byTime(int x, int y) {
    return tim[x] > tim[y];
}

bool byPos(int x, int y) {
    return pos[x] < pos[y];
}

void cdq(int l, int r) {
    if (l == r) return;
    int mid = (l + r) / 2;
    cdq(l, mid);
    cdq(mid + 1, r);
    int i = l;
    for (int j = mid + 1; j <= r; j++) {
        while (i <= mid && pos[id[i]] < pos[id[j]]) add(a[id[i]], 1), i++;
        lost[id[j]] += (i - l) - get(a[id[j]]);
    }
    for (int x = l; x < i; x++) add(a[id[x]], -1);
    i = mid;
    for (int j = r; j > mid; j--) {
        while (i >= l && pos[id[i]] > pos[id[j]]) add(a[id[i]], 1), i--;
        lost[id[j]] += get(a[id[j]] - 1);
    }
    for (int x = mid; x > i; x--) add(a[id[x]], -1);
    merge(id + l, id + mid + 1, id + mid + 1, id + r + 1, tmp + l, byPos);
    for (int x = l; x <= r; x++) id[x] = tmp[x];
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n >> m;
    ll total = 0;
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
        pos[i] = i, tim[i] = m + 1, id[i] = i;
        total += i - 1 - get(a[i]);
        add(a[i], 1);
    }
    for (int i = 1; i <= n; i++) add(a[i], -1);
    vector<int> where(n + 1), del(m + 1);
    for (int i = 1; i <= n; i++) where[a[i]] = i;
    for (int k = 1; k <= m; k++) {
        cin >> del[k];
        tim[where[del[k]]] = k;
    }
    stable_sort(id + 1, id + n + 1, byTime);
    cdq(1, n);
    for (int k = 1; k <= m; k++) {
        cout << total << '\n';
        total -= lost[where[del[k]]];
    }
}
5 3
3 1 5 2 4
5
1
3

O(nlog⁡2n)O(n \log^2 n).

CDQ cho quy hoạch động

Khi dpidp_i phụ thuộc mọi jj đứng trước thỏa vài bất đẳng thức (chuỗi dài nhất nhiều chiều), thứ tự gọi phải đổi: cdq(l, mid) xong hẳn, chuyển đóng góp từ nửa trái sang nửa phải, rồi mới gọi cdq(mid + 1, r). Lúc chuyển, mọi dpjdp_j của nửa trái đã cuối cùng; nửa phải nhận thêm đóng góp trước khi tự đệ quy.

Vì nửa phải còn phải đệ quy theo thứ tự tọa độ đầu, không được trộn mảng gốc theo tọa độ thứ hai. Sắp hai danh sách chỉ số riêng cho lần chuyển, mỗi tầng O(nlog⁡n)O(n \log n), tổng O(nlog⁡2n)O(n \log^2 n). Fenwick lưu cặp (độ dài tốt nhất, số cách) và ghép bằng “lấy độ dài lớn hơn, bằng nhau thì cộng số cách”.

Hỏi nhanh

Trong CDQ cho DP, vì sao không gọi cả hai nửa trước rồi mới chuyển đóng góp như khi đếm cặp?

Ví dụChuỗi Điểm Ba Chiều

Đề. T≤3T \le 3 test, mỗi test N≤105N \le 10^5 điểm ba chiều khác nhau. Tìm độ dài lớn nhất của chuỗi có cả ba tọa độ không giảm, và số tập điểm đạt độ dài đó theo mô-đun 2302^{30}.

Lời giải. lenilen_i là độ dài chuỗi dài nhất kết thúc ở điểm ii, cnticnt_i là số chuỗi đạt độ dài đó. Sắp theo (x,y,z)(x, y, z); các điểm khác nhau nên bước trước jj của ii (cả ba tọa độ không lớn hơn) luôn đứng trước ii. CDQ cho quy hoạch động phải đổi thứ tự gọi: xử lý xong hẳn nửa trái, chuyển đóng góp từ nửa trái sang nửa phải, rồi mới đệ quy nửa phải. Lúc chuyển, mọi lenjlen_j bên trái đã cuối cùng, nên giá trị truyền đi là đúng.

Bước 1: Fenwick giữ cặp. Mỗi ô giữ (độ dài tốt nhất, số cách), gộp theo luật “độ dài lớn hơn thì thay, bằng nhau thì cộng số cách”. Phép gộp này kết hợp và giao hoán, nên dùng được trong Fenwick. & MASK là lấy dư cho 2302^{30}. clear đặt lại đúng các ô đã đụng.

C++
void upd(int p, int l, ll c) {
    for (; p <= m; p += p & -p) {
        if (l > bl[p]) bl[p] = l, bc[p] = c;
        else if (l == bl[p]) bc[p] = (bc[p] + c) & MASK;
    }
}

Bước 2: thứ tự gọi. cdq(l, mid) trước. Rồi chép chỉ số hai nửa vào L, R và sắp theo yy; không sắp thẳng ord, vì nửa phải còn phải đệ quy theo thứ tự (x,y,z)(x, y, z).

C++
cdq(l, mid);
vector<int> L(ord + l, ord + mid + 1), R(ord + mid + 1, ord + r + 1);
sort(L.begin(), L.end(), byY);
sort(R.begin(), R.end(), byY);

Bước 3: chuyển đóng góp. Quét R theo yy tăng, đưa vào Fenwick các điểm trái có yy không lớn hơn, rồi hỏi tiền tố tới zjz_j (đã nén): được bước trước tốt nhất ở nửa trái.

C++
for (int j : R) {
    while (i < (int)L.size() && y[L[i]] <= y[j]) upd(z[L[i]], len[L[i]], cnt[L[i]]), i++;
    int bestL;
    ll bestC;
    ask(z[j], bestL, bestC);
    if (bestL + 1 > len[j]) len[j] = bestL + 1, cnt[j] = bestC;
    else if (bestL + 1 == len[j]) cnt[j] = (cnt[j] + bestC) & MASK;
}
for (int k = 0; k < i; k++) clear(z[L[k]]);
cdq(mid + 1, r);

Cuối cùng mới cdq(mid + 1, r): các điểm nửa phải đã nhận đủ đóng góp từ nửa trái trước khi truyền tiếp cho nhau.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

const int N = 1e5 + 5;
const ll MASK = (1 << 30) - 1;
int n, m, x[N], y[N], z[N], ord[N], len[N], bl[N];
ll cnt[N], bc[N];

bool byXYZ(int i, int j) {
    if (x[i] != x[j]) return x[i] < x[j];
    if (y[i] != y[j]) return y[i] < y[j];
    return z[i] < z[j];
}

bool byY(int i, int j) {
    return y[i] < y[j];
}

void upd(int p, int l, ll c) {
    for (; p <= m; p += p & -p) {
        if (l > bl[p]) bl[p] = l, bc[p] = c;
        else if (l == bl[p]) bc[p] = (bc[p] + c) & MASK;
    }
}

void ask(int p, int &l, ll &c) {
    l = 0, c = 0;
    for (; p; p -= p & -p) {
        if (bl[p] > l) l = bl[p], c = bc[p];
        else if (bl[p] == l) c = (c + bc[p]) & MASK;
    }
}

void clear(int p) {
    for (; p <= m; p += p & -p) bl[p] = 0, bc[p] = 0;
}

void cdq(int l, int r) {
    if (l == r) return;
    int mid = (l + r) / 2;
    cdq(l, mid);
    vector<int> L(ord + l, ord + mid + 1), R(ord + mid + 1, ord + r + 1);
    sort(L.begin(), L.end(), byY);
    sort(R.begin(), R.end(), byY);
    int i = 0;
    for (int j : R) {
        while (i < (int)L.size() && y[L[i]] <= y[j]) upd(z[L[i]], len[L[i]], cnt[L[i]]), i++;
        int bestL;
        ll bestC;
        ask(z[j], bestL, bestC);
        if (bestL + 1 > len[j]) len[j] = bestL + 1, cnt[j] = bestC;
        else if (bestL + 1 == len[j]) cnt[j] = (cnt[j] + bestC) & MASK;
    }
    for (int k = 0; k < i; k++) clear(z[L[k]]);
    cdq(mid + 1, r);
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    int T;
    cin >> T;
    while (T--) {
        cin >> n;
        vector<int> zs;
        for (int i = 1; i <= n; i++) cin >> x[i] >> y[i] >> z[i], zs.push_back(z[i]), ord[i] = i;
        sort(zs.begin(), zs.end());
        zs.erase(unique(zs.begin(), zs.end()), zs.end());
        m = zs.size();
        for (int i = 1; i <= n; i++) {
            z[i] = lower_bound(zs.begin(), zs.end(), z[i]) - zs.begin() + 1;
            len[i] = 1, cnt[i] = 1;
        }
        sort(ord + 1, ord + n + 1, byXYZ);
        cdq(1, n);
        int best = 0;
        ll ways = 0;
        for (int i = 1; i <= n; i++) {
            if (len[i] > best) best = len[i], ways = cnt[i];
            else if (len[i] == best) ways = (ways + cnt[i]) & MASK;
        }
        cout << best << ' ' << ways << '\n';
    }
}
1
4
1 1 1
2 2 2
2 3 1
3 3 3

Mỗi tầng sắp hai danh sách và dùng Fenwick, đều O(nlog⁡n)O(n \log n): tổng O(nlog⁡2n)O(n \log^2 n).

Khi đi thi

  • Đếm cặp hay tổng đóng góp với ba điều kiện thứ tự (hoặc hai điều kiện cộng thời gian): CDQ chia theo chiều đầu, ghép theo chiều thứ hai, Fenwick chiều thứ ba, O(nlog⁡2n)O(n \log^2 n).
  • Kiểm kỹ điểm trùng và dấu bằng: gộp bản sao, đặt cập nhật trước truy vấn khi bằng tọa độ, hoàn tác Fenwick đúng phần đã cộng.
  • DP nhiều chiều: trái, chuyển, rồi mới phải; sắp bản sao chỉ số để không phá thứ tự của nửa phải.

Tóm tắt

  • Mọi cặp hoặc nằm trong một nửa, hoặc vắt từ trái sang phải; CDQ xử lý loại sau ở mỗi tầng.
  • Thời gian làm chiều thứ nhất biến cập nhật xen truy vấn thành bài tĩnh ngoại tuyến.
  • Bài xóa dần: gán thời điểm xóa, đếm đóng góp mất đi rồi trừ dần khỏi tổng.
  • CDQ cho DP: nửa trái xong hẳn trước khi truyền sang nửa phải.

Bài tập

1
Ba Bảng Xếp Hạngcanuc80k6001Hai đội đều tự cho mình mạnh hơn khi không đội nào trội đội kia cả ba lần; đếm cặp trội hoàn toàn bằng CDQ rồi lấy phần bù.
LQDOJ
2
Khoảng Cách Tương Laicanuc80k6002Sắp theo vị trí ban đầu; cặp xi<xjx_i < x_j có khoảng cách nhỏ nhất dương khi vi≤vjv_i \le v_j, bằng xj−xix_j - x_i; Fenwick trên vận tốc nén giữ số lượng và tổng tọa độ.
LQDOJ
3
Điểm Gần Nhất Manhattancanuc80k6006Bỏ trị tuyệt đối theo bốn hướng xoay; mỗi hướng CDQ theo thời gian tìm cực đại X+YX + Y của các điểm đã thêm ở góc dưới trái.
LQDOJ
4
Giao Hai Hoán Vịcanuc80k6007Mỗi giá trị là điểm (vị trí trong aa, vị trí trong bb); đổi chỗ trong bb là xóa hai điểm, thêm hai điểm; CDQ theo thời gian đếm điểm trong hình chữ nhật.
LQDOJ
5
Cặp Thiết Bị Tương Thíchcanuc80k6008Xét thiết bị theo tầm giảm dần: thiết bị đã xét có tầm không nhỏ hơn; đếm thiết bị đã xét có vị trí trong tầm và chỉ số lệch không quá KK.
LQDOJ
6
Tuyến Chặn Tối Ưucanuc80k6009Chuỗi không tăng hai chiều theo thời gian: CDQ cho DP từ hai phía lấy độ dài và số cách; xác suất mỗi mục tiêu là tích số cách hai phía chia tổng.
LQDOJ
7
Dãy Con Bền Vữngcanuc80k6010Mỗi vị trí có giá trị gốc, nhỏ nhất và lớn nhất khi thay đổi; jj đứng trước ii được khi max⁡j≤ai\max_j \le a_i và aj≤min⁡ia_j \le \min_i; CDQ cho DP.
LQDOJ
8
Giao Cắt Có Điều Kiệncanuc80k6011Cặp đoạn giao nhau là nghịch thế giữa hai thứ tự; thêm điều kiện nhãn cách nhau hơn KK thành bài ba chiều.
LQDOJ
9
Danh Mục Đầu Tưcanuc80k6012DP theo ngày cần cực đại của các đường thẳng tại hệ số góc khác nhau; CDQ dựng bao lồi nửa trái rồi hỏi cho nửa phải đã sắp theo hệ số góc.
LQDOJ
10
Chuỗi Không Giảm Bốn Chiềucanuc80k6013Chuỗi không giảm bốn chiều: CDQ lồng hai tầng, tầng ngoài tách chiều thứ hai, tầng trong như ba chiều.
LQDOJ
11
Hộp Đếm Ba Chiềucanuc80k6014Hộp tách thành tám góc có dấu; CDQ theo thời gian, chiều còn lại xử lý bằng CDQ lồng hoặc Fenwick hai chiều trên tọa độ nén.
LQDOJ

Đọc thêm

  • Chia để trịVNOI WikiNền tảng chia để trị và đếm cặp vắt qua hai nửa.
  • CDQ 分治OI WikiĐiểm bị trội ba chiều, chia theo thời gian và CDQ cho DP.
  • Cây chỉ số nhị phânVNOI WikiFenwick dùng ở chiều cuối, bằng tiếng Việt.