线段树什么的不是简简单单嘛,我教你!:基础篇
创始人
2024-02-07 17:12:26

线段树什么的不是简简单单嘛,我教你!:基础篇

零、序言——万物滴开篇

也许你是苦于笔试的打工人,也许你是步入算法圈不久的小小萌新(我也是萌新) ,也许你是在网上搜索数据结构课设的倒霉学生。不管怎么样,看完本篇文章,希望对您有所帮助。

QQ图片20221122204358.jpg

走起!

观前提醒:看本文章最好有一定的二叉树基础(至少要会递归遍历树的程度)和算法基础(咋的也得知道时间复杂度是什么)

线段树 是算法竞赛中非常常见一种的数据结构,功能强大,学术一点的话说就是:常用的用来维护 区间信息 的数据结构。线段树 可以在较小的时间复杂度内实现单点修改、区间修改、区间查询(区间求和,求区间最大值,求区间最小值)等操作。

没听懂?没事,以前我也听不懂,让我们来——

一、举个栗子

给你一个长度为 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) !线段树扳回一城!

屏幕截图 2022-11-22 142955.png

而就像我们开始说的,不光是求区间和,区间最值什么的也能在O(logn) 的复杂度内得出结果,不说单点修改数组的值,哪怕是修改数组的一段区间,它也可以在O(logn) 的复杂度内完成!

那么,线段树是怎么做到这一点的呢?

二、线段树——什么树?

实际上,线段树其实也就是一个二叉树。

首先,我们先来聊一聊它的基础形态:二叉树

二叉树算是非常基础的数据结构了,树如其名,每一个节点最多只有两个孩子,一左一右,即二叉。图中就是一个最简单的二叉树:

在这里插入图片描述

那么线段树是怎么做到能在O(n*logn) 的复杂度上得到区间和的呢?

  1. 我们先给上图这二叉树的两个孩子分别赋一个值:x 和 y
  2. 那么我们想求出这两个点的总和,只需要:root->left->val + root->right->val
  3. root的左右两个孩子都有值了,但root还是空荡荡的,我们不如把这个总和当作root节点的值。

此时,这个二叉树的情况是:

在这里插入图片描述

那么之后,如果想知道root的左右俩孩子的值的总和,我们并不需要遍历到这两个点,只要直接遍历到root上,就可以得出结果了。

现在你可能觉得没什么大不了的,也才两个点,我直接遍历他们和遍历一个点也没什么区别。

好,线段树觉得自己被瞧不起了,它开始生长:

在这里插入图片描述

现在想知道a、b、c、d四个点的总和,只需要看最上面的那一个点就行了。

还不够?它继续生长:

在这里插入图片描述

再长:

在这里插入图片描述

再长:

算了吧再长就放不下了。

这就是线段树的运行方式,我们就可以这样,一层一层的继续套下去,最后,如果想知道整个数组的总和,只要走到最上面的那个点就行,而不用遍历整个数组。这效率,显而易见的高。

线段树中的 ”树“ 我们已经体会到了,但是我们还有疑问。

三、线段树——何为线段?

首先说明一点:线段树的叶子,就是数组的元素

(为了方便,我们拿小一点的树来做介绍)

如果此时有一个长度为4的数组,它的元素分别为:a,b,c,d。那么在线段树中表现出来的就是:

线段树3.png

然后我们又知道,元素a在数组中的出现范围是 {1,1} ,元素b的出现范围是 {2,2} ,c的是 {3,3} ,d是 {4,4}

我们根据这一点,把线段树用另一种形式表现出来:

线段树6.png

每个节点上的范围,就表示了这个节点管辖的数组范围

比如a是{1,1},b是{2,2},那么他们的父亲管辖的范围就是{1,2},如果我们想知道数组中第一个数到第二个数的总和,只需要走到点e就可以了。以此类推。

而这,就是线段树中的——线段。 (说是线段,不如说范围更合适。)

在线段树中,每一个节点都有其负责的一个范围,当我们遇到区间查询问题的时候,只要走到对应的节点就可以得出结果了。

什么,你问我线段树上没有节点负责{1,3}范围,你想问{1,3}范围的总和怎么办?那当然是节点e的结果加上节点c的结果了啊,笨猪猪捏(~ ̄(OO) ̄)ブ。

我们每次询问都是从上往下遍历树的节点,而我们知道,对于一个有n个叶子的二叉树,它的深度是log2(n) ,所以不论是查询还是修改,我们都只用跑log2(n) 层,时间复杂度就为:O(logn)

要是看到这里,对线段树终于有所了解,有茅塞顿开之感的同学能不能在屏幕前给咱鼓个掌捏。

