Chương 5·Bài 51·Nâng cao

Parallel Binary Search

Mỗi ngày người ta xây thêm một con đường. Có nhiều câu hỏi, câu nào cũng dạng: đến ngày nào thì thành phố uu và thành phố vv đi được tới nhau?

Với một câu hỏi, ta chặt nhị phân theo ngày. Nhưng mỗi lần thử phải dựng lại đường sá của mid\text{mid} ngày đầu, và có hàng trăm nghìn câu hỏi. Tìm kiếm nhị phân song song cho mọi câu hỏi chặt cùng nhịp: mỗi vòng, một lượt quét thời gian trả lời được tất cả. Biến thể đệ quy, chặt nhị phân tổng thể, chia cả truy vấn lẫn dữ liệu theo miền giá trị, giải bài phần tử thứ kk trên đoạn.

Quét song song

Điều kiện kiểm Pi(t)P_i(t) của truy vấn ii phải đơn điệu: sai, sai, …, đúng, đúng khi tt tăng. Đồ thị chỉ thêm cạnh là ví dụ chuẩn: đã liên thông thì mãi liên thông. Với một truy vấn, chặt nhị phân giữ lo, hi, res: thử mid, đúng thì ghi res = mid và sang trái, sai thì sang phải.

Chặt riêng từng truy vấn: mỗi lần thử phải dựng trạng thái sau mid bước từ đầu, qq truy vấn tốn O(q⋅mlog⁡m)O(q \cdot m \log m). Chặt song song đảo vòng lặp:

  1. Mỗi truy vấn còn lo <= hi bỏ chỉ số của nó vào giỏ bucket[mid].
  2. Làm rỗng cấu trúc, quét t=0,1,…,mt = 0, 1, \dots, m: áp dụng bước tt, rồi kiểm mọi truy vấn trong bucket[t] và cập nhật cận của chúng.
  3. Lặp tới khi không truy vấn nào còn khoảng.

Mỗi vòng tốn một lượt quét cộng qq lần kiểm; khoảng của mỗi truy vấn giảm một nửa nên có ⌈log⁡2(m+1)⌉\lceil \log_2(m + 1) \rceil vòng. Thứ tự “cập nhật rồi mới kiểm” phải khớp đề: sau bước tt trạng thái chứa đúng tt cạnh đầu.

Trong hình, mỗi hàng là một truy vấn. Thanh xanh là khoảng còn lại, chấm đỏ là điểm giữa được kiểm trong lượt quét, ô xanh lá là đáp án đã biết.

Mỗi vòng, mọi truy vấn chặt đôi khoảng của mình sau một lượt quét.

Hỏi nhanh

Có m=2⋅105m = 2 \cdot 10^5 bước thời gian và q=2⋅105q = 2 \cdot 10^5 truy vấn. Tìm kiếm nhị phân song song cần khoảng bao nhiêu lượt quét toàn bộ mm bước?

Ví dụNgày Kết Nối Đầu Tiên

Đề. nn đỉnh, ngày ii thêm cạnh thứ ii (m≤2⋅105m \le 2 \cdot 10^5). Mỗi truy vấn (u,v)(u, v) hỏi số ngày ít nhất để u,vu, v liên thông (ngày 0 là chưa thêm cạnh), hoặc −1-1.

Lời giải. Với một truy vấn, “uu, vv liên thông sau tt ngày” đúng thì đúng với mọi ngày sau, nên đáp án chặt nhị phân được trên [0,m][0, m]. Chặt riêng từng truy vấn thì mỗi lần kiểm phải dựng lại DSU. Chặt song song: mỗi vòng, mọi truy vấn bỏ vào giỏ ứng với điểm giữa của nó; dựng DSU một lần, thêm cạnh theo ngày, tới ngày tt thì kiểm hết các truy vấn trong giỏ tt. Mỗi vòng thu hẹp mọi khoảng còn một nửa.

Bước 1: khoảng chặt. lo[i], hi[i] là khoảng còn chặt của truy vấn ii; lo > hi là đã xong. res = -1 sẵn cho truy vấn không bao giờ nối được.

C++
lo[i] = 0, hi[i] = m, res[i] = -1;

Bước 2: chia giỏ. Đầu mỗi vòng xóa giỏ, bỏ từng truy vấn còn chặt vào giỏ (lo + hi) / 2. Không còn truy vấn nào thì dừng.

