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

Persistent Segment Tree

Cây phân đoạn bền vững giữ mọi phiên bản sau các lần cập nhật. Một cập nhật điểm chỉ đổi các nút trên đường từ gốc tới một lá, nên ta tạo O(log⁡n)O(\log n) nút mới cho đường đó và dùng chung mọi nhánh còn lại với phiên bản cũ. Ngoài việc quay lại quá khứ, kỹ thuật này cho lời giải trực tuyến của nhiều bài đoạn: dựng phiên bản cho từng tiền tố, rồi lấy hiệu hai phiên bản như lấy hiệu tổng tiền tố.

Sao chép đường đi

Lưu nút bằng ba mảng lc, rc, val và một bộ đếm tot; nút 0 là cây rỗng (con của nó trỏ về chính nó, giá trị 0). Cập nhật tại vị trí pp trên phiên bản gốc old:

  1. Tạo nút mới, chép con và giá trị từ old.
  2. Nếu là lá, sửa giá trị; nếu không, chỉ đệ quy vào con chứa pp và trỏ con đó sang nút mới vừa tạo.

Mỗi lần cập nhật tạo đúng (độ sâu cây + 1) nút, khoảng ⌈log⁡2n⌉+1\lceil \log_2 n \rceil + 1. Tổng bộ nhớ O(n+mlog⁡n)O(n + m \log n); khai báo mảng nút theo con số này, ví dụ 2n+21m2n + 21m khi n≤106n \le 10^6. Không bao giờ sửa một nút đã tạo, nên mọi phiên bản cũ vẫn đúng nguyên vẹn.

Mỗi màu là nút tạo ở một phiên bản; cập nhật chỉ sao chép đường từ gốc xuống lá. Chọn phiên bản để xem cây của nó.

Hỏi nhanh

Mảng n=105n = 10^5 phần tử và 10510^5 lần cập nhật điểm, mỗi lần tạo một phiên bản. Bộ nhớ nút cỡ bao nhiêu?

Ví dụDòng Thời Gian Của Mảng

Đề. Mảng n≤106n \le 10^6 là phiên bản 0. Thao tác thứ ii (m≤106m \le 10^6) tạo phiên bản ii từ một phiên bản cũ vv: hoặc đổi vị trí pp thành xx, hoặc giữ nguyên và in phần tử ở vị trí pp.

Lời giải. Cây phân đoạn bền vững trên vị trí. Phiên bản 0 dựng đầy đủ một lần. Đổi vị trí pp ở phiên bản vv chỉ chép lại các nút trên đường từ gốc tới lá pp, khoảng 21 nút; mọi nhánh khác dùng chung với phiên bản cũ.

Bước 1: nút đánh số tự do. Nút cấp bằng ++tot; lc, rc là chỉ số nút con, val chỉ có nghĩa ở lá. Kích thước M đủ cho 2n2n nút của phiên bản đầu cộng mm lần chép, mỗi lần khoảng log⁡2106+1=21\log_2 10^6 + 1 = 21 nút. build dựng phiên bản 0 như cây phân đoạn thường, nhưng trả về chỉ số nút thay vì dùng 2x2x, 2x+12x + 1.

Bước 2: cập nhật tạo nút mới. Nút x sao chép old, rồi chỉ thay con nằm phía có pp bằng kết quả đệ quy. Con còn lại vẫn trỏ vào nút của phiên bản cũ. Phiên bản cũ không bị đụng tới, nên vẫn đọc đúng như trước.

C++
int update(int old, int l, int r, int p, int v) {
    int x = ++tot;
    lc[x] = lc[old], rc[x] = rc[old], val[x] = val[old];
    if (l == r) {
        val[x] = v;
        return x;
    }
    int mid = (l + r) / 2;
    if (p <= mid) lc[x] = update(lc[old], l, mid, p, v);
    else rc[x] = update(rc[old], mid + 1, r, p, v);
    return x;
}

Bước 3: đọc cũng tạo phiên bản. Thao tác đọc tạo phiên bản giống hệt phiên bản vv, nên chỉ gán root[i] = root[v], không tạo nút nào. Vẫn phải gán vì thao tác sau có thể dựa trên phiên bản ii.

