线段树
基本操作
区修区查:
- 第 1、2、……k 小/大(\(k\) 不太大)
- 区间和、积、异或和、区间矩阵乘法
维护差分信息
P4243 [JSOI2009] 等差数列
若要在序列上处理等差数列,可以考虑差分法。此时,我们不必将差分数组和数列中的元素一一对应(这会影响理解),而是将差分数组中的一个元素和原序列中对应的两个元素关联(我的理解盲区)。
这样,使用线段树时,对于(差分数组的下标)区间 \([l,r]\),我们可以记录 \([l,r]\),\([l,r-1]\),\([l+1,r]\),\([l+1,r-1]\) 的答案;合并时,通过考虑这些区间对应到原序列的哪些元素,对左右区间的重合部分统计额外贡献,不重合的情况直接相加。
对于较短的区间(\(len\le 2\)),如何处理边界条件?当 \(len=1\) 时,区间 \([l+1,r]\) 或 \([l,r-1]\) 的答案在差分数组上无意义,但却能对应到原数组上的 \(1\) 个元素;对于 \([l+1,r-1]\),它在两个数组上都没有意义,因此直接赋值为 \(0\) 或 \(\pm\infty\)。
\(1\) 条差分信息 -> 约束两个位置
\(n\) 条差分信息 -> 约束 \(n+1\) 个位置
|
V
\(0\) 条差分信息 -> 代表一个位置
代码
#include<iostream>
using namespace std;
const int N = 1E5 + 10;
const int INF = 1E7 + 10;
struct Node {
int mans, lans, rans, ans, tag;
int ld, rd, len;
Node(int _ma = 0, int _la = 0, int _ra = 0, int _a = 0, int _ld = 0, int _rd = 0, int _tg = 0, int _len = 0) {
mans = _ma;
lans = _la;
rans = _ra;
ans = _a;
ld = _ld;
rd = _rd;
tag = _tg;
len = _len;
}
};
inline Node operator+(const Node &a, const Node &b) {
Node res;
res.mans = min(a.rans + b.lans - (a.rd == b.ld), min(a.mans + b.lans, a.rans + b.mans));
res.lans = min(a.ans + b.lans - (a.rd == b.ld), min(a.lans + b.lans, a.ans + b.mans));
res.rans = min(a.rans + b.ans - (a.rd == b.ld), min(a.mans + b.ans, a.rans + b.rans));
res.ans = min(a.ans + b.ans - (a.rd == b.ld), min(a.lans + b.ans, a.ans + b.rans));
res.ld = a.ld;
res.rd = b.rd;
return res;
}
int n, q;
int a[N], d[N];
namespace Seg_T {
inline int lc(int x) { return x << 1; }
inline int rc(int x) { return x << 1 | 1; }
Node tr[4 * N];
inline void push_up(int p) {
tr[p] = tr[lc(p)] + tr[rc(p)];
}
inline void move_tag(int p, int tg) {
tr[p].ld += tg;
tr[p].rd += tg;
tr[p].tag += tg;
}
inline void push_down(int p) {
if(!tr[p].tag) return;
move_tag(lc(p), tr[p].tag);
move_tag(rc(p), tr[p].tag);
tr[p].tag = 0;
}
void build(int p, int l, int r) {
if(l == r) {
tr[p] = {0, 1, 1, 1, d[l], d[l], 0};
return;
}
int mid = (l + r) >> 1;
build(lc(p), l, mid);
build(rc(p), mid + 1, r);
push_up(p);
}
void modify(int p, int l, int r, int q, int v) {
if(l == r) {
tr[p].ld += v;
tr[p].rd += v;
return;
}
push_down(p);
int mid = (l + r) >> 1;
if(mid >= q) modify(lc(p), l, mid, q, v);
else modify(rc(p), mid + 1, r, q, v);
push_up(p);
}
void modify(int p, int l, int r, int ql, int qr, int v) {
if(ql <= l && r <= qr) {
move_tag(p, v);
return;
}
push_down(p);
int mid = (l + r) >> 1;
if(mid >= ql) modify(lc(p), l, mid, ql, qr, v);
if(mid < qr) modify(rc(p), mid + 1, r, ql, qr, v);
push_up(p);
}
Node query(int p, int l, int r, int ql, int qr) {
if(ql <= l && r <= qr) {
return tr[p];
}
push_down(p);
int mid = (l + r) >> 1;
if(mid >= qr) return query(lc(p), l, mid, ql, qr);
if(mid < ql) return query(rc(p), mid + 1, r, ql, qr);
return query(lc(p), l, mid, ql, qr) + query(rc(p), mid + 1, r, ql, qr);
}
}
int main() {
cin >> n;
for(int i = 1; i <= n; i++) {
cin >> a[i];
d[i] = a[i] - a[i - 1];
}
Seg_T::build(1, 1, n + 1);
cin >> q;
while(q--) {
char op;
cin >> op;
if(op == 'A') {
int l, r, a, b;
cin >> l >> r >> a >> b;
if(l == r) {
Seg_T::modify(1, 1, n + 1, l, a);
Seg_T::modify(1, 1, n + 1, r + 1, -a);
continue;
}
Seg_T::modify(1, 1, n + 1, l, a);
Seg_T::modify(1, 1, n + 1, l + 1, r, b);
Seg_T::modify(1, 1, n + 1, r + 1, -a - (r - l) * b);
} else {
int l, r;
cin >> l >> r;
if(l == r) {
cout << 1 << '\n';
continue;
}
cout << Seg_T::query(1, 1, n + 1, l + 1, r).ans << '\n';
}
}
return 0;
}
主席树
P3919 【模板】可持久化线段树 1(可持久化数组)
主席树是可持久化权值线段树的简称,可以在 \(O(\log V)\) 的时间和空间复杂度内实现一次插入,同时可以保存所有历史版本。
模板代码
#include<iostream>
using namespace std;
const int N = 1E6 + 10;
const int LOGN = 25;
int n, q;
int a[N], rt[N];
namespace Seg_T {
int nn;
int lc[N * LOGN], rc[N * LOGN], sum[N * LOGN];
int addNode(int p) {
int nw = ++nn;
lc[nw] = lc[p];
rc[nw] = rc[p];
return nw;
}
inline void push_up(int p) {
sum[p] = sum[lc[p]] + sum[rc[p]];
}
void build(int &p, int l, int r) {
if(p == 0) p = ++nn;
if(l == r) {
sum[p] = a[l];
return;
}
int mid = (l + r) >> 1;
build(lc[p], l, mid);
build(rc[p], mid + 1, r);
push_up(p);
}
int modify(int p1, int l, int r, int q, int v) {
int p = addNode(p1);
if(l == r) {
sum[p] = v;
return p;
}
int mid = (l + r) >> 1;
if(mid >= q) lc[p] = modify(lc[p1], l, mid, q, v);
else rc[p] = modify(rc[p1], mid + 1, r, q, v);
push_up(p);
return p;
}
int query(int p, int l, int r, int q) {
if(l == r) {
return sum[p];
}
int mid = (l + r) >> 1;
if(mid >= q) return query(lc[p], l, mid, q);
else return query(rc[p], mid + 1, r, q);
}
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin >> n >> q;
for(int i = 1; i <= n; i++) {
cin >> a[i];
}
Seg_T::build(rt[0], 1, n);
for(int i = 1; i <= q; i++) {
int ver, op, pos, val;
cin >> ver >> op;
if(op == 1) {
cin >> pos >> val;
rt[i] = Seg_T::modify(rt[ver], 1, n, pos, val);
} else {
cin >> pos;
cout << Seg_T::query(rt[ver], 1, n, pos) << '\n';
rt[i] = rt[ver];
}
}
return 0;
}
P3834 【模板】可持久化线段树 2
查区间第 \(k\) 小
使用主席树我们可以对原数组的每一个前缀都“建”一棵权值线段树,通过对两棵不同下标对应的树作差,我们可以对任意下标区间 \([l,r]\) 使用线段树二分(即:得到了一棵不能查询最值,但可以查询区间和,区间绝对众数等的线段树)
时间复杂度 \(O(n\log V)\) - \(O(m\log V)\)。
模板代码
#include<iostream>
#define int long long
using namespace std;
const int N = 2E5 + 10;
const int A = 1E9;
const int LOGA = 20;
int n, m;
int rt[N];
namespace Seg_T {
int nn;
int lc[N * LOGA], rc[N * LOGA], sum[N * LOGA];
int addNode(int p1) {
int p = ++nn;
lc[p] = lc[p1];
rc[p] = rc[p1];
sum[p] = sum[p1];
return p;
}
void push_up(int p) {
sum[p] = sum[lc[p]] + sum[rc[p]];
}
int insert(int p1, int l, int r, int q, int v) {
int p = addNode(p1);
if(l == r) {
sum[p] += v;
return p;
}
int mid = (l + r) >> 1;
if(mid >= q) lc[p] = insert(lc[p1], l, mid, q, v);
else rc[p] = insert(rc[p1], mid + 1, r, q, v);
push_up(p);
return p;
}
int queryKth(int pl, int pr, int l, int r, int k) {
if(l == r) {
return l;
}
int mid = (l + r) >> 1;
if(k <= sum[lc[pr]] - sum[lc[pl]]) return queryKth(lc[pl], lc[pr], l, mid, k);
else return queryKth(rc[pl], rc[pr], mid + 1, r, k - (sum[lc[pr]] - sum[lc[pl]]));
}
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin >> n >> m;
for(int i = 1; i <= n; i++) {
int x;
cin >> x;
rt[i] = Seg_T::insert(rt[i - 1], 0, A, x, 1);
}
for(int i = 1; i <= m; i++) {
int l, r, k;
cin >> l >> r >> k;
cout << Seg_T::queryKth(rt[l - 1], rt[r], 0, A, k) << '\n';
}
return 0;
}
P7252 [JSOI2011] 棒棒糖
查询区间绝对众数
代码
#include<iostream>
using namespace std;
const int N = 5E4 + 10;
const int V = 5E4 + 10;
const int LOGV = 16;
int n, m;
int c[N], rt[N];
namespace Seg_T {
int sum[N * LOGV], lc[N * LOGV], rc[N * LOGV], nn;
inline void push_up(int p) {
sum[p] = sum[lc[p]] + sum[rc[p]];
}
int insert(int p1, int l, int r, int q, int v) {
int p = ++nn;
lc[p] = lc[p1];
rc[p] = rc[p1];
sum[p] = sum[p1];
if(l == r) {
sum[p] += v;
return p;
}
int mid = (l + r) >> 1;
if(mid >= q) lc[p] = insert(lc[p1], l, mid, q, v);
else rc[p] = insert(rc[p1], mid + 1, r, q, v);
push_up(p);
if(sum[p] != sum[p1] + 1) throw -1;
return p;
}
int query(int pl, int pr, int l, int r, int k) {
if(l == r) {
return l;
}
int mid = (l + r) >> 1;
if(sum[lc[pr]] - sum[lc[pl]] >= k) return query(lc[pl], lc[pr], l, mid, k);
if(sum[rc[pr]] - sum[rc[pl]] >= k) return query(rc[pl], rc[pr], mid + 1, r, k);
return 0;
}
}
int main() {
cin >> n >> m;
for(int i = 1; i <= n; i++) {
cin >> c[i];
rt[i] = Seg_T::insert(rt[i - 1], 1, V, c[i], 1);
}
while(m--) {
int l, r;
cin >> l >> r;
cout << Seg_T::query(rt[l - 1], rt[r], 1, V, (r - l + 1) / 2 + 1) << '\n';
}
return 0;
}
CF786C Till I Collapse
子问题:求任意区间内不同数字的个数(重复的数字算一种);
考虑从左向右给每个前缀都建一棵线段树,线段树下标和原数组对应(意思是,这些线段树不是权值线段树),并使前缀中相同的数字只有最右边的一个 \(val=1\)。我们可以考虑用 last
数组记录每个数字上一次出现的位置,每次插入新数字时,若发现其左边有重复的数字,则对重复的位置 \(-1\) 去掉贡献。这样,我们在前缀 \([1,r]\) 的线段树中查询 \([l,r]\) 的区间和,就可以知道区间 \([l,r]\) 中有多少种不同的数字。
本题还需要从左往右做贪心,同时还需要线段树上二分,故需要把上面的过程左右颠倒一下,但总体思路不变。
代码
#include<iostream>
using namespace std;
const int N = 1E5 + 10;
const int LOGN = 20;
int n;
int rt[N], a[N], last[N];
namespace Seg_T {
int sum[2 * N * LOGN], lc[2 * N * LOGN], rc[2 * N * LOGN], nn;
inline void push_up(int p) { sum[p] = sum[lc[p]] + sum[rc[p]]; }
int insert(int p1, int l, int r, int q, int v) {
int p = ++nn;
lc[p] = lc[p1];
rc[p] = rc[p1];
sum[p] = sum[p1];
if(l == r) {
sum[p] += v;
return p;
}
int mid = (l + r) >> 1;
if(mid >= q) lc[p] = insert(lc[p1], l, mid, q, v);
else rc[p] = insert(rc[p], mid + 1, r, q, v);
push_up(p);
return p;
}
int query(int p, int l, int r, int k) {
if(l == r) {
return l;
}
int mid = (l + r) >> 1;
if(sum[lc[p]] > k) return query(lc[p], l, mid, k);
return query(rc[p], mid + 1, r, k - sum[lc[p]]);
}
}
int main() {
cin >> n;
for(int i = 1; i <= n; i++) {
cin >> a[i];
}
for(int i = n; i >= 1; i--) {
rt[i] = Seg_T::insert(rt[i + 1], 1, n + 1, i, 1);
if(last[a[i]]) {
rt[i] = Seg_T::insert(rt[i], 1, n + 1, last[a[i]], -1);
}
last[a[i]] = i;
}
for(int k = 1; k <= n; k++) {
int cur = 1, ans = 0;
while(cur <= n) {
cur = Seg_T::query(rt[cur], 1, n + 1, k);
ans++;
}
cout << ans << ' ';
}
cout << endl;
return 0;
}
线段树优化建图
优化最短路松弛
若使用 BFS 等最短路算法,每个节点只会考虑一次。因此在总边数很多的情况下,我们可以使用线段树优化枚举出边的过程,并且满足每次松弛结束,该节点就不会再被松弛,我们就可以用线段树把这个节点标记为无效,优化后面的松弛,从而达到较低的均摊时间复杂度。