C++
for (int t = 0; t <= m; t++) bucket[t].clear();
for (int i = 1; i <= q; i++)
    if (lo[i] <= hi[i]) {
        bucket[(lo[i] + hi[i]) / 2].push_back(i);
        any = 1;
    }
if (!any) break;

Bước 3: quét theo ngày. Ngày 0 chưa có cạnh; ngày t>0t > 0 thêm cạnh tt. Rồi kiểm các truy vấn trong giỏ tt: liên thông thì ghi res = t và chặt nửa trái, ngược lại chặt nửa phải.

C++
for (int u = 1; u <= n; u++) par[u] = u;
for (int t = 0; t <= m; t++) {
    if (t) par[find(eu[t])] = find(ev[t]);
    for (int i : bucket[t]) {
        if (find(qu[i]) == find(qv[i])) res[i] = t, hi[i] = t - 1;
        else lo[i] = t + 1;
    }
}

find nén nửa đường (par[u] = par[par[u]]), không cần đệ quy.

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

const int N = 2e5 + 5;
int n, m, q, eu[N], ev[N], qu[N], qv[N], lo[N], hi[N], res[N], par[N];
vector<int> bucket[N];

int find(int u) {
    while (par[u] != u) u = par[u] = par[par[u]];
    return u;
}

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

    cin >> n >> m >> q;
    for (int i = 1; i <= m; i++) cin >> eu[i] >> ev[i];
    for (int i = 1; i <= q; i++) {
        cin >> qu[i] >> qv[i];
        lo[i] = 0, hi[i] = m, res[i] = -1;
    }
    while (true) {
        bool any = 0;
        for (int t = 0; t <= m; t++) bucket[t].clear();
        for (int i = 1; i <= q; i++)
            if (lo[i] <= hi[i]) {
                bucket[(lo[i] + hi[i]) / 2].push_back(i);
                any = 1;
            }
        if (!any) break;
        for (int u = 1; u <= n; u++) par[u] = u;
        for (int t = 0; t <= m; t++) {
            if (t) par[find(eu[t])] = find(ev[t]);
            for (int i : bucket[t]) {
                if (find(qu[i]) == find(qv[i])) res[i] = t, hi[i] = t - 1;
                else lo[i] = t + 1;
            }
        }
    }
    for (int i = 1; i <= q; i++) cout << res[i] << '\n';
}
6 4 4
1 2
3 4
2 3
1 4
1 4
1 6
2 2
3 1

Mỗi vòng O((m+q) α(n))O((m + q)\,\alpha(n)), khoảng log⁡2m+1=19\log_2 m + 1 = 19 vòng.

Tổng tích lũy

Điều kiện không nhất thiết là liên thông. Mỗi bước thời gian cộng thêm một lượng không âm, truy vấn hỏi “tổng đã đạt ngưỡng chưa”: điều kiện cũng đơn điệu. Trạng thái dựng bằng Fenwick: mỗi vòng xóa cây, áp lần lượt các bước, kiểm truy vấn trong giỏ.

Một truy vấn có thể đọc nhiều điểm, nhưng tổng số điểm đọc trong một vòng vẫn bằng kích thước dữ liệu, nên mỗi vòng O((m+k)log⁡m)O((m + k) \log m). Khi cộng dồn, dừng sớm ngay khi đạt ngưỡng: nhanh hơn và tránh tràn số.

Ví dụMưa Thiên Thạch

Đề. Vòng tròn mm khu vực, khu ii thuộc quốc gia oio_i; quốc gia ii cần pip_i mẫu. Sự kiện jj cộng aja_j mẫu cho mỗi khu trên cung l..rl..r (có thể vòng qua mm). Với mỗi quốc gia, in sự kiện sớm nhất mà tổng mẫu của nó đạt pip_i, hoặc NIE. Mọi giới hạn tới 3⋅1053 \cdot 10^5.

Lời giải. Tổng mẫu của một quốc gia chỉ tăng theo sự kiện, nên “đã đạt pip_i sau sự kiện jj” là điều kiện đơn điệu: chặt nhị phân song song trên chỉ số sự kiện, khung vòng lặp như ví dụ trước. Khác ở trạng thái: lượng mẫu ở mỗi khu, cập nhật bằng cộng đoạn.

