学习笔记:线段树
学习笔记:线段树
在已经掌握线段树的基本用法后的做题整理。给自己复习用的。
用 \(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\) 如何变化。对公式变形:
于是只需要维护区间平方和 \(sum2\) 如何变化。
点击查看代码
#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\)。
因此我们还需要维护一个 \(\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\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\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\) 个操作,操作分为三种:
在当前 \(S\) 的末尾加一对括号(即 \(S\) 变为
S()
);在当前 \(S\) 的最外面加一对括号(即 \(S\) 变为
(S)
);取消第 \(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:
对于操作 2:
于是答案可以看作初始矩阵 \(\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\) 的数组,支持如下两种操作:
- 在某个历史版本上修改某一个位置上的值;
- 访问某个历史版本上的某一位置的值。
此外,每进行一次操作(对于操作 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
操作不会访问到未来的时间戳。
标记永久化是指,在打标记之后,如果下一次修改或询问到这里,不下传 / 上传标记,而是在询问的答案中直接加上该标记的贡献(即标记的值乘以区间长度)。也就是说,打完标记之后,询问时在线段树上经过的路径上的标记需要被计算。
因此我们不需要 pushdown
和 pushup
(除了 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个串
有一个长度为 \(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]\),最大的子串和,即
我们从 \(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;
}