C++
if (type == 1) {
    int x;
    cin >> x;
    root[i] = update(root[v], 1, n, p, x);
} else {
    cout << get(root[v], 1, n, p) << '\n';
    root[i] = root[v];
}
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

const int N = 1e6 + 5, M = 2 * N + 21 * N;
int n, m, tot, a[N], root[N], lc[M], rc[M], val[M];

int build(int l, int r) {
    int x = ++tot;
    if (l == r) {
        val[x] = a[l];
        return x;
    }
    int mid = (l + r) / 2;
    lc[x] = build(l, mid);
    rc[x] = build(mid + 1, r);
    return x;
}

int update(int old, int l, int r, int p, int v) {
    int x = ++tot;
    lc[x] = lc[old], rc[x] = rc[old], val[x] = val[old];
    if (l == r) {
        val[x] = v;
        return x;
    }
    int mid = (l + r) / 2;
    if (p <= mid) lc[x] = update(lc[old], l, mid, p, v);
    else rc[x] = update(rc[old], mid + 1, r, p, v);
    return x;
}

int get(int x, int l, int r, int p) {
    while (l < r) {
        int mid = (l + r) / 2;
        if (p <= mid) x = lc[x], r = mid;
        else x = rc[x], l = mid + 1;
    }
    return val[x];
}

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

    cin >> n >> m;
    for (int i = 1; i <= n; i++) cin >> a[i];
    root[0] = build(1, n);
    for (int i = 1; i <= m; i++) {
        int v, type, p;
        cin >> v >> type >> p;
        if (type == 1) {
            int x;
            cin >> x;
            root[i] = update(root[v], 1, n, p, x);
        } else {
            cout << get(root[v], 1, n, p) << '\n';
            root[i] = root[v];
        }
    }
}
4 6
5 8 2 9
0 2 2
0 1 3 7
2 2 3
1 1 2 -4
4 2 2
2 2 2

Mỗi thao tác O(log⁡n)O(\log n) thời gian và bộ nhớ.

Phiên bản theo tiền tố: phần tử thứ k

Nén giá trị về 0..m−10..m-1. Phiên bản ii là cây đếm trên miền giá trị, chứa a1,…,aia_1, \dots, a_i: root[i] là root[i-1] thêm 1 tại hạng của aia_i. Số phần tử của đoạn [l,r][l, r] có hạng trong một nút là cnt của nút đó ở phiên bản rr trừ ở phiên bản l−1l - 1, vì hai phiên bản có cùng hình dạng.

Tìm phần tử nhỏ thứ kk: đi xuống đồng thời trên hai phiên bản. Ở mỗi nút, cc là hiệu số đếm của hai con trái; k≤ck \le c thì sang trái, ngược lại trừ kk đi cc rồi sang phải. Mỗi truy vấn O(log⁡n)O(\log n), trả lời trực tuyến được, khác với cách chặt nhị phân tổng thể.

Ví dụThứ Hạng Trong Đoạn

Đề. Mảng tĩnh n≤2⋅105n \le 2 \cdot 10^5 phần tử, m≤2⋅105m \le 2 \cdot 10^5 truy vấn: phần tử nhỏ thứ kk của đoạn [l,r][l, r].

Lời giải. Nén giá trị về hạng 0..m−10..m-1. Phiên bản ii là cây đếm trên miền hạng, chứa a1,…,aia_1, \dots, a_i, dựng bằng cách thêm 1 vào phiên bản i−1i - 1 tại hạng của aia_i. Mọi phiên bản có cùng hình dạng, nên số phần tử của đoạn [l,r][l, r] rơi vào một nút là hiệu cnt của nút đó ở phiên bản rr và phiên bản l−1l - 1.

Bước 1: cây rỗng dùng chung. Nút 0 có cnt[0] = 0 và lc[0] = rc[0] = 0, nên đi xuống từ nút 0 mãi vẫn ở nút 0 với số đếm 0. Nhờ vậy root[0] = 0 không cần dựng.

