线段树

引入

线段树是算法竞赛中常用的用来维护 区间信息 的数据结构。

线段树可以在 \(O(\log N)\) 的时间复杂度内实现单点修改、区间修改、区间查询(区间求和,求区间最大值,求区间最小值)等操作。

线段树

线段树的基本结构与建树

过程

线段树将每个长度不为 \(1\) 的区间划分成左右两个区间递归求解,把整个线段划分为一个树形结构,通过合并左右两区间信息来求得该区间的信息。这种数据结构可以方便的进行大部分的区间操作。

有个大小为 \(5\) 的数组 \(a=\{10,11,12,13,14\}\),要将其转化为线段树,有以下做法:设线段树的根节点编号为 \(1\),用数组 \(d\) 来保存我们的线段树,\(d_i\) 用来保存线段树上编号为 \(i\) 的节点的值(这里每个节点所维护的值就是这个节点所表示的区间总和)。

我们先给出这棵线段树的形态,如图所示:

图中每个节点中用红色字体标明的区间,表示该节点管辖的 \(a\) 数组上的位置区间。如 \(d_1\) 所管辖的区间就是 \([1,5]\)(\(a_1,a_2, \cdots ,a_5\)),即 \(d_1\) 所保存的值是 \(a_1+a_2+ \cdots +a_5\),\(d_1=60\) 表示的是 \(a_1+a_2+ \cdots +a_5=60\)。

通过观察不难发现,\(d_i\) 的左儿子节点就是 \(d_{2\times i}\),\(d_i\) 的右儿子节点就是 \(d_{2\times i+1}\)。如果 \(d_i\) 表示的是区间 \([s,t]\)(即 \(d_i=a_s+a_{s+1}+ \cdots +a_t\))的话,那么 \(d_i\) 的左儿子节点表示的是区间 \([ s, \frac{s+t}{2} ]\),\(d_i\) 的右儿子表示的是区间 \([ \frac{s+t}{2} +1,t ]\)。

在实现时,我们考虑递归建树。设当前的根节点为 \(p\),如果根节点管辖的区间长度已经是 \(1\),则可以直接根据 \(a\) 数组上相应位置的值初始化该节点。否则我们将该区间从中点处分割为两个子区间,分别进入左右子节点递归建树,最后合并两个子节点的信息。

实现

此处给出代码实现,可参考注释理解:

void build(int s, int t, int p) {
// 对 [s,t] 区间建立线段树,当前根的编号为 p
if (s == t) {
d[p] = a[s];
return;
}
int m = s + ((t - s) >> 1);
// 移位运算符的优先级小于加减法,所以加上括号
// 如果写成 (s + t) >> 1 可能会超出 int 范围
build(s, m, p * 2), build(m + 1, t, p * 2 + 1);
// 递归对左右区间建树
d[p] = d[p * 2] + d[(p * 2) + 1];
}

关于线段树的空间:如果采用堆式存储(\(2p\) 是 \(p\) 的左儿子,\(2p+1\) 是 \(p\) 的右儿子),若有 \(n\) 个叶子结点,则 d 数组的范围最大为 \(2^{\left\lceil\log{n}\right\rceil+1}\)。

分析:容易知道线段树的深度是 \(\left\lceil\log{n}\right\rceil\) 的,则在堆式储存情况下叶子节点(包括无用的叶子节点)数量为 \(2^{\left\lceil\log{n}\right\rceil}\) 个,又由于其为一棵完全二叉树,则其总节点个数 \(2^{\left\lceil\log{n}\right\rceil+1}-1\)。当然如果你懒得计算的话可以直接把数组长度设为 \(4n\),因为 \(\frac{2^{\left\lceil\log{n}\right\rceil+1}-1}{n}\) 的最大值在 \(n=2^{x}+1(x\in N_{+})\) 时取到,此时节点数为 \(2^{\left\lceil\log{n}\right\rceil+1}-1=2^{x+2}-1=4n-5\)。

而堆式存储存在无用的叶子节点,可以考虑使用内存池管理线段树节点,每当需要新建节点时从池中获取。自底向上考虑,必有每两个底层节点合并为一个上层节点,因此可以类似哈夫曼树地证明,如果有 \(n\) 个叶子节点,这样的线段树总共有 \(2n-1\) 个节点。其空间效率优于堆式存储,并且是可能的最优情况。

这样的线段树可以自底向上维护,参考「统计的力量 - 张昆玮」。

线段树的区间查询

过程

区间查询,比如求区间 \([l,r]\) 的总和(即 \(a_l+a_{l+1}+ \cdots +a_r\))、求区间最大值/最小值等操作。

仍然以最开始的图为例,如果要查询区间 \([1,5]\) 的和,那直接获取 \(d_1\) 的值(\(60\))即可。

如果要查询的区间为 \([3,5]\),此时就不能直接获取区间的值,但是 \([3,5]\) 可以拆成 \([3,3]\) 和 \([4,5]\),可以通过合并这两个区间的答案来求得这个区间的答案。

一般地,如果要查询的区间是 \([l,r]\),则可以将其拆成最多为 \(O(\log n)\) 个 极大 的区间,合并这些区间即可求出 \([l,r]\) 的答案。

实现

此处给出代码实现,可参考注释理解:

int getsum(int l, int r, int s, int t, int p) {
// [l, r] 为查询区间, [s, t] 为当前节点包含的区间, p 为当前节点的编号
if (l <= s && t <= r)
return d[p]; // 当前区间为询问区间的子集时直接返回当前区间的和
int m = s + ((t - s) >> 1), sum = 0;
if (l <= m) sum += getsum(l, r, s, m, p * 2);
// 如果左儿子代表的区间 [s, m] 与询问区间有交集, 则递归查询左儿子
if (r > m) sum += getsum(l, r, m + 1, t, p * 2 + 1);
// 如果右儿子代表的区间 [m + 1, t] 与询问区间有交集, 则递归查询右儿子
return sum;
}

线段树的区间修改与懒惰标记

过程

如果要求修改区间 \([l,r]\),把所有包含在区间 \([l,r]\) 中的节点都遍历一次、修改一次,时间复杂度无法承受。我们这里要引入一个叫做 「懒惰标记」 的东西。

懒惰标记,简单来说,就是通过延迟对节点信息的更改,从而减少可能不必要的操作次数。每次执行修改时,我们通过打标记的方法表明该节点对应的区间在某一次操作中被更改,但不更新该节点的子节点的信息。实质性的修改则在下一次访问带有标记的节点时才进行。

仍然以最开始的图为例,我们将执行若干次给区间内的数加上一个值的操作。我们现在给每个节点增加一个 \(t_i\),表示该节点带的标记值。

最开始时的情况是这样的(为了节省空间,这里不再展示每个节点管辖的区间):

现在我们准备给 \([3,5]\) 上的每个数都加上 \(5\)。根据前面区间查询的经验,我们很快找到了两个极大区间 \([3,3]\) 和 \([4,5]\)(分别对应线段树上的 \(3\) 号点和 \(5\) 号点)。

