学习笔记:线段树

学习笔记:线段树

在已经掌握线段树的基本用法后的做题整理。给自己复习用的。

\(mid\) 表示 \((l+r)/2\)\(u\) 表示当前区间节点(父区间),\(ls,rs\) 分别表示当前区间的左、右子区间节点。

1 普通线段树

普通维护序列

P2023 [AHOI2009] 维护序列

修改:区间加,区间乘;询问:区间求和。

双倍经验:P3373 【模板】线段树 2

维护两个标记,加 add 和乘 mul。在 pushdown 函数中,要先乘再加,乘的时候把 add 标记也更新。

点击查看代码
#include <iostream>
#define f(x, y, z) for (int x = (y); x <= (z); ++x)
#define ls (u << 1)
#define rs (u << 1 | 1)
using namespace std;
typedef long long ll;
const int N = 1e5 + 10;
int n, m, p, a[N];

inline int &AddEq(int &a, int const &b) { return (a += b) >= p ? (a -= p) : a; }
inline int &MulEq(int &a, int const &b) { ll r = 1ll * a * b; return a = (r >= p ? r % p : r); }

struct Node { int l, r, len, sum, add, mul; } tr[N << 2];

inline void pushup(int u) { return tr[u].sum = (tr[ls].sum + tr[rs].sum) % p, void(); }

inline void pushdown(int u) {
    if (tr[u].mul ^ 1) {
        MulEq(tr[ls].mul, tr[u].mul);
        MulEq(tr[rs].mul, tr[u].mul);
        MulEq(tr[ls].add, tr[u].mul);
        MulEq(tr[rs].add, tr[u].mul);
        MulEq(tr[ls].sum, tr[u].mul);
        MulEq(tr[rs].sum, tr[u].mul);
        tr[u].mul = 1;
    }
    if (tr[u].add) {
        AddEq(tr[ls].add, tr[u].add);
        AddEq(tr[rs].add, tr[u].add);
        AddEq(tr[ls].sum, 1ll * tr[u].add * tr[ls].len % p);
        AddEq(tr[rs].sum, 1ll * tr[u].add * tr[rs].len % p);
        tr[u].add = 0;
    }
    return;
}

void build(int u, int l, int r) {
    tr[u].l = l, tr[u].r = r, tr[u].len = r - l + 1;
    tr[u].sum = 0, tr[u].add = 0, tr[u].mul = 1;
    if (l == r) return tr[u].sum = a[l], void();
    int mid = l + r >> 1;
    build(ls, l, mid);
    build(rs, mid + 1, r);
    pushup(u);
    return;
}

void add(int u, int l, int r, int c) {
    if (l <= tr[u].l && tr[u].r <= r) {
        AddEq(tr[u].add, c);
        AddEq(tr[u].sum, 1ll * c * tr[u].len % p);
        return;
    }
    pushdown(u);
    int mid = tr[u].l + tr[u].r >> 1;
    if (l <= mid) add(ls, l, r, c);
    if (r > mid) add(rs, l, r, c);
    pushup(u);
    return;
}

void mul(int u, int l, int r, int c) {
    if (l <= tr[u].l && tr[u].r <= r) {
        MulEq(tr[u].mul, c);
        MulEq(tr[u].add, c);
        MulEq(tr[u].sum, c);
        return;
    }
    pushdown(u);
    int mid = tr[u].l + tr[u].r >> 1;
    if (l <= mid) mul(ls, l, r, c);
    if (r > mid) mul(rs, l, r, c);
    pushup(u);
    return;
}

int Query(int u, int l, int r) {
    if (l <= tr[u].l && tr[u].r <= r) return tr[u].sum;
    pushdown(u);
    int mid = tr[u].l + tr[u].r >> 1, res = 0;
    if (l <= mid) AddEq(res, Query(ls, l, r));
    if (r > mid) AddEq(res, Query(rs, l, r));
    return res;
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0), cout.tie(0);
    
    cin >> n >> p;
    f(i, 1, n) cin >> a[i];
    build(1, 1, n);
    cin >> m;
    int op, l, r, x;
    while (m--) {
        cin >> op >> l >> r;
        if (op == 1) cin >> x, mul(1, l, r, x);
        else if (op == 2) cin >> x, add(1, l, r, x);
        else cout << Query(1, l, r) << '\n';
    }
    
    return 0;
}

P4513 小白逛公园(区间最大子段和)

修改:单点加;询问:区间最大子段和。

考虑如何合并。一个区间 \([l,r]\) 的最大子段可能在 \([l,mid]\) 中或 \((mid,r]\) 中,也可能横跨左右两个子区间。

对于前一种情况,我们直接对子区间的答案取 \(\max\)。对于后一种情况,我们记录前缀最大值 \(lmx\) 和后缀最大值 \(rmx\),用 \(lmx_{ls}+rmx_{rs}\) 更新 \(u\) 的答案。

对于 \(lmx\) 的更新,可能在 \([l,mid]\) 中,也可能是 \([l,j]\),其中 \(j\in(mid,r]\)\(rmx\) 同理。

点击查看代码
#include <climits>
#include <iostream>
#include <algorithm>
#define f(x, y, z) for (int x = (y); x <= (z); ++x)
#define ls (u << 1)
#define rs (u << 1 | 1)
using namespace std;
const int N = 5e5 + 10;
int n, m, a[N];

struct Node {
    int lmx, rmx, ans, sum;
} tr[N << 2];

inline Node pushup(Node A, Node B) {
    Node C;
    C.lmx = max(A.lmx, A.sum + B.lmx);
    C.rmx = max(B.rmx, B.sum + A.rmx);
    C.sum = A.sum + B.sum;
    C.ans = max({A.ans, B.ans, A.rmx + B.lmx});
    return C;
}

void build(int u, int l, int r) {
    if (l == r) {
        tr[u].lmx = tr[u].rmx = tr[u].ans = tr[u].sum = a[l];
        return;
    }
    int mid = l + r >> 1;
    build(ls, l, mid);
    build(rs, mid + 1, r);
    tr[u] = pushup(tr[ls], tr[rs]);
    return;
}

void modify(int u, int l, int r, int x, int v) {
    if (l == x && r == x) {
        tr[u].lmx = tr[u].rmx = tr[u].ans = tr[u].sum = v;
        return;
    }
    int mid = l + r >> 1;
    if (x <= mid) modify(ls, l, mid, x, v);
    else modify(rs, mid + 1, r, x, v);
    tr[u] = pushup(tr[ls], tr[rs]);
    return;
}

Node query(int u, int l, int r, int L, int R) {
    if (L <= l && r <= R) return tr[u];
    int mid = l + r >> 1, res = INT_MIN;
    if (R <= mid) return query(ls, l, mid, L, R);
    if (L > mid) return query(rs, mid + 1, r, L, R);
    return pushup(query(ls, l, mid, L, R), query(rs, mid + 1, r, L, R));
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0), cout.tie(0);
    
    cin >> n >> m;
    f(i, 1, n) cin >> a[i];
    build(1, 1, n);
    int k, c, b;
    while (m--) {
        cin >> k >> c >> b;
        if (k == 1) {
            if (c > b) swap(c, b);
            cout << query(1, 1, n, c, b).ans << '\n';
        } else modify(1, 1, n, c, b);
    }
    
    return 0;
}

P1471 方差

修改:区间加;询问:区间平均数,区间方差。

区间平均数 \(avg\) 即为区间和 \(sum\) 除以区间长度 \(len\),分别维护即可。

区间方差有点难搞,考虑区间加 \(k\) 时方差 \(dat\) 如何变化。对公式变形:

\[\begin{aligned} \sigma^2&=\frac1n\sum\limits_{i=1}^n\left(x_i-\overline x\right)^2 \\ &=-\overline x^2+\frac1n\sum x_i^2 \end{aligned} \]

于是只需要维护区间平方和 \(sum2\) 如何变化。

\[\sum(x_i+k)^2=\sum x_i^2+2k\sum x_i+nk^2 \]

点击查看代码
#include <iomanip>
#include <iostream>
#define f(x, y, z) for (int x = (y); x <= (z); ++x)
#define ls (u << 1)
#define rs (u << 1 | 1)
using namespace std;
typedef double db;
int constexpr N = 1e5 + 10;
int n, m;
db x[N];

inline db sq(db const &x) { return x * x; }

struct Node {
    int len;
    db sum, sum2, add;
} tr[N << 2];

inline Node pushup(Node A, Node B) {
    Node C;
    C.add = 0;
    C.len = A.len + B.len;
    C.sum = A.sum + B.sum;
    C.sum2 = A.sum2 + B.sum2;
    return C;
}

inline void update(int u, db k) {
    tr[u].add += k;
    tr[u].sum2 += 2 * k * tr[u].sum + tr[u].len * k * k;
    tr[u].sum += k * tr[u].len;
    return;
}

inline void pushdown(int u) {
    if (tr[u].add != 0) {
        db k = tr[u].add;
        update(ls, k), update(rs, k);
        tr[u].add = 0;
    }
    return;
}

void build(int u, int l, int r) {
    if (l == r) {
        tr[u].len = 1;
        tr[u].sum = x[l];
        tr[u].sum2 = sq(x[l]);
        return;
    }
    int mid = l + r >> 1;
    build(ls, l, mid);
    build(rs, mid + 1, r);
    tr[u] = pushup(tr[ls], tr[rs]);
    return;
}

void add(int u, int l, int r, int L, int R, db k) {
    if (L <= l && r <= R) return update(u, k);
    pushdown(u);
    int mid = l + r >> 1;
    if (L <= mid) add(ls, l, mid, L, R, k);
    if (R > mid) add(rs, mid + 1, r, L, R, k);
    tr[u] = pushup(tr[ls], tr[rs]);
    return;
}

Node query(int u, int l, int r, int L, int R) {
    if (L <= l && r <= R) return tr[u];
    pushdown(u);
    int mid = l + r >> 1;
    if (R <= mid) return query(ls, l, mid, L, R);
    if (L > mid) return query(rs, mid + 1, r, L, R);
    return pushup(query(ls, l, mid, L, R), query(rs, mid + 1, r, L, R));
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0), cout.tie(0);
    
    cin >> n >> m;
    f(i, 1, n) cin >> x[i];
    cout << fixed << setprecision(4);
    build(1, 1, n);
    int op, x, y; db k;
    while (m--) {
        cin >> op >> x >> y;
        if (op == 1) {
            cin >> k;
            add(1, 1, n, x, y, k);
        } else {
            Node tmp = query(1, 1, n, x, y);
            if (op == 2) cout << tmp.sum / tmp.len << '\n';
            else if (op == 3) cout << -sq(tmp.sum / tmp.len) + tmp.sum2 / tmp.len << '\n';
        }
    }
    
    return 0;
}

P6327 区间加区间sin和

修改:区间加;询问:区间 \(\sin\) 和,即 \(\sum_{i=l}^r\sin a_i\)

\[\begin{aligned} \sum_{i=l}^r\sin(a_i+k)&=\sum(\sin a_i\cos k+\cos a_i\sin k)\\ &=\cos k\sum\sin a_i+\sin k\sum\cos a_i \end{aligned} \]

因此我们还需要维护一个 \(\cos\) 和。维护方式同理。注意更新时要先临时保存原来的 \(\sin\) 和与 \(\cos\) 和。

注意区间加的懒标记需要开 long long

点击查看代码
#include <iomanip>
#include <iostream>
#define f(x, y, z) for (int x = (y); x <= (z); ++x)
#define ls (u << 1)
#define rs (u << 1 | 1)
#define sin __builtin_sin
#define cos __builtin_cos
using namespace std;
typedef long long ll;
typedef double db;
int constexpr N = 2e5 + 10;
int n, m, a[N];

struct Node {
    db s, c;
    ll add;
} tr[N << 2];

inline void pushup(int const &u) {
    tr[u].s = tr[ls].s + tr[rs].s;
    tr[u].c = tr[ls].c + tr[rs].c;
    return;
}

inline void update(int const &u, ll const &k, db const S, db const C) {
    tr[u].add += k;
    db s = tr[u].s, c = tr[u].c;
    tr[u].s = s * C + c * S;
    tr[u].c = c * C - s * S;
    return;
}

inline void pushdown(int const &u) {
    db s = sin(tr[u].add), c = cos(tr[u].add);
    update(ls, tr[u].add, s, c);
    update(rs, tr[u].add, s, c);
    tr[u].add = 0;
    return;
}

void build(int u, int l, int r) {
    if (l == r) {
        tr[u].s = sin(a[l]);
        tr[u].c = cos(a[l]);
        return;
    }
    int mid = l + r >> 1;
    build(ls, l, mid);
    build(rs, mid + 1, r);
    return pushup(u);
}

void add(int u, int l, int r, int L, int R, int k) {
    if (L > R) return;
    if (L <= l && r <= R) return update(u, k, sin(k), cos(k));
    pushdown(u);
    int mid = l + r >> 1;
    if (L <= mid) add(ls, l, mid, L, R, k);
    if (R > mid) add(rs, mid + 1, r, L, R, k);
    return pushup(u);
}

db query(int u, int l, int r, int L, int R) {
    if (L <= l && r <= R) return tr[u].s;
    pushdown(u);
    int mid = l + r >> 1;
    db res = 0;
    if (L <= mid) res += query(ls, l, mid, L, R);
    if (R > mid) res += query(rs, mid + 1, r, L, R);
    return res;
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0), cout.tie(0);
    
    cout << fixed << setprecision(1);
    cin >> n;
    f(i, 1, n) cin >> a[i];
    build(1, 1, n);
    cin >> m;
    int op, l, r, v;
    while (m--) {
        cin >> op >> l >> r;
        if (op == 1)
            cin >> v, add(1, 1, n, l, r, v);
        else if (op == 2)
            cout << query(1, 1, n, l, r) << '\n';
    }
    
    return 0;
}

**P7706 「Wdsr-2.7」文文的摄影布置

定义 \(\psi(x,y)=A_x+A_y-\min\limits_{x<i<y}\{B_i\}\)

修改:单点修改 \(A_i\)\(B_i\);询问:\(\max\limits_{l\le x<y\le r}\{\psi(x,y)\}\)(给定 \(l,r\))。

\(1\le n,m\le5\times10^5\)\(1\le A_i,B_i\le10^8\)

考虑如何合并。设 \(ans\) 为维护的答案。首先肯定是对 \(ans_{ls}\)\(ans_{rs}\)\(\max\)。然后考虑横跨左右子区间的情况,即 \(x\le mid,y>mid\)

讨论 \(\max B_i\) 在左子区间内还是右子区间内。

如果在左子区间内,我们转化式子:

\[\max\psi(x,y)=\max\left\{A_x-\min_{x<i\le mid}\{B_i\}\right\}+\max A_y. \]

注意到可以分成两部分,\(\max\left\{A_x-\min\limits_{x<i\le mid}\{B_i\}\right\}\) 只与左子区间有关,\(\max A_y\) 只与右子区间有关。

于是可以分别维护,设 \(ab_u=\max\limits_{l\le x\le r}\left\{A_x-\min\limits_{x<i\le r}\{B_i\}\right\}\)\(mxa_u=\max\limits_{l\le i\le r}A_i\)

考虑如何从子区间更新 \(ab_u\)。首先显然用 \(\max(ab_{ls},ab_{rs})\) 更新。

然后,考虑 \(x\in[l,mid]\),但使得 \(B_i\) 最小的 \(i\)\((mid,r]\) 内的情况。为了让两项都最大,我们用 \(\max\limits_{l\le x\le mid}A_x\) 减去 \(\min\limits_{mid<i\le r}B_i\)

于是还要维护区间最小值 \(mnb_u=\min\limits_{l\le i\le r}B_i\)

最后答案: \(ab_u=\max(ab_{ls},ab_{rs},mxa_{ls}-mnb_{rs})\)

如果在右子区间内,同理有:

\[\max\psi(x,y)=\max A_x+\max\left\{A_y-\min_{mid<i<y}\{B_i\}\right\} \]

同样维护区间 \(\max\left\{A_y-\min\limits_{mid<i<y}\{B_i\}\right\}\)\(\max A_x\) 即为 \(mxa_{ls}\)。设 \(ba_u=\max\limits_{l\le y\le r}\left\{A_y-\min\limits_{l<i<y}\{B_i\}\right\}\)

同理,有 \(ba_u=\max(ba_{ls},ba_{rs},mxa_{rs}-mnb_{ls})\)

然后就可以更新 \(ans_u\) 了。

具体见代码。

这个似乎有 \(l,r,x,y\) 的边界问题但我不知道怎么搞。

点击查看代码
#include <iostream>
#include <algorithm>
#define f(x, y, z) for (int x = (y); x <= (z); ++x)
#define lson (u << 1)
#define rson (u << 1 | 1)
using namespace std;
int constexpr N = 5e5 + 10;
int constexpr INF = 0x3f3f3f3f;
int n, m, a[N], b[N];

struct Node {
    int mxa, mnb, ab, ba, dat;
} tr[N << 1];

inline Node pushup(Node A, Node B) {
    Node C;
    C.mxa = max(A.mxa, B.mxa);
    C.mnb = min(A.mnb, B.mnb);
    C.ab = max({A.ab, B.ab, A.mxa - B.mnb});
    C.ba = max({A.ba, B.ba, -A.mnb + B.mxa});
    C.dat = max({A.dat, B.dat, A.ab + B.mxa, A.mxa + B.ba});
    return C;
}

void build(int u, int l, int r) {
    if (l == r) {
        tr[u].mxa = a[l], tr[u].mnb = b[l];
        tr[u].ab = tr[u].ba = tr[u].dat = -INF;
        return;
    }
    int mid = l + r >> 1;
    build(lson, l, mid);
    build(rson, mid + 1, r);
    tr[u] = pushup(tr[lson], tr[rson]);
    return;
}

void modify(int u, int l, int r, int x, int v, int op) {
    if (l == r) {
        if (op == 1) tr[u].mxa = v;
        else if (op == 2) tr[u].mnb = v;
        return;
    }
    int mid = l + r >> 1;
    if (x <= mid) modify(lson, l, mid, x, v, op);
    else modify(rson, mid + 1, r, x, v, op);
    tr[u] = pushup(tr[lson], tr[rson]);
    return;
}

Node query(int u, int l, int r, int L, int R) {
    if (L <= l && r <= R) return tr[u];
    int mid = l + r >> 1;
    if (R <= mid) return query(lson, l, mid, L, R);
    if (L > mid) return query(rson, mid + 1, r, L, R);
    return pushup(query(lson, l, mid, L, R), query(rson, mid + 1, r, L, R));
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0), cout.tie(0);
    
    cin >> n >> m;
    f(i, 1, n) cin >> a[i];
    f(i, 1, n) cin >> b[i];
    int op, x, y;
    build(1, 1, n);
    while (m--) {
        cin >> op >> x >> y;
        if (op == 1) modify(1, 1, n, x, y, 1);
        else if (op == 2) modify(1, 1, n, x, y, 2);
        else if (op == 3) cout << query(1, 1, n, x, y).dat << '\n';
    }
    
    return 0;
}

*CF438D The Child and Sequence(区间取模)

区间取模,单点修改,区间求和。

\(1\le n,m\le10^5\),值域 \(10^9\)

对于多次取模、多次除法、多次开方这样的问题,我们首先需要考虑:一个数 \(x\) 经过若干次操作,最后是否会保持为 \(0\),不管继续怎么操作都不变。

在本题中,我们可以发现,若 \(p\le x\),则有 \(x\bmod p\) 总小于 \(x/2\)。因此,对于每个数 \(x\),只需要取模 \(\log x\) 次就会变为 \(0\)

因此,我们不断地找区间内的最大值 \(mx\)\(\boldsymbol{p\le mx}\),则将 \(mx\)\(p\) 取模并更新区间最大值,然后对新的区间最大值重新执行该操作。单点修改和区间求和正常即可。

由于每个数最多取模 \(\log\) 次,因此时间复杂度为 \((n+m)\log n\log v\),其中 \(v\) 为值域大小。

点击查看代码
#include <iostream>
#define f(x, y, z) for (int x = (y); x <= (z); ++x)
#define lson (u << 1)
#define rson (u << 1 | 1)
#define int long long
using namespace std;
int constexpr N = 1e5 + 10;
int n, m, a[N];

struct Node {
    int mx, sum;
} tr[N << 2];

inline void pushup(int u) {
    tr[u].mx = max(tr[lson].mx, tr[rson].mx);
    tr[u].sum = tr[lson].sum + tr[rson].sum;
    return;
}