Bước 2: thêm một phần tử. Chép đường từ gốc tới lá pp, mỗi nút trên đường tăng cnt thêm 1.

C++
int insert(int old, int l, int r, int p) {
    int x = ++tot;
    lc[x] = lc[old], rc[x] = rc[old], cnt[x] = cnt[old] + 1;
    if (l == r) return x;
    int mid = (l + r) / 2;
    if (p <= mid) lc[x] = insert(lc[old], l, mid, p);
    else rc[x] = insert(rc[old], mid + 1, r, p);
    return x;
}

Bước 3: đi xuống trên hai phiên bản. u là phiên bản l−1l - 1, v là phiên bản rr; c là số phần tử của đoạn có hạng ở nửa trái. k≤ck \le c thì phần tử cần tìm ở nửa trái; ngược lại nó là phần tử thứ k−ck - c của nửa phải. Tới lá thì l là hạng cần tìm, vals[...] đổi về giá trị thật.

C++
int kth(int u, int v, int l, int r, int k) {
    while (l < r) {
        int mid = (l + r) / 2, c = cnt[lc[v]] - cnt[lc[u]];
        if (k <= c) u = lc[u], v = lc[v], r = mid;
        else k -= c, u = rc[u], v = rc[v], l = mid + 1;
    }
    return l;
}
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

const int N = 2e5 + 5, M = 20 * N;
int n, q, m, tot, a[N], root[N], lc[M], rc[M], cnt[M];
vector<int> vals;

int insert(int old, int l, int r, int p) {
    int x = ++tot;
    lc[x] = lc[old], rc[x] = rc[old], cnt[x] = cnt[old] + 1;
    if (l == r) return x;
    int mid = (l + r) / 2;
    if (p <= mid) lc[x] = insert(lc[old], l, mid, p);
    else rc[x] = insert(rc[old], mid + 1, r, p);
    return x;
}

int kth(int u, int v, int l, int r, int k) {
    while (l < r) {
        int mid = (l + r) / 2, c = cnt[lc[v]] - cnt[lc[u]];
        if (k <= c) u = lc[u], v = lc[v], r = mid;
        else k -= c, u = rc[u], v = rc[v], l = mid + 1;
    }
    return l;
}

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

    cin >> n >> q;
    for (int i = 1; i <= n; i++) cin >> a[i], vals.push_back(a[i]);
    sort(vals.begin(), vals.end());
    vals.erase(unique(vals.begin(), vals.end()), vals.end());
    m = vals.size();
    for (int i = 1; i <= n; i++) {
        int p = lower_bound(vals.begin(), vals.end(), a[i]) - vals.begin();
        root[i] = insert(root[i - 1], 0, m - 1, p);
    }
    while (q--) {
        int l, r, k;
        cin >> l >> r >> k;
        cout << vals[kth(root[l - 1], root[r], 0, m - 1, k)] << '\n';
    }
}
7 4
4 0 4 7 2 0 5
1 7 3
2 5 2
3 6 4
4 4 1

Mỗi truy vấn O(log⁡n)O(\log n), không cần đọc trước các truy vấn khác.

Đếm theo ngưỡng trên đoạn

Cùng các phiên bản tiền tố trả lời được “có bao nhiêu phần tử của [l,r][l, r] có giá trị trong khoảng [x,y][x, y]”: lấy tổng cnt của khoảng hạng tương ứng ở phiên bản rr trừ ở phiên bản l−1l - 1. Khi không bắt buộc trực tuyến, bài này còn giải được bằng Fenwick và sắp truy vấn theo ngưỡng; phiên bản bền vững hữu ích khi đề mã hóa truy vấn bằng đáp án trước, hoặc khi số chiều điều kiện nhiều hơn.

Thay cnt bằng tổng giá trị thì được “tổng các phần tử không vượt XX trong đoạn”; thay điểm cập nhật thì được đếm số giá trị phân biệt (chỉ giữ lần xuất hiện cuối).

Ví dụVượt Qua Ngưỡng

