蓝桥杯第十三讲--树状数组与线段树【例题】

举报
辰chen 发表于 2022/06/14 22:19:47 2022/06/14
【摘要】 文章目录 前言动态求连续区间和题目要求思路分析树状数组代码(树状数组)线段树代码(线段树) 数星星题目要求思路分析代码 数列区间最大值题目要求思路分析代码 前言 蓝桥杯官...

前言

蓝桥杯官网:蓝桥杯大赛——全国大学生TMT行业赛事
✨本博客讲解 蓝桥杯C/C++ 备赛所涉及算法知识,此博客为第十三讲:树状数组与线段树【例题】

本篇博客所包含习题有:
👊动态求连续区间和
👊数星星
👊数列区间最大值

树状数组与线段树【习题】见博客:蓝桥杯第十三讲–树状数组与线段树【习题】

注: 蓝桥杯对于树状数组与线段树的考察十分的基础简单,且历年只考察过两道题目,读者不需要了解其原理,只需要背诵模板学会调用即可。如果想要知道具体实现方式,见博客:树状数组线段树(一)线段树(二) 【尚未更新】

博客内容以题代讲,通过讲解题目的做法来帮助读者快速理解算法内容,需要注意:学习算法不能光过脑,更要实践,请读者务必自己敲写一遍本博客相关代码!!!


动态求连续区间和

题目要求

题目描述:

给定 n n n 个数组成的一个数列,规定有两种操作,一是修改某个元素,二是求子数列 [ a , b ] [a,b] [a,b] 的连续和。

输入格式:

第一行包含两个整数 n n n m m m,分别表示数的个数和操作次数。

第二行包含 n n n 个整数,表示完整数列。

接下来 m m m 行,每行包含三个整数 k , a , b k,a,b k,a,b k = 0 k=0 k=0,表示求子数列 [ a , b ] [a,b] [a,b] 的和; k = 1 k=1 k=1,表示第 a a a 个数加 b b b)。

数列从 1 1 1 开始计数。

输出格式:

输出若干行数字,表示 k = 0 k=0 k=0 时,对应的子数列 [ a , b ] [a,b] [a,b] 的连续和。

数据范围:

1 ≤ n ≤ 100000 , 1≤n≤100000, 1n100000,
1 ≤ m ≤ 100000 , 1≤m≤100000, 1m100000
1 ≤ a ≤ b ≤ n , 1≤a≤b≤n, 1abn,
数据保证在任何时候,数列中所有元素之和均在 i n t int int 范围内。

输入样例:

10 5
1 2 3 4 5 6 7 8 9 10
1 1 5
0 1 3
0 4 8
1 7 5
0 4 8

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

输出样例:

11
30
35

  
 
  • 1
  • 2
  • 3

思路分析

本题用 树状数组 以及 线段树 两种方法去写,同时介绍模板

树状数组

使用树状数组会让时间复杂度变为: O ( log ⁡ n ) O(\log n) O(logn) 树状数组作用有两个:

  • 让某个位置上的数加上一个数(单点修改)
  • 求一个前缀和(区间查询)

当然,树状数组还有其他功能,但是对于蓝桥杯而言,只需要掌握以上两种即可

树状数组虽然是一维的,但是我们可以把它理解为是二维的,类似树的结构,如下图所示:
在这里插入图片描述
这里有几个需要注意的点:

  1. 树状数组必须从 1 1 1 开始
  2. 虽然看起来是二维的类似树的结构,但是存储还是一维数组的形式
  3. 上图中黑色的是原数组,蓝色的是树状数组
  4. 红色的线代表的是数组之间的关系,比如 2 2 2 1 1 1 2 2 2 相连,就代表着,树状数组 2 2 2 由树状数组 1 1 1 和原数组 2 2 2 相加得到。而树状数组 1 1 1 就是原数组 1 1 1 ;再举个例子: 16 16 16 = 8 8 8 + 12 12 12 + 14 14 14 + 15 15 15 + 16 16 16