void build(int u, int l, int r) {
    if (l == r) return tr[u].mx = tr[u].sum = a[l], void();
    int mid = l + r >> 1;
    build(lson, l, mid);
    build(rson, mid + 1, r);
    pushup(u);
    return;
}

void modify(int u, int l, int r, int x, int v) {
    if (l == r) return tr[u].mx = tr[u].sum = v, void();
    int mid = l + r >> 1;
    if (x <= mid) modify(lson, l, mid, x, v);
    else modify(rson, mid + 1, r, x, v);
    pushup(u);
    return;
}

int query(int u, int l, int r, int L, int R) {
    if (L <= l && r <= R) return tr[u].sum;
    int mid = l + r >> 1, res = 0;
    if (L <= mid) res += query(lson, l, mid, L, R);
    if (R > mid) res += query(rson, mid + 1, r, L, R);
    return res;
}

void modulo(int u, int l, int r, int L, int R, int x) {
    if (tr[u].mx < x) return;
    if (l == r) return tr[u].mx %= x, tr[u].sum %= x, void();
    int mid = l + r >> 1;
    if (L <= mid) modulo(lson, l, mid, L, R, x);
    if (R > mid) modulo(rson, mid + 1, r, L, R, x);
    pushup(u);
    return;
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0), cout.tie(0);
    
    cin >> n >> m;
    f(i, 1, n) cin >> a[i];
    int op, l, r, x;
    build(1, 1, n);
    while (m--) {
        cin >> op;
        if (op == 1) cin >> l >> r, cout << query(1, 1, n, l, r) << '\n';
        else if (op == 2) cin >> l >> r >> x, modulo(1, 1, n, l, r, x);
        else if (op == 3) cin >> l >> x, modify(1, 1, n, l, x);
    }
    
    return 0;
}

*P4344 [SHOI2015] 脑洞治疗仪

一个长度为 \(n\)\(0/1\) 序列,初始均为 \(1\)

  • 操作 0:将 \([l,r]\) 设为 \(0\)
  • 操作 1:把 \([l_0,r_0]\) 内所有的 \(1\) 拿出来(原位置变成 \(0\)),填在 \([l_1,r_1]\) 内所有的 \(0\) 位置上,且尽量靠前填,如果不够则不填,如果有多出来的 \(1\) 则直接扔掉;
  • 查询:\([l,r]\) 内最长的连续 \(0\) 的长度。

\(1\le n,m\le2\times10^5\)

对于操作 0,直接区间打标记即可。

对于操作 1,我们数出区间 \([l_0,r_0]\) 内 1 的个数,然后把区间全部设为 \(0\)(即操作 0),最后用这些 1 按照题目要求填进 \([l_1,r_1]\) 内。

具体地,如果左区间的空位数 \(x\) 大于等于当前要填的 \(1\) 的个数 \(y\),那么全部填到左区间;如果左区间的空位数 \(x\) 小于当前要填的 \(1\) 的个数 \(y\),那么把左区间全部设为 \(1\)(方法同操作 0),并且用 \(y-x\)\(1\) 填入右区间,这样就变成了一个子问题。

对于询问,我们考虑如何合并,讨论最大值是左、右还是横跨左和右即可。如上面的 P4513 小白逛公园。

要维护的有:标记、区间长度、\(1\) 的个数、前缀最长连续 \(0\)、后缀最长连续 \(0\)、区间最长连续 \(0\)

点击查看代码
#include <iostream>
#include <algorithm>
#define f(x, y, z) for (int x = (y); x <= (z); ++x)
#define lson (u << 1)
#define rson (u << 1 | 1)
using namespace std;
int constexpr N = 2e5 + 10;
int n, m;

struct Node {
    int l, r, a;
    int sum; //1 的个数
    int pre, suf, len; //连续 0
    int tag; //1: 全部为 0; 2: 全部为 1
} tr[N << 2];

inline void pushup(int u) {
    tr[u].sum = tr[lson].sum + tr[rson].sum;
    if (tr[lson].sum == 0) tr[u].pre = tr[lson].a + tr[rson].pre;
    else tr[u].pre = tr[lson].pre;
    if (tr[rson].sum == 0) tr[u].suf = tr[rson].a + tr[lson].suf;
    else tr[u].suf = tr[rson].suf;
    tr[u].len = max({tr[lson].len, tr[rson].len, tr[lson].suf + tr[rson].pre});
    return;
}

inline void sett(int u, int fl) {
    if (fl == 1) tr[u].tag = 1, tr[u].sum = 0, tr[u].len = tr[u].pre = tr[u].suf = tr[u].a;
    else if (fl == 2) tr[u].tag = 2, tr[u].sum = tr[u].a, tr[u].len = tr[u].pre = tr[u].suf = 0;
    return;
}

inline void pushdown(int u) {
    if (!tr[u].tag) return;
    sett(lson, tr[u].tag), sett(rson, tr[u].tag);
    tr[u].tag = 0;
    return;
}

void build(int u, int l, int r) {
    tr[u].l = l, tr[u].r = r;
    tr[u].a = r - l + 1;
    tr[u].tag = 0;
    if (l == r) {
        tr[u].sum = 1;
        // tr[u].tag = 2;
        tr[u].pre = tr[u].suf = tr[u].len = 0;
        return;
    }
    int mid = l + r >> 1;
    build(lson, l, mid);
    build(rson, mid + 1, r);
    pushup(u);
    return;
}

void modify(int u, int l, int r) { //全部设为 0
    if (l <= tr[u].l && tr[u].r <= r) {
        sett(u, 1);
        return;
    }
    pushdown(u);
    int mid = tr[u].l + tr[u].r >> 1;
    if (l <= mid) modify(lson, l, r);
    if (r > mid) modify(rson, l, r);
    pushup(u);
    return;
}

int query(int u, int l, int r) { //查询 1 的个数
    if (l <= tr[u].l && tr[u].r <= r)
        return tr[u].sum;
    int mid = tr[u].l + tr[u].r >> 1;
    pushdown(u);
    int res = 0;
    if (l <= mid) res += query(lson, l, r);
    if (r > mid) res += query(rson, l, r);
    return res;
}

int mend(int u, int l, int r, int x) { //[l,r] 区间内 (尽量靠前地) 填 x 个 1; 返回剩余 1 的数量
    if (!x) return 0;
    int t = tr[u].a - tr[u].sum;
    if (l <= tr[u].l && tr[u].r <= r && t <= x) {
        sett(u, 2);
        return x - t;
    }
    pushdown(u);
    int mid = tr[u].l + tr[u].r >> 1;
    int ans;
    if (l > mid) ans = mend(rson, l, r, x);
    else if (r <= mid) ans = mend(lson, l, r, x);
    else ans = mend(rson, l, r, mend(lson, l, r, x));
    pushup(u);
    return ans;
}

int query0(int u, int l, int r) {
    if (l <= tr[u].l && tr[u].r <= r) return tr[u].len;
    pushdown(u);
    int mid = tr[u].l + tr[u].r >> 1;
    if (l > mid) return query0(rson, l, r);
    if (r <= mid) return query0(lson, l, r);
    return max({query0(lson, l, r), query0(rson, l, r), min(tr[lson].suf, mid - l + 1) + min(tr[rson].pre, r - mid)});
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0), cout.tie(0);
    
    cin >> n >> m;
    build(1, 1, n);
    int op, l, r, ll, rr;
    while (m--) {
        cin >> op >> l >> r;
        if (op == 1) {
            cin >> ll >> rr;
            int sum = query(1, l, r);
            modify(1, l, r);
            mend(1, ll, rr, sum);
        } else if (op == 0) {
            modify(1, l, r);
        } else if (op == 2) {
            cout << query0(1, l, r) << '\n';
        }
    }
    
    return 0;
}

维护某个值

实际上就是把题意所需要维护的东西转化,变成序列上的问题,并且可以用线段树维护。

**P6864 [RC-03] 记忆(维护矩阵的积)

有一个括号串 \(S\),一开始 \(S\) 中只包含一对括号(即初始的 \(S\)()),接下来有 \(n\) 个操作,操作分为三种:

  1. 在当前 \(S\) 的末尾加一对括号(即 \(S\) 变为 S());

  2. 在当前 \(S\) 的最外面加一对括号(即 \(S\) 变为 (S));

  3. 取消第 \(x\) 个操作,即去除第 \(x\) 个操作造成过的一切影响(例如,如果第 \(x\) 个操作也是取消操作,且取消了第 \(y\) 个操作,那么当前操作的实质就是恢复了第 \(y\) 个操作的作用效果)。

每次操作后,你需要输出 \(S\) 的能够括号匹配的非空子串(子串要求连续)个数。

一个括号串能够括号匹配,当且仅当其左右括号数量相等,且任意一个前缀中左括号数量不少于右括号数量。

对于全部数据:\(1\leq n\leq 2\times 10^5\)\(op\in \{1,2,3\}\)\(1\leq x\leq n\),一个操作在形式上最多只会被取消一次(即所有 \(x\) 互不相同)。

