线段树复习
线段树是基于分治思想的二叉树,用来维护区间信息(区间和,区间最值,区间\(gcd\)等),可以在\(nlogn\)的时间内执行区间修改和区间查询 。
线段树的叶子节点存储元素本身,非叶子节点存储区间内的元素统计值。
建树
关于线段树为什么开\(4 \times N\)大小:
单点修改 \(O(logn)\)
从根节点进入,递归找到叶子节点\([x, x]\),对该节点增加\(k\),然后从下往上进入祖先节点进行更新
区间查询 \(O(logn)\)
利用拆分与拼凑的思想, 例如区间\([5, 7]\), 可以被拆分成\([5, 5],[6, 8],[9, 9]\),通过合并三个区间的答案求得查询答案。
从根节点进入,递归执行一下过程:
- 若查询区间\([x, y]\) 完全覆盖当前节点的区间,则立即回溯,并返回该节点的sum值
- 若当前节点的左子树节点与\([x, y]\)有重叠,则递归访问左子树
- 若当前节点的右子树节点与\([x, y]\)有重叠,则递归访问右子树
int query(int p, int x, int y) {
if (x <= tr[p].l && y <= tr[p].r)
return tr[p].sum;
int m = tr[p].l + tr[p].r >> 1, sum = 0;
if (x <= m) sum += query(lc, x, y);
if (y >= m) sum += query(rc, x, y);
return sum;
}
区间修改 \(O(logn)\)-懒标记
如果对于区间\([1, n]\)进行修改,我们需要修改叶子节点,然后更改祖宗节点的值,那么复杂度将会是\(O(n)\)的,我们需要换个思路。
我们做懒惰修改,如果当前区间完全覆盖\([x, y]\), 当\([x, y]\)完全覆盖节点区间\([ a, b]\)时,先修改区间的\(sum\)值,在打上一个懒标记, 然后立即返回,当下次需要的时候,在下传懒标记,这样可以把每次修改和查询的时间都控制在\(O(logn)\)(复杂度与查询相同)
模板
#define lc p<<1
#define rc p<<1|1
const int N = 1e5 + 10;
using i64 = long long;
int n, w[N];
struct node {
i64 l, r, sum, add;
}tr[4 * N];
void pushdown(int p) {
if (tr[p].add) {
tr[lc].sum += (tr[lc].r - tr[lc].l + 1) * tr[p].add;
tr[rc].sum += (tr[rc].r - tr[rc].l + 1) * tr[p].add;
tr[lc].add += tr[p].add;
tr[rc].add += tr[p].add;
tr[p].add = 0;
}
}
void pushup(int p) {
tr[p].sum = tr[lc].sum + tr[rc].sum;
}
void build(int p, int l, int r) {
tr[p] = {l, r, w[l], 0};
if(l == r) return;
int m = l + r >> 1;
build(lc, l, m);
build(rc, m + 1, r);
pushup(p);
}
i64 query(int p, int x, int y) {
if (tr[p].l >= x && tr[p].r <= y) {
return tr[p].sum;
}
int m = tr[p].l + tr[p].r >> 1;
i64 sum = 0;
pushdown(p);
if (x <= m) sum += query(lc, x, y);
if (y > m) sum += query(rc, x, y);
return sum;
}
void update(int p, int x, int y, int k) {
if (tr[p].l >= x && tr[p].r <= y) {
tr[p].sum += (tr[p].r - tr[p].l + 1) * k;
tr[p].add += k;
return;
}
int m = tr[p].l + tr[p].r >> 1;
pushdown(p);
if (x <= m) update(lc, x, y, k);
if (y > m) update(rc, x, y, k);
pushup(p);
}
线段树应用
区间 gcd
在更相损减法的\(n\)个推广情况下:\(gcd(a_1, a_2, \dots, a_n) = gcd(a_1, a_2 - a_1, \dots a_n - a_{n - 1})\), 显然右侧是一个差分序列
所以\(gcd[l, r] = gcd(a_l, gcd[b_{l + 1}, b_r])\)
\(差分 \rightarrow(求前缀和)\rightarrow 原序列\)
\(原序列 \rightarrow(区间修改[l,r])\rightarrow差分序列单点修改\)
现在我们只需要维护一个单点查询,和单点修改(其实单点修改完还要求前缀, 可以直接上树状数组)。
当然一个线段树也可以维护
#include <iostream>
#define lc p<<1
#define rc p<<1|1
using i64 = long long;
const int N = 5e5 + 10;
struct node {
int l, r;
i64 sum, b;
} tr[4 * N];
i64 w[N];
i64 gcd(i64 a, i64 b) {
return b?gcd(b, a % b):a;
}
void pushup(int p) {
tr[p].sum = tr[lc].sum + tr[rc].sum;
tr[p].b = gcd(tr[lc].b, tr[rc].b);
}
void build(int p, int l, int r) {
tr[p] = {l, r, w[r] - w[r - 1], w[r] - w[r - 1]};
if (l == r) return;
int m = l + r >> 1;
build(lc, l, m);
build(rc, m + 1, r);
pushup(p);
}
void update(int p, int x, int y, i64 k) {
if (tr[p].l >= x && tr[p].r <= y) {
tr[p].b += k;
tr[p].sum += k;
return;
}
int m = tr[p].l + tr[p].r >> 1;
if (x <= m) update(lc, x, y, k);
if (y > m) update(rc, x, y, k);
pushup(p);
}
i64 query(int p, int x, int y) {
if (tr[p].l >= x && tr[p].r <= y) {
return tr[p].b;
}
i64 res = 0;
int m = tr[p].l + tr[p].r >> 1;
if (x <= m) res = gcd(res, query(lc, x, y));
if (y > m) res = gcd(res, query(rc, x, y));
return res;
}
i64 query2(int p, int x, int y) {
if (tr[p].l >= x && tr[p].r <= y) {
return tr[p].sum;
}
i64 res = 0;
int m = tr[p].l + tr[p].r >> 1;
if (x <= m) res += query2(lc, x, y);
if (y > m) res += query2(rc, x, y);
return res;
}
int main() {
int n, m;
std::cin >> n >> m;
for (int i = 1; i <= n; i ++) {
std::cin >> w[i];
}
build(1, 1, n);
while (m --) {
std::string op;
int l, r;
i64 d;
std::cin >> op >> l >> r;
if (op == "C") {
std::cin >> d;
update(1, l, l, d);
if (r + 1 <= n) update(1, r + 1, r + 1, -d);
} else {
std::cout << std::abs(gcd(query2(1, 1, l), query(1, l + 1, r))) << "\n";
}
}
}
区间求max
不需要讲啥的\(max\)的性质很多, 交换律,结合律都满足,直接求就行, 这里多提一嘴,区间更改直接记录add, 另外需要返回下标用\(pair\)结构来query
.
#include <iostream>
#define lc p<<1
#define rc p<<1|1
using i64 = long long;
const int N = 2e5 + 10;
struct node {
i64 l, r, sum, tag;
} tr[4 * N];
int w[N];
void pushup(int p) {
tr[p].sum = std::max(tr[lc].sum, tr[rc].sum);
if (tr[lc].sum > tr[rc].sum) tr[p].tag = tr[lc].tag;
else tr[p].tag = tr[rc].tag;
}
void build(int p, int l, int r) {
tr[p] = {l, r, w[l], l};
if (l == r) return;
int m = l + r >> 1;
build(lc, l, m);
build(rc, m + 1, r);
pushup(p);
}
std::pair<i64, i64> query(int p, int x, int y) {
if (tr[p].l >= x && tr[p].r <= y) {
return {tr[p].sum, tr[p].tag};
}
int m = tr[p].l + tr[p].r >> 1;
std::pair<i64, i64> res;
res.first = -0x3f3f3f3f;
if (x <= m) res = std::max(res, query(lc, x, y));
if (y > m) res = std::max(res, query(rc, x, y));
return res;
}
void update(int p, int x, int k) {
if (tr[p].l == tr[p].r && tr[p].l == x) {
tr[p].sum = k;
return;
}
int m = tr[p].l + tr[p].r >> 1;
if (x <= m) update(lc, x, k);
if (x > m) update(rc, x, k);
pushup(p);
}
int main() {
int n, q;
std::cin >> n >> q;
for (int i = 1; i <= n; i ++) std::cin >> w[i];
build(1, 1, n);
while (q --) {
int a, b, c, d;
std::cin >> a >> b >> c;
if (a) {
auto t = query(1, b, c);
std::cout << t.second << " " << t.first << "\n";
} else {
update(1, b, c);
}
}
}
扫描线
具体做法就是从\(x\)轴遍历,存入\(y\)坐标当成矩形贡献,每次计算\(S \times 贡献数量\), 碰到矩形右边的时候,减去该贡献,由于坐标轴可能很大,我们可以将每条线离散化处理。
模板:
#include <iostream>
#include <algorithm>
#define lc p<<1
#define rc p<<1|1
using i64 = long long;
const int N = 2e5 + 10;
struct Line {
int x, y1, y2;
int flag;
bool operator<(Line &b) {return x < b.x;}
} L[N];
struct node {
int l, r, len;
int cnt;
} tr[8 * N];
int n, Y[N];
void pushup(int p) {
if (tr[p].cnt) tr[p].len = tr[p].r - tr[p].l;
else tr[p].len = tr[lc].len + tr[rc].len;
}
void build(int p, int l, int r) {
tr[p].l = Y[l], tr[p].r = Y[r];
if (r == l + 1) return;
int mid = l + r >> 1;
build(lc, l, mid);
build(rc, mid, r);
}
void change(int p, int a, int b, int c) {
if (a >= tr[p].r || b <= tr[p].l) return;
if (a <= tr[p].l && tr[p].r <= b) {
tr[p].cnt += c;
pushup(p);
return;
}
change(lc, a, b, c);
change(rc, a, b, c);
pushup(p);
}
int main() {
int n;
std::cin >> n;
for (int i = 1; i <= n; i ++) {
int x1, y1, x2, y2;
std::cin >> x1 >> y1 >> x2 >> y2;
L[i] = {x1, y1, y2, 1}, L[i + n] = {x2, y1, y2, -1};
Y[i] = y1, Y[i + n] = y2;
}
n *= 2;
std::sort(L + 1, L + n + 1);
std::sort(Y + 1, Y + n + 1);
build(1, 1, n);
i64 ans = 0;
for (int i = 1; i < n; i ++) {
change(1, L[i].y1, L[i].y2, L[i].flag);
ans += 1LL * (L[i + 1].x - L[i].x) * tr[1].len;
}
std::cout << ans;
}
求历史最大值
求区间异或
我们知道异或满足前缀和的性质:\(f(l, r) = f(r) - f(l - 1)\), 所基本上可以像求\(sum\)那样写。对于区间异或懒标记写法是独特的,因为我们对某个区间异或上固定的数多次,我们只取奇数次操作。
#include <iostream>
#define lc p<<1
#define rc p<<1|1
using i64 = long long;
const int N = 2e5 + 10;
int n, m;
struct node {
int l, r, sum, add;
} tr[4 * N];
int w[N];
void pushup(int p) {
tr[p].sum = tr[lc].sum + tr[rc].sum;
}
void pushdown(int p) {
if (tr[p].add) {
tr[lc].sum = (tr[lc].r - tr[lc].l + 1) - tr[lc].sum;
tr[rc].sum = (tr[rc].r - tr[rc].l + 1) - tr[rc].sum;
tr[lc].add ^= 1;
tr[rc].add ^= 1;
tr[p].add = 0;
}
}
void build(int p, int l, int r) {
tr[p] = {l, r, w[l], 0};
if (l == r) return;
int m = l + r >> 1;
build(lc, l, m);
build(rc, m + 1, r);
pushup(p);
}
void update(int p, int x, int y) {
if (tr[p].l >= x && tr[p].r <= y) {
tr[p].sum = (tr[p].r - tr[p].l + 1) - tr[p].sum;
tr[p].add ^= 1;
return;
}
int m = tr[p].l + tr[p].r >> 1;
pushdown(p);
if (x <= m) update(lc, x, y);
if (y > m) update(rc, x, y);
pushup(p);
}
int query(int p, int x, int y) {
if (tr[p].l >= x && tr[p].r <= y) {
return tr[p].sum;
}
int m = tr[p].l + tr[p].r >> 1;
pushdown(p);
int cnt = 0;
if (x <= m) cnt += query(lc, x, y);
if (y > m) cnt += query(rc, x, y);
return cnt;
}
int main() {
std::cin >> n >> m;
for (int i = 1; i <= n; i ++) {
char c;
std::cin >> c;
w[i] = c - '0';
}
build(1, 1, n);
while (m --) {
int op, l, r;
std::cin >> op >> l >> r;
if (!op) {
update(1, l, r);
} else {
std::cout << query(1, l, r) << "\n";
}
}
}