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

news2024/11/29 22:30:12

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

零、序言——万物滴开篇

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

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 次操作,操作有两种:

这一题和上一题不一样的是,多了个修改数组的操作。而我们知道,前缀和是必须要预处理出前缀和数组的,预处理的复杂度是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

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/26379.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

2049. 统计最高分的节点数目-数组树构造+遍历求解最大值数目

2049. 统计最高分的节点数目-数组树构造遍历求解最大值数目 给你一棵根节点为 0 的 二叉树 &#xff0c;它总共有 n 个节点&#xff0c;节点编号为 0 到 n - 1 。同时给你一个下标从 0 开始的整数数组 parents 表示这棵树&#xff0c;其中 parents[i] 是节点 i 的父节点。由于…

音视频 - 视频编码原理

目录 视频编码主要分为 图像的冗余 熵编码 帧内预测 帧间预测 DCT变换和量化 编码器比较 清晰度和耗时对比 一部电影1080P&#xff0c;帧率25fps&#xff0c;时长2小时&#xff0c;文件大小 1920x1080x1.5x25x2x360 521.4G 数据量非常大&#xff0c;对存储和网络传输都…

GMC Graph-Based Multi-View Clustering

GMC Graph-Based Multi-View Clustering 基于图的多视图聚类 abstract 现有的大多数方法没有充分考虑不同视图的权重&#xff0c;需要额外的聚类步骤来生成最终的聚类。还通常基于所有视图的固定图相似矩阵来优化目标。 本文提出了一种通用的基于图的多视图聚类算法(GMC)来解…

Android程序设计之学生考勤管理系统

基于安卓平台开发的学生考勤管理系统&#xff0c;本系统采用java语言设计&#xff0c;数据存储使用SQLite轻量级数据库实现 SQLite 简介 SQLite是一个软件库&#xff0c;实现了自给自足的、无服务器的、零配置的、事务性的 SQL 数据库引擎。SQLite是一个增长最快的数据库引擎&…

JSON 对比工具

文章目录JSON对比工具JSON对比工具 JSON 是 Web 开发领域中最常用的数据传输格式之一&#xff0c;因为 JSON 的可读性较高&#xff0c;对于一些简单的 JSON 数据&#xff0c;我们不需要借助任何工具就可以轻易的读取。但对于复杂的 JSON 数据就需要借助工具才行&#xff0c;本…

公众号文案写作技巧有哪些?教你几招

公众号文案写作是每个公众号运营者心中的痛&#xff1a; 你是否每天纠结写什么&#xff1f; 你是否写着写着就词穷了&#xff1f; 你是否不知道该如何下手&#xff1f; 公众号文案应该怎么写&#xff1f;今天伯乐网络传媒就来给大家分享一份超实用的公众号文案写作技巧&…

增量模型和迭代模型的优点与缺点

增量模型&#xff1a; 举个例子&#xff1a; 用户有一个需求&#xff0c;功能包含A,B,C... ABC 增量模型&#xff1a; 开发完A我就直接上线供给用户去使用 开发完C我就直接上线供给用户去使用 开发完B我就直接上线供给用户去使用 增量模型的特点 增量模型的特点…

度量BGP监测源数量对AS可见性的影响

首先&#xff0c;本文介绍了两个公开的BGP数据源项目情况&#xff1b;其次&#xff0c;从可见AS数量和可见AS边关系数量两个方面来分析度量BGP监测源中对等AS的可见性。 BGP数据源介绍 BGP数据源有2个公开的项目&#xff0c;分别是RIPE RIS和Route Views&#xff0c;它们使用路…

VUE基础编程(三)

案例要求 基于Vue Cli和嵌套路由技术&#xff0c;完成以下功能&#xff1a; 站点打开后会默认显示如图3.1所示的“关于公司”页面&#xff0c;单击图3.1页面上的“公司简介”链接&#xff0c;站点会显示如图3.2所示的“公司简介”页面&#xff0c;单击图3.1页面上的“公司治理…

【JAVA程序设计】基于SSM的学校教务管理系统-有文档

基于SSM的学校教务管理系统-有文档项目获取项目简介开发环境项目技术功能结构文档目录运行截图项目获取 获取方式&#xff08;点击下载&#xff09;&#xff1a;是云猿实战 项目经过多人测试运行&#xff0c;可以确保100%成功运行。 项目简介 本项目是基于SSM的学校教务管理…

[附源码]java毕业设计校园失物招领平台

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

idea反编译

1、问题描述 只有jar包&#xff0c;反编译下&#xff0c;看几个配置&#xff1b; 2、问题说明 用的idea里面的插件&#xff0c;java Decoplier&#xff0c;可以反编译jar包&#xff0c;效果挺好的&#xff0c;反编译出来的.java没乱码&#xff0c;可以直接看&#xff1b; 2…

139.深度学习分布式计算框架-2

139.1 Spark MLllib MLlib(Machine Learnig lib) 是Spark对常用的机器学习算法的实现库&#xff0c;同时包括相关的测试和数据生成器。MLlib是MLBase一部分&#xff0c;其中MLBase分为四部分&#xff1a;MLlib、MLI、ML Optimizer和MLRuntime。 ML Optimizer会选择它认为最适合…

4款企业常用的工时管理系统盘点

4款企业常用的工时管理系统有&#xff1a;1、Excel&#xff1b;2、8Manage 工时表&#xff1b;3、诺明软件&#xff1b;4、Aceteamwork。 “时间就是金钱”&#xff0c;相信大家都听过这句话。对于企业来说&#xff0c;管理员工工时&#xff0c;其实就是管理企业的人力成本和实…

数据结构-难点突破(C++实现树的双亲表示法,孩子表示法,孩子兄弟表示法(树转化为二叉树))

文章目录1. 树的双亲表示法2. 孩子表示法3. 孩子兄弟表示法&#xff08;树转化为二叉树&#xff09;普通树的存储一半采用三种方式&#xff1a; 双亲表示法&#xff1b;孩子表示法&#xff1b;孩子兄弟表示法&#xff1b; 1. 树的双亲表示法 思路和图片来源 采用双亲表示法…

智慧停车解决方案-最新全套文件

智慧停车解决方案-最新全套文件一、建设背景痛点分析二、建设思路准确、安全、可靠、及时性原则统一规划、分布实施保护以往投资、整合现有资源资源共享和整体性、统一性原则可扩展性原则三、建设方案四、获取 - 智慧停车全套最新解决方案合集一、建设背景 痛点分析 随着经济…

stm32cubemx hal学习记录:FreeRTOS事件

一、事件 事件是一种实现任务间通信的机制&#xff0c;主要用于实现多任务间的同步&#xff0c;但事件通信只能是事件类型的通信&#xff0c;无数据传输。与信号量不同的是&#xff0c;它可以实现一对多&#xff0c;多对多的同步。即一个任务可以等待多个事件的发生&#xff1a…

C语言源代码系列-管理系统之机房机位预定系统

往期文章分享点击跳转>《导航贴》- Unity手册&#xff0c;系统实战学习点击跳转>《导航贴》- Android手册&#xff0c;重温移动开发 &#x1f449;关于作者 众所周知&#xff0c;人生是一个漫长的流程&#xff0c;不断克服困难&#xff0c;不断反思前进的过程。在这个过…

华为电量分段图表实现过程

以前一直是改的MPAndroidChart&#xff0c;但最近看到华为手机的电池图表发现一旦设计不符合常规图表逻辑实现起来就很困难&#xff0c; 考虑过path相减(areaPath.op(-,- Path.Op.DIFFERENCE))、图像混合&#xff08;paint.setXfermode&#xff09;、裁剪区域&#xff08;clipR…

学生HTML个人网页作业作品下载 动漫主题网页设计制作 大学生个人网站作业模板 dreamweaver简单个人网页制作

HTML实例网页代码, 本实例适合于初学HTML的同学。该实例里面有设置了css的样式设置&#xff0c;有div的样式格局&#xff0c;这个实例比较全面&#xff0c;有助于同学的学习,本文将介绍如何通过从头开始设计个人网站并将其转换为代码的过程来实践设计。 ⚽精彩专栏推荐&#x1…