首先考虑如果没有撤销操作怎么做。设新串为 \(S'\)\(\overline{ST}\) 表示字符(串)\(S\)\(T\) 的拼接。

考虑操作 1,\(S'=\overline{S\texttt{()}}\)。在末尾加了一对括号后,原来的 \(S\)所有合法后缀子串(包括它本身)都对答案贡献了 \(1\)。设 \(S\) 的合法后缀子串为 \(cnt(S)\),则有 \(ans\left(S'\right)=ans(S)+cnt(S)+1\)(别忘了加上新加的一对括号的贡献),同时 \(cnt\left(S'\right)=cnt(S)+1\)

再考虑操作 2,\(S'=\overline{\texttt(S\texttt)}\)。显然是 \(ans\left(S'\right)=ans(S)+1\)\(cnt\left(S'\right)=1\)

初始 \(ans\left(\texttt{()}\right)=cnt\left(\texttt{()}\right)=1\)

然而有撤销操作,这让我们不得不保存下来所有操作,然后用数据结构实现取消某个操作的影响。

观察 \(ans\)\(cnt\) 的转移,发现全部都是 线性 的转移式。这启发我们用 向量与矩阵的乘法 来刻画这些转移。

对于操作 1:

\[\begin{bmatrix}ans(S')&cnt(S')&1\end{bmatrix} =\begin{bmatrix}ans(S)&cnt(S)&1\end{bmatrix}\begin{bmatrix}1&0&0\\1&1&0\\1&1&1\end{bmatrix} \]

对于操作 2:

\[\begin{bmatrix}ans(S')&cnt(S')&1\end{bmatrix} =\begin{bmatrix}ans(S)&cnt(S)&1\end{bmatrix}\begin{bmatrix}1&0&0\\0&0&0\\1&1&1\end{bmatrix} \]

于是答案可以看作初始矩阵 \(\begin{bmatrix}1&1&1\end{bmatrix}\) 经过一系列矩阵乘法作用后的结果。

操作序列(不包含撤销)造成的影响可以看作是一系列转移矩阵的乘积。

考虑如何实现撤销。首先,我们把题目中的撤销操作转化为:将某个操作(非撤销)从进行改为不进行,或从不进行改为进行。

一次操作如果什么也没做(就是不进行操作),可以用单位矩阵 \(I_3\) 表示其造成的影响。于是,要把操作从进行改为不进行,就是把该操作单点修改为单位矩阵。反之亦然。

用线段树维护所有矩阵,支持单点修改,全局求积。因为矩阵有 结合律,所以可以用线段树维护。

时间复杂度 \(O(k^3n\log n)\),其中 \(k=3\)

另外,如果直接递归撤销会 TLE します。所以我们要保存每个撤销操作的信息,即作用于哪一个非撤销操作、作用是使其进行还是不进行。

矩阵别忘记开 long long

点击查看代码
#include <iostream>
#define f(x, y, z) for (int x = (y); x <= (z); ++x)
#define ls (u << 1)
#define rs (u << 1 | 1)
using namespace std;
typedef long long ll;
typedef pair<int, int> pii;
int constexpr N = 2e5 + 10;
int n, op[N], x;
pair<int, int> undo[N];

struct Matrix {
    ll a[4][4];
} E, I, A, B;

Matrix operator*(Matrix const &a, Matrix const &b) {
    Matrix c = E;
    f(i, 1, 3) f(j, 1, 3) {
        c.a[i][j] += a.a[i][1] * b.a[1][j];
        c.a[i][j] += a.a[i][2] * b.a[2][j];
        c.a[i][j] += a.a[i][3] * b.a[3][j];
    }
    return c;
}

struct Node {
    Matrix mat;
} tr[N << 2];

inline void pushup(int u) {
    tr[u].mat = tr[ls].mat * tr[rs].mat;
    return;
}

void build(int u, int l, int r) {
    tr[u].mat = I;
    if (l == r) return;
    int mid = l + r >> 1;
    build(ls, l, mid);
    build(rs, mid + 1, r);
    return;
}

void change(int u, int l, int r, int x, int op) {
    if (l == r) return tr[u].mat = op ? (op == 1 ? A : B) : I, void();
    int mid = l + r >> 1;
    if (x <= mid) change(ls, l, mid, x, op);
    else change(rs, mid + 1, r, x, op);
    return pushup(u);
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0), cout.tie(0);
    
    I.a[1][1] = I.a[2][2] = I.a[3][3] = 1;
    A.a[1][1] = A.a[2][1] = A.a[2][2] = A.a[3][1] = A.a[3][2] = A.a[3][3] = 1;
    B.a[1][1] = B.a[3][1] = B.a[3][2] = B.a[3][3] = 1;
    cin >> n;
    build(1, 1, n);
    f(i, 1, n) {
        cin >> op[i];
        if (op[i] ^ 3) change(1, 1, n, i, op[i]);
        else {
            cin >> x;
            int lst = undo[x].first;
            undo[i].first = lst ? lst : x;
            int pos = undo[i].first;
            undo[i].second = lst ? (undo[x].second ? 0 : op[lst]) : 0; 
            change(1, 1, n, undo[i].first, undo[i].second);
        }
        cout << tr[1].mat.a[1][1] + tr[1].mat.a[2][1] + tr[1].mat.a[3][1] << '\n';
    }
    
    return 0;
}

P8990 [北大集训 2021] 小明的树

小明有一棵以 \(1\) 为根的 \(n\) 个节点的树,树上每一个非根节点上有一盏灯,他有一个 \(2 \thicksim n\) 的排列 \(a_1,a_2,\dots,a_{n-1}\)。他还有一个计数器,初始为 \(0\)

他会按照排列依次点亮这 \(n-1\) 盏灯,每进行一次点灯操作后,他会检查整个树是否是美丽的,如果是美丽的,计数器会加上此时点灯的节点形成的连通块的个数。

\(n-1\) 次点灯后计数器的值,记为这棵树的答案。

一个树是美丽的当前仅当对于每一个被点亮的节点,这个节点子树内的节点都是点亮的。

小明认为这个问题太简单了,他觉得应该让树动起来。

在初始查询后,他会删掉树中一条边并加上一条边,保证修改后还是一棵树,他想知道每一次修改后将计数器清零后重新点灯并计数,这棵树的答案是多少。

保证满足 \(2 \leq n \leq 500000\)\(0\leq m \leq 500000\)

单侧递归类问题

*P4198 楼房重建

2 可持久化线段树

其实可持久化线段树只是一个工具,重要的是学习如何用一个线段树维护某些数据。

可持久化线段树的特殊之处就在于,它可以维护历史版本,以及支持多次查询区间。

因此可以先考虑如何做不用维护历史版本,或者不用查询区间的问题,再考虑能不能转化为可持久化线段树维护题目中的问题。

**P2839 [国家集训队] middle

不知道该放到哪一类。

给你一个长度为 \(n\) 的序列 \(s\),回答 \(Q\) 个这样的询问:

  • \(s\) 的左端点在 \([a,b]\) 之间,右端点在 \([c,d]\) 之间的子区间中,最大的中位数。

其中 \(a<b<c<d\)位置从 \(0\) 开始标号。强制在线。

\(1\leq n \leq 20000\)\(1\leq Q \leq 25000\)\(1\leq a_i\leq 10 ^ 9\)

考虑二分答案,转化为判定性问题:对于一个 \(x\),判定是否存在一个中位数 \(\ge x\)

对于一个 \(x\)新建一个序列 \(t\),满足:若 \(s_i<x\),则 \(t_i=-1\);若 \(s_i\ge x\),则 \(t_i=1\)

于是问题转化为:判断是否存在 \(x\in[a,b],y\in[c,d]\) 使得 \(\sum_{i=x}^yt_i\ge 0\)

于是我们只需要求出所有子区间和的最大值即可。

考虑把一个区间 \([x,y]\) 分成 \([x,b),[b,c],(c,y]\) 三段。

设区间和 \(sum(i,j)=\sum_{k=i}^jt_k\),前缀最大值 \(pre(i,j)=\max\limits_{i\le k\le j}\{sum(i,k)\}\),后缀最大值 \(suf(i,j)=\max\limits_{i\le k\le j}\{sum(k,j)\}\)

那么对于一组 \(a,b,c,d\),答案即为 \(\max(0,suf(a,b-1))+sum(b,c)+\max(0,pre(c+1,d))\)。注意,由于 \(pre\)\(suf\) 可以不选,所以需要和 \(0\)\(\max\)

看到 \(sum,pre,suf\),自然想到用线段树维护。

接下来是重点

注意到给定序列 \(s\)序列 \(t\) 只与 \(x\) 有关,而 \(x\) 的取值必然是 \(s\) 中的某个数。考虑对每一个 \(x\) 处理出一棵线段树。

\(s\) 从小到大排序,那么 \(s_i\) 对应的线段树 \(T_i\) 可以由 \(s_{i-1}\) 对应的线段树 \(T_{i-1}\) 稍作修改得到。具体地,把 \(T_{i-1}\) 中在 \(s_{i-1}\) 所对应的原序列位置的值从 \(1\) 改为 \(-1\)。因此我们可以用可持久化线段树解决。初始时 \(s_1\) 所对应的 \(t\) 的每一项都为 \(1\)

点击查看代码
#include <bits/stdc++.h>
#define f(x, y, z) for (int x = (y); x <= (z); ++x)
using namespace std;
int constexpr N = 2e4 + 10;
int n, a[N], Q, id[N], rt[N], q[4], tmp[N];

struct PerSegTree {
    int tot;
    struct Node {
        int ls, rs, pr, su, sum;
        Node() { ls = rs = pr = su = sum = 0; }
    } tr[N << 5];
    inline Node pushup(Node ls, Node rs) {
        Node res = Node();
        res.pr = max(ls.pr, ls.sum + rs.pr);
        res.su = max(rs.su, ls.su + rs.sum);
        res.sum = ls.sum + rs.sum;
        return res;
    }
    void build(int &u, int l, int r) {
        u = ++tot;
        if (l == r) return void(tr[u].pr = tr[u].su = tr[u].sum = 1);
        int mid = l + r >> 1;
        build(tr[u].ls, l, mid);
        build(tr[u].rs, mid + 1, r);
        Node t = pushup(tr[tr[u].ls], tr[tr[u].rs]);
        tr[u].pr = t.pr, tr[u].su = t.su, tr[u].sum = t.sum;
        return;
    }
    void modify(int &u, int pre, int l, int r, int x) {
        tr[u = ++tot] = tr[pre];
        if (l == r) return void(tr[u].pr = tr[u].su = tr[u].sum = -1);
        int mid = l + r >> 1;
        if (x <= mid) modify(tr[u].ls, tr[pre].ls, l, mid, x);
        else modify(tr[u].rs, tr[pre].rs, mid + 1, r, x);
        Node t = pushup(tr[tr[u].ls], tr[tr[u].rs]);
        tr[u].pr = t.pr, tr[u].su = t.su, tr[u].sum = t.sum;
        return;
    }
    Node query(int u, int l, int r, int L, int R) {
        if (L <= l && r <= R) return tr[u];
        int mid = l + r >> 1;
        if (R <= mid) return query(tr[u].ls, l, mid, L, R);
        if (L > mid) return query(tr[u].rs, mid + 1, r, L, R);
        return pushup(query(tr[u].ls, l, mid, L, R), query(tr[u].rs, mid + 1, r, L, R));
    }
    inline int query(int x) {
        return max(0, query(rt[x], 1, n, q[0], q[1] - 1).su) + query(rt[x], 1, n, q[1], q[2]).sum + max(0, query(rt[x], 1, n, q[2] + 1, q[3]).pr);
    }
} T;

signed main() {
    cin >> n;
    f(i, 1, n) cin >> a[i];
    iota(id + 1, id + n + 1, 1);
    sort(id + 1, id + n + 1, [&](const int &p, const int &q) { return a[p] < a[q]; });
    T.build(rt[id[1]], 1, n);
    f(i, 2, n) T.modify(rt[id[i]], rt[id[i - 1]], 1, n, id[i - 1]);
    int l, r, mid, x = 0;
    cin >> Q;
    while (Q--) {
        f(i, 0, 3) cin >> q[i], (q[i] += x) %= n;
        sort(q, q + 4);
        f(i, 0, 4) ++q[i];
        l = 1, r = n + 1;
        while (l + 1 < r) {
            int mid = l + r >> 1;
            if (T.query(id[mid]) >= 0) l = mid;
            else r = mid;
        }
        cout << (x = a[id[l]]) << '\n';
    }
    return 0;
}

维护历史版本

P3919 【模板】可持久化线段树 1(可持久化数组)

维护这样的一个长度为 \(N\) 的数组,支持如下两种操作:

  1. 在某个历史版本上修改某一个位置上的值;
  2. 访问某个历史版本上的某一位置的值。

此外,每进行一次操作(对于操作 2,即为生成一个完全一样的版本,不作任何改动),就会生成一个新的版本。版本编号即为当前操作的编号(从 1 开始编号,版本 0 表示初始状态数组)。

\(1\le N,M\le10^6\)\(-10^9\le a_i,v_i\le10^9\)

对于每一个版本,我们实际上只需要在之前版本的基础上改动 \(O(\log N)\) 次。因此动态开出新版本有改动的点,无改动的点直接连边。

每一次询问,记录每个版本的根节点,以便于之后访问。

时间复杂度 \(O((N+M)\log N)\),空间复杂度 \(O(N\log N)\)(所以线段树要开 \(23\) 倍)。

点击查看代码
#include <bits/stdc++.h>
#define f(x, y, z) for (int x = (y); x <= (z); ++x)
using namespace std;
int const N = 1e6 + 10;
int n, m, rt[N], v, op, x, y, a[N];
struct PersSegTree {
    int tot;
    struct Node {
        int ls, rs, v;
    } tr[N * 23];
    void build(int &u, int l, int r) {
        u = ++tot;
        if (l == r) return void(tr[u].v = a[l]);
        int mid = l + r >> 1;
        build(tr[u].ls, l, mid);
        build(tr[u].rs, mid + 1, r);
        return;
    }
    void Modify(int &u, int pre, int l, int r, int x, int v) { //pre: previous version same layer
        tr[u = ++tot] = tr[pre];
        if (l == r) return void(tr[u].v = v);
        int mid = l + r >> 1;
        if (x <= mid) Modify(tr[u].ls, tr[pre].ls, l, mid, x, v);
        else Modify(tr[u].rs, tr[pre].rs, mid + 1, r, x, v);
        return;
    }
    int Query(int u, int l, int r, int x) {
        if (l == r) return tr[u].v;
        int mid = l + r >> 1;
        if (x <= mid) return Query(tr[u].ls, l, mid, x);
        return Query(tr[u].rs, mid + 1, r, x);
    }
} T;
signed main() {
    cin >> n >> m;
    f(i, 1, n) cin >> a[i];
    T.build(rt[0], 1, n);
    f(i, 1, m) {
        cin >> v >> op >> x;
        if (op == 1) cin >> y, T.Modify(rt[i], rt[v], 1, n, x, y);
        else cout << T.Query(rt[v], 1, n, x) << '\n', rt[i] = rt[v];
    }
    return 0;
}

*SP11470 TTM - To the moon(可持久化区间加,标记永久化)

一个长度为 \(N\) 的数组 \(\{A\}\)\(4\) 种操作 :

  • C l r d:区间 \([l,r]\) 中的数都加 \(d\) ,同时当前的时间戳加 \(1\)

  • Q l r:查询当前时间戳区间 \([l,r]\) 中所有数的和 。

  • H l r t:查询时间戳 \(t\) 区间 \([l,r]\) 的和 。

  • B t:将当前时间戳置为 \(t\)

在刚开始没有进行任何操作时,时间戳为 \(0\)

数据保证:\(1\le N,M\le 10^5\)\(|A_i|\le 10^9\)\(1\le l \le r \le N\)\(|d|\le10^4\)。保证 B 操作不会访问到未来的时间戳。

标记永久化是指,在打标记之后,如果下一次修改或询问到这里,不下传 / 上传标记,而是在询问的答案中直接加上该标记的贡献(即标记的值乘以区间长度)。也就是说,打完标记之后,询问时在线段树上经过的路径上的标记需要被计算。

因此我们不需要 pushdownpushup(除了 build 计算初始答案的时候要 pushup)。

注意,在打标记时,经过的每一层的 sum 也要加上标记的贡献,因为在后续的询问中,如果访问到这一层,而标记在下面某一层没有 pushdown 以及 pushup,就会导致这个标记没有被计算。同时,计算答案时在最后一层只计入 sum 而不管标记,否则就算重了。

具体请看代码实现。

注意时间戳的维护,尤其是 B 操作。注意数组大小。注意开 long long

点击查看代码
#include <bits/stdc++.h>
#define f(x, y, z) for (int x = (y); x <= (z); ++x)
using namespace std;
typedef long long ll;
int constexpr N = 1e5 + 10;
int n, m, rt[N << 1], a[N], ver[N], tim, ttt;

struct PerSegTree {
    int tot;
    struct Node {
        int ls, rs;
        ll add, sum;
    } tr[N << 5];
    void build(int &u, int l, int r) {
        u = ++tot;
        if (l == r) return void(tr[u].sum = a[l]);
        int mid = l + r >> 1;
        build(tr[u].ls, l, mid);
        build(tr[u].rs, mid + 1, r);
        tr[u].sum = tr[tr[u].ls].sum + tr[tr[u].rs].sum;
        return; //只需要最开始 pushup 一次就行了, 之后的版本直接继承
    }
    void Add(int &u, int pre, int l, int r, int L, int R, int x) {
        tr[u = ++tot] = tr[pre];
        tr[u].sum += 1ll * (min(r, R) - max(l, L) + 1) * x; //
        if (L <= l && r <= R) return void(tr[u].add += x);
        int mid = l + r >> 1;
        if (L <= mid) Add(tr[u].ls, tr[pre].ls, l, mid, L, R, x);
        if (R > mid) Add(tr[u].rs, tr[pre].rs, mid + 1, r, L, R, x);
        return;
    }
    ll Sum(int u, int l, int r, int L, int R) {
        if (L <= l && r <= R) return tr[u].sum;
        int mid = l + r >> 1;
        ll res = 0ll;
        if (L <= mid) res += Sum(tr[u].ls, l, mid, L, R);
        if (R > mid) res += Sum(tr[u].rs, mid + 1, r, L, R);
        return res + tr[u].add * (min(r, R) - max(l, L) + 1);
    }
} T;

signed main() {
    cin >> n >> m;
    f(i, 1, n) cin >> a[i];
    T.build(rt[0], 1, n);
    while (m--) {
        char op; int l, r, x; cin >> op;
        switch (op) {
            case 'C': cin >> l >> r >> x; ver[++tim] = ++ttt; T.Add(rt[ttt], rt[ttt - 1], 1, n, l, r, x); break;
            case 'Q': cin >> l >> r; cout << T.Sum(rt[ttt], 1, n, l, r) << '\n'; break;
            case 'H': cin >> l >> r >> x; cout << T.Sum(rt[ver[x]], 1, n, l, r) << '\n'; break;
            case 'B': cin >> tim; rt[++ttt] = rt[ver[tim]]; break; //差点没写错了
            default: cout << "????wtf\n"; break;
        }
    }
    return 0;
}

查询区间

可持久化权值线段树可以用于解决「答案只与每一个数值的出现次数有关」的问题。

具体来说,开始时权值线段树 \(T_0\) 为空,每加进来序列中的一个数 \(A_i\),就从 \(T_{i-1}\)\(T_i\) 转移。

也就是说,\(T_i\) 是在 \(T_{i-1}\) 的基础上改动而来。这样就可以用动态开点像上面一样解决。

求区间 \(A[l..r]\) 的答案时,用差分的思想,同步遍历 \(T_r\)\(T_{l-1}\),对应位置作差(因为答案只跟数值的出现次数有关)。

由于每次询问都要用到 \(T_r\)\(T_{l-1}\) 这两个版本,所以要可持久化。

P3834 【模板】可持久化线段树 2(静态区间第 \(k\) 小)

给定 \(n\) 个整数构成的序列 \(a\),将对于指定的 \(m\) 个闭区间 \([l,r]\) 查询其区间内的第 \(k\) 小值。

\(1\le n,m\le2\times10^5\)\(|a_i|\le10^9\)

首先,静态全局第 \(k\) 小我们是会求的:开一个权值线段树(即用线段树维护值域上每一个数的出现次数),在线段树上递归地二分即可。

现在对多个区间进行询问,我们处理的步骤如下:

  • 建出 \(T_1,T_2,\dots,T_n\)\(T_i\) 表示 \(a[1,i]\) 对应的权值线段树;
  • 采用差分的思想,把 \(T_r-T_{l-1}\) 作为一个新的权值线段树,然后在其上二分。

当然,我们并不需要真的把整棵树都作差。由于每棵权值线段树都是同构的,且询问的区间相同(\([V_{\min},V_{\max}]\)),我们直接用对应位置的值的差作为新权值线段树上的值即可。

由于值域太大,需要对值域进行离散化。

时间复杂度 \(O((n+m)\log n)\),空间 \(O(n\log n)\)

空间开成 N * 18 貌似卡得太死了,考试时记得开大点,然后测测极限数据。

点击查看代码
#include <bits/stdc++.h>
#define f(x, y, z) for (int x = (y); x <= (z); ++x)
using namespace std;
int const N = 2e5 + 10;
int n, m, a[N], b[N], cnt, rt[N];

struct PersSegTreeOnRange {
    int tot;
    struct Node {
        int ls, rs, sum;
    } tr[N * 18];
    void build(int &u, int l, int r) {
        u = ++tot;
        if (l == r) return;
        int mid = l + r >> 1;
        build(tr[u].ls, l, mid);
        build(tr[u].rs, mid + 1, r);
        return;
    }
    void Modify(int &u, int pre, int l, int r, int x) {
        tr[u = ++tot] = tr[pre];
        ++tr[u].sum;
        if (l == r) return;
        int mid = l + r >> 1;
        if (x <= mid) Modify(tr[u].ls, tr[pre].ls, l, mid, x);
        else Modify(tr[u].rs, tr[pre].rs, mid + 1, r, x);
        return;
    }
    int Query(int u, int v, int l, int r, int x) {
        if (l == r) return b[l];
        int y = tr[tr[v].ls].sum - tr[tr[u].ls].sum;
        int mid = l + r >> 1;
        if (y >= x) return Query(tr[u].ls, tr[v].ls, l, mid, x);
        else return Query(tr[u].rs, tr[v].rs, mid + 1, r, x - y);
    }
} T;

signed main() {
    cin >> n >> m;
    f(i, 1, n) cin >> a[i], b[i] = a[i];
    sort(b + 1, b + n + 1);
    cnt = unique(b + 1, b + n + 1) - b - 1;
    T.build(rt[0], 1, cnt);
    f(i, 1, n) {
        int x = lower_bound(b + 1, b + cnt + 1, a[i]) - b;
        T.Modify(rt[i], rt[i - 1], 1, cnt, x);
    }
    while (m--) {
        int l, r, k; cin >> l >> r >> k;
        cout << T.Query(rt[l - 1], rt[r], 1, cnt, k) << '\n';
    }
    return 0;
}

P2633 Count on a tree(树上路径第 \(k\) 小)

给定一棵 \(n\) 个节点的树,每个点有一个权值。有 \(m\) 个询问,每次给你 \(u,v,k\),你需要回答 \(u\operatorname{xor}\mathrm{lastans}\)\(v\) 这两个节点间第 \(k\) 小的点权。

\(1\le n,m\le 10^5\),点权在 \([1,2^{31}-1]\) 范围内。

刚才是在序列上进行权值线段树的差分,现在改成树上差分,原理是一样的。

\(T_u+T_v-T_{\operatorname{lca}(u,v)}-T_{\operatorname{father}(\operatorname{lca}(u,v))}\) 上二分即可。

时间复杂度 \(O((n+m)\log n)\),空间复杂度 \(O(n\log n)\)

点击查看代码
#include <bits/stdc++.h>
#define f(x, y, z) for (int x = (y); x <= (z); ++x)
#define g(x, y, z) for (int x = (y); x >= (z); --x)
using namespace std;
int const N = 1e5 + 10;
int n, m, A[N], B[N], cnt, lg, rt[N];
vector<int> edge[N];

struct PersSegTreeOnRange {
    int tot;
    struct Node {
        int ls, rs, sum;
    } tr[N * 20];
    void build(int &u, int l, int r) {
        u = ++tot;
        if (l == r) return;
        int mid = l + r >> 1;
        build(tr[u].ls, l, mid);
        build(tr[u].rs, mid + 1, r);
        return;
    }
    void Modify(int &u, int pre, int l, int r, int x) {
        tr[u = ++tot] = tr[pre];
        ++tr[u].sum;
        if (l == r) return;
        int mid = l + r >> 1;
        if (x <= mid) Modify(tr[u].ls, tr[pre].ls, l, mid, x);
        else Modify(tr[u].rs, tr[pre].rs, mid + 1, r, x);
        return;
    }
    int Query(int u, int v, int a, int b, int l, int r, int x) {
        if (l == r) return B[l];
        int mid = l + r >> 1;
        int val = tr[tr[u].ls].sum + tr[tr[v].ls].sum - tr[tr[a].ls].sum - tr[tr[b].ls].sum;
        if (x <= val) return Query(tr[u].ls, tr[v].ls, tr[a].ls, tr[b].ls, l, mid, x);
        else return Query(tr[u].rs, tr[v].rs, tr[a].rs, tr[b].rs, mid + 1, r, x - val);
    }
} T;

int f[17][N], dep[N];
void dfs(int u, int fa) {
    dep[u] = dep[fa] + 1;
    f[0][u] = fa;
    f(i, 1, lg) f[i][u] = f[i - 1][f[i - 1][u]];
    T.Modify(rt[u], rt[fa], 1, cnt, lower_bound(B + 1, B + cnt + 1, A[u]) - B);
    for (int v: edge[u]) if (v ^ fa) dfs(v, u);
    return;
}
int lca(int x, int y) {
    if (dep[x] < dep[y]) swap(x, y);
    g(i, lg, 0) if (dep[f[i][x]] >= dep[y]) x = f[i][x];
    if (x == y) return x;
    g(i, lg, 0) if (f[i][x] ^ f[i][y]) x = f[i][x], y = f[i][y];
    return f[0][x];
}

signed main() {
    cin >> n >> m; lg = __builtin_log2(n);
    f(i, 1, n) cin >> A[i], B[i] = A[i];
    sort(B + 1, B + n + 1);
    cnt = unique(B + 1, B + n + 1) - B - 1;
    f(i, 2, n) {
        int u, v; cin >> u >> v;
        edge[u].push_back(v), edge[v].push_back(u);
    }
    T.build(rt[0], 1, cnt);
    dfs(1, 0);
    int lst = 0;
    while (m--) {
        int u, v, k; cin >> u >> v >> k; u ^= lst;
        int a = lca(u, v);
        cout << (lst = T.Query(rt[u], rt[v], rt[a], rt[f[0][a]], 1, cnt, k)) << '\n';
    }
    return 0;
}

*P3567 [POI2014] KUR-Couriers(区间众数)

给一个长度为 \(n\) 的正整数序列 \(a\)。共有 \(m\) 组询问,每次询问一个区间 \([l,r]\) ,是否存在一个数在 \([l,r]\) 中出现的次数严格大于一半。如果存在,输出这个数,否则输出 \(0\)

\(1\le n,m\le5\times10^5\)\(1\le a_i\le n\)

题意就是求区间的众数,但是这个众数出现次数必须大于区间长度的一半。

有一个关键的性质:如果一个数在区间 \([l,r]\) 中出现次数超过 \(\dfrac{r-l+1}2\),那么将这个区间排序后,这个数就是中位数。

考虑权值数组,如果一个数出现超过 \(x\) 次,那么对于所有值域区间 \([l,r]\) 包含 \(x\),这个区间内的所有数的出现次数之和必然大于 \(x\)

具体在权值线段树上查找的方式是,设 \(sum_u\)\(u\) 所代表的值域区间内所有数的出现次数之和,那么如果 \(sum_{ls}>x\),则 \(ls\) 有可能包含出现超过 \(x\) 次的数,\(rs\) 同理。

能不能查找到这个数,只与权值数组有关,因此可以用可持久化权值线段树差分。于是我们利用这个原理,在差分后的权值线段树中查找出现次数超过 \(\dfrac{r-l+1}2\) 的数即可(显然不多于一个)。

时间 \(O((n+m)\log n)\),空间 \(O(n\log n)\)

点击查看代码
#include <bits/stdc++.h>
#define f(x, y, z) for (int x = (y); x <= (z); ++x)
using namespace std;
int const N = 5e5 + 10;
int n, m, a[N], rt[N];

struct PersSegTreeOnRange {
    int tot;
    struct Node {
        int ls, rs, sum;
    } tr[N * 22];
    void build(int &u, int l, int r) {
        u = ++tot;
        if (l == r) return;
        int mid = l + r >> 1;
        build(tr[u].ls, l, mid);
        build(tr[u].rs, mid + 1, r);
        return;
    }
    void Modify(int &u, int pre, int l, int r, int x) {
        tr[u = ++tot] = tr[pre];
        ++tr[u].sum;
        if (l == r) return;
        int mid = l + r >> 1;
        if (x <= mid) Modify(tr[u].ls, tr[pre].ls, l, mid, x);
        else Modify(tr[u].rs, tr[pre].rs, mid + 1, r, x);
        return;
    }
    int Query(int u, int v, int l, int r, int x) {
        if (l == r) return l;
        int mid = l + r >> 1;
        int L = tr[tr[v].ls].sum - tr[tr[u].ls].sum;
        int R = tr[tr[v].rs].sum - tr[tr[u].rs].sum;
        if (x < L) return Query(tr[u].ls, tr[v].ls, l, mid, x);
        else if (x < R) return Query(tr[u].rs, tr[v].rs, mid + 1, r, x);
        return 0;
    }
} T;

signed main() {
    cin >> n >> m;
    f(i, 1, n) cin >> a[i];
    T.build(rt[0], 1, n);
    f(i, 1, n) T.Modify(rt[i], rt[i - 1], 1, n, a[i]);
    while (m--) {
        int l, r; cin >> l >> r;
        cout << T.Query(rt[l - 1], rt[r], 1, n, r - l + 1 >> 1) << '\n';
    }
    return 0;
}

**P3293 [SCOI2016] 美味

一家餐厅有 \(n\) 道菜,编号 \(1, 2, \ldots, n\),大家对第 \(i\) 道菜的评价值为 \(a_i\)。有 \(m\) 位顾客,第 \(i\) 位顾客的期望值为 \(b_i\),而他的偏好值为 \(x_i\)。因此,第 \(i\) 位顾客认为第 \(j\) 道菜的美味度为 \(b_i\oplus (a_j + x_i)\)\(\oplus\) 表示按位异或。

\(i\) 位顾客希望从这些菜中挑出他认为最美味的菜,即美味值最大的菜,但由于价格等因素,他只能从第 \(l_i\) 道到第 \(r_i\) 道中选择。请你帮助他们找出最美味的菜。

\(1 \le n \le 2 \times 10^5\)\(0 \le a_i,b_i,x_i < 10^5\)\(1 \le m \le 10^5\)

对于给定的 \(x\) 和某个给定集合中的任意 \(y\),求 \(x\oplus y\) 的最大值,这个问题可以用 01-Trie 解决。然而我不会可持久化 Trie,怎么办?

01-Trie 实质上就是存了所有的 \(y\) 是否存在,然后从高到低按位贪心,能异或成 \(1\) 就异或。

事实上,对于 01-Trie 的一个节点 \(u\),我们设它代表的位数是 \(i\),它的左 / 右儿子代表 \(i-1\) 位是 \(0\) / \(1\),同时设 \(u\) 子树代表的值域为 \([l,l+2^i)\),那么左儿子代表 \([l,l+2^{i-1})\),右儿子代表 \([l+2^{i-1},l+2^i)\)

其实如果从高到低枚举每一位,这就相当于询问当前答案 \(ans\) 在 01-Trie 上的左 / 右儿子所代表的值域区间内是否有数出现过,左 / 右取决于 \(b_i\) 的这一位是 \(0\) 还是 \(1\)。因此我们可以用权值线段树来维护数值的出现次数,查询则直接区间查询。

询问是对于区间的,因此用可持久化权值线段树加差分解决。

点击查看代码
#include <bits/stdc++.h>
#define f(x, y, z) for (int x = (y); x <= (z); ++x)
#define g(x, y, z) for (int x = (y); x >= (z); --x)
using namespace std;
int constexpr N = 2e5 + 10;
int n, m, rt[N], mx = 1e5;

struct PersSegTreeOnRange {
    int tot;
    struct Node {
        int ls, rs, sum;
    } tr[N * 18];
    void build(int &u, int l, int r) {
        u = ++tot;
        if (l == r) return;
        int mid = l + r >> 1;
        build(tr[u].ls, l, mid);
        build(tr[u].rs, mid + 1, r);
        return;
    }
    void Modify(int &u, int pre, int l, int r, int x, int v) {
        tr[u = ++tot] = tr[pre];
        tr[u].sum += v;
        if (l == r) return;
        int mid = l + r >> 1;
        if (x <= mid) Modify(tr[u].ls, tr[pre].ls, l, mid, x, v);
        else Modify(tr[u].rs, tr[pre].rs, mid + 1, r, x, v);
        return;
    }
    int Query(int u, int v, int l, int r, int L, int R) {
        if (L <= l && r <= R) return tr[u].sum - tr[v].sum;
        int mid = l + r >> 1;
        if (R <= mid) return Query(tr[u].ls, tr[v].ls, l, mid, L, R);
        if (L > mid) return Query(tr[u].rs, tr[v].rs, mid + 1, r, L, R);
        return Query(tr[u].ls, tr[v].ls, l, mid, L, R) + Query(tr[u].rs, tr[v].rs, mid + 1, r, L, R);
    }
} T;

signed main() {
    cin >> n >> m;
    f(i, 1, n) {
        int x; cin >> x;
        T.Modify(rt[i], rt[i - 1], 0, mx, x, 1);}
    f(i, 1, m) {
        int b, x, L, R, ans = 0;
        cin >> b >> x >> L >> R;
        g(j, 17, 0) {
            int c = b >> j & 1, t = c ^ 1;
            int l = ans + (t << j) - x, r = ans + (t << j) + (1 << j) - 1 - x;
            if (r < 0 || l > mx) ans |= c << j;
            else if (T.Query(rt[R], rt[L - 1], 0, mx, max(l, 0), min(r, mx)))
                ans |= t << j;
            else ans |= c << j;
        }
        cout << (ans ^ b) << '\n';
    }
    return 0;
}

**P4587 [FJOI2016] 神秘数

一个可重复数字集合 \(S\) 的神秘数定义为最小的不能被 \(S\) 的子集的和表示的正整数。

例如 \(S=\{1,1,1,4,13\}\),有:\(1 = 1\)\(2 = 1+1\)\(3 = 1+1+1\)\(4 = 4\)\(5 = 4+1\)\(6 = 4+1+1\)\(7 = 4+1+1+1\)

\(8\) 无法表示为集合 \(S\) 的子集的和,故集合 \(S\) 的神秘数为 \(8\)

现给定长度为 \(n\)正整数序列 \(a\)\(m\) 次询问,每次询问包含两个参数 \(l,r\),你需要求出由 \(a_l,a_{l+1},\cdots,a_r\) 所组成的可重集合的神秘数。

\(1\le n,m\le10^5\)\(\sum a\le10^9\)

观察题面中给的例子。把 \(S\) 中的数按升序排列,假设现在用前三个数,能表示出的数的范围为 \([1,3]\)。下一个数是 \(4\),能更新这个范围,于是能表示出的范围变为 \([1,3+4]=[1,7]\)。下一个数是 \(13\),显然用它一定不能表示 \([8,12]\) 内的数。于是神秘数即为 \(8\)

让我们把这个过程抽象出来:把所考虑的集合中的数升序排列,设当前为 \(a_i\),用 \(a_1,a_2,\dots,a_i\) 能表示出的范围为 \([1,x]\)。于是可以求出神秘数:

  • 如果 \(a_{i+1}\le x+1\),那么能表示出的范围可以更新为 \([1,x+a_{i+1}]\),然后令 \(i\gets i+1\) 继续执行此过程;
  • 如果 \(a_{i+1}>x+1\),那么神秘数即为 \(x\)

设集合大小为 \(k\),那么这样直接扫一遍的时间复杂度是 \(O(k)\) 的。然而我们可以考虑一次加入尽量多的数以降低时间复杂度。

设当前加入了值域为 \([1,k]\) 的数,能表示出的范围为 \([1,x]\)。那么,现在可以用于扩展的数的范围是 \([k+1,x+1]\)。设这些数的总和为 \(s\),那么扩展之后加入的数的范围变为 \([1,x+1]\),能表示出的范围变为 \([1,s]\),即:\(k\gets x+1\)\(x\gets s\)。于是 \(s\) 可以用权值线段树维护。

这样做的时间复杂度是 \(O(\log\sum a_i)\) 的。(我也不知道为什么,题解区说的)(也有说 \(\log\max a_i\) 的)

由于题目中所询问的是多个区间,而做法只与每个数值的出现次数有关(即查找 \(s\) 的过程),因此可以直接用可持久化权值线段树维护。

不需要离散化因为太麻烦,直接动态开点即可。

点击查看代码
#include <bits/stdc++.h>
#define f(x, y, z) for (int x = (y); x <= (z); ++x)
using namespace std;
int constexpr N = 1e5 + 10, mx = 1e9;
int n, m, rt[N];

struct PersSegTreeOnRange {
    int tot;
    struct Node {
        int ls, rs, sum;
    } tr[N << 5];
    void Modify(int &u, int pre, int l, int r, int x, int v) {
        tr[u = ++tot] = tr[pre];
        tr[u].sum += v;
        if (l == r) return;
        int mid = l + r >> 1;
        if (x <= mid) Modify(tr[u].ls, tr[pre].ls, l, mid, x, v);
        else Modify(tr[u].rs, tr[pre].rs, mid + 1, r, x, v);
        return;
    }
    int Query(int u, int v, int l, int r, int L, int R) {
        if (L <= l && r <= R) return tr[u].sum - tr[v].sum;
        int mid = l + r >> 1;
        if (R <= mid) return Query(tr[u].ls, tr[v].ls, l, mid, L, R);
        if (L > mid) return Query(tr[u].rs, tr[v].rs, mid + 1, r, L, R);
        return Query(tr[u].ls, tr[v].ls, l, mid, L, R) + Query(tr[u].rs, tr[v].rs, mid + 1, r, L, R);
    }
} T;

signed main() {
    cin >> n;
    f(i, 1, n) {
        int x; cin >> x;
        T.Modify(rt[i], rt[i - 1], 1, mx, x, x);
    }
    cin >> m;
    while (m--) {
        int l, r; cin >> l >> r;
        int a = 0, b = 0;
        while (true) {
            int tmp = T.Query(rt[r], rt[l - 1], 1, mx, a + 1, b + 1);
            if (!tmp) break;
            a = b + 1, b += tmp;
        }
        cout << b + 1 << '\n';
    }
    return 0;
}

*BZOJ 4504 K个串

Virtual Judge | 码创未来

有一个长度为 \(n\) 的数字序列 \(a\),求第 \(k\) 大的子串和。计算子串和时,相同数字只算一次

\(1\le n\le100000\)\(1\le k\le200000\)\(0\le|a_i|\le10^9\)。数据保证存在第 \(k\) 大的和。

\(sum(l,r)\) 表示子串和,即 \(\sum_{i=l}^ra_i\)

我们考虑沿用 超级钢琴 的思路。设 \(f(r,s,t)\) 表示右端点为 \(r\),左端点可以取的区间为 \([s,t]\),最大的子串和,即

\[f(r,s,t):=\max\{sum(l,r)\mid l\in[s,t]\}. \]

我们从 \(1\)\(n\) 枚举 \(r\),把 \(f(r,1,r)\) 加入大根堆。

设堆顶为 \(f(r_0,s_0,t_0)\),取堆顶同时向堆中加入 \(f(r_0,s_0,x-1)\)\(f(r_0,x+1,t_0)\),其中 \(x\) 表示使 \(sum(l,r_0)\) 最大的 \(l\in[s_0,t_0]\)

重复取堆顶 \(k\) 次,第 \(k\) 次即为所求。

现在的问题是,如何处理相同数字只算一次的问题?

考虑当前 \(a_i=x\),它上一次出现在序列 \(a\) 中的位置是 \(lst(x)\)。那么,它有贡献的子串的左端点的范围为 \([lst(x)+1,i]\),而左端点在更前面的子串与原来相同。

我们用可持久化线段树来保存所有子串和,第 \(i\) 棵树 \(T_i\) 维护了右端点为 \(i\) 的所有子串。

那么,在 \(T_{i-1}\) 的基础上,我们可以处理出 \(T_i\):在区间 \([lst(x)+1,i]\) 上加 \(a_i=x\)。标记永久化即可实现区间加法。

于是可以求出来 \(f\),进而解决这道题。

点击查看代码
#include <bits/stdc++.h>
#define f(x, y, z) for (int x = (y); x <= (z); ++x)
#define fi first
#define se second
using namespace std;
typedef long long ll;
typedef pair<ll, int> pli;
int constexpr N = 1e5 + 10;
ll constexpr INF = 0x3f3f3f3f3f3f3f3f;
int n, k, a[N], rt[N];

struct PersSegTree {
    int tot;
    struct Node {
        int ls, rs;
        pli mx;
        ll add;
    } tr[N * 40];
    inline void pushup(Node &A, Node L, Node R) { A.mx = max(L.mx, R.mx); }
    void build(int &u, int l, int r) {
        u = ++tot;
        tr[u].mx.se = l;
        if (l == r) return;
        int mid = l + r >> 1;
        build(tr[u].ls, l, mid);
        build(tr[u].rs, mid + 1, r);
        return;
    }
    void Add(int &u, int pre, int l, int r, int L, int R, int v) {
        u = ++tot;
        tr[u] = tr[pre];
        if (L <= l && r <= R) {
            tr[u].mx.fi += v;
            tr[u].add += v;
            return;
        }
        int mid = l + r >> 1;
        if (L <= mid) Add(tr[u].ls, tr[pre].ls, l, mid, L, R, v);
        if (R > mid) Add(tr[u].rs, tr[pre].rs, mid + 1, r, L, R, v);
        pushup(tr[u], tr[tr[u].ls], tr[tr[u].rs]);
        tr[u].mx.fi += tr[u].add;
        return;
    }
    pli query(int u, int l, int r, int L, int R) {
        if (L <= l && r <= R) return tr[u].mx;
        int mid = l + r >> 1;
        pli res = {-INF, l};
        if (L <= mid) res = max(res, query(tr[u].ls, l, mid, L, R));
        if (R > mid) res = max(res, query(tr[u].rs, mid + 1, r, L, R));
        res.fi += tr[u].add;
        return res;
    }
} T;

struct Seq {
    int r, s, t;
    pli v;
    Seq(int _r, int _s, int _t): r(_r), s(_s), t(_t) { v = T.query(rt[r], 1, n, s, t); }
    inline bool operator<(Seq const &o) const { return v < o.v; }
};
priority_queue<Seq> pq;
unordered_map<int, int> mp;

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0), cout.tie(0);
    
    cin >> n >> k;
    T.build(rt[0], 1, n);
    f(i, 1, n) {
        int x; cin >> x;
        if (mp.find(x) == mp.end()) T.Add(rt[i], rt[i - 1], 1, n, 1, i, x);
        else T.Add(rt[i], rt[i - 1], 1, n, mp[x] + 1, i, x);
        mp[x] = i;
        pq.push(Seq(i, 1, i));
    }
    while (k--) {
        Seq t = pq.top(); pq.pop();
        if (!k) {
            cout << t.v.fi << '\n';
            break;
        }
        if (t.v.se > t.s) pq.push(Seq(t.r, t.s, t.v.se - 1));
        if (t.v.se < t.t) pq.push(Seq(t.r, t.v.se + 1, t.t));
    }
    
    return 0;
}