Bước 1: cộng đoạn, đọc điểm. Fenwick trên mảng hiệu: range(l, r, v) cộng v ở ô ll và trừ ở ô r+1r + 1; get(p) là tổng tiền tố, tức lượng ở khu pp.

C++
void range(int l, int r, ll v) {
    add(l, v);
    add(r + 1, -v);
}

Bước 2: cung vòng. Cung vòng qua mm tách thành hai đoạn l..ml..m và 1..r1..r.

C++
if (el[j] <= er[j]) range(el[j], er[j], ea[j]);
else range(el[j], m, ea[j]), range(1, er[j], ea[j]);

Bước 3: kiểm một quốc gia. pos[o] là danh sách khu của quốc gia oo; tổng độ dài mọi danh sách là mm, nên mỗi vòng đọc tối đa mm điểm. Dừng sớm khi đã đủ: vừa nhanh hơn, vừa tránh tràn, vì tổng thật có thể tới 3⋅105⋅3⋅105⋅1093 \cdot 10^5 \cdot 3 \cdot 10^5 \cdot 10^9.

C++
ll got = 0;
for (int p : pos[i]) {
    got += get(p);
    if (got >= need[i]) break;
}
if (got >= need[i]) res[i] = j, hi[i] = j - 1;
else lo[i] = j + 1;

bit xóa đầu mỗi vòng; res = 0 là không bao giờ đủ, in NIE.

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

const int N = 3e5 + 5;
int n, m, k, el[N], er[N], lo[N], hi[N], res[N];
ll need[N], ea[N], bit[N];
vector<int> pos[N], bucket[N];

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

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

void range(int l, int r, ll v) {
    add(l, v);
    add(r + 1, -v);
}

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

    cin >> n >> m;
    for (int i = 1; i <= m; i++) {
        int o;
        cin >> o;
        pos[o].push_back(i);
    }
    for (int i = 1; i <= n; i++) cin >> need[i];
    cin >> k;
    for (int j = 1; j <= k; j++) cin >> el[j] >> er[j] >> ea[j];
    for (int i = 1; i <= n; i++) lo[i] = 1, hi[i] = k, res[i] = 0;
    while (true) {
        bool any = 0;
        for (int j = 1; j <= k; j++) bucket[j].clear();
        for (int i = 1; i <= n; i++)
            if (lo[i] <= hi[i]) {
                bucket[(lo[i] + hi[i]) / 2].push_back(i);
                any = 1;
            }
        if (!any) break;
        memset(bit, 0, sizeof(bit));
        for (int j = 1; j <= k; j++) {
            if (el[j] <= er[j]) range(el[j], er[j], ea[j]);
            else range(el[j], m, ea[j]), range(1, er[j], ea[j]);
            for (int i : bucket[j]) {
                ll got = 0;
                for (int p : pos[i]) {
                    got += get(p);
                    if (got >= need[i]) break;
                }
                if (got >= need[i]) res[i] = j, hi[i] = j - 1;
                else lo[i] = j + 1;
            }
        }
    }
    for (int i = 1; i <= n; i++) {
        if (res[i]) cout << res[i] << '\n';
        else cout << "NIE\n";
    }
}
5 7
1 2 1 3 4 2 3
5 6 10 4 1
4
6 2 2
3 5 3
1 7 1
4 4 10

Mỗi vòng O((k+m)log⁡m)O((k + m) \log m), khoảng 19 vòng.

Ngưỡng và thành phần

DSU giữ thêm kích thước thành phần thì điều kiện có thể là “số đỉnh phủ được đạt zz”.

Khi các cạnh sắp theo trọng số, “thời điểm” thành “ngưỡng”: đúng ở ngưỡng WW thì đúng ở mọi ngưỡng lớn hơn. Nhiều cạnh cùng trọng số thì mỗi bước là cả nhóm cạnh bằng nhau, để không kiểm giữa chừng một nhóm.

Thông tin in kèm đáp án (như kích thước) được ghi ngay lúc kiểm đúng; lần ghi cuối cùng ứng với res cuối cùng.

Ví dụNgưỡng Liên Thông

Đề. Đồ thị n,m≤105n, m \le 10^5 có trọng số. Với truy vấn (x,y)(x, y), tìm WW nhỏ nhất để chỉ giữ cạnh trọng số không vượt WW thì x,yx, y liên thông, và in kèm kích thước thành phần đó sau khi thêm mọi cạnh trọng số không vượt WW; không được thì −1-1.

