同步发表于洛谷@梦回江南
这一篇文章我们将对线段树中的常规操作进行详细的讨论。
- 以下所提到的复杂度如无特殊说明均为时间复杂度。 log \log log 的底数均为 2 2 2。
不开 long long 见祖宗!
文章目录
- 第一部 普通线段树
- 一、引入
- 二、优化方案
- 三、懒标记(lazytage)
- 四、Don't talk more, show me the code.
- 第二部 乘法线段树
- 一、引入
- 二、使用lazytage的原理
- 三、如何使用lazytage
- 四、Don't talk more, show me the code.
- 第三部 区间根号线段树
- 一、引入
- 二、特殊性
- 三、如何利用特殊性
- 四、Don't talk more, show me the code.
第一部 普通线段树
一、引入
对于一串数,举例为 a a a,需要实现以下几种操作(修改操作均为加减法):
- 单点修改
- 单点查询
- 区间修改
- 区间查询(区间和)
显然 1 1 1 与 2 2 2 被 3 3 3 与 4 4 4 包含。但为了讨论的清晰,我们也将对其单独讨论。
如果我们用常规方式(数组)存储这串数,其查询操作为 O ( 1 ) O(1) O(1),更改操作为 O ( 1 ) O(1) O(1),求和操作为 O ( l ) O(l) O(l)( l l l 为所求序列长度)。
#define LL long long // 不开 long long 见祖宗!
#define ref(i, a, b, p) for (signed(i) = (a); (i) <= signed(b); (i) += signed(p))
const int maxn = 50005;
LL a[maxn], n, q, sum;
void work()
{
cin >> n;
ref (i, 1, n, 1)
cin >> a[i];
cin >> q;
ref (__, 1, q, 1)
{
int x, k, l, r;
cin >> x;
if (x == 1) // 以下均如题
{
cin >> k;
a[x] = k;
}
else if (x == 2)
cout << a[x] << endl;
else if (x == 3)
{
cin >> l >> r >> k;
ref (i, l, r, 1)
a[x] += k;
}
else
{
sum = 0;
cin >> l >> r;
ref (i, l, r, 1)
sum += a[i];
cout << sum << endl;
}
}
}
如果用前缀和来操作的话,虽然求和操作为 O ( 1 ) O(1) O(1),但是更改数值后更新前缀和的复杂度为 O ( n ) O(n) O(n),没有起到太大的优化效果,不再放代码。
有人说 O ( n ) O(n) O(n) 的时间复杂度已经很优了,但是设想,如果有 q q q 次询问,那么总的复杂度为 O ( n q ) O(nq) O(nq),不可接受。
二、优化方案
经过前面优先队列等数据结构的铺垫,我们知道,树是一种能在复杂度为
O
(
log
n
)
O(\log n)
O(logn) 的情况下处理大部分操作的数据结构。
那么我们可以这样想:对于一串数
a
a
a,我们把它看成一条长度为
n
n
n 的线段,标记线段左端点为
1
1
1,右端点为
n
n
n,那么我们让线段上的
1
1
1 对应
a
1
a_1
a1,让
2
2
2 对应
a
2
a_2
a2……让
n
n
n 对应
a
n
.
a_n.
an.,这样我们就用一条线段表示出了
a
a
a。
我们此时建立一棵完全二叉树(偶数个数的情况下为满二叉树),以刚刚建立的线段上的每一个点(从
a
1
a_1
a1 到
a
n
a_n
an)为叶子节点,用父节点表示其左右儿子的和,那么根节点就为整条线段(也就是整串数)的和。
具体来看(代码放到最后):
建树时,先建立叶子节点,然后逐层回溯计算父结点的值,每个节点需要存储的变量有它的值以及左右端点(就是区间范围)。时间复杂度
O
(
n
log
n
)
O(n\log n)
O(nlogn)。
如下图,先建立节点
1
,
2
,
3
…
8
1,2,3\dots8
1,2,3…8(代表
a
1
,
a
2
,
a
3
…
a
8
a_1,a_2,a_3\dots a_8
a1,a2,a3…a8,下同),然后逐层回溯更新节点
a
,
b
,
c
…
g
a,b,c\dots g
a,b,c…g,更改顺序:红、黄、绿、蓝。
在此图中:
a
=
1
+
2
,
b
=
3
+
4
,
c
=
5
+
6
,
d
=
7
+
8.
a=1+2,\,b=3+4,\,c=5+6,\,d=7+8.
a=1+2,b=3+4,c=5+6,d=7+8.
e
=
a
+
b
=
∑
i
=
1
4
a
i
,
f
=
c
+
d
=
∑
i
=
5
8
a
i
.
e=a+b=\sum\limits_{i=1}^{4}a_i,\,f=c+d=\sum\limits_{i=5}^{8}a_i.
e=a+b=i=1∑4ai,f=c+d=i=5∑8ai.
g
=
e
+
f
=
a
+
b
+
c
+
d
=
∑
i
=
1
8
a
i
.
g=e+f=a+b+c+d=\sum\limits_{i=1}^{8}a_i.
g=e+f=a+b+c+d=i=1∑8ai.
单点修改时,只需更改叶子结点的值,然后逐层更新叶子节点,时间复杂度
O
(
log
n
)
O(\log n)
O(logn),
q
q
q 次询问时复杂度
O
(
q
log
n
)
O(q\log n)
O(qlogn)。
如下图,要修改节点
3
3
3,步骤:
- 找到节点
3
3
3,更改其值。
- 逐步更新其父节点。
单点查询时,一步到位直接查询叶子节点即可。复杂度
O
(
log
n
)
O(\log n)
O(logn)。
如下图,查询节点
3
3
3:
查询区间和时,假设需查询的区间左右端点分别为
l
,
r
l,r
l,r,我们从根节点向下dfs
,找到一个节点,然后遍历它的左右儿子。对于此节点(假设其值为
a
n
s
ans
ans,左端点为
l
e
le
le,右端点为
r
i
ri
ri),我们分以下几种情况讨论:
- l ≤ l e l\le le l≤le 且 r i ≤ r ri\le r ri≤r 即此区间被包含,直接返回 a n s ans ans 而无需再遍历其左右儿子。
- l > r i l>ri l>ri 或 r < l e r < le r<le 即两端区间的交集为空集(不相交),直接返回 0 0 0 而无需再遍历下去,因为其左右子树包含于其而肯定与所求区间无交集。
- r i > l ri > l ri>l 时,搜索其左子树, l e < r le < r le<r 时搜索其右子树(当然也可以定义一个 mid 变量来进行比较)。
- 总体时间复杂度 O ( log n ) O(\log n) O(logn)。
如下图,需查询区间 A ∈ [ 1 , 5 ] A\in[1, 5] A∈[1,5]:
-
找到根节点 g ∈ [ 1 , 8 ] g\in[1,8] g∈[1,8],属于情况 3 3 3,搜索其左右子树 e , f e,\,f e,f。
-
对于节点 e ∈ [ 1 , 4 ] e\in[1,4] e∈[1,4],属于情况 1 1 1,直接返回其值 a n s + = ∑ i = 1 4 a i ans+\!\!=\sum\limits_{i=1}^{4}a_i ans+=i=1∑4ai;对于节点 f ∈ [ 5 , 8 ] f\in[5,8] f∈[5,8],属于情况 3 3 3,搜索其左右子树 c , d c,\,d c,d。
-
对于节点 c ∈ [ 5 , 6 ] c\in[5,6] c∈[5,6],属于情况 3 3 3,搜索其左右子树 5 , 6 5,\, 6 5,6;对于节点 d ∈ [ 7 , 8 ] d\in[7,8] d∈[7,8],属于情况 2 2 2,直接返回值 0 0 0。
-
对于节点 5 5 5,属于情况 1 1 1,返回其值 5 5 5;对于节点 6 6 6,属于情况 2 2 2,直接返回值 0 0 0。
所以总的操作流程如下:
- 区间修改时,假设需查询的区间左右端点分别为
l
,
r
l,r
l,r,需要加的值为
k
k
k,我们从根节点向下
dfs
,找到一个节点,然后遍历它的左右儿子。对于此节点(假设其值为 a n s ans ans,左端点为 l e le le,右端点为 r i ri ri),我们依然可以分以下几种情况讨论:
- l ≤ l e l\le le l≤le 且 r i ≤ r ri\le r ri≤r 即此区间被包含,直接给 a n s ans ans 加上 k k k 而无需再遍历其左右儿子。
- l > r i l>ri l>ri 或 r < l e r < le r<le 即两端区间的交集为空集(不相交),直接返回而无需再遍历下去,因为其左右子树包含于其而肯定与所求区间无交集。
- r i > l ri > l ri>l 时,更改其左子树, l e < r le < r le<r 时更改其右子树(当然也可以定义一个 mid 变量来进行比较)。
- 记得回溯时更新父结点的值。
- 总体时间复杂度 O ( log n ) O(\log n) O(logn)。
但上面的思路有一个重大的缺陷,就是在更改时搜索到一个节点符合要求 1 1 1,那么将这个节点的值更改后直接回溯了,因此实际上叶子结点的值没有真正改变,当单点查询时还是会返回之前的值,导致结果错误。
但我们也不能直接搜索到底部更改叶子结点的值然后逐层更新父结点的值,这样复杂度还是
O
(
n
)
O(n)
O(n),没有起到优化的效果。并且,此节点的值会计算错误(少加了几个
k
k
k )。
比如说查询到节点
b
b
b 时,其符合情况
1
1
1,于是直接给
b
b
b 加上
k
k
k,(
k
k
k 为需要更改的值),但实际上它应该加两个
k
k
k,因为它的左右儿子各需加一个
k
k
k,而这里只加了一个。还有问题是,
b
b
b 的左右儿子并没有实际上被改变。
所以,为了解决这个问题,lazytage(懒标记) 应运而生。
三、懒标记(lazytage)
- 什么是懒标记?
记录此节点所更改的值(也就是上文所述的 k k k ),在遍历到此节点时,将此标记传递给其左右儿子,并通过此标记计算出此节点真正的值。 - 原理
将懒标记传递给左右儿子,其实就是给左右儿子补偿之前没有加上的值(懒标记乘以其子树节点个数)。
如图, e e e 节点的值被加上 k k k,我们给 e e e 的懒标记的值也加上 k k k,下一次在遍历到 e e e 时,将 e e e 的懒标记传递给它的左右儿子并更新节点的值。
- 更新方法:
- 将懒标记传给其左右儿子;
- 左儿子的值加上其父节点的懒标记×左儿子的子树大小;
- 右儿子的值加上其父节点的懒标记×右儿子的子树大小;
- 将父节点的懒标记清零;
- 逐层更新父结点的值。
可以额外写一个函数 pushdown 来维护懒标记。
- 什么时候能使用懒标记?
我们可以看到,懒标记的更新说实话是一个等式: a + b = a + b a+b=a+b a+b=a+b。有一些运算就不能用懒标记,比如根号。因为 a + b ≠ a + b \sqrt{a+b}\not=\sqrt{a}+\sqrt{b} a+b=a+b。
四、Don’t talk more, show me the code.
码风说明:
i
<
<
1
i << 1
i<<1 指
i
×
2
i\times2
i×2,一定程度上提高程序运行效率。
注:代码内有详细注释。
const ll maxn = 1e6 + 5;
ll n, m, p, cnt;
ll a[maxn];
struct common_segment_tree // 普通线段树
{
struct tree // 节点
{
int l, r, num, lz; // 左端点, 右端点, 值
tree() {} // 空构造函数
tree(const int l, const int r, const int num) // 赋值构造函数
{
this->l = l, this->r = r, this->num = num;
}
} tr[maxn << 2];
void build(int i, int l, int r) // 建立线段树, i 为当前节点, l 为区间左端点, r 为区间右端点
{
tr[i].l = l, tr[i].r = r; // 初始化
if (l == r) // 如果是叶子节点, 那么赋值然后返回
{
tr[i].num = a[l];
return;
}
int mid = (l + r) >> 1; // 不是叶子结点, 那么接着分叉
build(i << 1, l, mid); // 左子树
build(i << 1 | 1, mid + 1, r); // 右子树
tr[i].num = tr[i << 1].num + tr[i << 1 | 1].num;
return;
}
void push_down(int i) // 向下传递 "懒" 标记, i 为当前节点
{
if (tr[i].lz != 0) // "懒"标记不为 0
{
tr[i << 1].lz += tr[i].lz; // 左儿子加上"懒"标记
tr[i << 1 | 1].lz += tr[i].lz; // 右儿子加上"懒"标记
int mid = (tr[i].l + tr[i].r) >> 1;
tr[i << 1].num += tr[i].lz * (mid - tr[i << 1].l + 1);
tr[i << 1 | 1].num += tr[i].lz * (tr[i << 1 | 1].r - mid);
tr[i].lz = 0; // 清零
}
return;
}
void node_modify(int i, int x, int k) // 单点修改, i 为当前节点, x 为待修改节点下标, k 为待修改值
{
if (tr[i].l == tr[i].r) // 叶子结点说明已经找到, 加上值直接返回
{
tr[i].num += k;
return;
}
if (x <= tr[i << 1].r) // 在左子树
node_modify(i << 1, x, k);
else // 在右子树
node_modify(i << 1 | 1, x, k);
tr[i].num = tr[i << 1].num + tr[i << 1 | 1].num; // 维护当前节点的值
return;
}
int section_query(int i, int l, int r) // 区间查询, i 为当前节点, l 为区间左端点, r 为区间右端点
{
if (tr[i].l >= l && tr[i].r <= r) // 表示这个区间被所查询区间包含, 则直接返回这个区间的值
return tr[i].num;
if (tr[i].r < l || tr[i].l > r) // 表示这个区间与所查询区间的交集为空集, 则直接返回 0
return 0;
push_down(i); // 向下传递 "懒" 标记
int ans = 0, mid = (l + r) >> 1;
if (mid >= l) // 左子树与所查询区间有交集, 递归查询
ans += section_query(i << 1, l, r);
if (tr[i << 1 | 1].l <= r) // 右子树与所查询区间有交集, 递归查询
ans += section_query(i << 1 | 1, l, r);
return ans;
}
void section_modify(int i, int k, int l, int r) // 区间修改, i为当前节点, k 为需要修改的值, l 为区间左端点, r 为区间右端点
{
if (tr[i].l >= l && tr[i].r <= r) // 表示这个区间在要修改的范围内,那么修改完返回
{
tr[i].num += k * (tr[i].r - tr[i].l + 1);
tr[i].lz += k;
return;
}
push_down(i); // 如果不在区间内
if (tr[i << 1].r >= l) // 左区间与要修改范围有交集
section_modify(i << 1, k, l, r); // 在此区间的左子树内查找
if (tr[i << 1 | 1].l <= r) // 有区间与要修改范围有交集
section_modify(i << 1 | 1, k, l, r); // 在此区间的右子树内查找
tr[i].num = tr[i << 1].num + tr[i << 1 | 1].num; // 维护当前节点的值
return;
}
int node_query(int i, int k, int ans) // 单点查询, i 为当前节点, k 为要查找的数的下标, ans 为值
{
ans += tr[i].num; // 找到一个点就相加
if (tr[i].l == tr[i].r) // 如果是叶子节点就返回
return ans;
int mid = (tr[i].l + tr[i].r) >> 1; // 不是叶子节点就继续分叉
if (k <= mid) // 值在左子树
return node_query(i << 1, k, ans);
else // 值在右子树
return node_query(i << 1 | 1, k, ans);
}
// 以下是查询区间的最大最小值,原理同区间查询。复杂度 O(log n)
int section_max(int i, int l, int r)
{
if (l <= tr[i].l && tr[i].r <= r)
return tr[i].num;
if (tr[i].l > r || tr[i].r < l)
return 0;
int ans = -__INT_MAX__;
if (tr[i << 1].r >= l)
ans = max(ans, section_max(i << 1, l, r));
if (tr[i << 1 | 1].l <= r)
ans = max(ans, section_max(i << 1 | 1, l, r));
return ans;
}
int section_min(int i, int l, int r)
{
if (l <= tr[i].l && tr[i].r <= r)
return tr[i].num;
if (tr[i].l > r || tr[i].r < l)
return 0;
int ans = __INT_MAX__;
if (tr[i << 1].r >= l)
ans = min(ans, section_min(i << 1, l, r));
if (tr[i << 1 | 1].l <= r)
ans = min(ans, section_min(i << 1 | 1, l, r));
return ans;
}
};
第二部 乘法线段树
一、引入
通过对普通线段树的学习,我们了解了普通线段树(加法线段树)。那么对于乘法线段树,无非就是在之前操作的基础上加了一个区间乘法。也就是说,总体上没有什么大的变动,只需要在lazytage上做一些修改。
二、使用lazytage的原理
还是一个式子: ( a + b ) × k = a × k + b × k (a+b)\times k=a\times k+b\times k (a+b)×k=a×k+b×k。
三、如何使用lazytage
使用两个lazytage,一个记录加法,一个记录乘法。
当找到一个节点(以下所说“此节点”均指此),更新方法为:
- 将此节点加法懒标记与乘法懒标记传递给其左右儿子。
- 左儿子的值加上(左儿子的值 × \times ×乘法懒标记的值 + + +此节点左子树大小 × \times ×加法懒标记)。
- 右儿子的值加上(右儿子的值 × \times ×乘法懒标记的值 + + +此节点右子树大小 × \times ×加法懒标记)。
- 将左儿子的加法懒标记改为(左儿子的加法懒标记 × \times ×此节点乘法懒标记 + + +此节点加法懒标记)。
- 将右儿子的加法懒标记改为(右儿子的加法懒标记 × \times ×此节点乘法懒标记 + + +此节点加法懒标记)。
- 将左儿子的乘法懒标记乘上此节点的乘法懒标记。
- 将右儿子的乘法懒标记乘上此节点的乘法懒标记。
- 将此节点的加法懒标记清零,乘法懒标记的值改为 1 1 1。
四、Don’t talk more, show me the code.
其余就没有太大的改变了,具体详见代码。
- 注:为了不爆long long,我将其 % \% % 了一个 p p p。
struct multiplus_segment_tree // 乘法线段树
{
struct tree
{
ll l, r, num, mlz, plz;
} tr[maxn << 2];
void build(ll i, ll l, ll r)
{
tr[i].l = l, tr[i].r = r, tr[i].mlz = 1;
if (l == r)
{
tr[i].num = a[l] % p;
return;
}
ll mid = (l + r) >> 1;
build(i << 1, l, mid);
build(i << 1 | 1, mid + 1, r);
tr[i].num = (tr[i << 1].num + tr[i << 1 | 1].num) % p;
return;
}
void push_down(ll i)
{
ll pz = tr[i].plz, mz = tr[i].mlz;
tr[i << 1].num = (tr[i << 1].num * mz + ((tr[i << 1].r - tr[i << 1].l + 1) * pz) % p) % p;
tr[i << 1 | 1].num = (tr[i << 1 | 1].num * mz + ((tr[i << 1 | 1].r - tr[i << 1 | 1].l + 1) *pz) % p) % p;
tr[i << 1].mlz = (tr[i << 1].mlz * mz) % p;
tr[i << 1 | 1].mlz = (tr[i << 1 | 1].mlz * mz) % p;
tr[i << 1].plz = (tr[i << 1].plz * mz + pz) % p;
tr[i << 1 | 1].plz = (tr[i << 1 | 1].plz * mz + pz) % p;
tr[i].plz = 0; tr[i].mlz = 1;
return;
}
void section_modify_multi(ll i, ll l, ll r, ll k) // 区间乘法
{
if (tr[i].l >= l && tr[i].r <= r)
{
tr[i].num *= k, tr[i].num %= p;
tr[i].mlz *= k, tr[i].mlz %= p;
tr[i].plz *= k, tr[i].plz %= p;
return;
}
if (tr[i].l > r || tr[i].r < l)
return;
push_down(i);
if (tr[i << 1].r >= l)
section_modify_multi(i << 1, l, r, k);
if (tr[i << 1 | 1].l <= r)
section_modify_multi(i << 1 | 1, l, r, k);
tr[i].num = (tr[i << 1].num + tr[i << 1 | 1].num) % p;
return;
}
void section_modify_plus(ll i, ll l, ll r, ll k)
{
if (tr[i].l >= l && tr[i].r <= r)
{
tr[i].plz += k, tr[i].plz %= p;
tr[i].num += k * (tr[i].r - tr[i].l + 1), tr[i].num %= p;
return;
}
if (tr[i].l > r || tr[i].r < l)
return;
push_down(i);
if (tr[i << 1].r >= l)
section_modify_plus(i << 1, l, r, k);
if (tr[i << 1 | 1].l <= r)
section_modify_plus(i << 1 | 1, l, r, k);
tr[i].num = (tr[i << 1].num + tr[i << 1 | 1].num) % p;
return;
}
ll section_query(int i, int l, int r)
{
if (tr[i].l >= l && tr[i].r <= r)
return tr[i].num;
if (tr[i].l > r || tr[i].r < l)
return 0;
push_down(i);
ll ans = 0;
if (tr[i << 1].r >= l)
ans += section_query(i << 1, l, r), ans %= p;
if (tr[i << 1 | 1].l <= r)
ans += section_query(i << 1 | 1, l, r), ans %= p;
return ans;
}
};
第三部 区间根号线段树
一、引入
在普通线段树(加法线段树)和乘法线段树的基础之上,我们来到了区间根号线段树。
二、特殊性
- 区间根号线段树与另外两个线段树的不同之处在于,我们不能再使用lazytage了。我们在上文也说过, a + b ≠ a + b \sqrt{a}+\sqrt{b}\not=\sqrt{a+b} a+b=a+b,所以我们不能再使用懒标记来维护线段树。
- 但我们要注意的是,一个数再大,经过极少此开方之后,它的值都会变为 1 1 1(每次操作之后向下取整),例如 1 0 9 10^9 109 开过 5 5 5 次平方之后,它的值会变为 1 1 1(向下取整)。
三、如何利用特殊性
因为区间根号线段树的特殊性,我们可以用mx变量来维护线段树。
如何维护mx变量:
- mx的值等于其左右儿子mx的最大值,叶子节点mx的初始值为其本来的值。
- 每次开方之后判断其是否大于 1 1 1,如果其值小于等于 1 1 1,那么就无需再搜索其左右儿子,因为左右儿子的值均小于等于 1 1 1。
四、Don’t talk more, show me the code.
struct squareroot_segment_tree // 根号线段树, P4145, SP2713
{
struct tree
{
ll l, r, num, mx;
} tr[maxn << 2];
void update(ll i)
{
tr[i].num = tr[i << 1].num + tr[i << 1 | 1].num;
tr[i].mx = max(tr[i << 1].mx, tr[i << 1 | 1].mx);
return;
}
void build(ll i, ll l, ll r)
{
tr[i].l = l, tr[i].r = r;
if (tr[i].l == tr[i].r)
{
tr[i].mx = tr[i].num = a[l];
return;
}
ll mid = (l + r) >> 1;
build(i << 1, l, mid);
build(i << 1 | 1, mid + 1, r);
update(i);
return;
}
void section_squarerot(int i, int l, int r)
{
if (tr[i].l >= l && tr[i].r <= r && tr[i].l == tr[i].r)
{
tr[i].num = tr[i].mx = sqrt(tr[i].num);
return;
}
if (tr[i].r < l || tr[i].l > r)
return;
ll mid = (tr[i].l + tr[i].r) >> 1;
if (mid >= l && tr[i << 1].mx > 1)
section_squarerot(i << 1, l, r);
if (mid < r && tr[i << 1 | 1].mx > 1)
section_squarerot(i << 1 | 1, l, r);
update(i);
return;
}
ll section_query(ll i, ll l, ll r)
{
if (tr[i].l >= l && tr[i].r <= r)
return tr[i].num;
if (tr[i].r < l || tr[i].l > r)
return 0;
ll ans = 0, mid = (tr[i].l + tr[i].r) >> 1;
if (mid >= l)
ans += section_query(i << 1, l, r);
if (mid < r)
ans += section_query(i << 1 | 1, l, r);
return ans;
}
};
谢谢支持,求三连!
const long long love = you;