Appearance
zkw 线段树
普通线段树是一棵二叉树,zkw 线段树是一棵满二叉树,同样采用堆式建树。因为其满二叉树的性质,使得它能容易地获取叶子节点编号,也可以通过位运算简单地获取区间。具有代码短,常数小的优点。
了解即可,实用价值有,但是不大。
以维护加法为例。
建树:
先根据序列长度确定叶子数,因为区间询问的要求,所以维护的序列要从第二个叶子开始,即叶子数要大于序列长度。再根据堆式建树父节点等于子节点编号除以 push_up 即可。
cpp
void build(int m, vector<int> &a) {
for (n = 1; n <= m; n <<= 1);
for(int i = n + 1; i <= n + m; i++) tr[i].sum = a[i - n];
for(int i = n - 1; i >= 1; i--){
tr[i].sum = tr[i << 1].sum + tr[i << 1 | 1].sum;
}
}单点修改:
修改叶子后,自底向上 push_up 即可。
cpp
void update(int x, int v) {
tr[n + x].sum += v;
for (int cur = (n + x) >> 1; cur; cur >>= 1)
tr[cur].sum = tr[cur << 1].sum + tr[cur << 1 | 1].sum;
}区间修改:
先将左右端点分别变为
因为 zkw 线段树是自下而上更新的,所以不能使用懒标记(无法下传),只能使用标记永久化。
cpp
void update(int l, int r, int v) {
for (l = n + l - 1, r = n + r + 1; (l | 1) != r; l >>= 1, r >>= 1) {
tr[l ^ 1].tag += (l & 1 ^ 1) * v;
tr[r ^ 1].tag += (r & 1) * v;
}
}单点询问:
从叶子层不断跳父节点并加上标记即可。
cpp
int query(int x) {
int res = tr[n + x];
for (int cur = n + x; cur; cur >>= 1) res += tr[cur].tag;
return res;
}区间询问:
和区间修改相同,将修改标记改为累计标记和区间答案即可。
cpp
int query(int l, int r) {
int res = 0;
for (l = n + l - 1, r = n + r + 1; (l | 1) != r; l >>= 1, r >>= 1) {
if (l & 1 ^ 1)
res += tr[l ^ 1].sum + tr[l ^ 1].tag;
if (r & 1)
res += tr[r ^ 1].sum + tr[r ^ 1].tag;
}
return res;
}zkw 线段树做单点修改、区间询问,没有问题。也容易实现线段树上二分。瓶颈在于无法下传标记,在区间修改上局限性较大,同时因为形态是固定的,所以当然没有可持久化版本。
猫树
名字由来似乎是首次在国内 OI 届引入此数据结构的选手的网名。
猫树是序列树和 zkw 线段树的结合。猫树用于解决静态的区间询问问题,时空复杂度和 ST 表一致,均为预处理
对于 zkw 线段树的两个叶子节点
若线段树上每个节点都维护了表示区间的一个前缀序列和一个后缀序列,那么把 push_up 维护即可,每个节点都储存了区间长度的数据,所以时空复杂度均为
对于 LCA,因为 zkw 是满二叉树,所以 LCA = l >> (log[l ^ r] + 1)。
cpp
struct node {
vector<int> pre, suf;
};
struct cat_segment {
node tr[N << 1];
int L[N << 1], R[N << 1], id[N];
void push_up(int t) {
int res = 0;
for (auto u : tr[t << 1].pre) {
res = max(res, u);
tr[t].pre.push_back(res);
}
for (auto u : tr[t << 1 | 1].pre) {
res = max(res, u);
tr[t].pre.push_back(res);
}
res = 0;
for (auto u : tr[t << 1 | 1].suf) {
res = max(res, u);
tr[t].suf.push_back(res);
}
for (auto u : tr[t << 1].suf) {
res = max(res, u);
tr[t].suf.push_back(res);
}
}
void build(int t, int l, int r, vector<int> &a) {
int mid = l + r >> 1;
L[t] = l, R[t] = r;
tr[t].pre.reserve(r - l + 1);
tr[t].suf.reserve(r - l + 1);
if (l == r) {
id[l] = t;
if (l >= a.size()) {
tr[t].pre.push_back(-1);
tr[t].suf.push_back(-1);
} else {
tr[t].pre.push_back(a[l]);
tr[t].suf.push_back(a[r]);
}
return;
}
build(t << 1, l, mid, a);
build(t << 1 | 1, mid + 1, r, a);
push_up(t);
}
void init(int n, vector<int> &a) {
int m = 1;
while (m < n)
m <<= 1;
build(1, 1, m, a);
}
int Lca(int x, int y) { return x >> (__lg(x ^ y) + 1); }
int query(int l, int r) {
if (l == r)
return tr[id[l]].pre[0];
int t = Lca(id[l], id[r]);
auto &suf = tr[t << 1].suf;
auto &pre = tr[t << 1 | 1].pre;
int mid = L[t] + R[t] >> 1;
int res1 = suf[mid - l], res2 = pre[r - (mid + 1)];
return max(res1, res2);
}
} tree;因为要同时维护前后缀,所以常数是 ST 表的两倍,且猫树要先将原序列补全成
时空复杂度至少是 ST 的四倍常数。
但是猫树比 ST 表功能更加强大,猫树能够维护不可交信息,ST 表只能维护可交信息。ST 表查询时,会将区间
李超线段树
李超线段树只用于维护在一个集合中动态插入线段,询问
李超树本质是一棵动态开点的线段树,类似标记永久化,其节点维护一条“主导线段” 表示定义域在节点对应区间
询问:
李超树仅支持单点询问,将一路递归经过的标记合并起来即可(即用覆盖了
下文默认维护最大值。
考虑“主导线段”有什么性质,对于另一条线段
询问只涉及完全覆盖
cpp
int query(int p, int l, int r, int x) {
if (!p)
return 0;
int mid = l + r >> 1, res;
if (l == r)
return tr[p].id;
if (x <= mid)
res = query(ls(p), l, mid, x);
else
res = query(rs(p), mid + 1, r, x);
if (!res)
res = tr[p].id;
if (line[tr[p].id].val(x) > line[res].val(x))
res = tr[p].id;
else if (line[tr[p].id].val(x) == line[res].val(x) && tr[p].id < res)
res = tr[p].id;
return res;
}插入:
先把插入的线段的定义域拆分成线段树上的
插入涉及到
cpp
void update(int &p, int l, int r, int id) {
int mid = l + r >> 1;
if (!tr[p].id) {
if (!p)
p = ++idx;
tr[p].id = id;
return;
}
if (line[id].val(mid) > line[tr[p].id].val(mid)) {
swap(tr[p].id, id);
}
if(l == r) return ;
if (line[id].k < line[tr[p].id].k) {
update(ls(p), l, mid, id);
} else if (line[id].k > line[tr[p].id].k) {
update(rs(p), mid + 1, r, id);
}
}
void insert(int &p, int l, int r, int id) {
int mid = l + r >> 1;
if (!p)
p = ++idx;
if (line[id].l <= l && r <= line[id].r) {
update(p, l, r, id);
return;
}
if (line[id].l <= mid)
insert(ls(p), l, mid, id);
if (line[id].r > mid)
insert(rs(p), mid + 1, r, id);
}因为李超树是动态开点线段树实现的,所以当维护线段定义域为实数时,要离散化。否则递归层数过多会导致空间爆炸。
另一种“主导线段定义”:
另一种“主导线段”的定义,只有当
cpp
void update(int &p, int l, int r, int id) {
int mid = l + r >> 1;
if (!tr[p].id) {
if (!p)
p = ++idx;
tr[p].id = id;
return;
}
ldb now_l = line[id].val(l), now_r = line[id].val(r),
pre_l = line[tr[p].id].val(l), pre_r = line[tr[p].id].val(r);
if (now_l <= pre_l && now_r <= pre_r)
return;
if (now_l > pre_l && now_r > pre_r) {
tr[p].id = id;
return;
}
if (l == r)
return;
update(ls(p), l, mid, id), update(rs(p), mid + 1, r, id);
}
void insert(int &p, int l, int r, int id) {
int mid = l + r >> 1;
if (!p)
p = ++idx;
if (line[id].l <= l && r <= line[id].r) {
update(p, l, r, id);
return;
}
if (line[id].l <= mid)
insert(ls(p), l, mid, id);
if (line[id].r > mid)
insert(rs(p), mid + 1, r, id);
}可持久化线段树
动态开点线段树:
因为是可持久化线段树的前置知识(严格需要),略提一下。
一般线段树建树时,将所有区间都在节点上表示了出来。但是事实上,对于一个节点
即:只有在访问到某个区间时,才创建对应的节点编号。如果只是将朴素线段树,以单点加、区间和为例替换成动态开点线段树,只需在函数体中,把节点编号改为引用,并把节点的左右儿子显式地存储在节点的结构体中,而不是沿用堆式建树的隐式儿子表示。当当前递归区间的节点不存在时,将其赋予新的编号即可。询问时若遇到空节点则表示没有内容,返回空即可。
cpp
void update(int &p, int l, int r, int x, int v) {
int mid = l + r >> 1;
if (!p)
p = ++idx;
if (x <= l && r <= y) {
tr[p].sum += v;
return;
}
if (x <= mid)
update(ls(p), l, mid, x, v);
else
update(rs(p), mid + 1, r, x, v);
push_up(t);
}
int query(int p, int l, int r, int x, int y) {
int mid = l + r >> 1, res = 0;
if (!p)
return 0;
if (x <= l && r <= y) {
return tr[p].sum;
}
if (x <= mid)
res += query(ls(p), l, mid, x, y);
if (y > mid)
res += query(rs(p), mid + 1, r, x, y);
return res;
}因为动态开点只涉及到需要的节点,所以可以支持维护序列长度很大的情况,每一次操作最多涉及线段树上
因为动态开点每次最劣会创建访问到的区间数量的节点数,所以动态开点的空间复杂度是
标记永久化:
因为是可持久化线段树的前置知识(不严格需要),略提一下。
在做线段树区间修改的时候,因为线段树上被修改区间完全覆盖的节点区间过多,
标记永久化意味着不能下传,与懒标记相比,对于不能累计的标记,较难处理。
可持久化数据结构意味可以维护历史版本,即第
因为线段树是自上而下的数据结构,对于一个确定的子树,其维护的信息是确定的。所以若节点
主席树:
可持久化权值(值域)线段树,一般被称作主席树。
cpp
node tr[N << 5];
int idx, root[N];
void up(Node &t, Node l, Node r) { t.sum = l.sum + r.sum; }
void push_up(int k) { up(tr[k], tr[ls(k)], tr[rs(k)]); }
void build(int &p, int l, int r, vector<int> &a) {
int mid = l + r >> 1;
if (!p)
p = ++idx;
if (l == r) {
tr[p].v = a[l];
return;
}
build(ls(p), l, mid, a);
build(rs(p), mid + 1, r, a);
}
void update(int p, int &q, int l, int r, int x) {
int mid = l + r >> 1;
if (!q)
q = ++idx;
tr[q].sum = tr[p].sum;
if (l == r) {
tr[q].sum ++;
return;
}
if (x <= mid)
rs(q) = rs(p), update(ls(p), ls(q), l, mid, x);
else
ls(q) = ls(p), update(rs(p), rs(q), mid + 1, r, x);
push_up(q);
}
int query(int p, int l, int r, int x) {
int mid = l + r >> 1;
if (l == r)
return tr[p].v;
if (x <= mid)
return query(ls(p), l, mid, x);
return query(rs(p), mid + 1, r, x);
}区间修改:
若第 push_down,常数略大,容易爆空间。 使用标记永久化,会有更好的效果。
Trick:
若第
吉司机线段树
历史最值线段树,经典的“势能线段树”,暂略。
线段树合并
线段树合并就是将两棵线段树上表示相同区间的信息合并起来,所以时间复杂度显然与节点数相关,所以线段树合并通常针对动态开点线段树,只合并需要用到的点。
实现不难理解,对于合并 push_up 更新祖先节点即可。
以区间和为例:
cpp
int merge(int p, int q, int l, int r) {
int mid = l + r >> 1;
if (!p)
return q;
if (!q)
return p;
if (l == r) {
tr[p].sum += tr[q].sum;
return p;
}
ls(p) = merge(ls(p), ls(q), l, mid);
rs(p) = merge(rs(p), rs(q), mid + 1, r);
push_up(p);
return p;
}线段树合并的总时间复杂度为
线段树合并从应用角度而言,通常用于合并权值线段树,并且更多地应用在树上,用于合并子树信息。
线段树分裂
线段树分裂是线段树合并的逆过程,但是有的信息是不可逆的(例如最值)。线段树分裂需要一个分裂参数,表示把大于这个参数的部分分裂到一棵线段树上,小于这个参数的部分保留在原线段树上。
以权值线段树统计数字出现次数为例:保留前
cpp
void split(int p, int &q, int k) {
if (!p)
return;
if (!q)
q = ++idx;
if (k > tr[ls(p)].sum)
split(rs(p), rs(q), k - tr[ls(p)].sum);
else
swap(rs(p), rs(q));
if (k < tr[ls(p)].sum)
split(ls(p), ls(q), k);
tr[q].sum = tr[p].sum - k;
tr[p].sum = k;
}线段树分裂的时间复杂度容易发现是单次
线段树优化建图
线段树优化建图用于将一个点和一个区间内的所有点连边的问题,例如将
根据任意一个区间
若
若
但是容易发现,这不能在同一棵线段树上进行。所以建两棵线段树,一棵向儿子节点连边,一棵向父亲节点连边。再在两棵线段树的叶子节点(也就是单个点)之间连权值为
以最短路为例:
cpp
struct segment {
int idx;
int id1[N], id2[N];
void build1(int t, int l, int r) {
idx = max(idx, t);
int mid = l + r >> 1;
if (l == r) {
id1[l] = t;
return;
}
add(t, t << 1, 0);
add(t, t << 1 | 1, 0);
build1(t << 1, l, mid);
build1(t << 1 | 1, mid + 1, r);
}
void build2(int t, int l, int r) {
int mid = l + r >> 1;
if (l == r) {
id2[l] = t + idx;
return;
}
add((t << 1) + idx, t + idx, 0);
add((t << 1 | 1) + idx, t + idx, 0);
build2(t << 1, l, mid);
build2(t << 1 | 1, mid + 1, r);
}
void link1(int t, int l, int r, int x, int y, int u, int w) {
int mid = l + r >> 1;
if (x <= l && r <= y) {
add(id2[u], t, w);
return;
}
if (x <= mid)
link1(t << 1, l, mid, x, y, u, w);
if (y > mid)
link1(t << 1 | 1, mid + 1, r, x, y, u, w);
}
void link2(int t, int l, int r, int x, int y, int u, int w) {
int mid = l + r >> 1;
if (x <= l && r <= y) {
add(t + idx, id1[u], w);
return;
}
if (x <= mid)
link2(t << 1, l, mid, x, y, u, w);
if (y > mid)
link2(t << 1 | 1, mid + 1, r, x, y, u, w);
}
} tree;
signed main() {
int n, q, s;
cin >> n >> q >> s;
tree.build1(1, 1, n);
tree.build2(1, 1, n);
int i;
For(i, 1, n) add(tree.id1[i], tree.id2[i], 0),
add(tree.id2[i], tree.id1[i], 0);
while (q--) {
int op;
cin >> op;
if (op == 1) {
int u, v, w;
cin >> u >> v >> w;
add(tree.id1[u], tree.id1[v], w);
add(tree.id2[u], tree.id2[v], w);
add(tree.id1[u], tree.id2[v], w);
add(tree.id2[u], tree.id2[v], w);
}
if (op == 2) {
int u, l, r, w;
cin >> u >> l >> r >> w;
tree.link1(1, 1, n, l, r, u, w);
}
if (op == 3) {
int u, l, r, w;
cin >> u >> l >> r >> w;
tree.link2(1, 1, n, l, r, u, w);
}
}
return 0;
}线段树优化建图本质上只是一个建图的工具,结合到具体题目,还是需要一定的图论知识。
线段树分治
线段树分治一定程度上类似线段树优化建图,本质还是利用线段树的性质:一个区间
对于这一段时间,可以拆分成
把一段时间
一个线段树分治的一个经典应用是和可撤销并查集的结合使用,用来维护点的连通性。
cpp
struct sege {
int n;
vector<int> tr[N << 2];
void update(int t, int l, int r, int x, int y, int id) {
int mid = l + r >> 1;
if (x <= l && r <= y) {
tr[t].pb(id);
return;
}
if (x <= mid)
update(t << 1, l, mid, x, y, id);
if (y > mid)
update(t << 1 | 1, mid + 1, r, x, y, id);
}
void solve(int t, int l, int r) {
int his = dsu.History();
for (auto u : tr[t]) {
dsu.merge(edge[u].x, edge[u].y, l, r);
}
if (l < r) {
int mid = l + r >> 1;
solve(t << 1, l, mid);
solve(t << 1 | 1, mid + 1, r);
}
while (dsu.History() != his)
dsu.roll(); // 可撤销并查集部分
}
} tree;线段树上二分
对于一个单调的区间询问,例如正权区间内第一个前缀和大于
如果询问不是全局的,同样地,将询问区间拆分成线段树上的
时间复杂度:
