首页 > 其他分享 >【数据结构】线段树

【数据结构】线段树

时间:2023-08-11 12:32:31浏览次数:37  
标签:int 线段 tr mid add 数据结构 节点


例题1:

给定一个正整数数列 【数据结构】线段树_子节点,每一个数都在 【数据结构】线段树_c++_02

  1. 添加操作:向序列后添加一个数,序列长度变成 【数据结构】线段树_算法_03
  2. 询问操作:询问这个序列中最后 【数据结构】线段树_线段树_04

程序运行的最开始,整数序列为空。 一共要对整数序列进行 【数据结构】线段树_子节点_05 次操作。 写一个程序,读入操作的序列,并输出询问操作的答案。
数据范围

【数据结构】线段树_算法_06
【数据结构】线段树_线段树_07
【数据结构】线段树_数据结构_08

这道题看第一眼:暴力,再看一眼:爆炸(bushi TLE。
这道题目就可以用我们今天要学的线段树来解决。

线段树的思路

线段树是一棵二叉树,它可以在很低的时间复杂度内完成一个序列的单点修改、区间修改、区间查询(最大数,最小数、求和等等)等的操作, 【数据结构】线段树_子节点_09 线段树支持的所有操作都可以将时间复杂度控制在【数据结构】线段树_算法_10

线段树俗称段错误树,因调试时经常段错误而得名

它的的思路很好理解, 顾名思义,它就是一个节点为线段的树;假设我们要用线段树维护一个区间 【数据结构】线段树_子节点_11

【数据结构】线段树_线段树_12

如何存储线段树

我们直接拿一个结构体来存储线段树,每一个节点都是一段区间,拿刚才的例题举例:

struct Node {
    int l, r;	//	区间的左端点和右端点
    int v;  //  区间[l, r]中的最大值
    //	这里可以存储你要维护的任何信息,例如最大/最小值,区间和等
} tr[N * 4];

一棵线段树的根节点编号为 【数据结构】线段树_算法_13 ,设一个不为根节点的节点编号为 【数据结构】线段树_算法_14 ,则这个节点的父节点是 【数据结构】线段树_线段树_15 ,它的左儿子编号为 【数据结构】线段树_子节点_16 ,右儿子编号为 【数据结构】线段树_线段树_17 ;因为一颗线段树最大是一棵满二叉树,N个叶子节点的满二叉树最多有 【数据结构】线段树_线段树_18 个节点;而最后一层(可以参考上面的 【数据结构】线段树_子节点_11 线段树图)最多还会剩余 【数据结构】线段树_数据结构_20 个节点。所以线段树通常需要开 【数据结构】线段树_线段树_21

如何建立线段树

线段树中如果表示的区间为 【数据结构】线段树_线段树_22 且这个节点不为叶子节点(【数据结构】线段树_子节点_23),则我们有一个 【数据结构】线段树_c++_24 , 这个点的左子树即为 【数据结构】线段树_数据结构_25 ,右子树即为 【数据结构】线段树_数据结构_26 ,递归建树即可。
代码:

void pushup(int u) {    //  由子节点的最大值,来更新父节点的信息
    tr[u].v = max(tr[u * 2].v, tr[u * 2 + 1].v);
}
void build(int u, int l, int r) {
    tr[u] = {l, r};
    if (l == r) return ;
    int mid = l + r >> 1;
    build(u * 2, l, mid), build(u * 2 + 1, mid + 1, r);
}

如何进行查询

递归查询,直到我们树中结点已经完全包含在我们需要查询的区间中
代码:

int query(int u, int l, int r) {
    if (tr[u].l >= l && tr[u].r <= r) return tr[u].v;   //  树中节点已经被完全包含在[l, r]中了
    int mid = tr[u].l + tr[u].r >> 1;
    int v = 0;
    if (l <= mid) v = query(u * 2, l, r);
    if (r > mid) v = max(v, query(u * 2 + 1, l, r));
    return v;
}

如何进行修改

递归寻找,直到我们找到了我们将要修改的叶子节点(只有一个数的区间),进行修改。

void modify(int u, int x, int v) {
    if (tr[u].l == x && tr[u].r == x) tr[u].v = v;
    else {
        int mid = tr[u].l + tr[u].r >> 1;
        if (x <= mid) modify(u * 2, x, v);
        else modify(u * 2 + 1, x, v);
        pushup(u);	//	别忘了告诉父节点我们刚刚进行更新的信息
    }
}

例题1完整代码:

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 200010;
int m, p;
struct Node {
    int l, r;
    int v;
} tr[N * 4];
void pushup(int u) {
    tr[u].v = max(tr[u * 2].v, tr[u * 2 + 1].v);
}
void build(int u, int l, int r) {
    tr[u] = {l, r};
    if (l == r) return ;
    int mid = l + r >> 1;
    build(u * 2, l, mid), build(u * 2 + 1, mid + 1, r);
}
int query(int u, int l, int r) {
    if (tr[u].l >= l && tr[u].r <= r) return tr[u].v;
    int mid = tr[u].l + tr[u].r >> 1;
    int v = 0;
    if (l <= mid) v = query(u * 2, l, r);
    if (r > mid) v = max(v, query(u * 2 + 1, l, r));
    return v;
}
void modify(int u, int x, int v) {
    if (tr[u].l == x && tr[u].r == x) tr[u].v = v;
    else {
        int mid = tr[u].l + tr[u].r >> 1;
        if (x <= mid) modify(u * 2, x, v);
        else modify(u * 2 + 1, x, v);
        pushup(u);
    }
}
int main() {
    int n = 0, last = 0;
    scanf("%d%d", &m, &p);
    build(1, 1, m);
    char op[2];
    int x;
    while ( m -- ) {
        scanf("%s%d", op, &x);
        if (*op == 'Q') {
            last = query(1, n - x + 1, n);
            printf("%d\n", last);
        } else {
            modify(1, n + 1, ((ll)last + x) % p);
            n ++ ;
        }
    }
    return 0;
}

进阶线段树(线段树的懒标记)

例题2:

给定一个长度为 【数据结构】线段树_c++_27 的数列 【数据结构】线段树_算法_28,以及 【数据结构】线段树_数据结构_29

  1. C l r d,表示把 【数据结构】线段树_算法_30 都加上 【数据结构】线段树_线段树_31
  2. Q l r,表示询问数列中第 【数据结构】线段树_c++_32

对于每个询问,输出一个整数表示答案。
数据范围

【数据结构】线段树_c++_33
【数据结构】线段树_c++_34
【数据结构】线段树_线段树_35

这道题我之前讲过分块的做法,具体可以查看我的另一篇博客:C++分块详解 我在这篇博客里吐槽了段错误树懒标记,那我们就学一学懒标记是什么
我们之前写的代码里有一个pushup函数,意思是由子节点的信息更新父节点的信息;我们还是拿上面的线段树举例:假设我要维护线段树每个区间的和,把区间的 【数据结构】线段树_线段树_36 中的数字 【数据结构】线段树_算法_37 变成 【数据结构】线段树_子节点_38 ,则这段区间的和由 【数据结构】线段树_c++_39 变成了 【数据结构】线段树_c++_40,同时它的所有父节点即 【数据结构】线段树_算法_41【数据结构】线段树_子节点_11 的和全都需要更新。时间复杂度为【数据结构】线段树_子节点_43;但是我们之前说过,线段树支持的所有操作都可以将时间复杂度控制在【数据结构】线段树_算法_10,那我们该怎么优化它呢?
没错,这就需要我们现在要学的懒标记操作,也称延迟标记。意思就是说,我们可以在线段树的结构体内加上一个标记add,在执行修改命令时,直接将add赋值为我们想要增加的数,表示“这个节点被我修改过,但我还未更新下面的子节点的信息”;后续查询时,我们只需要检查这个节点的父节点有没有背过“懒标记”的锅,如果有,就将这个节点和它父节点的另外一个子节点也标记上懒标记,再清除父节点的懒标记即可。

例题2完整代码:

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 100010;
int n, m;
int w[N];
struct Node {
    int l, r;
    ll sum, add;    //  sum是区间的和,add是区间的懒标记
} tr[N * 4];
void pushup(int u) {
    tr[u].sum = tr[u * 2].sum + tr[u * 2 + 1].sum;
}
void pushdown(int u) {  //  向下传递懒标记
    auto &root = tr[u], &left = tr[u * 2], &right = tr[u * 2 + 1];
    if (root.add) {
        left.add += root.add, left.sum += (ll)(left.r - left.l + 1) * root.add;
        right.add += root.add, right.sum += (ll)(right.r - right.l + 1) * root.add;
        root.add = 0;
    }
}
void build(int u, int l, int r) {
    if (l == r) tr[u] = {l, r, w[r], 0};
    else {
        tr[u] = {l, r};
        int mid = l + r >> 1;
        build(u * 2, l, mid), build(u * 2 + 1, mid + 1, r);
        pushup(u);
    }
}
void modify(int u, int l, int r, int d) {
    if (tr[u].l >= l && tr[u].r <= r) {
        tr[u].sum += (ll)(tr[u].r - tr[u].l + 1) * d;
        tr[u].add += d;
    } else {    //  别忘了分开
        pushdown(u);
        int mid = tr[u].l + tr[u].r >> 1;
        if (l <= mid) modify(u * 2, l, r, d);
        if (r > mid) modify(u * 2 + 1, l, r, d);
        pushup(u);
    }
}
ll query(int u, int l, int r) {
    if (tr[u].l >= l && tr[u].r <= r) return tr[u].sum;
    pushdown(u);
    int mid = tr[u].l + tr[u].r >> 1;
    ll sum = 0;
    if (l <= mid) sum = query(u * 2, l, r);
    if (r > mid) sum += query(u * 2 + 1, l, r);
    return sum;
}
int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i ++ ) scanf("%d", &w[i]);
    build(1, 1, n);
    char op[2];
    int l, r, d;
    while (m -- ) {
        scanf("%s%d%d", op, &l, &r);
        if (*op == 'C') {
            scanf("%d", &d);
            modify(1, l, r, d);
        } else {
            printf("%lld\n", query(1, l, r));
        }
    }
    return 0;
}