Đề. Mảng n≤3⋅104n \le 3 \cdot 10^4, q≤2⋅105q \le 2 \cdot 10^5 truy vấn: đếm i∈[l,r]i \in [l, r] có ai>ka_i > k.

Lời giải. Cùng các phiên bản tiền tố như ví dụ trước (insert giống hệt). Phần tử lớn hơn kk là những phần tử có hạng từ upper_bound(k) trở đi; số phần tử như vậy trong đoạn [l,r][l, r] là tổng đếm của khoảng hạng đó ở phiên bản rr trừ ở phiên bản l−1l - 1.

Bước 1: đếm hạng ≥ql\ge ql. Nút nằm hẳn bên trái qlql, hoặc không có phần tử nào của đoạn, thì trả 0. Nút nằm hẳn bên phải qlql thì trả cả hiệu đếm. Còn lại tách làm hai nửa.

C++
int countFrom(int u, int v, int l, int r, int ql) {
    if (r < ql || cnt[v] - cnt[u] == 0) return 0;
    if (ql <= l) return cnt[v] - cnt[u];
    int mid = (l + r) / 2;
    return countFrom(lc[u], lc[v], l, mid, ql) + countFrom(rc[u], rc[v], mid + 1, r, ql);
}

Bước 2: truy vấn. Nếu kk lớn hơn mọi giá trị thì p = m, mọi nút đều có r < ql và kết quả là 0.

C++
int p = upper_bound(vals.begin(), vals.end(), k) - vals.begin();
cout << countFrom(root[l - 1], root[r], 0, m - 1, p) << '\n';
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

const int N = 3e4 + 5, M = 20 * N;
int n, q, m, tot, a[N], root[N], lc[M], rc[M], cnt[M];
vector<int> vals;

int insert(int old, int l, int r, int p) {
    int x = ++tot;
    lc[x] = lc[old], rc[x] = rc[old], cnt[x] = cnt[old] + 1;
    if (l == r) return x;
    int mid = (l + r) / 2;
    if (p <= mid) lc[x] = insert(lc[old], l, mid, p);
    else rc[x] = insert(rc[old], mid + 1, r, p);
    return x;
}

int countFrom(int u, int v, int l, int r, int ql) {
    if (r < ql || cnt[v] - cnt[u] == 0) return 0;
    if (ql <= l) return cnt[v] - cnt[u];
    int mid = (l + r) / 2;
    return countFrom(lc[u], lc[v], l, mid, ql) + countFrom(rc[u], rc[v], mid + 1, r, ql);
}

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

    cin >> n;
    for (int i = 1; i <= n; i++) cin >> a[i], vals.push_back(a[i]);
    sort(vals.begin(), vals.end());
    vals.erase(unique(vals.begin(), vals.end()), vals.end());
    m = vals.size();
    for (int i = 1; i <= n; i++) {
        int p = lower_bound(vals.begin(), vals.end(), a[i]) - vals.begin();
        root[i] = insert(root[i - 1], 0, m - 1, p);
    }
    cin >> q;
    while (q--) {
        int l, r, k;
        cin >> l >> r >> k;
        int p = upper_bound(vals.begin(), vals.end(), k) - vals.begin();
        cout << countFrom(root[l - 1], root[r], 0, m - 1, p) << '\n';
    }
}
6
5 1 9 5 2 8
5
1 6 5
2 5 4
3 6 8
1 3 9
4 4 1

Mỗi truy vấn đi qua O(log⁡n)O(\log n) nút như truy vấn đoạn của cây phân đoạn thường.

Phiên bản trên cây

Trên cây gốc 1, cho phiên bản của đỉnh vv là phiên bản của cha thêm giá trị của vv: nó chứa đúng các giá trị trên đường từ gốc tới vv. Đa tập trên đường u→vu \to v với w=lca⁡(u,v)w = \operatorname{lca}(u, v) là

Tu+Tv−Tw−Tpar⁡(w),T_u + T_v - T_w - T_{\operatorname{par}(w)},