以上就是对树状数组的一些定义说明,读者 不需要 知道为什么这么定义以及其中细节,感兴趣的可以看博客:树状数组 【尚未更新】,里面会有讲解。

下面介绍树状数组的几个基本函数:

int lowbit(int x)
{
    return x & -x;
}

void add(int x, int v)      // 往 x 的位置加上一个数字 v
{
    for (int i = x; i <= n; i += lowbit(i)) tr[i] += v;
}

int query(int x)           // 求 [1, x] 原数组的前缀和
{
    int res = 0;
    for (int i = x; i; i -= lowbit(i)) res += tr[i];

    return res;
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

上述代码需要:背背背!!!

代码(树状数组)

#include <cstdio>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 1000010;

int n, m;
int a[N], tr[N];

int lowbit(int x)
{
    return x & -x;
}

void add(int x, int v)
{
    for (int i = x; i <= n; i += lowbit(i)) tr[i] += v;
}

int query(int x)
{
    int res = 0;
    for (int i = x; i; i -= lowbit(i)) res += tr[i];
    return res;
}

int main()
{
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i ++ ) scanf("%d", &a[i]);
    for (int i = 1; i <= n; i ++ ) add(i, a[i]);

    while(m -- )
    {
        int k, x, y;
        scanf("%d%d%d", &k, &x, &y);
        if (k == 0) printf("%d\n", query(y) - query(x - 1));
        else add(x, y);
    }

    return 0;
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44

线段树

使用树状数组会让时间复杂度变为: O ( log ⁡ n ) O(\log n) O(logn) ,线段树的作用特别之多,包含树状数组的所有功能并且有很多额外的功能,但是代码相较与树状数组而言比较冗长。

线段树可以简单的理解成一个二叉树:
在这里插入图片描述
即线段树是对一段的数组,每次均分为两份,一直均分直到不能均分,红线代表的是求和的过程。

原理不进行讲解,读者 不需要 知道为什么这么定义以及其中细节,感兴趣的可以看博客:线段树(一)线段树(二) 【尚未更新】,里面会有讲解。

下面介绍线段树的几个函数:

struct Node
{
    int l, r;    // 涵盖范围,如上图中的蓝色字体就是l,r的取值
    int sum;
}tr[4 * N];      // 需要开题目要求的4倍大小,不需要知道其原理

void pushup(int u)        // 动态维护线段树
{
    tr[u].sum = tr[u << 1].sum + tr[u << 1 | 1].sum;
}

void build(int u, int l, int r)   // 建一个线段树,u为根,l和r代表两个子树的涵盖范围
{
    if (l == r) tr[u] = {l, r, w[l]};
    else
    {
        tr[u] = {l, r};
        int mid = l + r >> 1;
        build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);
        pushup(u);
    }
}

int query(int u, int l, int r)    // 查询操作,求[l, r]的和,可以换为别的,核心是递归
{
    if (tr[u].l >= l && tr[u].r <= r) return tr[u].sum;
    int mid = tr[u].l + tr[u].r >> 1;
    int sum = 0;
    if (l <= mid) sum = query(u << 1, l, r);
    if (r > mid) sum += query(u << 1 | 1, l, r);
    return sum;
}

void modify(int u, int x, int v)// 修改操作,把x的位置的值加上一个v,可以换为别的,核心是递归
{
    if (tr[u].l == tr[u].r) tr[u].sum += v;
    else
    {
        int mid = tr[u].l + tr[u].r >> 1;
        if (x <= mid) modify(u << 1, x, v);
        else modify(u << 1 | 1, x, v);
        pushup(u);
    }
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44

代码(线段树)

#include <cstdio>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 100010;

int n, m;
int w[N];
struct Node
{
    int l, r;
    int sum;
}tr[4 * N];

void pushup(int u)
{
    tr[u].sum = tr[u << 1].sum + tr[u << 1 | 1].sum;
}

void build(int u, int l, int r)
{
    if (l == r) tr[u] = {l, r, w[l]};
    else
    {
        tr[u] = {l, r};
        int mid = l + r >> 1;
        build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);
        pushup(u);
    }
}

