- 使用 Spring Initializr 创建 Spring Boot 应用程序
- 在Spring Boot中配置Cassandra
- 在 Spring Boot 上配置 Tomcat 连接池
- 将Camel消息路由到嵌入WildFly的Artemis上
1.线段树介绍
2.线段树原理及其实现
2.1区间修改
2.2区间查询
2.3区间更新
什么是线段树?线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。 [1]
对于线段树中的每一个非叶子节点[a,b],它的左儿子表示的区间为[a,(a+b)/2],右儿子表示的区间为[(a+b)/2+1,b]。因此线段树是平衡二叉树,最后的子节点数目为N,即整个线段区间的长度。
使用线段树可以快速的查找某一个节点在若干条线段中出现的次数,时间复杂度为O(logN)。而未优化的空间复杂度为2N,因此有时需要离散化让空间压缩。-----来自百度。
线段树主要是把一段大区间 平均地划分 成两段小区间进行维护,再用小区间的值来更新大区间。这样既能保证正确性,又能使时间保持在 log 级别(因为这棵线段树是平衡的)。也就是说一个[L,R]区间会被划分为两个区间分别是[L,(L+R)/2]和[(L+R)/2+1,R]这两个小区间。然后再递归下去分直达L=R。
下图就是一棵 [ 1 , 10 ] 的线段树的分解过程(相同颜色的节点在同一层)如图所示:
我们可以发现,这棵线段树的最大深度不超过[ log2 ( n − 1 ) ]+2.
1.实现的基本框架
我们如何实现这种结构呢?难道真的是用二叉树吗?当然不是我们可以用我们之前学过的堆来实现这种结构。本文中使用满二叉树来实现线段树,可能有老铁会问了如果给定数组的长度不是2的某次方时又该怎么办了?在这里如果他不是满二叉树我们给他补成满二叉树。
2.空间的计算
实现线代树我采用了大根堆来实现,这是因为大根堆是一个完全二叉树因此我们可以使用数组来表示,只不过当数组的长度不是2^i时我们需要补齐。如图所示
现在我们来估计一下大概需要多少空间。假设区间有n个元素,对于满二叉树来说:
1.第一层到第k层一共有2^k-1个节点(约有2^k)个节点
2.最后一层共有2^k-1个节点
我们可以得出:满二叉树中最后一层节点的个数大约是前面节点的个数之和
1.如果n=2^i则需要2n个节点
2.如果n=2^i+1时此时情况最坏需要4*n个节点(估计值)
3.表示方式:
在这里我们我们数组的下标是从1开始,这是为了让左右孩子的下标能够使用位运算来进而提高效率。如果一个父亲节点对应的索引为i那么则有下面公式:
1.左孩子=2*i;
2.右孩子=2*i+1
用位运算来表示为:
1.左孩子=i<<1;
2.右孩子=i<<1|1;
4.线段树的构建对应代码:
#pragma once
#include<iostream>
#include<vector>
using std::vector;
using std::cout;
using std::endl;
class SegmentTree {
public:
SegmentTree(const vector<int>& origin) {
MAXN = origin.size() + 1;
arr.resize(MAXN);
for (int i = 1; i < MAXN; i++) {//arr[0]不用从arr[1]开始
arr[i] = origin[i - 1];
}
sum.resize(MAXN << 2);//相当与MAXN*4;用来支持脑补概念中,某一个范围的累加和信息
lazy.resize(MAXN << 2);//用来支持脑补概念中,某一个没有在往下的累加和标记
change.resize(MAXN << 2);
update.resize(MAXN << 2);
}
void build(int l, int r, int rt) {
if (l == r) {//只有一个数
sum[rt] = arr[l];
return;
}
int mid = (l + r) >> 1;
build(l, mid, rt << 1);
build(mid + 1, r, (rt << 1) | 1);//相当于2*rt+1;
pushUp(rt);//计算玩向上返回累加和
};
void pushUp(int rt) {
sum[rt] = sum[rt << 1] + sum[(rt << 1) | 1];//rt<<1|1相当于2*rt+1;
}
private:
int MAXN;
vector<int>arr;//arr维护的是原序列信息从0开始,但是在这里arr从1开始
vector<int>sum;//模拟线段树维护区间和
vector<int>lazy;//为累加懒惰信息标记
vector<int>change;//对应位置的更新值
vector<bool>update;//为更新懒惰标记
};
修改大概可以分为两步:
1.找到这段区间。
2.修改这一区间所有的点
大致流程如下:
1.如果当前区间完全被覆盖在目标区间里将这个区间sum数组的的位置加上(r-l+1)*C
2.如果没有完全覆盖则先下传懒标记
3.如果这个区间的左孩子和目标区间有交集,那么就递归给左孩子分配任务
4.如果这个区间的右孩子和目标区间有交集,则递归分配任务给右孩子。
懒更新:
在线段树中为了提高效率引入了懒跟新:
1.标记的含义:区间已经被更新过,但是子区间没有被更新过,被更新的信息是什么(区间求和只用记录有没有被访问过,而区间的四则运算等多种操作的问题则要记录是那一种操作)。
这里再引入两个很重要的东西:相对标记和绝对标记
相对标记:指的是可以共存的标记,且打标记的顺序与答案无关,即标记可以叠加。 比如说给一段区间中的所有数字都 + a ,我们就可以把标记叠加一下,比如上一次打了一个 加2 的标记,这一次要给这一段区间 +5 ,那么就把 + 2 的标记变成 + 5
绝对标记:
是指不可以共存的标记,每一次都要先把标记下传,再给当前节点打上新的标记。这些标记不能改变次序,否则会出错。 比如说给一段区间的数字重新赋值,或是给一段区间进行多种操作。
例如:
有了 懒惰标记 这种神奇的东西,我们区间修改时就可以偷一下懒,先修改当前节点,然后直接把信息挂在节点上就可以了!
如下面这棵线段树,当我们要修改区间 [ 1 , 4 ] [1,4][1,4] ,将元素赋值为 1 时,我们可以先找到所有的整个区间都要被修改的节点,显然是储存区间 [1,3] 和 [4,4] 的这两个节点。我们就可以先把 [1,3] 的 sum 改为(3−1+1)∗1=3 ,把 [ 4 , 4 ] 的 sum 改为 (1-1+1)*1=1 ,然后给它们打上值为 1 的懒惰标记,然后就可以了。
这样一来,我们每一次修改区间时只要找到目标区间就可以了,不用再向下递归到叶节点,从而提高了效率。
下传标记
碰到 相对标记 这种容易欺负的小朋友,我们只用打一下懒惰标记就可以了。
但是,遇到 绝对标记 ,或是下文提到的 区间查询 ,简单地打上懒惰标记就明显GG了。毕竟, 懒惰标记 只是简单地在节点挂上一个信息而已,遇到复杂的情况可是不行的啊!
于是,懒惰标记的 下传操作 就诞生了。
顾名思义, 下传标记 就是把一个节点的懒惰标记传给它的左右儿子,再把该节点的懒惰标记删去。
我们先来回顾一下标记的含义:
标记的含义:本区间已经被更新过了,但是子区间却没有被更新过,被更新的信息是什么。
显然,父区间是包含子区间的,也就是对于父区间的标记和子区间是有联系的。在大多数情况下,父区间和子区间的标记是 相同的 。因此,我们可以由父区间的标记推算出子区间应当是什么标记。
注意:以下所说的问题都是指区间赋值,除非有什么特别的申明。
如果要给一个节点中的所有元素重新赋值为 x ,那么它的儿子也必定要被赋值成 x 。所以,我们直接在子节点处修改 sumsum 值,再把子节点的标记改变一下就可以了(由于区间赋值要用 绝对标记 ,因此当子节点已经有标记时,要先下传子节点的标记,再下传该节点的标记。但是区间赋值会覆盖掉子节点的值,因此在这个问题中,直接修改标记就可以了)
下面是在对应区间加C的代码:
void pushUp(int rt) {//更新父亲的累加和
sum[rt] = sum[rt << 1] + sum[rt << 1 | 1];
}
void pushDown(int rt, int ln, int rn) {
if (update[rt]) {//update方法需要使用
update[rt << 1] = true;//往下发
update[rt << 1|1] = true;
change[rt << 1] = change[rt];
change[rt << 1 | 1] = change[rt];
lazy[rt << 1] = 0;//清空左右孩子的懒数组
lazy[rt << 1 | 1] = 0;
sum[rt << 1] = change[rt] * ln;//左孩子总和
sum[rt << 1 | 1] = change[rt] * rn;//右孩子总和
update[rt] = false;//当前位置已经下发改成false;
}
if (lazy[rt]) {
lazy[rt << 1] += lazy[rt];
lazy[(rt << 1) | 1] += lazy[rt];
sum[rt << 1] += lazy[rt] * ln;
sum[rt << 1 | 1] += lazy[rt] * rn;
lazy[rt] = 0;
}
}
void add(int L, int R, int C, int l, int r, int rt) {//L和R表示实际要修改的范围 l到r表示实际的范围 rt 表示这个范围
//的信息我去数组中那个位置去找
if (L <= l && r <= R) {//懒住
//修改相关信息
sum[rt] += C * (r - l + 1);
lazy[rt] += C;
return;
}
//当前任务躲不掉,无法懒更新,要往下发
int mid = (l + r) >> 1;
pushDown(rt, mid - l + 1, r - mid);//先将懒信息下发给左右孩子
if (L <= mid) {//左孩子需要下发任务
add(L, R, C, l, mid, rt << 1);
}
if (R > mid) {//右孩子需要下发任务
add(L, R, C, mid + 1, r, rt << 1 | 1);
}
pushUp(rt);//更新父亲的累加和
}
对于add中的参数解释:
1.L,R代表要修改的区间范围
2.l,r 代表实际的范围
3.C要加的值
4.rt表示l到r位置对于的信息在数组的那个位置去拿
对于pushdown方法的解释:
1.包括了update方法的和add的懒操作
区间查询的和add基本类似:
1.如果当前区间完全被覆盖在目标区间里将对于sum位置的数据返回
2.如果没有完全覆盖则先下传懒标记
3.如果这个区间的左孩子和目标区间有交集,那么就递归给左孩子分配任务
4.如果这个区间的右孩子和目标区间有交集,则递归分配任务给右孩子。
对应代码:
long long query(int L, int R, int l, int r, int rt) {
if (L <= l && r <= R) {//刚好覆盖这个区间直接在这里面拿值
return sum[rt];
}
int mid = (l + r) >> 1;
pushDown(rt, mid - l + 1, r - mid);//先将懒信息分配给左右孩子
long long ans = 0;
if (L <= mid) {
ans+=query(L, R, l, mid, rt << 1);
}
if (R > mid) {
ans += query(L, R, mid + 1, r, rt << 1 | 1);
}
return ans;
}
区间更新和区间的修改也是基本类似但也所不同
1.如果某一个区间被设置成对应的数那么该区间的lazy数组中的数据全部置为0
2.如果当前区间懒不住要先下发懒信息给左右孩子在分配任务
具体请看代码:
void Update(int L, int R, int C, int l, int r, int rt) {
if (L <= l && r <= R) {
update[rt] = true;//标记已经更新过
change[rt] = C;
sum[rt] = C * (r - l + 1);
lazy[rt] = 0;//清空
return;
}
//当前任务躲不掉,无法懒更新,要往下发
int mid = (l + r) >> 1;
pushDown(rt, mid - l + 1, r - mid);
if (L <= mid) {
Update(L, R, C, l, mid, rt << 1);
}
if (R > mid) {
Update(L, R, C, mid + 1, r, rt << 1 | 1);
}
pushUp(rt);//更新父亲节点的累加和
}
对应总代码:
#pragma once
#include<iostream>
#include<vector>
using std::vector;
using std::cout;
using std::endl;
class SegmentTree {
public:
SegmentTree(const vector<int>& origin) {
MAXN = origin.size() + 1;
arr.resize(MAXN);
for (int i = 1; i < MAXN; i++) {//arr[0]不用从arr[1]开始
arr[i] = origin[i - 1];
}
sum.resize(MAXN << 2);//相当与MAXN*4;用来支持脑补概念中,某一个范围的累加和信息
lazy.resize(MAXN << 2);//用来支持脑补概念中,某一个没有在往下的累加和标记
change.resize(MAXN << 2);
update.resize(MAXN << 2);
}
void build(int l, int r, int rt) {
if (l == r) {
sum[rt] = arr[l];
return;
}
int mid = (l + r) >> 1;
build(l, mid, rt << 1);
build(mid + 1, r, rt << 1 | 1);//相当于2*rt+1;
pushUp(rt);
};
void pushUp(int rt) {//更新父亲的累加和
sum[rt] = sum[rt << 1] + sum[rt << 1 | 1];
}
void pushDown(int rt, int ln, int rn) {
if (update[rt]) {//update方法需要使用
update[rt << 1] = true;//往下发
update[rt << 1|1] = true;
change[rt << 1] = change[rt];
change[rt << 1 | 1] = change[rt];
lazy[rt << 1] = 0;//清空左右孩子的懒数组
lazy[rt << 1 | 1] = 0;
sum[rt << 1] = change[rt] * ln;//左孩子总和
sum[rt << 1 | 1] = change[rt] * rn;//右孩子总和
update[rt] = false;//当前位置已经下发改成false;
}
if (lazy[rt]) {//下发懒信息
lazy[rt << 1] += lazy[rt];
lazy[(rt << 1) | 1] += lazy[rt];
sum[rt << 1] += lazy[rt] * ln;
sum[rt << 1 | 1] += lazy[rt] * rn;
lazy[rt] = 0;
}
}
void add(int L, int R, int C, int l, int r, int rt) {//L和R表示实际要修改的范围 l到r表示实际的范围 rt 表示这个范围
//的信息我去数组中那个位置去找
if (L <= l && r <= R) {//懒住
//修改相关信息
sum[rt] += C * (r - l + 1);
lazy[rt] += C;
return;
}
//当前任务躲不掉,无法懒更新,要往下发
int mid = (l + r) >> 1;
pushDown(rt, mid - l + 1, r - mid);//先将懒信息下发给左右孩子
if (L <= mid) {//左孩子需要下发任务
add(L, R, C, l, mid, rt << 1);
}
if (R > mid) {//右孩子需要下发任务
add(L, R, C, mid + 1, r, rt << 1 | 1);
}
pushUp(rt);//更新父亲的累加和
}
void Update(int L, int R, int C, int l, int r, int rt) {
if (L <= l && r <= R) {
update[rt] = true;//标记已经更新过
change[rt] = C;
sum[rt] = C * (r - l + 1);
lazy[rt] = 0;//清空
return;
}
//当前任务躲不掉,无法懒更新,要往下发
int mid = (l + r) >> 1;
pushDown(rt, mid - l + 1, r - mid);
if (L <= mid) {
Update(L, R, C, l, mid, rt << 1);
}
if (R > mid) {
Update(L, R, C, mid + 1, r, rt << 1 | 1);
}
pushUp(rt);//更新父亲节点的累加和
}
long long query(int L, int R, int l, int r, int rt) {
if (L <= l && r <= R) {//刚好覆盖这个区间直接在这里面拿值
return sum[rt];
}
int mid = (l + r) >> 1;
pushDown(rt, mid - l + 1, r - mid);//先将懒信息分配给左右孩子
long long ans = 0;
if (L <= mid) {
ans+=query(L, R, l, mid, rt << 1);
}
if (R > mid) {
ans += query(L, R, mid + 1, r, rt << 1 | 1);
}
return ans;
}
private:
int MAXN;
vector<int>arr;//arr维护的是原序列信息从0开始,但是在这里arr从1开始
vector<int>sum;//模拟线段树维护区间和
vector<int>lazy;//为累加懒惰信息标记
vector<int>change;//对应位置的更新值
vector<bool>update;//为更新懒惰标记
};
上述代码博主经过测试基本正确如有错误请在评论区留言!
背景: 我最近一直在使用 JPA,我为相当大的关系数据库项目生成持久层的轻松程度给我留下了深刻的印象。 我们公司使用大量非 SQL 数据库,特别是面向列的数据库。我对可能对这些数据库使用 JPA 有一
我已经在我的 maven pom 中添加了这些构建配置,因为我希望将 Apache Solr 依赖项与 Jar 捆绑在一起。否则我得到了 SolarServerException: ClassNotF
interface ITurtle { void Fight(); void EatPizza(); } interface ILeonardo : ITurtle {
我希望可用于 Java 的对象/关系映射 (ORM) 工具之一能够满足这些要求: 使用 JPA 或 native SQL 查询获取大量行并将其作为实体对象返回。 允许在行(实体)中进行迭代,并在对当前
好像没有,因为我有实现From for 的代码, 我可以转换 A到 B与 .into() , 但同样的事情不适用于 Vec .into()一个Vec . 要么我搞砸了阻止实现派生的事情,要么这不应该发
在 C# 中,如果 A 实现 IX 并且 B 继承自 A ,是否必然遵循 B 实现 IX?如果是,是因为 LSP 吗?之间有什么区别吗: 1. Interface IX; Class A : IX;
就目前而言,这个问题不适合我们的问答形式。我们希望答案得到事实、引用资料或专业知识的支持,但这个问题可能会引发辩论、争论、投票或扩展讨论。如果您觉得这个问题可以改进并可能重新打开,visit the
我正在阅读标准haskell库的(^)的实现代码: (^) :: (Num a, Integral b) => a -> b -> a x0 ^ y0 | y0 a -> b ->a expo x0
我将把国际象棋游戏表示为 C++ 结构。我认为,最好的选择是树结构(因为在每个深度我们都有几个可能的移动)。 这是一个好的方法吗? struct TreeElement{ SomeMoveType
我正在为用户名数据库实现字符串匹配算法。我的方法采用现有的用户名数据库和用户想要的新用户名,然后检查用户名是否已被占用。如果采用该方法,则该方法应该返回带有数据库中未采用的数字的用户名。 例子: “贾
我正在尝试实现 Breadth-first search algorithm , 为了找到两个顶点之间的最短距离。我开发了一个 Queue 对象来保存和检索对象,并且我有一个二维数组来保存两个给定顶点
我目前正在 ika 中开发我的 Python 游戏,它使用 python 2.5 我决定为 AI 使用 A* 寻路。然而,我发现它对我的需要来说太慢了(3-4 个敌人可能会落后于游戏,但我想供应 4-
我正在寻找 Kademlia 的开源实现C/C++ 中的分布式哈希表。它必须是轻量级和跨平台的(win/linux/mac)。 它必须能够将信息发布到 DHT 并检索它。 最佳答案 OpenDHT是
我在一本书中读到这一行:-“当我们要求 C++ 实现运行程序时,它会通过调用此函数来实现。” 而且我想知道“C++ 实现”是什么意思或具体是什么。帮忙!? 最佳答案 “C++ 实现”是指编译器加上链接
我正在尝试使用分支定界的 C++ 实现这个背包问题。此网站上有一个 Java 版本:Implementing branch and bound for knapsack 我试图让我的 C++ 版本打印
在很多情况下,我需要在 C# 中访问合适的哈希算法,从重写 GetHashCode 到对数据执行快速比较/查找。 我发现 FNV 哈希是一种非常简单/好/快速的哈希算法。但是,我从未见过 C# 实现的
目录 LRU缓存替换策略 核心思想 不适用场景 算法基本实现 算法优化
1. 绪论 在前面文章中提到 空间直角坐标系相互转换 ,测绘坐标转换时,一般涉及到的情况是:两个直角坐标系的小角度转换。这个就是我们经常在测绘数据处理中,WGS-84坐标系、54北京坐标系
在软件开发过程中,有时候我们需要定时地检查数据库中的数据,并在发现新增数据时触发一个动作。为了实现这个需求,我们在 .Net 7 下进行一次简单的演示. PeriodicTimer .
二分查找 二分查找算法,说白了就是在有序的数组里面给予一个存在数组里面的值key,然后将其先和数组中间的比较,如果key大于中间值,进行下一次mid后面的比较,直到找到相等的,就可以得到它的位置。
我是一名优秀的程序员,十分优秀!