线段树杂谈
动态开点线段树
引入
普通的线段树写法有一个显然的缺点——空间。堆式存贮使得我们开线段树时需要用 $ 4n $ 的空间。冗余空间高达 $ 2n $ 。而且,在大多数情况下线段树中并不是每个节点都会被用到,这时我们就可以使用动态开点线段树,不仅所用的空间小,实现起来的代码也比普通线段树短。
思想
动态开点线段树,顾名思义,就是一棵使用动态开点的线段树。(废话
在常用的堆式存储中,我们用 $ p \times 2 $ 和 $ p \times 2 + 1 $ 来表示节点 $ p $ 的两个儿子。而在动态开点线段树中,我们用 $ lson $ 和 $ rson $ 两个数组来记录每个节点的两个儿子的编号。并且 节点只有在被用到的时候才创建。这样,我们就能有效避免冗余空间的出现。
在修改时,如果我们发现当前节点并没有在线段树上,那么我们就创建这个节点。
//模板(区间修改)
void modify(int &p, int L, int R, int l, int r, int c) {//L,R表示当前节点所包含的区间,l, r表示修改区间
//注意到是&p,这样可以使得上一层递归中不用再次手动修改传入的节点
if(!p) p = ++tot;//创建新节点
if(l <= L && R <= r) {
······//修改
return ;
}
pushdown(p, L, R);
int mid = L + R >> 1;
if(l <= mid) modify(t[p].ls, L, mid, l, r, c);
if(r > mid) modify(t[p].rs, mid + 1, R, l, r, c);
pushup(p);
}
在询问时,如果当前节点并未被创建,那么就可以返回 $ 0 $ 。这是因为如果当前节点没有被创建,说明这个区间没有被修改过,换句话说,这个区间所维护的东西不存在,即为 $ 0 $。
//模板(区间查询)
int query(int p, int L, int R, int l, int r) {
if(!p) return 0;
if(l <= L && R <= r) return ···;//返回区间所维护的东西
pushdown(p, L, R);
int mid = L + R >> 1, res = 0;
if(l <= mid) ···; //查询并更新
if(r > mid) ···;
return res;
}
如果线段树上有初值的话,我们可以将其看做若干个修改。这样就不用写 $ build $ 函数了。
例题
$ \color {#52c41a} P3372 【模板】线段树 1 $
题意
如题,已知一个数列,你需要进行下面两种操作:
- 将某区间每一个数加上 $ k $ 。
- 求出某区间每一个数的和。
思路
线段树维护。
代码
#include<bits/stdc++.h>
#define int long long
#define Debug puts("Oops!")
using namespace std;
const int N = 1e5 + 5, M = 5e5 + 5;
int n, m;
struct Node {
int ls, rs;
int lazy, dat;
};
struct Segment_Tree {
int root, tot;
Node t[N << 1];
void pushup(int p) {t[p].dat = t[t[p].ls].dat + t[t[p].rs].dat;}
void pushdown(int p, int l, int r) {
if(!t[p].ls) t[p].ls = ++tot;
if(!t[p].rs) t[p].rs = ++tot;
t[t[p].ls].lazy += t[p].lazy;
t[t[p].rs].lazy += t[p].lazy;
int mid = l + r >> 1;
t[t[p].ls].dat += t[p].lazy * (mid - l + 1);
t[t[p].rs].dat += t[p].lazy * (r - mid);
t[p].lazy = 0;
}
void add(int &p, int L, int R, int l, int r, int c) {
if(!p) p = ++tot;
if(l <= L && R <= r) {
t[p].dat += c * (R - L + 1);
t[p].lazy += c;
return ;
}
pushdown(p, L, R);
int mid = L + R >> 1;
if(l <= mid) add(t[p].ls, L, mid, l, r, c);
if(r > mid) add(t[p].rs, mid + 1, R, l, r, c);
pushup(p);
}
int query(int p, int L, int R, int l, int r) {
if(!p) return 0;
if(l <= L && R <= r) return t[p].dat;
pushdown(p, L, R);
int mid = L + R >> 1, res = 0;
if(l <= mid) res += query(t[p].ls, L, mid, l, r);
if(r > mid) res += query(t[p].rs, mid + 1, R, l, r);
return res;
}
}st;
inline int read() {
int x = 0, f = 1; char c = getchar();
while(!isdigit(c)) {if(c == '-') f = -1; c = getchar();}
while(isdigit(c)) x = x * 10 + c - '0', c = getchar();
return x * f;
}
signed main() {
// freopen(".in", "r", stdin);
// freopen(".out", "w", stdout);
n = read(), m = read();
for(int i = 1, x; i <= n; i++) {
x = read();
st.add(st.root, 1, n, i, i, x);
}
while(m--) {
int op = read(), l = read(), r = read();
if(op == 1) {
int x = read();
st.add(st.root, 1, n, l, r, x);
}
else {
cout << st.query(st.root, 1, n, l, r) << endl;
}
}
return 0;
}
线段树合并
引入
顾名思义,线段树合并是指建立一棵新的线段树,这棵线段树的每个节点都是两棵原线段树对应节点合并后的结果。它常常被用于维护树上或是图上的信息。
过程
线段树合并的过程其实就是暴力遍历两颗线段树上的每个节点,并将其上的内容合并。
线段树合并的时间复杂度
显然,对于两颗满的线段树,合并操作的复杂度是 $ O(n\log n) $ 的。但实际情况下使用的常常是权值线段树,总点数和 $ n $ 的规模相差并不大。并且合并时一般不会重复地合并某个线段树,所以我们最终增加的点数大致是 $ n\log n $ 级别的。这样,总的复杂度就是 $ O(n\log n) $ 级别的。当然,在一些情况下,可并堆可能是更好的选择。
模板
void merge(int &x, int y, int l, int r) {
//和动态开点线段树一样,&x可以减少码长。
if(!x) {x = y; return ;}
if(!y) return ;
if(l == r) {t[x].s += t[y].s; return ;}
int mid = l + r >> 1;
merge(t[x].ls, t[y].ls, l, mid);
merge(t[x].rs, t[y].rs, mid + 1, r);
pushup(x);
}
例题
$ \color {#9d3dcf} P4556 [Vani有约会] 雨天的尾巴 /【模板】线段树合并 $
题意
首先村落里的一共有 $ n $ 座房屋,并形成一个树状结构。然后救济粮分 $ m $ 次发放,每次选择两个房屋 $ (x, y) $,然后对于 $ x $ 到 $ y $ 的路径上(含 $ x $ 和 $ y $)每座房子里发放一袋 $ z $ 类型的救济粮。
然后深绘里想知道,当所有的救济粮发放完毕后,每座房子里存放的最多的是哪种救济粮。
思路
显然可以想到使用树上差分来快速维护 $ x $ 到 $ y $ 的区间操作。
考虑如何快速更新差分数组。
容易想到在每个节点上开一棵动态开点线段树,维护每种救济粮的袋数以及区间最大值以及它的位置。
所有操作结束后树上前缀和恢复原数组,使用线段树合并。
代码
#include<bits/stdc++.h>
#define int long long
#define Debug puts("Oops!")
using namespace std;
const int N = 5e6 + 5, M = 5e5 + 5;
int n, m;
int res[N];
int cnt = -1, head[N];
struct Edge {
int nxt, to;
}e[N << 1];
void addedge(int u, int v) {
cnt++;
e[cnt].nxt = head[u];
e[cnt].to = v;
head[u] = cnt;
}
struct Node {
int ls, rs;
int s, pos;
};
struct Segment_Tree {
int root[N];//储存每个节点的线段树的根节点编号
Node t[N];
int tot;
void pushup(int p) {
if(t[t[p].ls].s >= t[t[p].rs].s) t[p].s = t[t[p].ls].s, t[p].pos = t[t[p].ls].pos;
else t[p].s = t[t[p].rs].s, t[p].pos = t[t[p].rs].pos;
}
void modify(int &p, int l, int r, int x, int c) {
if(!p) p = ++tot;
if(l == r) {
t[p].s += c, t[p].pos = x;
return ;
}
int mid = l + r >> 1;
if(x <= mid) modify(t[p].ls, l, mid, x, c);
if(x > mid) modify(t[p].rs, mid + 1, r, x, c);
pushup(p);
}
void merge(int &x, int y, int l, int r) {
if(!x) {x = y; return ;}
if(!y) return ;
if(l == r) {t[x].s += t[y].s; return ;}
int mid = l + r >> 1;
merge(t[x].ls, t[y].ls, l, mid);
merge(t[x].rs, t[y].rs, mid + 1, r);
pushup(x);
}
}st;
//树剖求lca
int fa[N], son[N], dep[N], sz[N];
void dfs1(int x, int father) {
fa[x] = father;
sz[x] = 1;
dep[x] = dep[fa[x]] + 1;
for(int i = head[x]; ~i; i = e[i].nxt) {
int y = e[i].to;
if(y == father) continue;
dfs1(y, x);
sz[x] += sz[y];
if(sz[y] > sz[son[x]]) son[x] = y;
}
}
int top[N];
void dfs2(int x, int topx) {
top[x] = topx;
if(!son[x]) return ;
dfs2(son[x], topx);
for(int i = head[x]; ~i; i = e[i].nxt) {
int y = e[i].to;
if(y == fa[x] || y == son[x]) continue;
dfs2(y, y);
}
}
int lca(int x, int y) {
while(top[x] != top[y]) {
if(dep[top[x]] < dep[top[y]]) swap(x, y);
x = fa[top[x]];
}
if(dep[x] > dep[y]) swap(x, y);
return x;
}
//树上前缀和
void solve(int x) {
for(int i = head[x]; ~i; i = e[i].nxt) {
int y = e[i].to;
if(y == fa[x]) continue;
solve(y);
st.merge(st.root[x], st.root[y], 1, 100000);
}
res[x] = st.t[st.root[x]].pos;
if(st.t[st.root[x]].s == 0) res[x] = 0;
}
inline int read() {
int x = 0, f = 1; char c = getchar();
while(!isdigit(c)) {if(c == '-') f = -1; c = getchar();}
while(isdigit(c)) x = x * 10 + c - '0', c = getchar();
return x * f;
}
signed main() {
// freopen(".in", "r", stdin);
// freopen(".out", "w", stdout);
memset(head, -1, sizeof head);
n = read(), m = read();
for(int i = 1, u, v; i < n; i++) {
u = read(), v = read();
addedge(u, v);
addedge(v, u);
}
dfs1(1, 0);
dfs2(1, 1);
st.tot = n;
while(m--) {
int x = read(), y = read(), z = read(), f = lca(x, y);
//树上差分
st.modify(st.root[x], 1, 100000, z, 1);
st.modify(st.root[y], 1, 100000, z, 1);
st.modify(st.root[f], 1, 100000, z, -1);
st.modify(st.root[fa[f]], 1, 100000, z, -1);
}
solve(1);
for(int i = 1; i <= n; i++) cout << res[i] << endl;
return 0;
}
李超线段树
李超线段树是针对一类特定的题目而存在的:
$\color{#9d3dcf} 【模板】李超线段树 / [HEOI2013] Segment $
题目描述
要求在平面直角坐标系下维护两个操作:
- 在平面上加入一条线段。记第 \(i\) 条被插入的线段的标号为 \(i\)。
- 给定一个数 \(k\),询问与直线 \(x = k\) 相交的线段中,交点纵坐标最大的线段的编号。
思路
线段不是很好处理,所以我们可以把它看做一个定义域为 $ (x_0, x_1) $ 的一次函数。
李超线段树的一个节点 $ (l, r) $ 里存的是在 $ mid = (l + r) / 2 $ 这一位置上最大的,且定义域包含 $ (l, r) $ 的一次函数。
对于一条新线段 $ f $,它能更新的节点只有被它完全包含的节点,所以我们现在线段树上找到那些被其完全包含的最大的节点。
如果新线段在 $ mid $ 上的值比原线段 $ g $ 大,则交换 $ f $ 和 $ g $。
考虑若新线段在 $ mid $ 上的值小于原线段如何更新子节点。
注意到由于线段是直的,所以 肯定有一条线段只能作为左半边的答案或只能作为右半边的答案。
1.若 $ f $ 在左端点上大于 $ g $,则递归左子树用 $ f $ 更新。
2.若 $ f $ 在右端点上大于 $ g $,则递归右子树用 $ f $ 更新。
查询时我们可以用标记永久化的思想,对从根节点到 $ x $ 路径上的所有函数取 $ max $ 。
代码
#include<bits/stdc++.h>
#define int long long
#define Debug puts("Oops!")
using namespace std;
const int N = 1e5 + 5, M = 5e5 + 5;
const double eps = 1e-9;
int n, len;
pair<double, double> pl[N];
struct Segment_Tree {
struct Node {int l, r, dat;} t[N << 2];
#define l(p) p << 1
#define r(p) p << 1 | 1
void build(int p, int l, int r) {
t[p].l = l, t[p].r = r;
if(l == r) return ;
int mid = l + r >> 1;
build(l(p), l, mid); build(r(p), mid + 1, r);
}
void addline(int ax, int ay, int bx, int by) {
len++;
if(ax == bx) pl[len] = {0, max(ay, by)};
else pl[len] = {(by - ay) * 1.0 / (bx - ax), ay - (by - ay) * 1.0 / (bx - ax) * ax};
}
double calc(int p, int x) { return pl[p].first * x + pl[p].second; }
void update(int p, int id) {
if(t[p].dat == 0) {t[p].dat = id; return ;}
int mid = t[p].l + t[p].r >> 1;
double t1 = calc(id, mid), t2 = calc(t[p].dat, mid);
if(t1 - t2 > eps || (abs(t1 - t2) <= eps && t[p].dat > id)) swap(t[p].dat, id);
double lf = calc(id, t[p].l), rf = calc(id, t[p].r);
double lg = calc(t[p].dat, t[p].l), rg = calc(t[p].dat, t[p].r);
if(lf - lg > eps || (abs(lf - lg) <= eps && t[p].dat > id)) update(l(p), id);
if(rf - rg > eps || (abs(rf - rg) <= eps && t[p].dat > id)) update(r(p), id);
}
void modify(int p, int l, int r, int id) {
if(l <= t[p].l && t[p].r <= r) {update(p, id); return ;}
int mid = t[p].l + t[p].r >> 1;
if(l <= mid) modify(l(p), l, r, id);
if(r > mid) modify(r(p), l, r, id);
}
int query(int p, int x) {
if(t[p].l == t[p].r) return t[p].dat;
int mid = t[p].l + t[p].r >> 1, res = t[p].dat, tmp;
if(x <= mid) tmp = query(l(p), x);
else tmp = query(r(p), x);
if(calc(tmp, x) - calc(res, x) > eps || (abs(calc(tmp, x) - calc(res, x)) <= eps && tmp < res)) res = tmp;
return res;
}
}st;
inline int read() {
int x = 0, f = 1; char c = getchar();
while(!isdigit(c)) {if(c == '-') f = -1; c = getchar();}
while(isdigit(c)) x = x * 10 + c - '0', c = getchar();
return x * f;
}
signed main() {
// freopen(".in", "r", stdin);
// freopen(".out", "w", stdout);
n = read();
st.build(1, 1, 80000);
int lst = 0;
while(n--) {
int op = read();
if(op == 0) {
int x = (read() + lst + 39988) % 39989 + 1;
lst = st.query(1, x);
printf("%lld\n", lst);
}
else {
int ax = read(), ay = read(), bx = read(), by = read();
ax = (ax + lst + 39988) % 39989 + 1; bx = (bx + lst + 39988) % 39989 + 1;
ay = (ay + lst + 999999999) % 1000000000 + 1; by = (by + lst + 999999999) % 1000000000 + 1;
if(ax > bx) swap(ax, bx), swap(ay, by);
st.addline(ax, ay, bx, by);
st.modify(1, ax, bx, len);
}
}
return 0;
}
线段树分裂
待续
线段树维护单调栈
待续
势能线段树
待续
吉司机线段树
待续

浙公网安备 33010602011771号