int query(int u, int l, int r)
{
    if (tr[u].l >= l && tr[u].r <= r) return tr[u].sum;
    int mid = tr[u].l + tr[u].r >> 1;
    int sum = 0;
    if (l <= mid) sum = query(u << 1, l, r);
    if (r > mid) sum += query(u << 1 | 1, l, r);
    return sum;
}

void modify(int u, int x, int v)
{
    if (tr[u].l == tr[u].r) tr[u].sum += v;
    else
    {
        int mid = tr[u].l + tr[u].r >> 1;
        if (x <= mid) modify(u << 1, x, v);
        else modify(u << 1 | 1, x, v);
        pushup(u);
    }
}

int main()
{
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i ++ ) scanf("%d", &w[i]);
    build(1, 1, n);

    int k, a, b;
    while (m -- )
    {
        scanf("%d%d%d", &k, &a, &b);
        if (k == 0) printf("%d\n", query(1, a, b));
        else modify(1, a, b);
    }

    return 0;
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71

数星星

题目要求

题目描述:

天空中有一些星星,这些星星都在不同的位置,每个星星有个坐标。

如果一个星星的左下方(包含正左和正下)有 k k k 颗星星,就说这颗星星是 k k k 级的。

在这里插入图片描述
例如,上图中星星 5 5 5 3 3 3 级的( 1 , 2 , 4 1,2,4 1,2,4 在它左下),星星 2 , 4 2,4 2,4 1 1 1 级的。

例图中有 1 1 1 0 0 0 级, 2 2 2 1 1 1 级, 1 1 1 2 2 2 级, 1 1 1 3 3 3 级的星星。

给定星星的位置,输出各级星星的数目。

换句话说,给定 N N N 个点,定义每个点的等级是在该点左下方(含正左、正下)的点的数目,试统计每个等级有多少个点。

输入格式:

第一行一个整数 N N N,表示星星的数目;

接下来 N N N 行给出每颗星星的坐标,坐标用两个整数 x , y x,y x,y 表示;

不会有星星重叠。星星按 y y y 坐标增序给出, y y y 坐标相同的按 x x x 坐标增序给出。

输出格式:

N N N 行,每行一个整数,分别是 0 0 0 级, 1 1 1 级, 2 2 2 级,……, N − 1 N−1 N1 级的星星的数目。

数据范围:

1 ≤ N ≤ 15000 , 1≤N≤15000, 1N15000,
0 ≤ x , y ≤ 32000 0≤x,y≤32000 0x,y32000

输入样例:

5
1 1
5 1
7 1
3 3
5 5

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

输出样例:

1
2
1
1
0

  
 
  • 1
  • 2
  • 3
  • 4
  • 5

思路分析

对于一颗星星的等级是查看这个星星的下方和左方有多少颗星星,因为我们的输入是从下到上,从左到右进行输入的,所以我们在对于每一颗星星进行统计的时候,可以边输入边计算,因为这样我们就只需要考虑横坐标的星星,即对于一颗横坐标为 x x x 的星星,我们在输入它后,只需要查看一下从位置 1 1 1 ~ x x x (树状数组要求从 1 1 1 开始,但是我们的数据范围可以取到 0 0 0,故我们直接让 x ++; 即可)有多少颗星星即可,这样就转化为了一个求前缀和的问题,因为整个前缀和的过程是动态的,所以我们使用树状数组进行优化,因为我们的代码思路是边输入边进行统计,这样构造线段树的话会有些麻烦,故本题建议使用树状数组而不是线段树。 l e v e l [ i ] level[i] level[i] 表示的就是等级为 i i i 的星星的个数。