我们直接在这两个节点上进行修改,并给它们打上标记:

我们发现,\(3\) 号节点的信息虽然被修改了(因为该区间管辖两个数,所以 \(d_3\) 加上的数是 \(5 \times 2=10\)),但它的两个子节点却还没更新,仍然保留着修改之前的信息。不过不用担心,虽然修改目前还没进行,但当我们要查询这两个子节点的信息时,我们会利用标记修改这两个子节点的信息,使查询的结果依旧准确。

接下来我们查询一下 \([4,4]\) 区间上各数字的和。

我们通过递归找到 \([4,5]\) 区间,发现该区间并非我们的目标区间,且该区间上还存在标记。这时候就到标记下放的时间了。我们将该区间的两个子区间的信息更新,并清除该区间上的标记。

现在 \(6\)、\(7\) 两个节点的值变成了最新的值,查询的结果也是准确的。

实现

接下来给出在存在标记的情况下,区间修改和查询操作的参考实现。

区间修改(区间加上某个值):

void update(int l, int r, int c, int s, int t, int p) {
// [l, r] 为修改区间, c 为被修改的元素的变化量, [s, t] 为当前节点包含的区间, p
// 为当前节点的编号
if (l <= s && t <= r) {
d[p] += (t - s + 1) * c, b[p] += c;
return;
} // 当前区间为修改区间的子集时直接修改当前节点的值,然后打标记,结束修改
int m = s + ((t - s) >> 1);
if (b[p] && s != t) {
// 如果当前节点的懒标记非空,则更新当前节点两个子节点的值和懒标记值
d[p * 2] += b[p] * (m - s + 1), d[p * 2 + 1] += b[p] * (t - m);
b[p * 2] += b[p], b[p * 2 + 1] += b[p]; // 将标记下传给子节点
b[p] = 0; // 清空当前节点的标记
}
if (l <= m) update(l, r, c, s, m, p * 2);
if (r > m) update(l, r, c, m + 1, t, p * 2 + 1);
d[p] = d[p * 2] + d[p * 2 + 1];
}

区间查询(区间求和):

int getsum(int l, int r, int s, int t, int p) {
// [l, r] 为查询区间, [s, t] 为当前节点包含的区间, p 为当前节点的编号
if (l <= s && t <= r) return d[p];
// 当前区间为询问区间的子集时直接返回当前区间的和
int m = s + ((t - s) >> 1);
if (b[p]) {
// 如果当前节点的懒标记非空,则更新当前节点两个子节点的值和懒标记值
d[p * 2] += b[p] * (m - s + 1), d[p * 2 + 1] += b[p] * (t - m);
b[p * 2] += b[p], b[p * 2 + 1] += b[p]; // 将标记下传给子节点
b[p] = 0; // 清空当前节点的标记
}
int sum = 0;
if (l <= m) sum = getsum(l, r, s, m, p * 2);
if (r > m) sum += getsum(l, r, m + 1, t, p * 2 + 1);
return sum;
}

如果你是要实现区间修改为某一个值而不是加上某一个值的话,代码如下:

void update(int l, int r, int c, int s, int t, int p) {
if (l <= s && t <= r) {
d[p] = (t - s + 1) * c, b[p] = c, v[p] = 1;
return;
}
int m = s + ((t - s) >> 1);
// 额外数组储存是否修改值
if (v[p]) {
d[p * 2] = b[p] * (m - s + 1), d[p * 2 + 1] = b[p] * (t - m);
b[p * 2] = b[p * 2 + 1] = b[p];
v[p * 2] = v[p * 2 + 1] = 1;
v[p] = 0;
}
if (l <= m) update(l, r, c, s, m, p * 2);
if (r > m) update(l, r, c, m + 1, t, p * 2 + 1);
d[p] = d[p * 2] + d[p * 2 + 1];
} int getsum(int l, int r, int s, int t, int p) {
if (l <= s && t <= r) return d[p];
int m = s + ((t - s) >> 1);
if (v[p]) {
d[p * 2] = b[p] * (m - s + 1), d[p * 2 + 1] = b[p] * (t - m);
b[p * 2] = b[p * 2 + 1] = b[p];
v[p * 2] = v[p * 2 + 1] = 1;
v[p] = 0;
}
int sum = 0;
if (l <= m) sum = getsum(l, r, s, m, p * 2);
if (r > m) sum += getsum(l, r, m + 1, t, p * 2 + 1);
return sum;
}

动态开点线段树

前面讲到堆式储存的情况下,需要给线段树开 \(4n\) 大小的数组。为了节省空间,我们可以不一次性建好树,而是在最初只建立一个根结点代表整个区间。当我们需要访问某个子区间时,才建立代表这个区间的子结点。这样我们不再使用 \(2p\) 和 \(2p+1\) 代表 \(p\) 结点的儿子,而是用 \(\text{ls}\) 和 \(\text{rs}\) 记录儿子的编号。总之,动态开点线段树的核心思想就是:结点只有在有需要的时候才被创建

单次操作的时间复杂度是不变的,为 \(O(\log n)\)。由于每次操作都有可能创建并访问全新的一系列结点,因此 \(m\) 次单点操作后结点的数量规模是 \(O(m\log n)\)。最多也只需要 \(2n-1\) 个结点,没有浪费。

单点修改:

// root 表示整棵线段树的根结点;cnt 表示当前结点个数
int n, cnt, root;
int sum[n * 2], ls[n * 2], rs[n * 2]; // 用法:update(root, 1, n, x, f); 其中 x 为待修改节点的编号
void update(int& p, int s, int t, int x, int f) { // 引用传参
if (!p) p = ++cnt; // 当结点为空时,创建一个新的结点
if (s == t) {
sum[p] += f;
return;
}
int m = s + ((t - s) >> 1);
if (x <= m)
update(ls[p], s, m, x, f);
else
update(rs[p], m + 1, t, x, f);
sum[p] = sum[ls[p]] + sum[rs[p]]; // pushup
}

区间询问:

// 用法:query(root, 1, n, l, r);
int query(int p, int s, int t, int l, int r) {
if (!p) return 0; // 如果结点为空,返回 0
if (s >= l && t <= r) return sum[p];
int m = s + ((t - s) >> 1), ans = 0;
if (l <= m) ans += query(ls[p], s, m, l, r);
if (r > m) ans += query(rs[p], m + 1, t, l, r);
return ans;
}

区间修改也是一样的,不过下放标记时要注意如果缺少孩子,就直接创建一个新的孩子。或者使用标记永久化技巧。

一些优化