QQ图片20221122153759.png

好哩,这下线段树的原理都解释清楚啦!!!接下来终于要到了——

四、代码是如何实现的?

因为掘金的各位友友可能都是非算法竞赛选手,比起数组形式的树可能更习惯结构体形式,所以这一环节我会用结构体形式的代码做讲解(数组形式的代码我会在最后面说一下)

这是最重要也最难的一节啦(因为代码太多了,可能有点看不过来)!通过这一节你就成功啦!加油!!

我们还是先回到二叉树,一般我们写二叉树的代码是:

别问我为啥不用力扣的二叉树结构体,指针那么恶心的玩意我不想碰

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]。

对于线段树类型的题(像我们第一节举那个的栗子),一般都会先给我们一个数组。那么,我们首先要做的就是利用这个数组来生成线段树:

  1. 如果数组的长度为n,那么总节点(Node[1])的最初范围是 {1,n}
  2. 然后我们将范围一分为二,左孩子的范围就是: {1,mid} ,右孩子的范围就是: {mid+1,n}
  3. 到了孩子节点,我们继续分,以此类推。
  4. 当到了某个节点,范围变成了 {l,l} (即左右端点相等,表示这个点只代表一个数),就说明这个节点就是叶子节点,我们把数组的值赋给它(范围是啥就把对于的值给它,可不是乱赋值嗷)
  5. 当某个节点的左右节点都赋完值了,他们的父亲节点的值,就是这两个孩子节点的值的总和。
代码如下:
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
using namespace std;
#include
#include
#include
#include
#include 
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
​
//#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 pairPII;
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);elsecout << calc(1, x, y) << endl;}return 0;
}

屏幕截图 2022-11-22 164337.png

哈!满分!太棒啦!

为了对比,我们使用暴力做法看看能不能通过:

屏幕截图 2022-11-22 164624.png

可以看出,小型数据还是可以通过的,但是数据大了就会超时。

顺带一提,实际上结构体做线段树不太优,更常见的做法是用数组模拟,具体细节可以看注释,代码如下:

#include
using namespace std;
#include
#include
#include
#include
#include 
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
​
//#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 pairPII;
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);elseif (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~nbuild_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;
}

屏幕截图 2022-11-22 191644.png

我们对比一下结构体线段树的用时和内存会发现,数组模拟线段树是优于结构体线段树的,特别是在内存方面

至此,就是线段树的入门篇的全部内容啦!是不是感觉自己学习到了一个很厉害的新知识而开心不已呢?

不过先别急着走鸭,学会了新本领,就要找地方练练,不然岂不是白瞎了!

所以,我们来——

五、写题!我要写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
using namespace std;
#include
#include
#include
#include
#include 
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
​
//#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 pairPII;
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);elseif (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
using namespace std;
#include
#include
#include
#include
#include 
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
​
//#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 pairPII;
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);elseif (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

这一题中,不光需要你求出区间内的最小值,还要求出区间内有多少个这个最小值。

在这一题,我们可以额外开一个数组cntf[i]表示当前区间的最小值是多少,cnt[i]表示当前区间有多少个最小值,很明显的,每个叶子的cnt[i]初始为1。

那么对于父亲节点来说:

  • 如果左孩子和右孩子的最小值一样,父亲节点的最小值就是他们,而父亲节点的cnt,就是两个孩子的cnt加在一起。
  • 如果左孩子的最小值小于右孩子,父亲节点的最小值就是左孩子最小值,父亲节点的cnt就是左孩子的cnt。
  • 如果左孩子的最小值大于右孩子,父亲节点的最小值就是右孩子最小值,父亲节点的cnt就是右孩子的cnt。

在询问操作中,当遇到区间分开的情况,我们也要重复如上操作。

为此,这里我把询问函数的返回值从单个整数改成了一个数对:first是最小值,second是个数cnt

AC代码

#include
using namespace std;
#include
#include
#include
#include
#include 
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
​
//#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 pairPII;
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初始为1cnt[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
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);elseif (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,如果各位同学看到这里,感觉有所收获的话,能否给一个小小的赞支持一下下,您的支持就是我的动力。

要是能顺便留下您的评论让我知道对您有收获那我就更开心啦。

QQ图片20221122200509.jpg

(希望能被官方推一下吧求求官方哩呜呜呜)

各位再见!如果明天见不到你的话,就祝你早上、中午、晚上都好

相关内容

热门资讯

埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
demo什么意思 demo版本... 618快到了,各位的小金库大概也在准备开闸放水了吧。没有小金库的,也该向老婆撒娇卖萌服个软了,一切只...
猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...
埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...