trừ TwT_w hai lần sẽ mất ww, nên chỉ trừ một lần và trừ thêm phiên bản của cha nó. Tìm phần tử thứ kk bằng cách đi xuống đồng thời trên bốn phiên bản. Dựng phiên bản theo thứ tự BFS để cha luôn có trước con.

Hỏi nhanh

Đường u→vu \to v có LCA là ww. Vì sao không dùng Tu+Tv−2TwT_u + T_v - 2T_w?

Ví dụThứ Hạng Trên Đường Cây

Đề. Cây n≤105n \le 10^5 đỉnh có trọng số, m≤105m \le 10^5 truy vấn trực tuyến: đầu mút thật u=U⊕lastu = U \oplus last, tìm trọng số nhỏ thứ kk trên đường u→vu \to v.

Lời giải. Phiên bản của đỉnh uu là phiên bản của cha thêm trọng số của uu, nên nó chứa đúng các trọng số trên đường từ gốc tới uu. Đường u→vu \to v với c=lca⁡(u,v)c = \operatorname{lca}(u, v) có đa tập trọng số bằng Tu+Tv−Tc−Tpar⁡(c)T_u + T_v - T_c - T_{\operatorname{par}(c)}; tìm phần tử thứ kk bằng cách đi xuống đồng thời trên bốn phiên bản. Đề mã hóa đầu mút uu bằng đáp án trước, nên phải trả lời lần lượt từng truy vấn.

Bước 1: phiên bản theo BFS. Khi lấy uu ra khỏi order, cha up[0][u] đã được xử lý trước nên root[up[0][u]] có sẵn. Gốc có cha là 0, tức cây rỗng.

C++
for (int i = 0; i < (int)order.size(); i++) {
    int u = order[i];
    int p = lower_bound(vals.begin(), vals.end(), w[u]) - vals.begin();
    root[u] = insert(root[up[0][u]], 0, m - 1, p);
    for (int v : adj[u])
        if (v != up[0][u]) up[0][v] = u, dep[v] = dep[u] + 1, order.push_back(v);
}

Bước 2: LCA bằng nhảy nhị phân. up[j][v] là tổ tiên thứ 2j2^j, dựng sau BFS. Nâng đỉnh sâu hơn lên cùng độ sâu, rồi nâng cả hai từ bước lớn xuống bước nhỏ khi tổ tiên còn khác nhau.

C++
int lca(int u, int v) {
    if (dep[u] < dep[v]) swap(u, v);
    for (int j = LOG - 1; j >= 0; j--)
        if (dep[u] - (1 << j) >= dep[v]) u = up[j][u];
    if (u == v) return u;
    for (int j = LOG - 1; j >= 0; j--)
        if (up[j][u] != up[j][v]) u = up[j][u], v = up[j][v];
    return up[0][u];
}

Bước 3: đi xuống trên bốn phiên bản. x là số trọng số trên đường có hạng ở nửa trái, tính bằng a+b−c−da + b - c - d theo đúng công thức đa tập. Khi LCA là gốc, up[0][c] = 0 và root[0] = 0 là cây rỗng, nên công thức vẫn đúng.

C++
int kth(int a, int b, int c, int d, int l, int r, int k) {
    while (l < r) {
        int mid = (l + r) / 2, x = cnt[lc[a]] + cnt[lc[b]] - cnt[lc[c]] - cnt[lc[d]];
        if (k <= x) a = lc[a], b = lc[b], c = lc[c], d = lc[d], r = mid;
        else k -= x, a = rc[a], b = rc[b], c = rc[c], d = rc[d], l = mid + 1;
    }
    return l;
}

last giữ đáp án trước, giải mã u = U ^ last trước khi tính.

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

const int N = 1e5 + 5, M = 20 * N, LOG = 17;
int n, q, m, tot, root[N], dep[N], up[LOG][N], lc[M], rc[M], cnt[M];
ll w[N];
vector<int> adj[N];
vector<ll> vals;

int insert(int old, int l, int r, int p) {
    int x = ++tot;
    lc[x] = lc[old], rc[x] = rc[old], cnt[x] = cnt[old] + 1;
    if (l == r) return x;
    int mid = (l + r) / 2;
    if (p <= mid) lc[x] = insert(lc[old], l, mid, p);
    else rc[x] = insert(rc[old], mid + 1, r, p);
    return x;
}

