数据结构专题-专项训练:线段树1
回顾:
上一篇博文 数据结构专题-学习笔记:线段树 中,我们学会了线段树的基础操作。那么接下来,让我们看看线段树又能玩出什么花样。
线段树作为一种数据结构,其实题目都有一定的套路性:
- 我们需要维护什么? 是 \(sum\) ,还是 \(max,min\) ?或是别的一些奇怪的东西?
- 线段树的每个叶子节点是什么? 是数字,是另外一棵线段树(树套树)(当然这里不讲)还是些别的?
- 需要 lazy_tag 吗?lazy_tag 又要维护什么呢?
- 要不要重载运算符?(有些题目使用重载运算符会简便的多)
- 最后又要怎么修改?怎么查询?
明白这 5 点,剩下的操作就是打代码。
那么就开始吧!
3.例题
题单:
题目有两个操作:区间加等差数列,单点询问。
我们对五个问题考虑一遍:
- 我们需要维护什么?
对于这个问题,我们发现直接维护显然是维护不了的,因此需要考虑一些别的东西。
想想等差数列的性质:相邻两个数之差相等。?相邻两个数之差相等?这难道不是 差分 干的事情吗?
于是这道题的第一个问题就确定了:我们维护一个差分数组 \(sum\) ,这样就将区间加等差数列操作变成了区间加操作。
这样不就是 『区间加』+『区间查询』 的模板了吗?(为什么是区间查询?见第 5 问)
但是维护差分数组 \(sum\) 的时候,有一些细节需要注意:
- \(sum_l\) 要加上首项。这个操作可以看成对 \([l,l]\) 的区间修改(虽然 \([l,l]\) 不符合书写规范,但是这个在代码里面是没有问题的,此时 \(l(p) >= l \&\& r(p) <= r\) 等价于 \(l==r\))。
- \(sum_{l+1,...,r}\) 要统一加上 \(d\)。
- 不要忘记在 \(sum_{r+1}\) 上减去 \(首项+d\times(r-l+1)\)。
- 不要忘记开 \(\text{long long}\)!!!!!。
- 线段树的每个叶子节点是什么?
根据我们上面的讨论,初始我们针对 \(sum\) 建树(下文称『在 \(sum\) 上建树』),\(sum\) 初始化为 0,每个叶子节点代表的是 \(sum\) 的值。
- 需要 lazy_tag 吗?lazy_tag 又要维护什么呢?
因为有区间加操作,于是我们需要维护加法 lazy_tag :\(add\)。
- 要不要重载运算符?
显然不要。
- 最后又要怎么修改?怎么查询?
修改上面已经讲了。
因为我们这里变成了差分数组,所以我们最后的答案应该是(假设查询 \(a_x\)):$$a_x+\sum_{i=1}^{x}sum_i$$
而后面的求和就是『区间查询』的基本操作。
代码:
#include <bits/stdc++.h>
typedef long long LL;
const int MAXN = 1e5 + 10;
int n, m, a[MAXN];
struct node
{
int l, r;
LL add, sum;
#define l(p) tree[p].l
#define r(p) tree[p].r
#define a(p) tree[p].add
#define s(p) tree[p].sum
}tree[MAXN << 2];
int read()
{
int sum = 0, fh = 1; char ch = getchar();
while (ch < '0' || ch > '9') {if (ch == '-') fh = -1; ch = getchar();}
while (ch >= '0' && ch <= '9') {sum = (sum << 3) + (sum << 1) + (ch ^ 48); ch = getchar();}
return sum * fh;
}
void build(int p, int l, int r)
{
l(p) = l, r(p) = r;
if (l == r) {s(p) = 0; return ;}
int mid = (l + r) >> 1;
build(p << 1, l, mid); build(p << 1 | 1, mid + 1, r);
s(p) = s(p << 1) + s(p << 1 | 1);
}//对 sum 建树,没有定义 sum 的原因是我们不需要 sum 这个数组出现
void spread(int p)
{
if (a(p))
{
s(p << 1) += a(p) * (r(p << 1) - l(p << 1) + 1);
s(p << 1 | 1) += a(p) * (r(p << 1 | 1) - l(p << 1 | 1) + 1);
a(p << 1) += a(p); a(p << 1 | 1) += a(p); a(p) = 0;
}
}
void add(int p, int l, int r, LL k)
{
if (l(p) >= l && r(p) <= r) {s(p) += k * (r(p) - l(p) + 1); a(p) += k; return ;}
spread(p);
int mid = (l(p) + r(p)) >> 1;
if (l <= mid) add(p << 1, l, r, k);
if (r > mid) add(p << 1 | 1, l, r, k);
s(p) = s(p << 1) + s(p << 1 | 1);
}
LL ask(int p, int l, int r)
{
if(l(p) >= l && r(p) <= r) return s(p);
spread(p);
int mid = (l(p) + r(p)) >> 1;LL val = 0;
if (l <= mid) val += ask(p << 1, l, r);
if (r > mid) val += ask(p << 1 | 1, l, r);
return val;
}
int main()
{
n = read(); m = read();
for (int i = 1; i <= n; ++i) a[i] = read();
build(1, 1, n);
for (int i = 1; i <= m; ++i)
{
int opt = read();
if (opt == 1)
{
int l = read(), r = read(), k = read(), d = read();
add(1, l, l, k);//加上首项
if (r > l) add(1, l + 1, r, d);//加上公差
if (r != n) add(1, r + 1, r + 1, -((LL)k + (r - l) * d));//减去总和
}
else
{
int p = read();
printf("%lld\n", ask(1, 1, p) + a[p]);
}
}
return 0;
}
这道题分块可以水过,但是我们看看线段树需要怎么做。
- 我们需要维护什么?
显然要维护和,但是怎么修改呢?
首先按按计算器:$$\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{10^{12}}}}}}} \approx 1$$
于是我们发现,每个数至多做 6 次开根号操作就变成了 1。
那么因此我们可以维护这样一个东西:区间最大值。
为什么要维护它呢?你想啊,如果我们知道这一段的区间最大值,那么如果区间最大值为 1 我们不是就可以直接跳过这一段区间而不进行操作了吗?
而如果最大值不是 1,那么暴力修改即可。
这样看起来好像非常暴力,但是实际上修改操作的时间复杂度至多为 \(O(6nlogn)\),而且在 6 次操作完以后就不会再进行区间修改操作,最后就变成了 \(O(mlogn)\) 的时间复杂度。
- 线段树的每个叶子节点是什么?
就是每一个数。
- 需要 lazy_tag 吗?lazy_tag 又要维护什么呢?
不需要 lazy_tag,因为我们是暴力修改。
- 要不要重载运算符?
显然不要。
- 最后又要怎么修改?怎么查询?
修改上面已经讲了。
经典区间查询,直接找即可。
代码:
#include <bits/stdc++.h>
#define Max(a, b) ((a > b) ? a : b)
using namespace std;
const int MAXN = 1e5 + 10;
typedef long long LL;
int n, m;
LL a[MAXN];
struct node
{
int l, r;
LL sum, maxn;
#define l(p) tree[p].l
#define r(p) tree[p].r
#define s(p) tree[p].sum
#define m(p) tree[p].maxn
}tree[MAXN << 2];
LL read()
{
LL sum = 0, fh = 1; char ch = getchar();
while (ch < '0' || ch > '9') {if (ch == '-') fh = -1; ch = getchar();}
while (ch >= '0' && ch <= '9') {sum = (sum << 3) + (sum << 1) + (ch ^ 48); ch = getchar();}
return sum * fh;
}
void build(int p, int l, int r)
{
l(p) = l, r(p) = r;
if(l == r) {s(p) = m(p) = a[l]; return ;}
int mid = (l + r) >> 1;
build(p << 1, l, mid); build(p << 1 | 1, mid + 1, r);
s(p) = s(p << 1) + s(p << 1 | 1);
m(p) = Max(m(p << 1), m(p << 1 | 1));
}
void change(int p, int l, int r)
{
if (l(p) == r(p)) {s(p) = sqrt(s(p)); m(p) = sqrt(m(p)); return ;}//找到叶子节点暴力修改
if (m(p) == 1) return ;//最大值为 1 则直接跳过
int mid = (l(p) + r(p)) >> 1;
if (l <= mid) change(p << 1, l, r);
if (r > mid) change(p << 1 | 1, l, r);
s(p) = s(p << 1) + s(p << 1 | 1);
m(p) = Max(m(p << 1), m(p << 1 | 1));
}
LL ask(int p, int l, int r)
{
if (l(p) >= l && r(p) <= r) return s(p);
int mid = (l(p) + r(p)) >> 1; LL val = 0;
if (l <= mid) val += ask(p << 1, l, r);
if (r > mid) val += ask(p << 1 | 1, l, r);
return val;
}
int main()
{
n = read();
for (int i = 1; i <= n; ++i) a[i] = read();
build(1, 1, n);
m = read();
for (int i = 1; i <= m; ++i)
{
int opt = read(), l = read(), r = read();
if (l > r) swap(l, r);
if (opt == 0) change(1, l, r);
else printf("%lld\n", ask(1, l, r));
}
return 0;
}
- 先说点闲话
这道题的题面太坑了。。。。。。 3 操作与 2 操作在题面中的序号竟然是颠倒的??????我一开始没看到,导致 WA
了很久。
- 我们需要维护什么?
这道题其实还是比较显然的吧。
我们需要维护美丽值总和,价格总和,价格最大值,价格最小值。
- 线段树的每个叶子节点是什么?
一开始建树的时候我们需要将其初始化为 0,然后过程中我们可以将加入/删除花束看成单点修改的操作(作者因为太懒写的是区间修改)。
- 需要 lazy_tag 吗?lazy_tag 又要维护什么呢?
单点修改当然不需要 lazy_tag。
- 要不要重载运算符?
显然不用。
- 最后又要怎么修改?怎么查询?
对于加入花束的操作,我们假设新加入的编号为 \(n\) ,那么执行一次对 \(n\) 的单点修改即可。
对于删除花束的操作,我们直接模仿二叉树左右儿子搜一搜就可以了。
查询?直接输出根节点的维护信息不就好了?
代码:
#include <bits/stdc++.h>
#define Max(a, b) ((a > b) ? a : b)
#define Min(a, b) ((a < b) ? a : b)
using namespace std;
const int MAXN = 1e5 + 10;
typedef long long LL;
int n;
struct node
{
int l, r, maxn, minn;
LL sumw, sumc;
#define l(p) tree[p].l
#define r(p) tree[p].r
#define maxn(p) tree[p].maxn
#define minn(p) tree[p].minn
#define sumw(p) tree[p].sumw
#define sumc(p) tree[p].sumc
}tree[MAXN << 2];
bool book[(MAXN << 3) + (MAXN << 1)];
int read()
{
int sum = 0, fh = 1; char ch = getchar();
while (ch < '0' || ch > '9') {if (ch == '-') fh = -1; ch = getchar();}
while (ch >= '0' && ch <= '9') {sum = (sum << 3) + (sum << 1) + (ch ^ 48); ch = getchar();}
return sum * fh;
}
void build(int p, int l, int r)
{
l(p) = l, r(p) = r;
if (l == r) {sumw(p) = sumc(p) = maxn(p) = 0; minn(p) = 0x7f7f7f7f; return ;}
int mid = (l(p) + r(p)) >> 1;
build(p << 1, l, mid); build(p << 1 | 1, mid + 1, r);
sumw(p) = sumc(p) = maxn(p) = 0; minn(p) = 0x7f7f7f7f;
}
void change(int p, int l, int r, int w, int c)
{
if (book[c]) return ;
if (l(p) >= l && r(p) <= r)
{
sumw(p) += w;
sumc(p) += c;
maxn(p) = Max(maxn(p), c);
minn(p) = Min(minn(p), c);
book[c] = 1;
return ;
}
int mid = (l(p) + r(p)) >> 1;
if (l <= mid) change(p << 1, l, r, w, c);
if (r > mid) change(p << 1 | 1, l, r, w, c);
sumw(p) = sumw(p << 1) + sumw(p << 1 | 1);
sumc(p) = sumc(p << 1) + sumc(p << 1 | 1);
maxn(p) = Max(maxn(p << 1), maxn(p << 1 | 1));
minn(p) = Min(minn(p << 1), minn(p << 1 | 1));
}
void Delete_the_cheapest(int p)
{
if (l(p) == r(p))
{
book[sumc(p)] = 0;
sumw(p) = sumc(p) = 0;
maxn(p) = 0; minn(p) = 0x7f7f7f7f;
return ;
}
if (minn(p) == 0x7f7f7f7f) return ;
if (minn(p << 1) == minn(p)) Delete_the_cheapest(p << 1);
else Delete_the_cheapest(p << 1 | 1);
sumw(p) = sumw(p << 1) + sumw(p << 1 | 1);
sumc(p) = sumc(p << 1) + sumc(p << 1 | 1);
maxn(p) = Max(maxn(p << 1), maxn(p << 1 | 1));
minn(p) = Min(minn(p << 1), minn(p << 1 | 1));
}
void Delete_the_most_expensive(int p)
{
if (l(p) == r(p))
{
book[sumc(p)] = 0;
sumw(p) = sumc(p) = 0;
maxn(p) = 0; minn(p) = 0x7f7f7f7f;
return ;
}
if (maxn(p) == 0) return ;
if (maxn(p << 1) == maxn(p)) Delete_the_most_expensive(p << 1);
else Delete_the_most_expensive(p << 1 | 1);
sumw(p) = sumw(p << 1) + sumw(p << 1 | 1);
sumc(p) = sumc(p << 1) + sumc(p << 1 | 1);
maxn(p) = Max(maxn(p << 1), maxn(p << 1 | 1));
minn(p) = Min(minn(p << 1), minn(p << 1 | 1));
}
int main()
{
build(1, 1, 100000);
while (1)
{
int opt = read();
if (opt == -1) break;
if (opt == 1)
{
int w = read(), c = read();
++n; change(1, n, n, w, c);
}
if (opt == 3) Delete_the_cheapest(1);
if (opt == 2) Delete_the_most_expensive(1);
}
printf("%lld %lld\n", tree[1].sumw, tree[1].sumc);
return 0;
}
这道题原先是 Ynoi 的,但是后面被除名了。
- 我们需要维护什么?
我们先看看目前数学课本上的两个公式:
所以呢?我们维护 \(\sin,\cos\) 两个东西,然后更新的时候按照上面的公式以区间加的形式更新即可。
为什么可以整体更新?考虑结合律。
- 线段树的每个叶子节点是什么?
初始值,及其 \(\sin,\cos\) 值。
- 需要 lazy_tag 吗?lazy_tag 又要维护什么呢?
区间加操作需要 lazy_tag。
- 要不要重载运算符?
不需要。
- 最后又要怎么修改?怎么查询?
修改直接区间修改即可。
查询直接区间查询即可。
代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int MAXN = 2e5 + 10;
int n, a[MAXN], m;
struct node
{
int l, r;
LL add;
double Sin, Cos;
#define l(p) tree[p].l
#define r(p) tree[p].r
#define s(p) tree[p].Sin
#define c(p) tree[p].Cos
#define a(p) tree[p].add
}tree[MAXN << 2];
int read()
{
int sum = 0; char ch = getchar();
while (ch < '0' || ch > '9') ch = getchar();
while (ch >= '0' && ch <= '9') {sum = (sum << 3) + (sum << 1) + (ch ^ 48); ch = getchar();}
return sum;
}
void build(int p, int l, int r)
{
l(p) = l, r(p) = r;
if (l == r) {s(p) = sin(a[l]), c(p) = cos(a[l]); return ;}
int mid = (l + r) >> 1;
build(p << 1, l, mid); build(p << 1 | 1, mid + 1, r);
s(p) = s(p << 1) + s(p << 1 | 1);
c(p) = c(p << 1) + c(p << 1 | 1);
}
void spread(int p)
{
if (a(p))
{
double s = s(p << 1), c = c(p << 1);//不要忘记提前存下来!
s(p << 1) = s * cos(a(p)) + c * sin(a(p));
c(p << 1) = c * cos(a(p)) - s * sin(a(p));
s = s(p << 1 | 1), c = c(p << 1 | 1);
s(p << 1 | 1) = s * cos(a(p)) + c * sin(a(p));
c(p << 1 | 1) = c * cos(a(p)) - s * sin(a(p));
a(p << 1) += a(p), a(p << 1 | 1) += a(p); a(p) = 0;
}
}
void change(int p, int l, int r, int val)
{
if (l(p) >= l && r(p) <= r)
{
double s = s(p), c = c(p);
s(p) = s * cos(val) + c * sin(val);
c(p) = c * cos(val) - s * sin(val);
a(p) += val; return ;
}
spread(p);
int mid = (l(p) + r(p)) >> 1;
if (l <= mid) change(p << 1, l, r, val);
if (r > mid) change(p << 1 | 1, l, r, val);
s(p) = s(p << 1) + s(p << 1 | 1);
c(p) = c(p << 1) + c(p << 1 | 1);
}
double ask(int p, int l, int r)
{
if (l(p) >= l && r(p) <= r) return s(p);
spread(p);
int mid = (l(p) + r(p)) >> 1; double val = 0;
if (l <= mid) val += ask(p << 1, l, r);
if (r > mid) val += ask(p << 1 | 1, l, r);
return val;
}
int main()
{
n = read();
for (int i = 1; i <= n; ++i) a[i] = read();
build(1, 1, n);
m = read();
for (int i = 1; i <= m; ++i)
{
int opt = read();
if (opt == 1)
{
int l = read(), r = read(), val = read();
change(1, l, r, val);
}
else
{
int l = read(), r = read();
printf("%.1lf\n", ask(1, l, r));
}
}
return 0;
}
这道题是道好题目,很考验各位的算法功底与思维能力。
P.S. 题目因为一些原因隐藏了。
update:据说这玩意有点像 DDP 是吧(
- 我们需要维护什么?
- 线段树的每个叶子节点是什么?
- 最后又要怎么修改?怎么查询?
这个三个问题其实就很不好想了。
我们想一想,\(2 \leq n \leq 5,m \leq 100000\),其实通过这个数据范围我们不难发现,我们需要在操作上建树,线段树需要维护的是关于 \(n\) 的一些东西。
那么 \(n\) 这么小,我们肯定可以相对暴力一点的维护 \(n\) 。
于是这里有一个思路:矩阵。
我们将操作矩阵化。
首先我们看看矩阵里面的一种特殊情形:单位矩阵。比如:
\(\begin{bmatrix}1&0&0\\0&1&0\\0&0&1\end{bmatrix}\)
那么如果我们用这个矩阵跟前面的单位矩阵相乘:
\(\begin{bmatrix}a_1&a_2&a_3\end{bmatrix}\)
那么结果矩阵恰好就是:
\(\begin{bmatrix}a_1&a_2&a_3\end{bmatrix}\)
因此,我们可以针对 \(m\) 个操作建树,一开始所有叶子节点都是单位矩阵。
然后呢?对于修改操作:
比如我们现在要将 1 的奶茶分配给 2,3,那么矩阵就变成了这样(当然实际题目要求 \(\dfrac{1}{2}\) 在 \(993244853\) 意义下的逆元):
\(\begin{bmatrix}0&\dfrac{1}{2}&\dfrac{1}{2}\\0&1&0\\0&0&1\end{bmatrix}\)
然后执行单点修改操作即可。
那么答案是什么呢?
这里线段树需要维护矩阵乘法的结果,最后取出根节点的矩阵与原始矩阵(\(a_i\) 构成的矩阵)相乘即可。
为什么是相乘?这与对矩阵的理解有关。
实际上如果你看过这篇博文(From myan)就会发现矩阵实质上是一种对坐标系的描述,而矩阵乘法 \(A \times B=C\) 实质上可以看成向量组 B 在坐标系 A 中描述的结果是向量组 C。
因此如果我们有两个已经操作过的矩阵 \(A,B\),现在要合并它们,那么我们只需要将 \(B\) 在 \(A\) 中表示出来即可。
那么如何撤销修改呢?考虑到矩阵乘法满足结合律,因此撤销哪次操作就将其修改为单位矩阵即可。
- 需要 lazy_tag 吗?lazy_tag 又要维护什么呢?
单点修改不需要 lazy_tag。
- 要不要重载运算符?
这里需要!我们需要在结构体当中重载运算符 \(*\) ,来方便我们进行矩阵乘法。
代码:
#include <bits/stdc++.h>
using std::queue;
typedef long long LL;
const int MAXM = 1e5 + 10, P = 993244853;
int n, m, cnt, inv[10];
queue<int>q[10];
struct node
{
int d[10][10];
int x, y;
node()
{
memset(d, 0, sizeof(d));
x = y = 0;
}
void init(int xx, int yy)
{
memset(d, 0, sizeof(d));
x = xx, y = yy;
for (int i = 1; i <= x; ++i) d[i][i] = 1;
return ;
}//初始化单位矩阵
node operator*(const node &b)
{
node c;
c.x = x; c.y = b.y;
for (int i = 1; i <= x; ++i)
for (int k = 1; k <= y; ++k)
for (int j = 1; j <= b.y; ++j)
c.d[i][j] = (1ll * c.d[i][j] + 1ll * d[i][k] * b.d[k][j] % P) % P;
return c;
}//重载运算符
void output()
{
for (int i = 1; i <= x; ++i)
{
for (int j = 1; j <= y; ++j) printf("%d ", d[i][j]);
printf("\n");
}
return ;
}//输出矩阵
}tree[MAXM << 2], a;
int read()
{
int sum = 0, fh = 1; char ch = getchar();
while (ch < '0' || ch > '9') {if (ch == '-') fh = -1; ch = getchar();}
while (ch >= '0' && ch <= '9') {sum = (sum << 3) + (sum << 1) + (ch ^ 48); ch = getchar();}
return sum * fh;
}
void getinv(int x)
{
inv[1] = 1;
for(int i = 2; i <= x; ++i) inv[i] = ((LL)P - (LL)P / i) * inv[P % i] % P;
}//求逆元
void build(int p, int l, int r)
{
if (l == r) {tree[p].init(n, n); return ;}
int mid = (l + r) >> 1;
build(p << 1, l, mid); build(p << 1 | 1, mid + 1, r);
tree[p].init(n, n);
}
void change(int p, int l, int r, int zzh, const node &s)
{
if (l == r) {tree[p] = s; return ;}
int mid = (l + r) >> 1;
if (zzh <= mid) change(p << 1, l, mid, zzh, s);
else change(p << 1 | 1, mid + 1, r, zzh, s);
tree[p] = tree[p << 1] * tree[p << 1 | 1];
}
int main()
{
n = read(); m = read(); a.x = 1; a.y = n;
for (int i = 1; i <= n; ++i) a.d[1][i]= read();
build(1, 1, m); getinv(n);
for (int i = 1; i <= m; ++i)
{
int opt = read();node t;
if (opt == 1)
{
++cnt;
int p = read(), k = read();
t.init(n,n);
t.d[p][p] = 0;
for (int j = 1; j <= k; ++j)
{
int tmp = read(); t.d[p][tmp] = inv[k];
}
change(1, 1, m, cnt, t);
q[p].push(cnt);
}
else
{
int p = read();
if (!q[p].empty())
{
t.init(n, n);
change(1, 1, m, q[p].front(), t);
q[p].pop();
}
}
node ans = a * tree[1];
ans.output();
}
return 0;
}
4.小总结
对于线段树中到底要维护的是什么,我们主要考虑以下这 5 个问题:
- 我们需要维护什么?
- 线段树的每个叶子节点是什么?
- 需要 lazy_tag 吗?lazy_tag 又要维护什么呢?
- 要不要重载运算符?
- 最后又要怎么修改?怎么查询?
想清楚这 5 个问题,线段树的题目就不难做了。
那么接下来,我们看看 GSS1-5 的题目用线段树如何解决吧!(本质上还是这 5 步,只不过玩法不一样了,更具思维性)
详情请见 数据结构专题-专项训练:线段树2(GSS1-5)。