Lời giải. Sắp cạnh theo trọng số, coi “thời điểm” là ngưỡng. Các cạnh cùng trọng số phải được thêm cùng lúc, vì đề hỏi kích thước sau khi thêm mọi cạnh không vượt WW; nên gom thành GG nhóm trọng số bằng nhau và chặt nhị phân song song trên chỉ số nhóm [1,G][1, G].

Bước 1: sắp và gom nhóm. e[i] là cặp (trọng số, chỉ số cạnh) để sắp theo trọng số mà vẫn nhớ hai đầu mút. gw[g] là trọng số của nhóm gg, gend[g] là chỉ số cạnh cuối của nhóm trong mảng đã sắp.

C++
sort(e + 1, e + m + 1);
for (int i = 1; i <= m; i++) {
    if (i == 1 || e[i].first != e[i - 1].first) gw[++G] = e[i].first;
    gend[G] = i;
}

Bước 2: quét theo nhóm. Ở nhóm gg, thêm các cạnh từ gend[g - 1] + 1 tới gend[g], rồi kiểm giỏ. Kiểm đúng thì ghi luôn kích thước thành phần. Mỗi lần kiểm đúng về sau chỉ xảy ra ở ngưỡng nhỏ hơn và ghi đè, nên giá trị cuối cùng ứng với ngưỡng nhỏ nhất.

C++
for (int g = 1; g <= G; g++) {
    for (int j = gend[g - 1] + 1; j <= gend[g]; j++) unite(eu[e[j].second], ev[e[j].second]);
    for (int i : bucket[g]) {
        int a = find(qx[i]);
        if (a == find(qy[i])) res[i] = g, ans[i] = sz[a], hi[i] = g - 1;
        else lo[i] = g + 1;
    }
}

res = 0 là không bao giờ nối được, in −1-1.

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

const int N = 1e5 + 5;
int n, m, q, G, eu[N], ev[N], qx[N], qy[N], lo[N], hi[N], res[N], ans[N], par[N], sz[N], gend[N], gw[N];
pair<int, int> e[N];
vector<int> bucket[N];

int find(int u) {
    while (par[u] != u) u = par[u] = par[par[u]];
    return u;
}

void unite(int a, int b) {
    a = find(a), b = find(b);
    if (a == b) return;
    if (sz[a] < sz[b]) swap(a, b);
    par[b] = a;
    sz[a] += sz[b];
}

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

    cin >> n >> m;
    for (int i = 1; i <= m; i++) {
        cin >> eu[i] >> ev[i] >> e[i].first;
        e[i].second = i;
    }
    sort(e + 1, e + m + 1);
    for (int i = 1; i <= m; i++) {
        if (i == 1 || e[i].first != e[i - 1].first) gw[++G] = e[i].first;
        gend[G] = i;
    }
    cin >> q;
    for (int i = 1; i <= q; i++) {
        cin >> qx[i] >> qy[i];
        lo[i] = 1, hi[i] = G, res[i] = 0;
    }
    while (true) {
        bool any = 0;
        for (int g = 1; g <= G; g++) bucket[g].clear();
        for (int i = 1; i <= q; i++)
            if (lo[i] <= hi[i]) {
                bucket[(lo[i] + hi[i]) / 2].push_back(i);
                any = 1;
            }
        if (!any) break;
        for (int u = 1; u <= n; u++) par[u] = u, sz[u] = 1;
        for (int g = 1; g <= G; g++) {
            for (int j = gend[g - 1] + 1; j <= gend[g]; j++) unite(eu[e[j].second], ev[e[j].second]);
            for (int i : bucket[g]) {
                int a = find(qx[i]);
                if (a == find(qy[i])) res[i] = g, ans[i] = sz[a], hi[i] = g - 1;
                else lo[i] = g + 1;
            }
        }
    }
    for (int i = 1; i <= q; i++) {
        if (res[i]) cout << gw[res[i]] << ' ' << ans[i] << '\n';
        else cout << -1 << '\n';
    }
}
7 6
1 2 4
2 3 7
3 4 7
5 6 2
4 5 10
2 6 10
5
1 3
1 6
5 6
1 5
1 7

O(mlog⁡m)O(m \log m) để sắp, mỗi vòng O((m+q) α(n))O((m + q)\,\alpha(n)).

Chặt nhị phân tổng thể