这里总结几个线段树的优化:

  • 在叶子节点处无需下放懒惰标记,所以懒惰标记可以不下传到叶子节点。

  • 下放懒惰标记可以写一个专门的函数 pushdown,从儿子节点更新当前节点也可以写一个专门的函数 maintain(或者对称地用 pushup),降低代码编写难度。

  • 标记永久化:如果确定懒惰标记不会在中途被加到溢出(即超过了该类型数据所能表示的最大范围),那么就可以将标记永久化。标记永久化可以避免下传懒惰标记,只需在进行询问时把标记的影响加到答案当中,从而降低程序常数。具体如何处理与题目特性相关,需结合题目来写。这也是树套树和可持久化数据结构中会用到的一种技巧。

C++ 模板

\(\text{SegTreeLazyRangeAdd}\) 可以区间加/求和的线段树模板:

#include <bits/stdc++.h>
using namespace std; template <typename T>
class SegTreeLazyRangeAdd {
vector<T> tree, lazy;
vector<T> *arr;
int n, root, n4, end; void maintain(int cl, int cr, int p) {
int cm = cl + (cr - cl) / 2;
if (cl != cr && lazy[p]) {
lazy[p * 2] += lazy[p];
lazy[p * 2 + 1] += lazy[p];
tree[p * 2] += lazy[p] * (cm - cl + 1);
tree[p * 2 + 1] += lazy[p] * (cr - cm);
lazy[p] = 0;
}
} T range_sum(int l, int r, int cl, int cr, int p) {
if (l <= cl && cr <= r) return tree[p];
int m = cl + (cr - cl) / 2;
T sum = 0;
maintain(cl, cr, p);
if (l <= m) sum += range_sum(l, r, cl, m, p * 2);
if (r > m) sum += range_sum(l, r, m + 1, cr, p * 2 + 1);
return sum;
} void range_add(int l, int r, T val, int cl, int cr, int p) {
if (l <= cl && cr <= r) {
lazy[p] += val;
tree[p] += (cr - cl + 1) * val;
return;
}
int m = cl + (cr - cl) / 2;
maintain(cl, cr, p);
if (l <= m) range_add(l, r, val, cl, m, p * 2);
if (r > m) range_add(l, r, val, m + 1, cr, p * 2 + 1);
tree[p] = tree[p * 2] + tree[p * 2 + 1];
} void build(int s, int t, int p) {
if (s == t) {
tree[p] = (*arr)[s];
return;
}
int m = s + (t - s) / 2;
build(s, m, p * 2);
build(m + 1, t, p * 2 + 1);
tree[p] = tree[p * 2] + tree[p * 2 + 1];
} public:
explicit SegTreeLazyRangeAdd<T>(vector<T> v) {
n = v.size();
n4 = n * 4;
tree = vector<T>(n4, 0);
lazy = vector<T>(n4, 0);
arr = &v;
end = n - 1;
root = 1;
build(0, end, 1);
arr = nullptr;
} void show(int p, int depth = 0) {
if (p > n4 || tree[p] == 0) return;
show(p * 2, depth + 1);
for (int i = 0; i < depth; ++i) putchar('\t');
printf("%d:%d\n", tree[p], lazy[p]);
show(p * 2 + 1, depth + 1);
} T range_sum(int l, int r) { return range_sum(l, r, 0, end, root); } void range_add(int l, int r, int val) { range_add(l, r, val, 0, end, root); }
};

\(\text{SegTreeLazyRangeSet}\) 可以区间修改/求和的线段树模板:

#include <bits/stdc++.h>
using namespace std; template <typename T>
class SegTreeLazyRangeSet {
vector<T> tree, lazy;
vector<T> *arr;
int n, root, n4, end; void maintain(int cl, int cr, int p) {
int cm = cl + (cr - cl) / 2;
if (cl != cr && lazy[p]) {
lazy[p * 2] = lazy[p];
lazy[p * 2 + 1] = lazy[p];
tree[p * 2] = lazy[p] * (cm - cl + 1);
tree[p * 2 + 1] = lazy[p] * (cr - cm);
lazy[p] = 0;
}
} T range_sum(int l, int r, int cl, int cr, int p) {
if (l <= cl && cr <= r) return tree[p];
int m = cl + (cr - cl) / 2;
T sum = 0;
maintain(cl, cr, p);
if (l <= m) sum += range_sum(l, r, cl, m, p * 2);
if (r > m) sum += range_sum(l, r, m + 1, cr, p * 2 + 1);
return sum;
} void range_set(int l, int r, T val, int cl, int cr, int p) {
if (l <= cl && cr <= r) {
lazy[p] = val;
tree[p] = (cr - cl + 1) * val;
return;
}
int m = cl + (cr - cl) / 2;
maintain(cl, cr, p);
if (l <= m) range_set(l, r, val, cl, m, p * 2);
if (r > m) range_set(l, r, val, m + 1, cr, p * 2 + 1);
tree[p] = tree[p * 2] + tree[p * 2 + 1];
} void build(int s, int t, int p) {
if (s == t) {
tree[p] = (*arr)[s];
return;
}
int m = s + (t - s) / 2;
build(s, m, p * 2);
build(m + 1, t, p * 2 + 1);
tree[p] = tree[p * 2] + tree[p * 2 + 1];
} public:
explicit SegTreeLazyRangeSet<T>(vector<T> v) {
n = v.size();
n4 = n * 4;
tree = vector<T>(n4, 0);
lazy = vector<T>(n4, 0);
arr = &v;
end = n - 1;
root = 1;
build(0, end, 1);
arr = nullptr;
} void show(int p, int depth = 0) {
if (p > n4 || tree[p] == 0) return;
show(p * 2, depth + 1);
for (int i = 0; i < depth; ++i) putchar('\t');
printf("%d:%d\n", tree[p], lazy[p]);
show(p * 2 + 1, depth + 1);
} T range_sum(int l, int r) { return range_sum(l, r, 0, end, root); } void range_set(int l, int r, int val) { range_set(l, r, val, 0, end, root); }
};

例题

luogu P3372【模板】线段树 1

已知一个数列,你需要进行下面两种操作:

  • 将某区间每一个数加上 \(k\)。
  • 求出某区间每一个数的和。

参考代码:

#include <iostream>
typedef long long LL;
LL n, a[100005], d[270000], b[270000]; void build(LL l, LL r, LL p) { // l:区间左端点 r:区间右端点 p:节点标号
if (l == r) {
d[p] = a[l]; // 将节点赋值
return;
}
LL m = l + ((r - l) >> 1);
build(l, m, p << 1), build(m + 1, r, (p << 1) | 1); // 分别建立子树
d[p] = d[p << 1] + d[(p << 1) | 1];
} void update(LL l, LL r, LL c, LL s, LL t, LL p) {
if (l <= s && t <= r) {
d[p] += (t - s + 1) * c, b[p] += c; // 如果区间被包含了,直接得出答案
return;
}
LL m = s + ((t - s) >> 1);
if (b[p])
d[p << 1] += b[p] * (m - s + 1), d[(p << 1) | 1] += b[p] * (t - m),
b[p << 1] += b[p], b[(p << 1) | 1] += b[p];
b[p] = 0;
if (l <= m)
update(l, r, c, s, m, p << 1); // 本行和下面的一行用来更新p*2和p*2+1的节点
if (r > m) update(l, r, c, m + 1, t, (p << 1) | 1);
d[p] = d[p << 1] + d[(p << 1) | 1]; // 计算该节点区间和
} LL getsum(LL l, LL r, LL s, LL t, LL p) {
if (l <= s && t <= r) return d[p];
LL m = s + ((t - s) >> 1);
if (b[p])
d[p << 1] += b[p] * (m - s + 1), d[(p << 1) | 1] += b[p] * (t - m),
b[p << 1] += b[p], b[(p << 1) | 1] += b[p];
b[p] = 0;
LL sum = 0;
if (l <= m)
sum =
getsum(l, r, s, m, p << 1); // 本行和下面的一行用来更新p*2和p*2+1的答案
if (r > m) sum += getsum(l, r, m + 1, t, (p << 1) | 1);
return sum;
} int main() {
std::ios::sync_with_stdio(0);
LL q, i1, i2, i3, i4;
std::cin >> n >> q;
for (LL i = 1; i <= n; i++) std::cin >> a[i];
build(1, n, 1);
while (q--) {
std::cin >> i1 >> i2 >> i3;
if (i1 == 2)
std::cout << getsum(i2, i3, 1, n, 1) << std::endl; // 直接调用操作函数
else
std::cin >> i4, update(i2, i3, i4, 1, n, 1);
}
return 0;
}

luogu P3373【模板】线段树 2

已知一个数列,你需要进行下面三种操作:

  • 将某区间每一个数乘上 \(x\)。
  • 将某区间每一个数加上 \(x\)。
  • 求出某区间每一个数的和。

参考代码:

#include <cstdio>
#define ll long long ll read() {
ll w = 1, q = 0;
char ch = ' ';
while (ch != '-' && (ch < '0' || ch > '9')) ch = getchar();
if (ch == '-') w = -1, ch = getchar();
while (ch >= '0' && ch <= '9') q = (ll)q * 10 + ch - '0', ch = getchar();
return (ll)w * q;
} int n, m;
ll mod;
ll a[100005], sum[400005], mul[400005], laz[400005]; void up(int i) { sum[i] = (sum[(i << 1)] + sum[(i << 1) | 1]) % mod; } void pd(int i, int s, int t) {
int l = (i << 1), r = (i << 1) | 1, mid = (s + t) >> 1;
if (mul[i] != 1) { // 懒标记传递,两个懒标记
mul[l] *= mul[i];
mul[l] %= mod;
mul[r] *= mul[i];
mul[r] %= mod;
laz[l] *= mul[i];
laz[l] %= mod;
laz[r] *= mul[i];
laz[r] %= mod;
sum[l] *= mul[i];
sum[l] %= mod;
sum[r] *= mul[i];
sum[r] %= mod;
mul[i] = 1;
}
if (laz[i]) { // 懒标记传递
sum[l] += laz[i] * (mid - s + 1);
sum[l] %= mod;
sum[r] += laz[i] * (t - mid);
sum[r] %= mod;
laz[l] += laz[i];
laz[l] %= mod;
laz[r] += laz[i];
laz[r] %= mod;
laz[i] = 0;
}
return;
} void build(int s, int t, int i) {
mul[i] = 1;
if (s == t) {
sum[i] = a[s];
return;
}
int mid = s + ((t - s) >> 1);
build(s, mid, i << 1); // 建树
build(mid + 1, t, (i << 1) | 1);
up(i);
} void chen(int l, int r, int s, int t, int i, ll z) {
int mid = s + ((t - s) >> 1);
if (l <= s && t <= r) {
mul[i] *= z;
mul[i] %= mod; // 这是取模的
laz[i] *= z;
laz[i] %= mod; // 这是取模的
sum[i] *= z;
sum[i] %= mod; // 这是取模的
return;
}
pd(i, s, t);
if (mid >= l) chen(l, r, s, mid, (i << 1), z);
if (mid + 1 <= r) chen(l, r, mid + 1, t, (i << 1) | 1, z);
up(i);
} void add(int l, int r, int s, int t, int i, ll z) {
int mid = s + ((t - s) >> 1);
if (l <= s && t <= r) {
sum[i] += z * (t - s + 1);
sum[i] %= mod; // 这是取模的
laz[i] += z;
laz[i] %= mod; // 这是取模的
return;
}
pd(i, s, t);
if (mid >= l) add(l, r, s, mid, (i << 1), z);
if (mid + 1 <= r) add(l, r, mid + 1, t, (i << 1) | 1, z);
up(i);
} ll getans(int l, int r, int s, int t,
int i) { // 得到答案,可以看下上面懒标记助于理解
int mid = s + ((t - s) >> 1);
ll tot = 0;
if (l <= s && t <= r) return sum[i];
pd(i, s, t);
if (mid >= l) tot += getans(l, r, s, mid, (i << 1));
tot %= mod;
if (mid + 1 <= r) tot += getans(l, r, mid + 1, t, (i << 1) | 1);
return tot % mod;
} int main() { // 读入
int i, j, x, y, bh;
ll z;
n = read();
m = read();
mod = read();
for (i = 1; i <= n; i++) a[i] = read();
build(1, n, 1); // 建树
for (i = 1; i <= m; i++) {
bh = read();
if (bh == 1) {
x = read();
y = read();
z = read();
chen(x, y, 1, n, 1, z);
} else if (bh == 2) {
x = read();
y = read();
z = read();
add(x, y, 1, n, 1, z);
} else if (bh == 3) {
x = read();
y = read();
printf("%lld\n", getans(x, y, 1, n, 1));
}
}
return 0;
}

HihoCoder 1078 线段树的区间修改

假设货架上从左到右摆放了 \(N\) 种商品,并且依次标号为 \(1\) 到 \(N\),其中标号为 \(i\) 的商品的价格为 \(Pi\)。小 \(\text{Hi}\) 的每次操作分为两种可能,第一种是修改价格:小 \(\text{Hi}\) 给出一段区间 \([L, R]\) 和一个新的价格 \(\textit{NewP}\),所有标号在这段区间中的商品的价格都变成 \(\textit{NewP}\)。第二种操作是询问:小 \(\text{Hi}\) 给出一段区间 \([L, R]\),而小 \(\text{Ho}\) 要做的便是计算出所有标号在这段区间中的商品的总价格,然后告诉小 \(\text{Hi}\)。

参考代码:

#include <iostream>

int n, a[100005], d[270000], b[270000];