int lca(int u, int v) {
    if (dep[u] < dep[v]) swap(u, v);
    for (int j = LOG - 1; j >= 0; j--)
        if (dep[u] - (1 << j) >= dep[v]) u = up[j][u];
    if (u == v) return u;
    for (int j = LOG - 1; j >= 0; j--)
        if (up[j][u] != up[j][v]) u = up[j][u], v = up[j][v];
    return up[0][u];
}

int kth(int a, int b, int c, int d, int l, int r, int k) {
    while (l < r) {
        int mid = (l + r) / 2, x = cnt[lc[a]] + cnt[lc[b]] - cnt[lc[c]] - cnt[lc[d]];
        if (k <= x) a = lc[a], b = lc[b], c = lc[c], d = lc[d], r = mid;
        else k -= x, a = rc[a], b = rc[b], c = rc[c], d = rc[d], l = mid + 1;
    }
    return l;
}

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

    cin >> n >> q;
    for (int i = 1; i <= n; i++) cin >> w[i], vals.push_back(w[i]);
    sort(vals.begin(), vals.end());
    vals.erase(unique(vals.begin(), vals.end()), vals.end());
    m = vals.size();
    for (int i = 1; i < n; i++) {
        int u, v;
        cin >> u >> v;
        adj[u].push_back(v);
        adj[v].push_back(u);
    }
    vector<int> order = {1};
    for (int i = 0; i < (int)order.size(); i++) {
        int u = order[i];
        int p = lower_bound(vals.begin(), vals.end(), w[u]) - vals.begin();
        root[u] = insert(root[up[0][u]], 0, m - 1, p);
        for (int v : adj[u])
            if (v != up[0][u]) up[0][v] = u, dep[v] = dep[u] + 1, order.push_back(v);
    }
    for (int j = 1; j < LOG; j++)
        for (int v = 1; v <= n; v++) up[j][v] = up[j - 1][up[j - 1][v]];
    ll last = 0;
    while (q--) {
        ll U;
        int v, k;
        cin >> U >> v >> k;
        int u = U ^ last, c = lca(u, v);
        last = vals[kth(root[u], root[v], root[c], root[up[0][c]], 0, m - 1, k)];
        cout << last << '\n';
    }
}
6 4
8 3 10 3 6 12
1 2
1 3
2 4
2 5
3 6
4 6 3
13 4 2
2 5 2
0 6 1

Mỗi truy vấn O(log⁡n)O(\log n).

Khi đi thi

  • Cần quay về phiên bản cũ, hoặc truy vấn đoạn bị mã hóa bằng đáp án trước: cây phân đoạn bền vững, phiên bản theo thời gian hay theo tiền tố.
  • Phần tử thứ kk, đếm theo ngưỡng, tổng theo ngưỡng trên đoạn: phiên bản tiền tố trên miền giá trị đã nén, lấy hiệu hai phiên bản. Trên đường cây: bốn phiên bản quanh LCA.
  • Tính trước bộ nhớ: (số lần cập nhật) nhân (log⁡n\log n + 1) nút, mỗi nút vài số int. Dùng nút 0 làm cây rỗng để khỏi dựng phiên bản đầu.

Tóm tắt

  • Cập nhật điểm tạo O(log⁡n)O(\log n) nút mới, dùng chung phần còn lại; phiên bản cũ không bị sửa.
  • Phiên bản tiền tố trên miền giá trị: số đếm của đoạn là hiệu hai phiên bản cùng hình dạng.
  • Phần tử thứ kk: đi xuống đồng thời trên các phiên bản, so kk với hiệu số đếm bên trái.
  • Phiên bản theo cha: đường u→vu \to v là Tu+Tv−Tw−Tpar⁡(w)T_u + T_v - T_w - T_{\operatorname{par}(w)}.

Bài tập