3 树状数组套线段树

P2617 Dynamic Rankings(动态区间第 \(k\) 小)

给定一个含有 \(n\) 个数的序列 \(a_1,a_2 \dots a_n\),需要支持两种操作:

  • Q l r k 表示查询下标在区间 \([l,r]\) 中的第 \(k\) 小的数;
  • C x y 表示将 \(a_x\) 改为 \(y\)

\(1\le n,m \le 10^5\)\(0 \le a_i,y \le 10^9\)

首先,静态全局第 \(k\) 小我们是会做的,直接在权值线段树上二分。

而对于静态区间第 \(k\) 小,我们是在差分权值线段树上二分。由于多次询问,需要保存多个前缀对应的权值线段树,然后进行差分,而前缀 \(i\) 对应权值线段树 \(T_i\) 可以在前面 \(T_{i-1}\) 的基础上稍作修改而成,所以要可持久化

现在的问题是动态区间第 \(k\) 小。仍然考虑差分权值线段树,但是随着 \(a_i\) 被修改,权值线段树 \(T_1,T_2,\dots,T_i\) 都需要修改。而我们要用的是权值线段树的前缀和,因此考虑用树状数组维护权值线段树。这样,每修改一个 \(a_i\),只需要修改 \(O(\log n)\) 棵权值线段树即可,查询前缀和也是 \(O(\log n)\)

相当于是用树状数组做到了 \(O(n)-O(1)\) 变为 \(O(\log n)-O(\log n)\)

时空复杂度 \(O(n\log^2n)\)(设 \(n,m\) 同阶)。注意离散化。注意数组大小。

点击查看代码
#include <bits/stdc++.h>
#define f(x, y, z) for (int x = (y); x <= (z); ++x)
using namespace std;
int const N = 2e5 + 10;
int n, m, a[N], b[N], cnt, rt[N];
int tmp[2][N], c[2];
struct Queries {
    char op;
    int x, y, k;
} Q[N];

struct PersSegTreeOnRange {
    int tot;
    struct Node {
        int ls, rs, sum;
    } tr[N * 130];
    void Modify(int &u, int l, int r, int x, int v) {
        if (!u) u = ++tot;
        tr[u].sum += v;
        if (l == r) return;
        int mid = l + r >> 1;
        if (x <= mid) Modify(tr[u].ls, l, mid, x, v);
        else Modify(tr[u].rs, mid + 1, r, x, v);
        return;
    }
    int Query(int l, int r, int x) {
        if (l == r) return b[l];
        int s = 0, mid = l + r >> 1;
        f(i, 1, c[0]) s -= tr[tr[tmp[0][i]].ls].sum;
        f(i, 1, c[1]) s += tr[tr[tmp[1][i]].ls].sum;
        if (x <= s) {
            f(i, 1, c[0]) tmp[0][i] = tr[tmp[0][i]].ls;
            f(i, 1, c[1]) tmp[1][i] = tr[tmp[1][i]].ls;
            return Query(l, mid, x);
        } else {
            f(i, 1, c[0]) tmp[0][i] = tr[tmp[0][i]].rs;
            f(i, 1, c[1]) tmp[1][i] = tr[tmp[1][i]].rs;
            return Query(mid + 1, r, x - s);
        }
    }
} T;