void build(int l, int r, int p) {  // 建树
if (l == r) {
d[p] = a[l];
return;
}
int m = l + ((r - l) >> 1);
build(l, m, p << 1), build(m + 1, r, (p << 1) | 1);
d[p] = d[p << 1] + d[(p << 1) | 1];
} void update(int l, int r, int c, int s, int t,
int p) { // 更新,可以参考前面两个例题
if (l <= s && t <= r) {
d[p] = (t - s + 1) * c, b[p] = c;
return;
}
int m = s + ((t - s) >> 1);
if (b[p]) {
d[p << 1] = b[p] * (m - s + 1), d[(p << 1) | 1] = b[p] * (t - m);
b[p << 1] = b[(p << 1) | 1] = b[p];
b[p] = 0;
}
if (l <= m) update(l, r, c, s, m, p << 1);
if (r > m) update(l, r, c, m + 1, t, (p << 1) | 1);
d[p] = d[p << 1] + d[(p << 1) | 1];
} int getsum(int l, int r, int s, int t, int p) { // 取得答案,和前面一样
if (l <= s && t <= r) return d[p];
int m = s + ((t - s) >> 1);
if (b[p]) {
d[p << 1] = b[p] * (m - s + 1), d[(p << 1) | 1] = b[p] * (t - m);
b[p << 1] = b[(p << 1) | 1] = b[p];
b[p] = 0;
}
int sum = 0;
if (l <= m) sum = getsum(l, r, s, m, p << 1);
if (r > m) sum += getsum(l, r, m + 1, t, (p << 1) | 1);
return sum;
} int main() {
std::ios::sync_with_stdio(0);
std::cin >> n;
for (int i = 1; i <= n; i++) std::cin >> a[i];
build(1, n, 1);
int q, i1, i2, i3, i4;
std::cin >> q;
while (q--) {
std::cin >> i1 >> i2 >> i3;
if (i1 == 0)
std::cout << getsum(i2, i3, 1, n, 1) << std::endl;
else
std::cin >> i4, update(i2, i3, i4, 1, n, 1);
}
return 0;
}

2018 Multi-University Training Contest 5 Problem G. Glad You Came

解题思路:

维护一下每个区间的永久标记就可以了,最后在线段树上跑一边 \(\text{DFS}\) 统计结果即可。注意打标记的时候加个剪枝优化,否则会 \(\text{TLE}\)。

线段树合并

过程

顾名思义,线段树合并是指建立一棵新的线段树,这棵线段树的每个节点都是两棵原线段树对应节点合并后的结果。它常常被用于维护树上或是图上的信息。

显然,我们不可能真的每次建满一颗新的线段树,因此我们需要使用上文的动态开点线段树。

线段树合并的过程本质上相当暴力:

假设两颗线段树为 \(\text{A}\) 和 \(\text{B}\),我们从 \(1\) 号节点开始递归合并。

递归到某个节点时,如果 \(\text{A}\) 树或者 \(\text{B}\) 树上的对应节点为空,直接返回另一个树上对应节点,这里运用了动态开点线段树的特性。

如果递归到叶子节点,我们合并两棵树上的对应节点。

最后,根据子节点更新当前节点并且返回。

线段树合并的复杂度:

显然,对于两颗满的线段树,合并操作的复杂度是 \(O(n\log n)\) 的。但实际情况下使用的常常是权值线段树,总点数和 \(n\) 的规模相差并不大。并且合并时一般不会重复地合并某个线段树,所以我们最终增加的点数大致是 \(n\log n\) 级别的。这样,总的复杂度就是 \(O(n\log n)\) 级别的。当然,在一些情况下,可并堆可能是更好的选择。

实现

int merge(int a, int b, int l, int r) {
if (!a) return b;
if (!b) return a;
if (l == r) {
// do something...
return a;
}
int mid = (l + r) >> 1;
tr[a].l = merge(tr[a].l, tr[b].l, l, mid);
tr[a].r = merge(tr[a].r, tr[b].r, mid + 1, r);
pushup(a);
return a;
}

例题

luogu P4556 [Vani 有约会] 雨天的尾巴/【模板】线段树合并

解题思路:

线段树合并模板题,用差分把树上修改转化为单点修改,然后向上 dfs 线段树合并统计答案即可。

参考代码:

#include <bits/stdc++.h>
using namespace std;
int n, fa[100005][22], dep[100005], rt[100005];
int sum[5000005], cnt = 0, res[5000005], ls[5000005], rs[5000005];
int m, ans[100005];
vector<int> v[100005]; void update(int x) {
if (sum[ls[x]] < sum[rs[x]]) {
res[x] = res[rs[x]];
sum[x] = sum[rs[x]];
} else {
res[x] = res[ls[x]];
sum[x] = sum[ls[x]];
}
} int merge(int a, int b, int x, int y) {
if (!a) return b;
if (!b) return a;
if (x == y) {
sum[a] += sum[b];
return a;
}
int mid = (x + y) >> 1;
ls[a] = merge(ls[a], ls[b], x, mid);
rs[a] = merge(rs[a], rs[b], mid + 1, y);
update(a);
return a;
} int add(int id, int x, int y, int co, int val) {
if (!id) id = ++cnt;
if (x == y) {
sum[id] += val;
res[id] = co;
return id;
}
int mid = (x + y) >> 1;
if (co <= mid)
ls[id] = add(ls[id], x, mid, co, val);
else
rs[id] = add(rs[id], mid + 1, y, co, val);
update(id);
return id;
} void initlca(int x) {
for (int i = 0; i <= 20; i++) fa[x][i + 1] = fa[fa[x][i]][i];
for (int i : v[x]) {
if (i == fa[x][0]) continue;
dep[i] = dep[x] + 1;
fa[i][0] = x;
initlca(i);
}
} int lca(int x, int y) {
if (dep[x] < dep[y]) swap(x, y);
for (int d = dep[x] - dep[y], i = 0; d; d >>= 1, i++)
if (d & 1) x = fa[x][i];
if (x == y) return x;
for (int i = 20; i >= 0; i--)
if (fa[x][i] != fa[y][i]) x = fa[x][i], y = fa[y][i];
return fa[x][0];
} void cacl(int x) {
for (int i : v[x]) {
if (i == fa[x][0]) continue;
cacl(i);
rt[x] = merge(rt[x], rt[i], 1, 100000);
}
ans[x] = res[rt[x]];
if (sum[rt[x]] == 0) ans[x] = 0;
} int main() {
ios::sync_with_stdio(0);
cin >> n >> m;
for (int i = 0; i < n - 1; i++) {
int a, b;
cin >> a >> b;
v[a].push_back(b);
v[b].push_back(a);
}
initlca(1);
for (int i = 0; i < m; i++) {
int a, b, c;
cin >> a >> b >> c;
rt[a] = add(rt[a], 1, 100000, c, 1);
rt[b] = add(rt[b], 1, 100000, c, 1);
int t = lca(a, b);
rt[t] = add(rt[t], 1, 100000, c, -1);
rt[fa[t][0]] = add(rt[fa[t][0]], 1, 100000, c, -1);
}
cacl(1);
for (int i = 1; i <= n; i++) cout << ans[i] << endl;
return 0;
}

线段树分裂

过程

线段树分裂实质上是线段树合并的逆过程。线段树分裂只适用于有序的序列,无序的序列是没有意义的,常用在动态开点的权值线段树。

注意当分裂和合并都存在时,我们在合并的时候必须回收节点,以避免分裂时会可能出现节点重复占用的问题。

从一颗区间为 \([1,N]\) 的线段树中分裂出 \([l,r]\),建一颗新的树:

从 1 号结点开始递归分裂,当节点不存在或者代表的区间 \([s,t]\) 与 \([l,r]\) 没有交集时直接回溯。

当 \([s,t]\) 与 \([l,r]\) 有交集时需要开一个新结点。

当 \([s,t]\) 包含于 \([l,r]\) 时,需要将当前结点直接接到新的树下面,并把旧边断开。

线段树分裂的复杂度:

可以发现被断开的边最多只会有 \(\log n\) 条,所以最终每次分裂的时间复杂度就是 \(O(\log⁡ n)\),相当于区间查询的复杂度。

实现

void split(int &p, int &q, int s, int t, int l, int r) {
if (t < l || r < s) return;
if (!p) return;
if (l <= s && t <= r) {
q = p;
p = 0;
return;
}
if (!q) q = New();
int m = s + t >> 1;
if (l <= m) split(ls[p], ls[q], s, m, l, r);
if (m < r) split(rs[p], rs[q], m + 1, t, l, r);
push_up(p);
push_up(q);
}

例题

P5494【模板】线段树分裂

解题思路:

线段树分裂模板题,将 \([x,y]\) 分裂出来。

  • 将 \(t\) 树合并入 \(p\) 树:单次合并即可。
  • \(p\) 树中插入 \(x\) 个 \(q\):单点修改。
  • 查询 \([x,y]\) 中数的个数:区间求和。
  • 查询第 \(k\) 小。

参考代码:

#include <iostream>
using namespace std;
const int N = 2e5 + 10;
int n, m;
int idx = 1;
long long sum[N << 5];
int ls[N << 5], rs[N << 5], root[N << 2], rub[N << 5], cnt, tot; // 内存分配与回收
int New() { return cnt ? rub[cnt--] : ++tot; } void Del(int &p) {
ls[p] = rs[p] = sum[p] = 0;
rub[++cnt] = p;
p = 0;
} void push_up(int p) { sum[p] = sum[ls[p]] + sum[rs[p]]; } void build(int s, int t, int &p) {
if (!p) p = New();
if (s == t) {
cin >> sum[p];
return;
}
int m = s + t >> 1;
build(s, m, ls[p]);
build(m + 1, t, rs[p]);
push_up(p);
} // 单点修改
void update(int x, int c, int s, int t, int &p) {
if (!p) p = New();
if (s == t) {
sum[p] += c;
return;
}
int m = s + t >> 1;
if (x <= m)
update(x, c, s, m, ls[p]);
else
update(x, c, m + 1, t, rs[p]);
push_up(p);
} // 合并
int merge(int p, int q, int s, int t) {
if (!p || !q) return p + q;
if (s == t) {
sum[p] += sum[q];
Del(q);
return p;
}
int m = s + t >> 1;
ls[p] = merge(ls[p], ls[q], s, m);
rs[p] = merge(rs[p], rs[q], m + 1, t);
push_up(p);
Del(q);
return p;
} // 分裂
void split(int &p, int &q, int s, int t, int l, int r) {
if (t < l || r < s) return;
if (!p) return;
if (l <= s && t <= r) {
q = p;
p = 0;
return;
}
if (!q) q = New();
int m = s + t >> 1;
if (l <= m) split(ls[p], ls[q], s, m, l, r);
if (m < r) split(rs[p], rs[q], m + 1, t, l, r);
push_up(p);
push_up(q);
} long long query(int l, int r, int s, int t, int p) {
if (!p) return 0;
if (l <= s && t <= r) return sum[p];
int m = s + t >> 1;
long long ans = 0;
if (l <= m) ans += query(l, r, s, m, ls[p]);
if (m < r) ans += query(l, r, m + 1, t, rs[p]);
return ans;
} int kth(int s, int t, int k, int p) {
if (s == t) return s;
int m = s + t >> 1;
long long left = sum[ls[p]];
if (k <= left)
return kth(s, m, k, ls[p]);
else
return kth(m + 1, t, k - left, rs[p]);
} int main() {
cin >> n >> m;
build(1, n, root[1]);
while (m--) {
int op;
cin >> op;
if (!op) {
int p, x, y;
cin >> p >> x >> y;
split(root[p], root[++idx], 1, n, x, y);
} else if (op == 1) {
int p, t;
cin >> p >> t;
root[p] = merge(root[p], root[t], 1, n);
} else if (op == 2) {
int p, x, q;
cin >> p >> x >> q;
update(q, x, 1, n, root[p]);
} else if (op == 3) {
int p, x, y;
cin >> p >> x >> y;
cout << query(x, y, 1, n, root[p]) << endl;
} else {
int p, k;
cin >> p >> k;
if (sum[root[p]] < k)
cout << -1 << endl;
else
cout << kth(1, n, k, root[p]) << endl;
}
}
}

线段树优化建图

在建图连边的过程中,我们有时会碰到这种题目,一个点向一段连续的区间中的点连边或者一个连续的区间向一个点连边,如果我们真的一条一条连过去,那一旦点的数量多了复杂度就爆炸了,这里就需要用线段树的区间性质来优化我们的建图了。

下面是一个线段树。

每个节点都代表了一个区间,假设我们要向区间 \([2, 4]\) 连边。

在一些题目中,还会出现一个区间连向一个点的情况,则我们将上面第一张图的有向边全部反过来即可,上面的树叫做入树,下面这个叫做出树。

Legacy

题目大意:有 \(n\) 个点、\(q\) 次操作。每一种操作为以下三种类型中的一种:

  • 操作一:连一条 \(u \rightarrow v\) 的有向边,权值为 \(w\)。
  • 操作二:对于所有 \(i \in [l,r]\) 连一条 \(u \rightarrow i\) 的有向边,权值为 \(w\)。
  • 操作三:对于所有 \(i \in [l,r]\) 连一条 \(i \rightarrow u\) 的有向边,权值为 \(w\)。

求从点 \(s\) 到其他点的最短路。

\(1 \le n,q \le 10^5, 1 \le w \le 10^9\)。

参考代码:

#include <bits/stdc++.h>
using namespace std;
typedef long long ll; const int N = 1e5 + 5; using pil = pair<int, ll>;
using pli = pair<ll, int>; int n, q, s, tot, rt1, rt2;
int pos[N];
ll dis[N << 3];
vector<pil> e[N << 3];
bitset<(N << 3)> vis; struct seg {
int l, r, lson, rson;
} t[N << 3]; inline int ls(int u) { // 左儿子
return t[u].lson;
} inline int rs(int u) { // 右儿子
return t[u].rson;
} void build(int &u, int l, int r) { // 动态开点建造入树
u = ++tot;
t[u] = seg{l, r};
if (l == r) {
pos[l] = u;
return;
}
int mid = (l + r) >> 1;
build(t[u].lson, l, mid);
build(t[u].rson, mid + 1, r);
e[u].emplace_back(ls(u), 0);
e[u].emplace_back(rs(u), 0);
} void build2(int &u, int l, int r) { // 动态开点建造出树
if (l == r) {
u = pos[l];
return;
}
u = ++tot;
t[u] = seg{l, r};
int mid = (l + r) >> 1;
build2(t[u].lson, l, mid);
build2(t[u].rson, mid + 1, r);
e[ls(u)].emplace_back(u, 0);
e[rs(u)].emplace_back(u, 0);
} void add1(int u, int lr, int rr, int v, ll w) { // 点向区间连边
if (lr <= t[u].l && t[u].r <= rr) {
e[v].emplace_back(u, w);
return;
}
int mid = (t[u].l + t[u].r) >> 1;
if (lr <= mid) {
add1(ls(u), lr, rr, v, w);
}
if (rr > mid) {
add1(rs(u), lr, rr, v, w);
}
} void add2(int u, int lr, int rr, int v, ll w) { // 区间向点连边
if (lr <= t[u].l && t[u].r <= rr) {
e[u].emplace_back(v, w);
return;
}
int mid = (t[u].l + t[u].r) >> 1;
if (lr <= mid) {
add2(ls(u), lr, rr, v, w);
}
if (rr > mid) {
add2(rs(u), lr, rr, v, w);
}
} void dij(int S) {
priority_queue<pli, vector<pli>, greater<pli> > q;
int tot = (n << 2);
for (int i = 1; i <= tot; ++i) {
dis[i] = 1e18;
}
dis[S] = 0;
q.emplace(dis[S], S);
while (!q.empty()) {
pli fr = q.top();
q.pop();
int u = fr.second;
if (vis[u]) continue;
for (pil it : e[u]) {
int v = it.first;
ll w = it.second;
if (dis[v] > dis[u] + w) {
dis[v] = dis[u] + w;
q.emplace(dis[v], v);
}
}
}
} int main() {
scanf("%d%d%d", &n, &q, &s);
build(rt1, 1, n);
build2(rt2, 1, n);
for (int i = 1, op, u; i <= q; ++i) {
scanf("%d%d", &op, &u);
if (op == 1) {
int v;
ll w;
scanf("%d%lld", &v, &w);
e[pos[u]].emplace_back(pos[v], w);
} else if (op == 2) {
int l, r;
ll w;
scanf("%d%d%lld", &l, &r, &w);
add1(rt1, l, r, pos[u], w);
} else {
int l, r;
ll w;
scanf("%d%d%lld", &l, &r, &w);
add2(rt2, l, r, pos[u], w);
}
}
dij(pos[s]);
for (int i = 1; i <= n; ++i) {
if (dis[pos[i]] == 1e18) {
printf("-1 ");
} else {
printf("%lld ", dis[pos[i]]);
}
}
return 0;
}

拓展 - 猫树

众所周知线段树可以支持高速查询某一段区间的信息和,比如区间最大子段和,区间和,区间矩阵的连乘积等等。

但是有一个问题在于普通线段树的区间询问在某些毒瘤的眼里可能还是有些慢了。

简单来说就是线段树建树的时候需要做 \(O(n)\) 次合并操作,而每一次区间询问需要做 \(O(\log{n})\) 次合并操作,询问区间和这种东西的时候还可以忍受,但是当我们需要询问区间线性基这种合并复杂度高达 \(O(\log^2{w})\) 的信息的话,此时就算是做 \(O(\log{n})\) 次合并有些时候在时间上也是不可接受的。

而所谓「猫树」就是一种不支持修改,仅仅支持快速区间询问的一种静态线段树。

构造一棵这样的静态线段树需要 \(O(n\log{n})\) 次合并操作,但是此时的查询复杂度被加速至 \(O(1)\) 次合并操作。

在处理线性基这样特殊的信息的时候甚至可以将复杂度降至 \(O(n\log^2{w})\)。

原理

在查询 \([l,r]\) 这段区间的信息和的时候,将线段树树上代表 \([l,l]\) 的节点和代表 \([r,r]\) 这段区间的节点在线段树上的 LCA 求出来,设这个节点 \(p\) 代表的区间为 \([L,R]\),我们会发现一些非常有趣的性质:

  1. \([L,R]\) 这个区间一定包含 \([l,r]\)。显然,因为它既是 \(l\) 的祖先又是 \(r\) 的祖先。

  2. \([l,r]\) 这个区间一定跨越 \([L,R]\) 的中点。由于 \(p\) 是 \(l\) 和 \(r\) 的 LCA,这意味着 \(p\) 的左儿子是 \(l\) 的祖先而不是 \(r\) 的祖先,\(p\) 的右儿子是 \(r\) 的祖先而不是 \(l\) 的祖先。因此,\(l\) 一定在 \([L,\mathit{mid}]\) 这个区间内,\(r\) 一定在 \((\mathit{mid},R]\) 这个区间内。

有了这两个性质,我们就可以将询问的复杂度降至 \(O(1)\) 了。

实现

具体来讲我们建树的时候对于线段树树上的一个节点,设它代表的区间为 \((l,r]\)。

不同于传统线段树在这个节点里只保留 \([l,r]\) 的和,我们在这个节点里面额外保存 \((l,\mathit{mid}]\) 的后缀和数组和 \((\mathit{mid},r]\) 的前缀和数组。

这样的话建树的复杂度为 \(T(n)=2T(n/2)+O(n)=O(n\log{n})\) 同理空间复杂度也从原来的 \(O(n)\) 变成了 \(O(n\log{n})\)。

下面是最关键的询问了。

如果我们询问的区间是 \([l,r]\) 那么我们把代表 \([l,l]\) 的节点和代表 \([r,r]\) 的节点的 LCA 求出来,记为 \(p\)。

根据刚才的两个性质,\(l,r\) 在 \(p\) 所包含的区间之内并且一定跨越了 \(p\) 的中点。

这意味这一个非常关键的事实是我们可以使用 \(p\) 里面的前缀和数组和后缀和数组,将 \([l,r]\) 拆成 \([l,\mathit{mid}]+(\mathit{mid},r]\) 从而拼出来 \([l,r]\) 这个区间。

而这个过程仅仅需要 \(O(1)\) 次合并操作!

不过我们好像忽略了点什么?

似乎求 LCA 的复杂度似乎还不是 \(O(1)\),暴力求是 \(O(\log{n})\) 的,倍增法则是 \(O(\log{\log{n}})\) 的,转 \(\text{ST}\) 表的代价又太大\(\dots\)

堆式建树

具体来将我们将这个序列补成 \(2\) 的整次幂,然后建线段树。