Bài “phần tử nhỏ thứ kk trong đoạn [l,r][l, r]” cũng là chặt nhị phân: đáp án là giá trị xx nhỏ nhất mà đoạn có ít nhất kk phần tử không vượt xx. Thay vì chạy theo vòng, ta đệ quy trên miền giá trị. Hàm solve(lo, hi, danh sách) nhận các truy vấn có đáp án trong [lo,hi][lo, hi] cùng các phần tử có giá trị trong [lo,hi][lo, hi]:

  1. Lấy mid. Thêm vào Fenwick theo vị trí mọi phần tử có giá trị không vượt mid.
  2. Với mỗi truy vấn, đếm cc phần tử như vậy trong [l,r][l, r]. Nếu k≤ck \le c, đáp án ở nửa trái; ngược lại trừ kk đi cc và đưa sang nửa phải, vì cc phần tử nhỏ đó đã được tính.
  3. Xóa các phần tử vừa thêm, chia danh sách làm hai, đệ quy hai nửa.

Mỗi phần tử và truy vấn xuất hiện ở một nhánh trên mỗi tầng, có log⁡V\log V tầng, nên tổng O((n+q)log⁡Vlog⁡n)O((n + q) \log V \log n). Muốn có cập nhật, coi mỗi phép gán là “xóa giá trị cũ, thêm giá trị mới” và giữ mọi thao tác theo thứ tự thời gian trong danh sách; khung đệ quy không đổi.

Mỗi lần gọi đệ quy ta chia danh sách làm hai. Tạo hai vector mới ở mỗi lần gọi là cách dễ viết, nhưng hằng số phải trả:

Chặt nhị phân tổng thể, 200 nghìn phần tử và truy vấn, trung vị 5 lần chạy, i7-10850H, g++ -O2.vector lf, rt mỗi lần gọi312 ms · gấp 1,5 lầnmột mảng tạm dùng chung204 ms
Chặt nhị phân tổng thể trên 200 nghìn phần tử và 200 nghìn truy vấn, đo trên i7-10850H, g++ -O2.

Dùng một mảng tạm chung (nửa trái ghi đè ngay tại chỗ, nửa phải qua mảng tạm rồi chép về) nhanh hơn khoảng 1,6 lần. Quy tắc: đệ quy chia đôi dữ liệu thì tránh cấp phát trong mỗi lần gọi.

Hỏi nhanh

Trong chặt nhị phân tổng thể, một truy vấn có k=5k = 5, và đoạn của nó có c=3c = 3 phần tử không vượt mid. Truy vấn đi đâu?

Ví dụTrung Vị Đoạn

Đề. Mảng n≤105n \le 10^5 phần tử, giá trị tới 10510^5. Mỗi truy vấn [l,r][l, r] hỏi phần tử thứ ⌊(len+1)/2⌋\lfloor (len + 1)/2 \rfloor khi sắp tăng dần đoạn đó.

Lời giải. Trung vị là phần tử thứ k=⌊(len+1)/2⌋=(r−l+2)/2k = \lfloor (len + 1)/2 \rfloor = (r - l + 2)/2 của đoạn. Chặt nhị phân tổng thể trên miền giá trị [1,105][1, 10^5]: solve(lo, hi, …) nhận các phần tử có giá trị trong [lo,hi][lo, hi] cùng các truy vấn có đáp án trong [lo,hi][lo, hi]. Đưa các phần tử ≤mid\le mid vào Fenwick theo vị trí; mỗi truy vấn đếm cc phần tử như vậy trong đoạn của nó. k≤ck \le c thì đáp án ≤mid\le mid; ngược lại đáp án >mid> mid và là phần tử thứ k−ck - c trong số các phần tử >mid> mid.

Bước 1: một mảng chung. id[L..R] chứa cả phần tử (số ≤n\le n) lẫn truy vấn (số n+jn + j). Ban đầu mọi phần tử đứng trước mọi truy vấn.

C++
for (int i = 1; i <= n; i++) cin >> a[i], id[i] = i;
for (int j = 1; j <= q; j++) {
    cin >> ql[j] >> qr[j];
    qk[j] = (qr[j] - ql[j] + 2) / 2;
    id[n + j] = n + j;
}

Bước 2: chia hai nhóm. Phần tử ≤mid\le mid vào Fenwick và sang trái, còn lại sang phải. Truy vấn đếm c rồi sang trái, hoặc trừ qk[j] đi c và sang phải. Vì phần tử đứng trước truy vấn, lúc một truy vấn đếm thì mọi phần tử ≤mid\le mid của đoạn đã vào Fenwick.