inline int lb(int x) { return x & -x; }
void Modify(int x, int v) {
    int y = lower_bound(b + 1, b + cnt + 1, a[x]) - b;
    while (x <= n) {
        T.Modify(rt[x], 1, cnt, y, v);
        x += lb(x);
    }
    return;
}
int Query(int l, int r, int k) {
    --l;
    c[0] = c[1] = 0;
    while (l) tmp[0][++c[0]] = rt[l], l -= lb(l);
    while (r) tmp[1][++c[1]] = rt[r], r -= lb(r);
    return T.Query(1, cnt, k);
}

signed main() {
    cin.tie(0)->sync_with_stdio(false);
    cin >> n >> m;
    f(i, 1, n) cin >> a[i], b[++cnt] = a[i];
    f(i, 1, m) {
        cin >> Q[i].op;
        if (Q[i].op == 'Q') cin >> Q[i].x >> Q[i].y >> Q[i].k;
        else cin >> Q[i].x >> Q[i].y, b[++cnt] = Q[i].y;
    }
    sort(b + 1, b + cnt + 1);
    cnt = unique(b + 1, b + cnt + 1) - b - 1;
    f(i, 1, n) Modify(i, 1);
    f(i, 1, m)
        if (Q[i].op == 'Q') cout << Query(Q[i].x, Q[i].y, Q[i].k) << '\n';
        else Modify(Q[i].x, -1), a[Q[i].x] = Q[i].y, Modify(Q[i].x, 1);
    return 0;
}