此时我们发现线段树上两个节点的 \(\text{LCA}\) 编号,就是两个节点二进制编号的最长公共前缀 \(\text{LCP}\)。

稍作思考即可发现发现在 \(x\) 和 \(y\) 的二进制下 lcp(x,y)=x>>log[x^y]

所以我们预处理一个 log 数组即可轻松完成求 \(\text{LCA}\) 的工作。

这样我们就构建了一个猫树。

由于建树的时候涉及到求前缀和和求后缀和,所以对于线性基这种虽然合并是 \(O(\log^2{w})\) 但是求前缀和却是 \(O(n\log{n})\) 的信息,使用猫树可以将静态区间线性基从 \(O(n\log^2{w}+m\log^2{w}\log{n})\) 优化至 \(O(n\log{n}\log{w}+m\log^2{w})\) 的复杂度。

参考

  • immortalCO 的博客
  • [Kle77] V. Klee, "Can the Measure of be Computed in Less than O (n log n) Steps?," Am. Math. Mon., vol. 84, no. 4, pp. 284–285, Apr. 1977.
  • [BeW80] Bentley and Wood, "An Optimal Worst Case Algorithm for Reporting Intersections of Rectangles," IEEE Trans. Comput., vol. C–29, no. 7, pp. 571–577, Jul. 1980.

Day 9 - 线段树的更多相关文章

  1. bzoj3932--可持久化线段树

    题目大意: 最近实验室正在为其管理的超级计算机编制一套任务管理系统,而你被安排完成其中的查询部分.超级计算机中的 任务用三元组(Si,Ei,Pi)描述,(Si,Ei,Pi)表示任务从第Si秒开始,在第 ...

  2. codevs 1082 线段树练习 3(区间维护)

    codevs 1082 线段树练习 3  时间限制: 3 s  空间限制: 128000 KB  题目等级 : 大师 Master 题目描述 Description 给你N个数,有两种操作: 1:给区 ...

  3. codevs 1576 最长上升子序列的线段树优化

    题目:codevs 1576 最长严格上升子序列 链接:http://codevs.cn/problem/1576/ 优化的地方是 1到i-1 中最大的 f[j]值,并且A[j]<A[i] .根 ...

  4. codevs 1080 线段树点修改

    先来介绍一下线段树. 线段树是一个把线段,或者说一个区间储存在二叉树中.如图所示的就是一棵线段树,它维护一个区间的和. 蓝色数字的是线段树的节点在数组中的位置,它表示的区间已经在图上标出,它的值就是这 ...

  5. codevs 1082 线段树区间求和

    codevs 1082 线段树练习3 链接:http://codevs.cn/problem/1082/ sumv是维护求和的线段树,addv是标记这歌节点所在区间还需要加上的值. 我的线段树写法在运 ...

  6. PYOJ 44. 【HNSDFZ2016 #6】可持久化线段树

    #44. [HNSDFZ2016 #6]可持久化线段树 统计 描述 提交 自定义测试 题目描述 现有一序列 AA.您需要写一棵可持久化线段树,以实现如下操作: A v p x:对于版本v的序列,给 A ...

  7. CF719E(线段树+矩阵快速幂)

    题意:给你一个数列a,a[i]表示斐波那契数列的下标为a[i],求区间对应斐波那契数列数字的和,还要求能够维护对区间内所有下标加d的操作 分析:线段树 线段树的每个节点表示(f[i],f[i-1])这 ...

  8. 【BZOJ-3779】重组病毒 LinkCutTree + 线段树 + DFS序

    3779: 重组病毒 Time Limit: 20 Sec  Memory Limit: 512 MBSubmit: 224  Solved: 95[Submit][Status][Discuss] ...

  9. 【BZOJ-3673&3674】可持久化并查集 可持久化线段树 + 并查集

    3673: 可持久化并查集 by zky Time Limit: 5 Sec  Memory Limit: 128 MBSubmit: 1878  Solved: 846[Submit][Status ...

  10. 【BZOJ-2653】middle 可持久化线段树 + 二分

    2653: middle Time Limit: 20 Sec  Memory Limit: 512 MBSubmit: 1298  Solved: 734[Submit][Status][Discu ...

随机推荐

  1. 上位机开发福利!快速掌握.NET中的Modbus通信

    安装nuget包 Wesky.Net.OpenTools  1.0.8或以上版本.支持.net framework 4.6以上版本,以及所有.net core以及以上版本引用. 开发一个简单的Winf ...

  2. C# 【思路】分享 构造可进行单元测试的波形数据

    需要单元测试自己写的识别特殊波峰的算法,所以必须构造波形数据. 一开始是自己在控件上手绘波形,虽然这种方便,但是能绘制的点太少,每次手画显得麻烦. 过后,又采用随机数构造波峰,这种虽说能构造很多点,产 ...

  3. 改造 Kubernetes 自定义调度器

    原文出处:改造 Kubernetes 自定义调度器 | Jayden's Blog (jaydenchang.top) Overview Kubernetes 默认调度器在调度 Pod 时并不关心特殊 ...

  4. go 通过指针修改结构体小写字段的值

    package main import ( "fmt" "unsafe" ) type W struct { b int32 c int64 } func ma ...

  5. C++笔记(2)拷贝构造函数

    拷贝构造函数是一种特殊的构造函数,它在创建对象时,是使用同一类中之前创建的对象来初始化新创建的对象. 如果在类中没有定义拷贝构造函数,编译器会自行定义一个.如果类带有指针变量,并有动态内存分配,则它必 ...

  6. .NET集成DeveloperSharp实现"高效分页"&"无主键分页"

    DeveloperSharp系列近期又被制造业ERP.民航飞行App.建筑BIM.电力掌上营业厅.等多家大型采用,站在巨人的肩膀上你能走的更远. 支持.Net Core2.0及以上,支持.Net Fr ...

  7. 【AI应用开发全流程】使用AscendCL开发板完成模型推理

    本文分享自华为云社区<[昇腾开发全流程]AscendCL开发板模型推理>,作者:沉迷sk. 前言 学会如何安装配置华为云ModelArts.开发板Atlas 200I DK A2.并打通一 ...

  8. 面试必会 --> MyBatis篇

    什么是MyBatis Mybatis是一个半ORM(对象关系映射)框架,它内部封装了JDBC,开发时只需要关注SQL语句本身,不需要花费精力去处理加载驱动.创建连接.创建statement等繁杂的过程 ...

  9. 实现并发新高度:23ai的无锁列值保留

    Oracle Database 23ai支持Lock-Free Reservation,中文通常译为"无锁列值保留". 本文将通过3个部分来阐述Lock-Free Reservat ...

  10. 基于redis的选主功能设计

    需求背景 在JAVA应用开发过程中,越来越多的项目采用了微服务架构,而采用微服务架构最直接作用是可以实现业务层解耦,有利于研发团队可以从业务层面进行划分,比如某几个人的小团队负责某几个微服务,总之,从 ...