C++
for (int i = L; i <= R; i++) {
    int x = id[i];
    if (x <= n) {
        if (a[x] <= mid) add(x, 1), lf.push_back(x);
        else rt.push_back(x);
    } else {
        int j = x - n, c = get(qr[j]) - get(ql[j] - 1);
        if (qk[j] <= c) lf.push_back(x);
        else qk[j] -= c, rt.push_back(x);
    }
}

Bước 3: dọn và đệ quy. Trừ lại đúng các phần tử đã thêm để Fenwick về 0. lf, rt giữ nguyên thứ tự tương đối, nên trong mỗi nhóm con phần tử vẫn đứng trước truy vấn.

C++
for (int x : lf)
    if (x <= n) add(x, -1);
int p = L;
for (int x : lf) id[p++] = x;
int M = p - 1;
for (int x : rt) id[p++] = x;
solve(lo, mid, L, M);
solve(mid + 1, hi, M + 1, R);

Khi lo == hi, mọi truy vấn còn lại có đáp án là lo.

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

const int N = 1e5 + 5;
int n, q, a[N], ql[N], qr[N], qk[N], ans[N], bit[N], id[2 * 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;
}

void solve(int lo, int hi, int L, int R) {
    if (L > R) return;
    if (lo == hi) {
        for (int i = L; i <= R; i++)
            if (id[i] > n) ans[id[i] - n] = lo;
        return;
    }
    int mid = (lo + hi) / 2;
    vector<int> lf, rt;
    for (int i = L; i <= R; i++) {
        int x = id[i];
        if (x <= n) {
            if (a[x] <= mid) add(x, 1), lf.push_back(x);
            else rt.push_back(x);
        } else {
            int j = x - n, c = get(qr[j]) - get(ql[j] - 1);
            if (qk[j] <= c) lf.push_back(x);
            else qk[j] -= c, rt.push_back(x);
        }
    }
    for (int x : lf)
        if (x <= n) add(x, -1);
    int p = L;
    for (int x : lf) id[p++] = x;
    int M = p - 1;
    for (int x : rt) id[p++] = x;
    solve(lo, mid, L, M);
    solve(mid + 1, hi, M + 1, R);
}

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

    cin >> n >> q;
    for (int i = 1; i <= n; i++) cin >> a[i], id[i] = i;
    for (int j = 1; j <= q; j++) {
        cin >> ql[j] >> qr[j];
        qk[j] = (qr[j] - ql[j] + 2) / 2;
        id[n + j] = n + j;
    }
    solve(1, 100000, 1, n + q);
    for (int j = 1; j <= q; j++) cout << ans[j] << '\n';
}
7 4
8 1 6 6 3 9 2
1 7
2 5
3 4
6 6

Đệ quy sâu log⁡2105≈17\log_2 10^5 \approx 17 tầng, mỗi tầng mỗi phần tử và truy vấn chỉ ở một nhánh: O((n+q)log⁡Vlog⁡n)O((n + q) \log V \log n).

Khi đi thi

  • Nhiều truy vấn “sớm nhất” hay “ngưỡng nhỏ nhất” trên một quá trình chỉ thêm, chỉ tăng: chặt song song, log⁡\log vòng, mỗi vòng một lượt quét.
  • Đặt lại cấu trúc đầu mỗi vòng; cạnh cùng trọng số gom thành một bước.
  • Phần tử thứ kk trên đoạn, hay có cập nhật: chặt nhị phân tổng thể, Fenwick theo vị trí, trừ kk khi sang nửa phải.

Tóm tắt

  • Điều chuyển được sang chặt song song là tính đơn điệu của điều kiện kiểm theo thời gian.
  • Mỗi truy vấn giữ lo, hi, res; mỗi vòng gom điểm giữa vào giỏ, quét một lượt, O((m+q)log⁡m)O((m + q) \log m) lần cập nhật và kiểm.
  • Trạng thái có thể là DSU (liên thông, kích thước) hay Fenwick (tổng tích lũy).
  • Chặt nhị phân tổng thể chia đồng thời phần tử và truy vấn theo giá trị, O((n+q)log⁡Vlog⁡n)O((n + q) \log V \log n).

Bài tập

Đọc thêm