高级搜索-线段树[C/C++]

线段树

文章目录

  • 线段树
    • 前言
    • 一、线段树的定义
    • 二、线段树的结构与建立
      • 2..1 节点定义
      • 2.2 递归建树
      • 2.3 静态数组空间的解释
    • 三、线段树的操作
      • 3.1 单点修改
      • 3.2 单点查询
      • 3.3 区间查询
      • 3.3 区间修改
    • 四、动态开点线段树
      • 递增分配器

前言

对于求数组区间和我们可以处理出前缀和后可以在O(1)的时间内计算出任意区间和,但是前缀和的缺点在于一旦区间发生改变我们就要重新计算前缀和,当频繁进行区间操作时前缀和的优势荡然无存,所以我们需要一种数据结构来解决这类需求。

**线段树(segment tree)**是分治思想在线性数据上的一种应用,通过把一段区间不断分治为小区间来进行建树。

我们把一段区间不断向下二分,会发现整体的结构形似一棵二叉树。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

一、线段树的定义

线段树是基于分治思想的二叉树,用来维护区间信息(区间信息,区间最值,区间gcd等),可以在logn的时间内执行区间修改和区间查询

线段树中每个叶子节点存储元素本身,非叶子节点存储区间内元素的统计值。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

上图就是一棵线段树。

二、线段树的结构与建立

这里选择静态建立

2…1 节点定义

//#define lc p << 1
//#define rc p << 1 | 1
//constexpr int N = 5e5 + 10;
//int n, a[N];
struct Node
{int l, r, sum;
} tree[N * 4];//为什么开N*4后面有证明

2.2 递归建树

建树的过程其实就是对一段数组不断往下分治的过程。

建树过程如下:

  1. l,r代表当前节点的区间,p代表节点下标索引
  2. 如果l == r,说明是叶子节点,建立完就return
  3. 否则,将区间[l , r]二分为[l , mid] , [mid + 1 , r]分别建立左右子树
  4. 更新当前节点的sum值
void build(int p, int l, int r)
{tree[p] = {l, r, a[l]};if (l == r)return;int m = (l + r) >> 1;build(lc, l, m);build(rc, m + 1, r);tree[p].sum = tree[lc].sum + tree[rc].sum;
}

2.3 静态数组空间的解释

我们为什么开辟4*N个节点呢?

先来看最简单的情况,我们的线段树恰好为一棵满二叉树,那么我们最后一层叶子节点个数即为数组长度n,它的上面所有节点的数目为n - 1,那么最大编号为(n - 1) * 2 + 1

否则,由于每次划分出的两个区间长度最多差1,我们的线段树的非满二叉树情况只可能是一棵满二叉树的若干叶子节点上分出若干二叉

也就是说我们此时n = 2 ^ m + 2 * k(k = 1 , 2 , 3…)

如下图为例,n = 2 ^ m + 2

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

对于非满二叉树的情况,就是在一棵满二叉树的叶子节点层增加若干二叉的情况,我们发现二叉会出现在长度大于1的奇数长度区间下,而且出现位置为左右的偶数编号位置交替出现(如上图如果一个二叉就是在8号,两个就是8号和12号,三个就是8号,10号,12号…)

那么我们最后一层有2个节点,倒数第二层有n - 2个节点,倒数第三层有n - 3个节点,我们的最大编号为(n - 2 - 1 + (n - 2) / 2 + 1) * 2 * 1 + 1 = 3 * n - 5

同样的,当n = 2 ^ m + 2 * k ,(k = 1 , 2 , 3 …)

我们最大编号为(n - 2 * k - 1 + (n - 2k) / 2k * (2k - 1) + 1) * 2 + 1 = (4 - 1/k) * n - (8*k - 3) < 4 * n

故开辟4*n个节点

三、线段树的操作

3.1 单点修改

类似于平衡树的查找,不断缩小查询范围,根据区间往下递归即可

void update(int p, int x, int k) // 在p为根的树中找到x,加上k
{if (tree[p].l == x && tree[p].r == x) // 找到了叶子{tree[p].sum += k;return;}int m = (tree[p].l + tree[p].r) >> 1;if (x > m)update(rc, x, k);elseupdate(lc, x, k);tree[p].sum = tree[lc].sum + tree[rc].sum;
}

3.2 单点查询

和单点修改差不多。

// 单点查询
int query(int p, int x)
{if (tree[p].l == x && tree[p].r == x) // 找到了叶子return tree[p].sum;if (x <= tree[lc].l)return query(lc, x);if (x >= tree[rc].l)return query(rc, x);assert(false);return -1;
}

3.3 区间查询

类似于treap的split和merge,例如区间[4 , 9]可以拆成[4 , 5] , [6 , 8] , [9 , 9],合并这三个区间的值就是我们查询的结果。

区间查询过程如下:

  1. 从根节点进入,开始递归查询[x , y]
  2. 如果x <= l <= r <= y,那么我们直接当前节点的结果。
  3. 否则,判断左右区间是否和查询区间有重叠,如果有,递归对应子树
