线段树什么的不是简简单单嘛,我教你!:基础篇
零、序言——万物滴开篇
也许你是苦于笔试的打工人,也许你是步入算法圈不久的小小萌新(我也是萌新) ,也许你是在网上搜索数据结构课设的倒霉学生。不管怎么样,看完本篇文章,希望对您有所帮助。
走起!
观前提醒:看本文章最好有一定的二叉树基础(至少要会递归遍历树的程度)和算法基础(咋的也得知道时间复杂度是什么)
线段树 是算法竞赛中非常常见一种的数据结构,功能强大,学术一点的话说就是:常用的用来维护 区间信息 的数据结构。线段树 可以在较小的时间复杂度内实现单点修改、区间修改、区间查询(区间求和,求区间最大值,求区间最小值)等操作。
没听懂?没事,以前我也听不懂,让我们来——
一、举个栗子
给你一个长度为 n 的数组,有 q 次询问,每次询问给你一个范围:l 和 r ,让你求出数组中第l个数到第r个总和是多少。
对于每次询问,我们可以从第l个数一直累加到第r个数,这样就能轻轻松松得出结果,但是这么做的时间复杂度是O(n) ,一共q次询问,那么总的时间复杂度就是O(n*q) 。对于数据大的情况下,这样是无法通过题目的。但是如果使用 线段树 ,就可以把单次询问的时间复杂度减小到 O(logn) ,这样q次询问下来,总的时间复杂度只有 O(q*logn) ,非常快哦!
说到这里,可能有同学学的比较好,学过 前缀和 算法,他不服气,说使用前缀和,单次询问的复杂度只有 O(1) ,总的复杂度就是 O(q) ,不比这个线段树还要快吗?
如果是当前这一题,那么当然是前缀和比线段树要快,无可否认(线段树:我成🤡了? ),但是,要是给题目加上一个条件之后,你再看:
给你一个长度为 n 的数组,有 q 次操作,操作有两种:
-
1:给你一个下标i和一个数x,需要你把数组中第i个数加上x;
-
2:给你一个范围:l 和 r ,让你求出数组中第l个数到第r个总和是多少。
这一题和上一题不一样的是,多了个修改数组的操作。而我们知道,前缀和是必须要预处理出前缀和数组的,预处理的复杂度是O(n) ,但是正常情况下,只用预处理一次,所以这一点我们一般可以忽略不计。可这里,数组可能随时会发生变化,如果数组发生了变化,那之前预处理出的前缀和数组就不对了,由此得到的区间和答案也不正确。除非每次修改后,重新预处理出前缀和数组,但是这样,单次询问的复杂度就从O(1) 退化到了O(n) ,总复杂度变成了O(n*q) 。
前缀和做法翻车哩!
此时我们回头看看刚刚的小丑线段树,线段树的询问是在O(logn) 中得出结果,但还有一点:线段树的修改也是O(logn) ,并且修改过后得出结果的复杂度仍然是O(logn) 。所以对于线段树来说,这道题总的复杂度依旧是O(logn) !线段树扳回一城!
而就像我们开始说的,不光是求区间和,区间最值什么的也能在O(logn) 的复杂度内得出结果,不说单点修改数组的值,哪怕是修改数组的一段区间,它也可以在O(logn) 的复杂度内完成!
那么,线段树是怎么做到这一点的呢?
二、线段树——什么树?
实际上,线段树其实也就是一个二叉树。
首先,我们先来聊一聊它的基础形态:二叉树。
二叉树算是非常基础的数据结构了,树如其名,每一个节点最多只有两个孩子,一左一右,即二叉。图中就是一个最简单的二叉树:
那么线段树是怎么做到能在O(n*logn) 的复杂度上得到区间和的呢?
- 我们先给上图这二叉树的两个孩子分别赋一个值:x 和 y。
- 那么我们想求出这两个点的总和,只需要:root->left->val + root->right->val。
- root的左右两个孩子都有值了,但root还是空荡荡的,我们不如把这个总和当作root节点的值。
此时,这个二叉树的情况是:
那么之后,如果想知道root的左右俩孩子的值的总和,我们并不需要遍历到这两个点,只要直接遍历到root上,就可以得出结果了。
现在你可能觉得没什么大不了的,也才两个点,我直接遍历他们和遍历一个点也没什么区别。
好,线段树觉得自己被瞧不起了,它开始生长:
现在想知道a、b、c、d四个点的总和,只需要看最上面的那一个点就行了。
还不够?它继续生长:
再长:
再长:
算了吧再长就放不下了。
这就是线段树的运行方式,我们就可以这样,一层一层的继续套下去,最后,如果想知道整个数组的总和,只要走到最上面的那个点就行,而不用遍历整个数组。这效率,显而易见的高。
线段树中的 ”树“ 我们已经体会到了,但是我们还有疑问。
三、线段树——何为线段?
首先说明一点:线段树的叶子,就是数组的元素
(为了方便,我们拿小一点的树来做介绍)
如果此时有一个长度为4的数组,它的元素分别为:a,b,c,d。那么在线段树中表现出来的就是:
然后我们又知道,元素a在数组中的出现范围是 {1,1} ,元素b的出现范围是 {2,2} ,c的是 {3,3} ,d是 {4,4} 。
我们根据这一点,把线段树用另一种形式表现出来:
每个节点上的范围,就表示了这个节点管辖的数组范围。
比如a是{1,1},b是{2,2},那么他们的父亲管辖的范围就是{1,2},如果我们想知道数组中第一个数到第二个数的总和,只需要走到点e就可以了。以此类推。
而这,就是线段树中的——线段。 (说是线段,不如说范围更合适。)
在线段树中,每一个节点都有其负责的一个范围,当我们遇到区间查询问题的时候,只要走到对应的节点就可以得出结果了。
什么,你问我线段树上没有节点负责{1,3}范围,你想问{1,3}范围的总和怎么办?那当然是节点e的结果加上节点c的结果了啊,笨猪猪捏(~ ̄(OO) ̄)ブ。
我们每次询问都是从上往下遍历树的节点,而我们知道,对于一个有n个叶子的二叉树,它的深度是log2(n) ,所以不论是查询还是修改,我们都只用跑log2(n) 层,时间复杂度就为:O(logn) 。
要是看到这里,对线段树终于有所了解,有茅塞顿开之感的同学能不能在屏幕前给咱鼓个掌捏。
好哩,这下线段树的原理都解释清楚啦!!!接下来终于要到了——四、代码是如何实现的?
因为掘金的各位友友可能都是非算法竞赛选手,比起数组形式的树可能更习惯结构体形式,所以这一环节我会用结构体形式的代码做讲解(数组形式的代码我会在最后面说一下)
这是最重要也最难的一节啦(因为代码太多了,可能有点看不过来)!通过这一节你就成功啦!加油!!
我们还是先回到二叉树,一般我们写二叉树的代码是:
别问我为啥不用力扣的二叉树结构体,指针那么恶心的玩意我不想碰
struct TreeNode {
int val;
//可能有同学不理解为什么这两个指向孩子的是整形变量不是指针
//left和right是数组Node的下标,所以准确来说这个节点的孩子不是left,而是Node[left]
int left,right;
}Node[N];
相较于普通的二叉树,线段树的结构体代码多出了两个变量,即代表范围的:l(表示范围左端点) 和 r(表示范围右端点)
代码如下:
struct TreeNode {
int val;
int left,right;
int l,r;
}Node[N];
总节点(就是最顶上的那一个),它的下标为1,即Node[1]。
对于线段树类型的题(像我们第一节举那个的栗子),一般都会先给我们一个数组。那么,我们首先要做的就是利用这个数组来生成线段树:
- 如果数组的长度为n,那么总节点(Node[1])的最初范围是 {1,n}
- 然后我们将范围一分为二,左孩子的范围就是: {1,mid} ,右孩子的范围就是: {mid+1,n} 。
- 到了孩子节点,我们继续分,以此类推。
- 当到了某个节点,范围变成了 {l,l} (即左右端点相等,表示这个点只代表一个数),就说明这个节点就是叶子节点,我们把数组的值赋给它(范围是啥就把对于的值给它,可不是乱赋值嗷)
- 当某个节点的左右节点都赋完值了,他们的父亲节点的值,就是这两个孩子节点的值的总和。
代码如下:
struct TreeNode {
int val;
int left, right;
int l, r;
}Node[N];
//a是题目给的数组;idx是建树过程中,用于给各个树打上序号
int a[N],idx = 1;
void build_tree(int pos)
{
//如果左右端点相同,说明这是叶子节点
if (Node[pos].l == Node[pos].r)
{
//把数组的值赋给他
Node[pos].val = a[Node[pos].l];
return;
}
//如果不相同,说明范围还能往下分
int mid = (Node[pos].l + Node[pos].r) / 2;
//左右孩子的下标
int left = ++idx, right = ++idx;
Node[pos].left = left;
Node[pos].right = right;
//左孩子的范围
Node[left].l = Node[pos].l;
Node[left].r = mid;
//右孩子的范围
Node[right].l = mid + 1;
Node[right].r = Node[pos].r;
//递归到下一层
build_tree(left);
build_tree(right);
//当前节点的值,就是两个孩子的值相加
Node[pos].val = Node[left].val + Node[right].val;
}
至于修改,我们也是遍历到对应的点,比如要改的是数组的第3个值,那么我们就找到树中,范围为{3,3}的那个叶子并修改。
void revise(int pos, int l, int x)
{
//如果这就是我们要找的范围,修改这个节点的值
if (Node[pos].l == l && Node[pos].r == l)
{
Node[pos].val += x;
return;
}
//如果不是,我们就看我们要找的点,是当前点的左孩子还是右孩子
int mid = (Node[pos].l + Node[pos].r) / 2;
if (l <= mid)revise(Node[pos].left, l, x);
else revise(Node[pos].right, l, x);
//因为叶子节点被修改,那么上面所有受影响的点的值都要更新
int left = Node[pos].left, right = Node[pos].right;
Node[pos].val = Node[left].val + Node[right].val;
}
最后是询问一整个区间的和。
int calc(int pos, int l, int r)
{
//如果这就是我们要找的范围,返回这个节点的值
if (Node[pos].l == l && Node[pos].r == r)
{
return Node[pos].val;
}
//如果不是,就根据当前节点的范围,判断我们下一步该往左走还是右走
int mid = (Node[pos].l + Node[pos].r) / 2;
//如果范围全在左边,就直接去左节点
if (r <= mid)return calc(Node[pos].left, l, r);
else
//如果范围全在右边,就直接去右节点
if (l > mid)return calc(Node[pos].right, l, r);
else
{
//如果范围既在左又在右,则分开跑。注意这里要修改范围。
int x = calc(Node[pos].left, l, mid);
int y = calc(Node[pos].right, mid + 1, r);
//返回两边的结果
return x + y;
}
}
到此,这就是线段树:单点修改+区间查询的全部模板代码了。
我们来提交一下这一题:P3374 【模板】树状数组 1(别管为什么题目叫树状数组)
题目总代码:
#include<iostream>
using namespace std;
#include<vector>
#include<algorithm>
#include<math.h>
#include<set>
#include <random>
#include<numeric>
#include<string>
#include<string.h>
#include<iterator>
#include<fstream>
#include<map>
#include<unordered_map>
#include<stack>
#include<list>
#include<queue>
#include<iomanip>
#include<bitset>
//#pragma GCC optimize(2)
//#pragma GCC optimize(3)
#define endl '\n'
#define int ll
#define all(a) a.begin(),a.end()
#define PI acos(-1)
#define INF 0x3f3f3f3f
typedef long long ll;
typedef unsigned long long ull;
typedef pair<ll, ll>PII;
const int N = 1e6 + 50, MOD = 1e9 + 7;
struct TreeNode {
int val;
int left, right;
int l, r;
}Node[N];
//a是题目给的数组;idx是建树过程中,用于给各个树打上序号
int a[N],idx = 1;
void build_tree(int pos)
{
//如果左右端点相同,说明这是叶子节点
if (Node[pos].l == Node[pos].r)
{
//把数组的值赋给他
Node[pos].val = a[Node[pos].l];
return;
}
//如果不相同,说明范围还能往下分
int mid = (Node[pos].l + Node[pos].r) / 2;
//左右孩子的下标
int left = ++idx, right = ++idx;
Node[pos].left = left;
Node[pos].right = right;
//左孩子的范围
Node[left].l = Node[pos].l;
Node[left].r = mid;
//右孩子的范围
Node[right].l = mid + 1;
Node[right].r = Node[pos].r;
//递归到下一层
build_tree(left);
build_tree(right);
//当前节点的值,就是两个孩子的值相加
Node[pos].val = Node[left].val + Node[right].val;
}
void revise(int pos, int l, int x)
{
//如果这就是我们要找的范围,修改这个节点的值
if (Node[pos].l == l && Node[pos].r == l)
{
Node[pos].val += x;
return;
}
//如果不是,我们就看我们要找的点,是当前点的左孩子还是右孩子
int mid = (Node[pos].l + Node[pos].r) / 2;
if (l <= mid)revise(Node[pos].left, l, x);
else revise(Node[pos].right, l, x);
//因为叶子节点被修改,那么上面所有受影响的点的值都要更新
int left = Node[pos].left, right = Node[pos].right;
Node[pos].val = Node[left].val + Node[right].val;
}
int calc(int pos, int l, int r)
{
//如果这就是我们要找的范围,返回这个节点的值
if (Node[pos].l == l && Node[pos].r == r)
{
return Node[pos].val;
}
//如果不是,就根据当前节点的范围,判断我们下一步该往左走还是右走
int mid = (Node[pos].l + Node[pos].r) / 2;
//如果范围全在左边,就直接去左节点
if (r <= mid)return calc(Node[pos].left, l, r);
else
//如果范围全在右边,就直接去右节点
if (l > mid)return calc(Node[pos].right, l, r);
else
{
//如果范围既在左又在右,则分开跑。注意这里要修改范围。
int x = calc(Node[pos].left, l, mid);
int y = calc(Node[pos].right, mid + 1, r);
//返回两边的结果
return x + y;
}
}
signed main()
{
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
int n, q, op, x, y;
cin >> n >> q;
for (int i = 1; i <= n; i++)cin >> a[i];
Node[1].l = 1, Node[1].r = n;
build_tree(1);
for (int i = 1; i <= q; i++)
{
cin >> op >> x >> y;
if (op == 1)
revise(1, x, y);
else
cout << calc(1, x, y) << endl;
}
return 0;
}
哈!满分!太棒啦!
为了对比,我们使用暴力做法看看能不能通过:
可以看出,小型数据还是可以通过的,但是数据大了就会超时。
顺带一提,实际上结构体做线段树不太优,更常见的做法是用数组模拟,具体细节可以看注释,代码如下:
#include<iostream>
using namespace std;
#include<vector>
#include<algorithm>
#include<math.h>
#include<set>
#include <random>
#include<numeric>
#include<string>
#include<string.h>
#include<iterator>
#include<fstream>
#include<map>
#include<unordered_map>
#include<stack>
#include<list>
#include<queue>
#include<iomanip>
#include<bitset>
//#pragma GCC optimize(2)
//#pragma GCC optimize(3)
#define endl '\n'
#define int ll
#define all(a) a.begin(),a.end()
#define PI acos(-1)
#define INF 0x3f3f3f3f
typedef long long ll;
typedef unsigned long long ull;
typedef pair<ll, ll>PII;
const int N = 1e6 + 50, MOD = 1e9 + 7;
//f的作用就相当于Node的val
//这里,如果当前节点的编号是k,那么它的左孩子是k+k,右孩子是k+k+1
//如果数组长度为n,那么线段树的数组长度则需要是4*n
int a[N], f[4 * N];
//数组形式的线段树,每个函数,开头都是三个变量:当前节点的编号k,当前节点的管辖区间的左端点l和右端点r
void build_tree(int k, int l, int r)
{
if (l == r)
{
f[k] = a[l];
return;
}
int mid = (l + r) / 2;
build_tree(k + k, l, mid);
build_tree(k + k + 1, mid + 1, r);
f[k] = f[k + k] + f[k + k + 1];
}
void revise(int k, int l, int r, int x, int y)
{
if (l == r)
{
f[k] += y;
return;
}
int mid = (l + r) / 2;
if (x <= mid)revise(k + k, l, mid, x, y);
else revise(k + k + 1, mid + 1, r, x, y);
f[k] = f[k + k] + f[k + k + 1];
}
int calc(int k, int l, int r, int x, int y)
{
if (l == x && r == y)return f[k];
int mid = (l + r) / 2;
if (y <= mid)return calc(k + k, l, mid, x, y);
else
if (x > mid)return calc(k + k + 1, mid + 1, r, x, y);
else return calc(k + k, l, mid, x, mid) + calc(k + k + 1, mid + 1, r, mid + 1, y);
}
signed main()
{
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
int n, q, op, x, y;
cin >> n >> q;
for (int i = 1; i <= n; i++)cin >> a[i];
//同样的,1号点的范围就是1~n
build_tree(1, 1, n);
for (int i = 1; i <= q; i++)
{
cin >> op >> x >> y;
if (op == 1)
revise(1, 1, n, x, y);
else
{
cout << calc(1, 1, n, x, y) << endl;
}
}
return 0;
}
我们对比一下结构体线段树的用时和内存会发现,数组模拟线段树是优于结构体线段树的,特别是在内存方面
至此,就是线段树的入门篇的全部内容啦!是不是感觉自己学习到了一个很厉害的新知识而开心不已呢?
不过先别急着走鸭,学会了新本领,就要找地方练练,不然岂不是白瞎了!
所以,我们来——
五、写题!我要打写10个!
别被题目吓到了哦,并不是真的有10个题(笑
关于线段树,其实本身并不难,难的是玩出花来。有时候可能会遇到:”我焯?这也能用线段树?“的情况。
所以一定要多写题训练!!
关于写题练习线段树,个人推荐Codeforces平台的EDU的题单,基本各种常见用法基础题型都有:
(注意看,是part1,part2是区间修改型线段树,现在我们讲的程度还做不出来,会自闭的)
A - Segment Tree, part 1 - Codeforces
这一题和前面那一题很像,只不过前面的是:给第i个数加上x,而这里是:把第i个数变成x。
其实就是修改函数的一点点不一样罢了,代码如下:
void revise(int k, int l, int r, int x, int y)
{
if (l == r)
{
//这是原来的代码
//f[k] += y;
//这是现在的代码
f[k]=y;
return;
}
int mid = (l + r) / 2;
if (x <= mid)revise(k + k, l, mid, x, y);
else revise(k + k + 1, mid + 1, r, x, y);
f[k] = f[k + k] + f[k + k + 1];
}
还有要注意的一点是,前面那一题我们的范围是1到n,这一题的范围是0到n-1,记得稍加修改。
AC代码
#include<iostream>
using namespace std;
#include<vector>
#include<algorithm>
#include<math.h>
#include<set>
#include <random>
#include<numeric>
#include<string>
#include<string.h>
#include<iterator>
#include<fstream>
#include<map>
#include<unordered_map>
#include<stack>
#include<list>
#include<queue>
#include<iomanip>
#include<bitset>
//#pragma GCC optimize(2)
//#pragma GCC optimize(3)
#define endl '\n'
#define int ll
#define all(a) a.begin(),a.end()
#define PI acos(-1)
#define INF 0x3f3f3f3f
typedef long long ll;
typedef unsigned long long ull;
typedef pair<ll, ll>PII;
const int N = 1e6 + 50, MOD = 1e9 + 7;
int a[N], f[4 * N];
void build_tree(int k, int l, int r)
{
if (l == r)
{
f[k] = a[l];
return;
}
int mid = (l + r) / 2;
build_tree(k + k, l, mid);
build_tree(k + k + 1, mid + 1, r);
f[k] = f[k + k] + f[k + k + 1];
}
void revise(int k, int l, int r, int x, int y)
{
if (l == r)
{
f[k] = y;
return;
}
int mid = (l + r) / 2;
if (x <= mid)revise(k + k, l, mid, x, y);
else revise(k + k + 1, mid + 1, r, x, y);
f[k] = f[k + k] + f[k + k + 1];
}
int calc(int k, int l, int r, int x, int y)
{
if (l == x && r == y)return f[k];
int mid = (l + r) / 2;
if (y <= mid)return calc(k + k, l, mid, x, y);
else
if (x > mid)return calc(k + k + 1, mid + 1, r, x, y);
else return calc(k + k, l, mid, x, mid) + calc(k + k + 1, mid + 1, r, mid + 1, y);
}
signed main()
{
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
int n, q, op, x, y;
cin >> n >> q;
for (int i = 1; i <= n; i++)cin >> a[i];
build_tree(1, 1, n);
for (int i = 1; i <= q; i++)
{
cin >> op >> x >> y;
if (op == 1)
{
x++;
revise(1, 1, n, x, y);
}
else
{
x++;
cout << calc(1, 1, n, x, y) << endl;
}
}
return 0;
}
B - Segment Tree, part 1 - Codeforces
这一题中,问的不再是区间和了,而是区间中的最小值。
我们可以想下,区间和中,父亲节点的值是:左孩子节点的值+右孩子节点的值
现在我们要的是区间中的最小值,那么只要把父亲节点的值修改成:min(左孩子节点的值,右孩子节点的值) 。即可。
AC代码
#include<iostream>
using namespace std;
#include<vector>
#include<algorithm>
#include<math.h>
#include<set>
#include <random>
#include<numeric>
#include<string>
#include<string.h>
#include<iterator>
#include<fstream>
#include<map>
#include<unordered_map>
#include<stack>
#include<list>
#include<queue>
#include<iomanip>
#include<bitset>
//#pragma GCC optimize(2)
//#pragma GCC optimize(3)
#define endl '\n'
#define int ll
#define all(a) a.begin(),a.end()
#define PI acos(-1)
#define INF 0x3f3f3f3f
typedef long long ll;
typedef unsigned long long ull;
typedef pair<ll, ll>PII;
const int N = 1e6 + 50, MOD = 1e9 + 7;
int a[N], f[4 * N];
void build_tree(int k, int l, int r)
{
if (l == r)
{
f[k] = a[l];
return;
}
int mid = (l + r) / 2;
build_tree(k + k, l, mid);
build_tree(k + k + 1, mid + 1, r);
//原来的代码
//f[k] = f[k + k] + f[k + k + 1];
//现在的代码
f[k] = min(f[k + k], f[k + k + 1]);
}
void revise(int k, int l, int r, int x, int y)
{
if (l == r)
{
f[k] = y;
return;
}
int mid = (l + r) / 2;
if (x <= mid)revise(k + k, l, mid, x, y);
else revise(k + k + 1, mid + 1, r, x, y);
//原来的代码
//f[k] = f[k + k] + f[k + k + 1];
//现在的代码
f[k] = min(f[k + k], f[k + k + 1]);
}
int calc(int k, int l, int r, int x, int y)
{
if (l == x && r == y)return f[k];
int mid = (l + r) / 2;
if (y <= mid)return calc(k + k, l, mid, x, y);
else
if (x > mid)return calc(k + k + 1, mid + 1, r, x, y);
//原来的代码
//else return calc(k + k, l, mid, x, mid) + calc(k + k + 1, mid + 1, r, mid + 1, y);
//现在的代码
else return min(calc(k + k, l, mid, x, mid), calc(k + k + 1, mid + 1, r, mid + 1, y));
}
signed main()
{
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
int n, q, op, x, y;
cin >> n >> q;
for (int i = 1; i <= n; i++)cin >> a[i];
build_tree(1, 1, n);
for (int i = 1; i <= q; i++)
{
cin >> op >> x >> y;
if (op == 1)
{
x++;
revise(1, 1, n, x, y);
}
else
{
x++;
cout << calc(1, 1, n, x, y) << endl;
}
}
return 0;
}
C - Segment Tree, part 1 - Codeforces
这一题中,不光需要你求出区间内的最小值,还要求出区间内有多少个这个最小值。
在这一题,我们可以额外开一个数组cnt。f[i]表示当前区间的最小值是多少,cnt[i]表示当前区间有多少个最小值,很明显的,每个叶子的cnt[i]初始为1。
那么对于父亲节点来说:
- 如果左孩子和右孩子的最小值一样,父亲节点的最小值就是他们,而父亲节点的cnt,就是两个孩子的cnt加在一起。
- 如果左孩子的最小值小于右孩子,父亲节点的最小值就是左孩子最小值,父亲节点的cnt就是左孩子的cnt。
- 如果左孩子的最小值大于右孩子,父亲节点的最小值就是右孩子最小值,父亲节点的cnt就是右孩子的cnt。
在询问操作中,当遇到区间分开的情况,我们也要重复如上操作。
为此,这里我把询问函数的返回值从单个整数改成了一个数对:first是最小值,second是个数cnt。
AC代码
#include<iostream>
using namespace std;
#include<vector>
#include<algorithm>
#include<math.h>
#include<set>
#include <random>
#include<numeric>
#include<string>
#include<string.h>
#include<iterator>
#include<fstream>
#include<map>
#include<unordered_map>
#include<stack>
#include<list>
#include<queue>
#include<iomanip>
#include<bitset>
//#pragma GCC optimize(2)
//#pragma GCC optimize(3)
#define endl '\n'
#define int ll
#define all(a) a.begin(),a.end()
#define PI acos(-1)
#define INF 0x3f3f3f3f
typedef long long ll;
typedef unsigned long long ull;
typedef pair<ll, ll>PII;
const int N = 1e6 + 50, MOD = 1e9 + 7;
int a[N], f[4 * N], cnt[4 * N];
void build_tree(int k, int l, int r)
{
if (l == r)
{
f[k] = a[l];
//每个叶子的cnt初始为1
cnt[k] = 1;
return;
}
int mid = (l + r) / 2;
build_tree(k + k, l, mid);
build_tree(k + k + 1, mid + 1, r);
//根据孩子的最小值情况来给父亲节点赋值
if (f[k + k] == f[k + k + 1])
{
f[k] = f[k + k];
cnt[k] = cnt[k + k] + cnt[k + k + 1];
}
else if (f[k + k] < f[k + k + 1])
{
f[k] = f[k + k];
cnt[k] = cnt[k + k];
}
else
{
f[k] = f[k + k + 1];
cnt[k] = cnt[k + k + 1];
}
}
void revise(int k, int l, int r, int x, int y)
{
if (l == r)
{
f[k] = y;
return;
}
int mid = (l + r) / 2;
if (x <= mid)revise(k + k, l, mid, x, y);
else revise(k + k + 1, mid + 1, r, x, y);
if (f[k + k] == f[k + k + 1])
{
f[k] = f[k + k];
cnt[k] = cnt[k + k] + cnt[k + k + 1];
}
else if (f[k + k] < f[k + k + 1])
{
f[k] = f[k + k];
cnt[k] = cnt[k + k];
}
else
{
f[k] = f[k + k + 1];
cnt[k] = cnt[k + k + 1];
}
}
//PII是数对:pair<int,int>
PII calc(int k, int l, int r, int x, int y)
{
//first是最小值,second是个数
if (l == x && r == y)return { f[k],cnt[k] };
int mid = (l + r) / 2;
if (y <= mid)return calc(k + k, l, mid, x, y);
else
if (x > mid)return calc(k + k + 1, mid + 1, r, x, y);
else
{
//当遇到这种区间分开的情况,我们也要根据最小值的情况确定答案
auto i= calc(k + k, l, mid, x, mid);
auto j = calc(k + k + 1, mid + 1, r, mid + 1, y);
if (i.first == j.first)
return { i.first,i.second + j.second };
else if (i.first < j.first)
return i;
else
return j;
}
}
signed main()
{
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
int n, q, op, x, y;
cin >> n >> q;
for (int i = 1; i <= n; i++)cin >> a[i];
build_tree(1, 1, n);
for (int i = 1; i <= q; i++)
{
cin >> op >> x >> y;
if (op == 1)
{
x++;
revise(1, 1, n, x, y);
}
else
{
x++;
auto t = calc(1, 1, n, x, y);
cout << t.first << " " << t.second << endl;
}
}
return 0;
}
练习部分取了三道题给大家做讲解,这个题单中还有许多其它题等着各位去训练,只要耐心把题都学会,你一定会有所收获!
(你不会真的打算让我讲10题吧,不会吧不会吧)
那么,最后就是我们的——
六、拜拜了您内
码字不易QAQ,如果各位同学看到这里,感觉有所收获的话,能否给一个小小的赞支持一下下,您的支持就是我的动力。
要是能顺便留下您的评论让我知道对您有收获那我就更开心啦。
(希望能被官方推一下吧求求官方哩呜呜呜)