好啦,那我们的线段树到这里就讲完啦,可以给我一个赞吗uwu


标签:int,线段,tr,mid,add,数据结构,节点
From: https://blog.51cto.com/u_16165905/7045924

相关文章

  • 《VTK图形图像开发进阶》第3章VTK基本数据结构——不同类型的数据集
    ......
  • 2.0 Python 数据结构与类型
    数据类型是编程语言中的一个重要概念,它定义了数据的类型和提供了特定的操作和方法。在python中,数据类型的作用是将不同类型的数据进行分类和定义,例如数字、字符串、列表、元组、集合、字典等。这些数据类型不仅定义了数据的类型,还为数据提供了一些特定的操作和方法,例如字符串支持......
  • 《VTK图形图像开发进阶》第3章VTK基本数据结构——属性数据
    属性数据(AttributeData)是与数据集组织结构相关联的信息。3.1标量数据#include<vtkAutoInit.h>VTK_MODULE_INIT(vtkRenderingOpenGL2);VTK_MODULE_INIT(vtkRenderingFreeType);VTK_MODULE_INIT(vtkInteractionStyle);#include<vtkSmartPointer.h>#include<vtkPoint......
  • 《VTK图形图像开发进阶》第3章VTK基本数据结构——单元类型
    数据集由一个或多个单元组成。一系列有序的点按指定类型连接所定义的结构就是单元(Cell),单元是VTK可视化系统的基础。这些顺序连接的点定义了单元的拓扑结构,而点的坐标定义了单元的几何结构。如下图是类型为六面体(Hexahedron)的单元,顶点列表(由点的索引号表示,即8-10-1-6-21-22-5......
  • #yyds干货盘点# LeetCode程序员面试金典:添加与搜索单词 - 数据结构设计
    题目:请你设计一个数据结构,支持添加新单词和查找字符串是否与任何先前添加的字符串匹配。实现词典类WordDictionary:WordDictionary()初始化词典对象voidaddWord(word)将word添加到数据结构中,之后可以对它进行匹配boolsearch(word)如果数据结构中存在字符串与 word匹......
  • 【学习笔记】线段树分治
    定义线段树分治是一种解决一类有插入、删除和整体查询操作的问题的方法。它是一种离线做法,通过在线段树上记录操作的时间区间来处理修改对询问的影响。每个操作被看作一个时间区间的修改,并在线段树上进行标记。然后通过深度优先搜索(DFS)依次执行这些操作,直到根节点来回答查询,并在......
  • 数据结构与数据库选型:构建高效业务系统的关键要素
    数据结构与数据库选型:构建高效业务系统的关键要素构建高效业务系统的关键要素之一是选择合适的数据结构和数据库。下面是一些关于数据结构和数据库选型的考虑因素:1.数据结构:-选择最适合业务需求的数据结构是非常重要的。常见的数据结构包括数组、链表、栈、队列、哈希表、......
  • 线段树的一些延伸
    一.动态开点线段树简介虽然思路简单,但对于一个习惯数组写法的人,这是一个比较难受的东西。动态开点一般是用来解决空间上的问题的。一般来说,普通的线段树是直接将一颗完整的线段建出来,但如碰到数据范围大或卡空间的时候,我们就只能在我们需要的时候再建,这个就叫做动态开点。(类似......
  • 【数据结构】bitset用法
    bitset用法bitset可以说是一个多位二进制数,每八位占用一个字节,因为支持基本的位运算,所以可用于状态压缩,n位bitset执行一次位运算的时间复杂度可视为n/32.输出只能用cout1.构造:inta=5;stringb="1011";charc[4]={'1','0','1','0'};bitset<10>s1(string("1001&qu......
  • ABC245E Wrapping Chocolate [线段树二分]
    也许更好的阅读体验\(\mathcal{Description}\)\(n\)个物品有长和宽,\(m\)个盒子也有长和宽,一个盒子最多可以装一个物品,问\(n\)个物品能否都放进盒子,物品和盒子不能旋转\(\mathcal{Solution}\)先离散化长和宽,将物品和盒子按照长从大到小排序考虑到当前物品时将所有长大于等于当......