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

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

零、序言——万物滴开篇

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

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<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;
}

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

哈!满分!太棒啦!

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

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

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

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

#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;
}

屏幕截图 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<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

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

在这一题,我们可以额外开一个数组cntf[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,如果各位同学看到这里,感觉有所收获的话,能否给一个小小的赞支持一下下,您的支持就是我的动力。

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

QQ图片20221122200509.jpg

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

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

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值