1
Những Bản Sao Độc Lậpcanuc80k5901Mỗi mảng là một gốc; gán điểm tạo gốc mới cho mảng đó, sao chép mảng chỉ là thêm một gốc trỏ vào gốc cũ; cây lưu tổng.
LQDOJ
2
Tổng Không Vượt Mốccanuc80k5904Phiên bản tiền tố lưu tổng giá trị trên miền giá trị; tổng các phần tử không vượt XX là hiệu hai phiên bản.
LQDOJ
3
Dấu Vết Cuối Cùngcanuc80k5905Số giá trị phân biệt: phiên bản rr chỉ giữ lần xuất hiện cuối của mỗi giá trị; đếm vị trí không nhỏ hơn ll.
LQDOJ
4
Khoảng Cách Trùng Nhỏ Nhấtcanuc80k5906Mỗi vị trí ghép với lần xuất hiện trước của cùng giá trị; truy vấn lấy khoảng cách nhỏ nhất của các cặp có đầu trái không nhỏ hơn ll ở phiên bản rr.
LQDOJ
5
Giới Hạn Đại Diệncanuc80k5907Với mỗi ii, đặt bib_i là vị trí lần xuất hiện thứ kk về trước của cùng giá trị; đếm i∈[l,r]i \in [l, r] có bi<lb_i < l.
LQDOJ
6
Một Giá Trị Đơn Độccanuc80k5908Phiên bản rr chỉ giữ lần xuất hiện cuối, khóa là vị trí lần xuất hiện trước; tìm trong [l,r][l, r] khóa nhỏ nhất, nhỏ hơn ll thì có đáp án.
LQDOJ
7
Số Khuyết Đầu Tiêncanuc80k5909Cây trên miền giá trị lưu vị trí xuất hiện cuối nhỏ nhất; ở phiên bản rr, giá trị nhỏ nhất có vị trí cuối dưới ll là mex.
LQDOJ
8
Nhánh Thời Giancanuc80k5910Cộng đoạn bền vững không đẩy lười: giữ nhãn cộng ở nút và cộng dồn khi đi xuống; quay về thời điểm cũ chỉ là đổi gốc.
LQDOJ
9
Tổng Ở Mốc Cập Nhậtcanuc80k5911Mỗi lệnh gán tạo một phiên bản; truy vấn tính tổng trên phiên bản thứ kk.
LQDOJ
10
Dãy Con Tối Ưucanuc80k5912Dãy con tối ưu gồm kk phần tử lớn nhất, ưu tiên chỉ số nhỏ; sắp theo khóa đó, phiên bản thứ kk cho chỉ số nhỏ thứ pospos.
LQDOJ
11
Vượt Tần Suấtcanuc80k5913Đi xuống như tìm thứ kk, chỉ rẽ vào con có số đếm hơn ngưỡng và ưu tiên con trái; số nút thăm bị chặn bởi kk.
LQDOJ
12
Lịch Ưu Tiêncanuc80k5916Phiên bản theo thời điểm từ hiệu bắt đầu và kết thúc nhiệm vụ trên miền ưu tiên; tổng kk ưu tiên nhỏ nhất bằng cách đi xuống.
LQDOJ
13
Điểm Trong Khungcanuc80k5917Phiên bản theo hoành độ, cây trên tung độ; số điểm trong hình chữ nhật là hiệu hai phiên bản.
LQDOJ
14
Vị Trí Đại Diệncanuc80k5919Dựng phiên bản từ phải sang trái chỉ giữ lần xuất hiện đầu; đếm số phân biệt rồi tìm vị trí thứ ⌈c/2⌉\lceil c/2 \rceil.
LQDOJ
15
Cửa Sổ Độ Sâucanuc80k5921Phiên bản theo độ sâu, cây trên thứ tự Euler lưu giá trị nhỏ nhất; truy vấn hỏi đoạn cây con ở phiên bản độ sâu depx+kdep_x + k.
LQDOJ
16
Điểm Bắt Đầu Tốt Nhấtcanuc80k5924Trie bền vững trên tiền tố XOR, mỗi nút lưu chỉ số lớn nhất đi qua; tham lam theo bit với ràng buộc vị trí.
LQDOJ

Đọc thêm