*P3157 [CQOI2011] 动态逆序对

对于序列 \(a\),它的逆序对数定义为集合 \(\{(i,j)\mid i<j\wedge a_i >a_j\}\) 中的元素个数。

现在给出 \(1\)\(n\) 的一个排列,按照某种顺序依次删除 \(m\) 个元素,你的任务是在每次删除一个元素之前统计整个序列的逆序对数。

\(1\le n \le 10^5\)\(1\le m \le 50000\)

可以将询问离线然后 CDQ 分治。(不好意思走错片场了)

考虑静态的逆序对问题,我们把 \((i,a_i)\) 看成平面上的一个点,那么它的贡献即为 \([1,i-1]\times[a_i+1,n]\)\([i+1,n]\times[1,a_i-1]\) 这两个矩形内点数之和。这是一个二维偏序问题,可以离线之后用树状数组维护。

这个问题也可以用权值线段树来维护。我们仍然考虑权值线段树的差分,设 \(T_i\) 表示 \(x=i\) 对应的权值线段树,其维护的是 \(x=i\) 这一列上的值域。设 \(f(T_i,l,r)\) 表示 \(T_i\)\(x=i,y\in[l,r]\) 的点的数量,那么矩形 \([x_1,x_2]\times[y_1,y_2]\) 的贡献即为 \(f(T_{x_2}-T_{x_1-1},y_1,y_2)\) 的值。

于是用树状数组维护权值线段树的前缀和即可。

点击查看代码
#include <bits/stdc++.h>
#define f(x, y, z) for (int x = (y); x <= (z); ++x)
using namespace std;
int constexpr N = 1e5 + 10;
int n, m, a[N], rt[N], r[N];
long long ans;

struct SegTree {
    int tot;
    struct Node {
        int ls, rs, sum;
    } tr[N << 8];
    void Modify(int &u, int l, int r, int x, int v) {
        if (!u) u = ++tot;
        tr[u].sum += v;
        if (l == r) return;
        int mid = l + r >> 1;
        if (x <= mid) Modify(tr[u].ls, l, mid, x, v);
        else Modify(tr[u].rs, mid + 1, r, x, v);
        return;
    }
    int Query(int u, int l, int r, int L, int R) {
        if (L <= l && r <= R) return tr[u].sum;
        int mid = l + r >> 1;
        if (R <= mid) return Query(tr[u].ls, l, mid, L, R);
        if (L > mid) return Query(tr[u].rs, mid + 1, r, L, R);
        return Query(tr[u].ls, l, mid, L, R) + Query(tr[u].rs, mid + 1, r, L, R);
    }
} T;

inline int lb(int x) { return x & -x; }
void Modify(int x, int y, int v) { while (x <= n) T.Modify(rt[x], 1, n, y, v), x += lb(x); }
int Query(int x, int yl, int yr) { int r = 0; while (x) r += T.Query(rt[x], 1, n, yl, yr), x -= lb(x); return r; }
int Query(int xl, int xr, int yl, int yr) { return yl > yr ? 0 : Query(xr, yl, yr) - Query(xl - 1, yl, yr); }

signed main() {
    cin >> n >> m;
    f(i, 1, n) cin >> a[i], r[a[i]] = i, Modify(i, a[i], 1);
    f(i, 2, n) ans += Query(1, i - 1, a[i] + 1, n);
    while (m--) {
        int x; cin >> x; x = r[x];
        cout << ans << '\n';
        ans -= Query(1, x - 1, a[x] + 1, n) + Query(x + 1, n, 1, a[x] - 1);
        Modify(x, a[x], -1);
    }
    return 0;
}
posted @ 2023-05-16 20:44  f2021ljh  阅读(51)  评论(3编辑  收藏  举报