// 区间查询
int query(int p, int x, int y)
{if (x <= tree[p].l && tree[p].r <= y)return tree[p].sum;int m = (tree[p].l + tree[p].r) >> 1, sum = 0;if (x <= m)sum += query(lc, x, y);if (y > m)sum += query(rc, x, y);return sum;
}

3.3 区间修改

如果我们执行区间长度次单点修改,那么我们的时间复杂度显然太高了,我们这里的处理方法是给

// 向上更新
void pushup(int p)
{tree[p].sum = tree[lc].sum + tree[rc].sum;
}
// 向下更新
void pushdown(int p)
{if (tree[p].add){tree[lc].sum += tree[p].add * (tree[lc].r - tree[lc].l + 1);tree[rc].sum += tree[p].add * (tree[rc].r - tree[rc].l + 1);tree[lc].add += tree[p].add;tree[rc].add += tree[p].add;tree[p].add = 0;}
}
// 区间修改
void update(int p, int x, int y, int k)
{if (x <= tree[p].l && tree[p].r <= y){tree[p].sum += k * (tree[p].r - tree[p].l + 1);tree[p].add += k;return;}int m = (tree[p].l + tree[p].r) >> 1;pushdown(p);if (x <= m)update(lc, x, y, k);if (y > m)update(rc, x, y, k);pushup(p);
}

四、动态开点线段树

递增分配器

template <class T>
class CachedObj
{
public:void *operator new(size_t s){if (!head){T *a = new T[SIZE];for (size_t i = 0; i < SIZE; ++i)add(a + i);}T *p = head;head = head->CachedObj<T>::next;return p;}void operator delete(void *p, size_t){if (p)add(static_cast<T *>(p));}virtual ~CachedObj() {}protected:T *next;private:static T *head;static const size_t SIZE;static void add(T *p){p->CachedObj<T>::next = head;head = p;}
};
template <class T>
T *CachedObj<T>::head = 0;
template <class T>
const size_t CachedObj<T>::SIZE = 10000;
class Node : public CachedObj<Node>
{
public:Node *left;Node *right;int add;bool v;
};

动态开点线段树模板,其实就是把原来节点里存的区间放到函数参数里面了

#define int long long
int a[500010], n, m;
template <class T>
class CacheObj
{
public:void *operator new(size_t){if (!head){T *a = new T[SIZE];for (size_t i = 0; i < SIZE; i++)add(a + i);}T *p = head;head = head->CacheObj<T>::next;return p;}void operator delete(void *p, size_t){if (p)add(static_cast<T *>(p));}virtual ~CacheObj() {}protected:T *next;private:static T *head;static const size_t SIZE;static void add(T *p){p->CacheObj<T>::next = head;head = p;}
};
template <class T>
T *CacheObj<T>::head = nullptr;
template <class T>
const size_t CacheObj<T>::SIZE = 10000;
class Node : public CacheObj<Node>
{
public:Node() : left(nullptr), right(nullptr), sum(0), add(0) {}Node *left;Node *right;int sum;int add;
};
class SegmentTree
{
public:SegmentTree() : _root(new Node()){}SegmentTree(int l, int r) : _root(build(new Node(), l, r)){}Node *build(Node *p, int l, int r){p->sum = a[l];if (l == r)return p;int mid = (r + l) >> 1;p->left = build(new Node(), l, mid);p->right = build(new Node(), mid + 1, r);pushup(p);return p;}int query(int x){return query(x, 1, n, _root);}int query(int x, int l, int r, Node *node){if (l == x && r == x)return node->sum;int mid = (r + l) >> 1;pushdown(node, l, r, mid);if (mid >= x)return query(x, l, mid, node->left);return query(x, mid + 1, r, node->right);}int query(int left, int right){return query(left, right, 1, n, _root);}int query(int left, int right, int l, int r, Node *node){if (left <= l && r <= right){return node->sum;}int mid = (r + l) >> 1, ret = 0;pushdown(node, l, r, mid);if (mid >= left)ret += query(left, right, l, mid, node->left);if (mid < right)ret += query(left, right, mid + 1, r, node->right);return ret;}void pushup(Node *p){p->sum = p->left->sum + p->right->sum;}void pushdown(Node *p, int l, int r, int mid){if (!p->left)p->left = new Node();if (!p->right)p->right = new Node();if (p->add){p->left->add += p->add;p->left->sum += p->add * (mid - l + 1);p->right->add += p->add;p->right->sum += p->add * (r - mid);p->add = 0;}}void update(int x, int k){update(x, 1, n, k, _root);}void update(int x, int l, int r, int k, Node *node){if (r == x && l == x){node->sum += k;node->add += k;}int mid = (r + l) >> 1;if (x <= mid)update(x, l, mid, k, node);elseupdate(x, mid + 1, r, k, node);}void update(int l, int r, int k){update(l, r, k, 1, n, _root);}void update(int left, int right, int k, int l, int r, Node *node)// l , r 是当前节点的区间{if (left <= l && r <= right){node->add += k;node->sum += k;return;}int mid = (r + l) >> 1;pushdown(node, l, r, mid);if (mid >= left)update(left, right, k, l, mid, node->left);if (mid < right)update(left, right, k, mid + 1, r, node->right);pushup(node);}private:Node *_root;
};

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