代码

#include <cstdio>
#include <algorithm>
#include <cstring>

using namespace std;

const int N = 32010;

int n;
int tr[N], level[N];

int lowbit(int x)
{
    return x & -x;
}

void add(int x)
{
    for (int i = x; i < N; i += lowbit(i)) tr[i] ++;
}

int query(int x)
{
    int res = 0;
    for (int i = x; i; i -= lowbit(i)) res += tr[i];
    
    return res;
}

int main()
{
    scanf("%d", &n);
    for (int i = 1; i <= n; i ++ ) 
    {
        int x, y;
        scanf("%d%d", &x, &y);
        x ++;
        level[query(x)] ++;
        add(x);
    }
    
    for (int i = 0; i < n; i ++ ) printf("%d\n", level[i]);
    
    return 0;
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45

数列区间最大值

题目要求

题目描述:

输入一串数字,给你 M M M 个询问,每次询问就给你两个数字 X , Y X,Y X,Y,要求你说出 X X X Y Y Y 这段区间内的最大数。

输入格式:

第一行两个整数 N , M N,M N,M 表示数字的个数和要询问的次数;

接下来一行为 N N N 个数;

接下来 M M M 行,每行都有两个整数 X , Y X,Y X,Y

输出格式:

输出共 M M M 行,每行输出一个数。

数据范围:

1 ≤ N ≤ 1 0 5 , 1≤N≤10^5, 1N105,
1 ≤ M ≤ 1 0 6 , 1≤M≤10^6, 1M106,
1 ≤ X ≤ Y ≤ N , 1≤X≤Y≤N, 1XYN,
数列中的数字均不超过 2 31 − 1 2^{31}−1 2311

输入样例:

10 2
3 2 4 5 6 8 1 2 9 7
1 4
3 8

  
 
  • 1
  • 2
  • 3
  • 4

输出样例:

5
8

  
 
  • 1
  • 2

思路分析

本题求的是最大值,和前缀和无关,即我们用树状数组无法解决问题,那么我们就可以使用线段树去解决本题,代码和本博客的第一道例题:动态求连续区间和 是类似的,只需要对求和部分改成求最大值就可以了,其中代码中出现了: I N T _ M I N INT\_MIN INT_MIN,这代表的是整数的最小值,即极小值,调用该函数需要加头文件:#include <climits>

代码

#include <cstdio>
#include <cstring>
#include <algorithm>
#include <climits>

using namespace std;

const int N = 100010;

int n, m;
int w[N];
struct Node
{
    int l, r;
    int maxv;
}tr[4 * N];

void pushup(int u)
{
    tr[u].maxv = max(tr[u << 1].maxv, tr[u << 1 | 1].maxv);
}

void build(int u, int l, int r)
{
    if (l == r) tr[u] = {l ,r, w[l]};
    else
    {
        tr[u] = {l, r};
        int mid = l + r >> 1;
        build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);
        pushup(u);
    }
}

int query(int u, int l, int r)
{
    if (tr[u].l >= l && tr[u].r <= r) return tr[u].maxv;
    int mid = tr[u].l + tr[u].r >> 1;
    int maxv = INT_MIN;
    if (l <= mid) maxv = max(maxv, query(u << 1, l ,r));
    if (r > mid) maxv = max(maxv, query(u << 1 | 1, l, r));
    return maxv;
}

int main()
{
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i ++ ) scanf("%d", &w[i]);
    build(1, 1, n);
    
    int l, r;
    while (m -- )
    {
        scanf("%d%d", &l, &r);
        printf("%d\n", query(1, l, r));
    }
    
    return 0;
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59

文章来源: chen-ac.blog.csdn.net,作者:辰chen,版权归原作者所有,如需转载,请联系作者。

原文链接:chen-ac.blog.csdn.net/article/details/122793618

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。