AI赋能数据表设计

数据表设计软件用过多种&#xff0c;用Ai 设计表几年Ai大模型爆发之后提升了新的高度 用navicat 设计表就是在跟团队的人介绍这次功能的表结构时&#xff0c;没办法看备注&#xff0c;只能看英文字段&#xff0c;导致在比较复杂的表中&#xff0c;总是在表结构和图形结构中来回…

【C++百宝箱】语法总结:命名空间 | 输入输出 | 缺省参数 | 函数重载

&#x1f6a9;纸上得来终觉浅&#xff0c; 绝知此事要躬行。 &#x1f31f;主页&#xff1a;June-Frost &#x1f680;专栏&#xff1a;C入门宝典 &#x1f525;本文主要探讨C的语法&#xff0c;并深入了解C如何针对C语言中存在的不合理之处进行优化改进。 目录&#xff1a; ⌛…

盘点60个Python爬虫源码Python爱好者不容错过

盘点60个Python爬虫源码Python爱好者不容错过 爬虫&#xff08;Spider&#xff09; 学习知识费力气&#xff0c;收集整理更不易。 知识付费甚欢喜&#xff0c;为咱码农谋福利。 链接&#xff1a;https://pan.baidu.com/s/1JWrDgl46_ammprQaJiKqaQ?pwd8888 提取码&#xff…

检索增强生成架构详解【RAG】

生成式AI技术很强大&#xff0c;但它们受到知识的限制。 虽然像 ChatGPT 这样的LLM可以执行许多任务&#xff0c;但每个LLM的基线知识都存在基于其训练数据的差距。 如果你要求LLM写一些关于最近趋势或事件的文章&#xff0c;LLM不会知道你在说什么&#xff0c;而且回答最好是混…

图片转换成pdf格式的软件ABBYY16

ABBYY PDF这款提供多种图像处理选项&#xff0c;可提高源图像的质量&#xff0c;便于准确地识别光学字符。我们扫描纸质文档或从图像文件创建 PDF 时&#xff0c;务必选择合适的图像处理选项。而在ABBYY PDF 中包含下列图像处理选项。 识别文本 — 选择此选项会将文本层放在图…

_STORAGE_WRITE_ERROR_ thinkphp报错问题原因

整个报错内容如下 Uncaught exception Think\Exception with message _STORAGE_WRITE_ERROR_:./Runtime/Cache/Home/1338db9dec777aab181d4e74d1bdf964.php in C:\inetpub\wwwroot\ThinkPHP\Common\functions.php:101 Stack trace: #0 C:\inetpub\wwwroot\ThinkPHP\Library\…

【Linux】匿名管道与命名管道,进程池的简易实现

文章目录 前言一、匿名管道1.管道原理2.管道的四种情况3.管道的特点 二、命名管道1. 特点2.创建命名管道1.在命令行上2.在程序中 3.一个程序执行打开管道并不会真正打卡 三、进程池简易实现1.makefile2.Task.hpp3.ProcessPool.cpp 前言 一、匿名管道 #include <unistd.h&g…

【实战】K8S Helm部署Redis Cluster Redisinsight

文章目录 前言部署Redis Cluster安装Redis Insight写在最后 前言 在Web服务的开发过程中&#xff0c;Redis一直以来都有着举足轻重的作用。基本上所有的后端服务都会用这个中间件实现具体的业务场景&#xff0c;比如常作为系统缓存、分布式锁&#xff0c;也可以实现排名、定位…

Android笔记(十四):JetPack Compose中附带效应(一)

在Android应用中可以通过定义可组合函数来搭建应用界面。应用界面的更新往往是与可组合函数内部定义的状态值相关联的。当界面的状态值发生变更&#xff0c;会导致应用界面进行更新。在Android笔记&#xff08;九&#xff09;&#xff1a;Compose组件的状态&#xff0c;对Compo…

C语言—指针和数组

写在前 一个指针变量指向某个普通变量&#xff0c;则指针变量就等于普通变量。 指针变量存放的是地址&#xff0c;普通变量存放的是数据。 int * p; int i5,j; p &i;此程序&#xff0c;*pi5&#xff0c;在所有出现 *p 或 i 的位置&#xff0c;两者都可以互相替换。 通过…

单文件组件MVVM

单文件组件&MVVM 所谓组件化开发&#xff0c;就是创建一个个组件。 Vue是一个大类&#xff0c;渲染一切从new Vue开始。 指定视图&#xff1a;el template render:jsx语法 $mount[数学公式] 编译App.vue&#xff0c;作为视图入口 单个组件&#xff1a;结构 样式 data compu…

十大排序之堆排序(详解)

文章目录 &#x1f412;个人主页&#x1f3c5;算法思维框架&#x1f4d6;前言&#xff1a; &#x1f380;堆排序 时间复杂度O(n*logn)&#x1f387;1. 算法步骤思想&#x1f387;2、动画演示&#x1f387;3.代码实现 &#x1f412;个人主页 &#x1f3c5;算法思维框架 &#x1…