1 线段树
线段树:又叫区间修改树,区间的修改、更新、查询一些东西,怎么修改、更新、查询的比较快(logO)
1.1 概念
arr[0] 不用,剩下全copy
arr不管有多大,都需要构建出一棵满二叉树
void add(L,R,V,arr) 在L....R范围上统一增加V
void updata(L,R,V,arr) 在L....R上统一变成V
void getsum(L,R,arr) 在L...R范围上返回累加和
1.2 要求
arr不管有多大,都需要构建出一棵满二叉树
数据量N正好是2的n次方要2N空间,任意N需要准备4N空间,2的n次方+1最不省空间。
sum累加和作为线段树
arr如果长度为6,在后面补0就可以补成满二叉树了。
把arr[3,2,5,7]转化为线段树,储存的数组是什么样子?
i位置的左孩子是2i,右孩子是2i+1,arr[0]不用。
1.3 代码
1.2.1 代码实现细节
300-600之间加5
在这里插入图片描述
任务执行完(执行到1-1,2-2那一层),最上面lazy会归0
1.2.2 总体代码
class SegmentTree:
def __init__(self,origin):
# arr[]
# 为原序列的信息从0开始,但在arr里是从1开始的
# sum[]
# 模拟线段树维护区间和
# lazy[]
# 为累加和懒惰标记
# change[]
# 为更新的值
# update[]
# 为更新慵懒标记
self.MAXN = len(origin) + 1
self.arr = [0] * self.MAXN # arr[0] 不用,一个数组上的所有信息
for i in range(1,self.MAXN):
self.arr[i] = origin[i-1]
self.sum = [0] * (self.MAXN << 2) # sum准备4倍,用来支持脑补概念中,某一个范围的累加和信息
self.lazy = [0] * (self.MAXN << 2) # lazy准备4倍,用来支持脑补概念中,某一个范围沒有往下傳遞的纍加任務
# 下面两个搭配 组合使用懒更新信息
self.change = [0] * (self.MAXN << 2) # 用来支持脑补概念中,某一个范围有没有更新操作的任务
self.is_update = [False] * (self.MAXN << 2) # 用来支持脑补概念中,某一个范围更新任务,更新成了什么
# 在初始化阶段,先把sum数组,填好
# 在arr[l~r]范围上,去build,1~N,
# rt : 这个范围在sum中的下标
def build(self,l,r,rt):
if l == r:
self.sum[rt] = self.arr[l]
return
mid = (l + r) >> 1
self.build(l, mid, rt << 1) # 左右树信息填好
self.build(mid + 1, r, rt << 1 | 1)
self.pushUp(rt)
def pushUp(self,rt): # 汇总,累加和信息
self.sum[rt] = self.sum[rt << 1] + self.sum[rt << 1 | 1]
# L ~ R 所有的值变成C
# l~r rt
# L...R表示任务范围,所有的值累加上C
# l,r 实际的范围
# rt 去哪找l,r范围上的信息
def add(self,L,R,C,l,r,rt):
# 任务的范围彻底覆盖了,当前表达的范围
if L <= l and r <= R:
self.sum[rt] += C * (r - l + 1) # r - l + 1 实际数的数量
self.lazy[rt] += C # 多任务可调整累加和
return
# 任务并没有把l...r全包住
# 要把当前任务往下发
# 任务L...R 没有把本身表达范围l,r彻底包住
# l...mid 左边范围,下标是 rt*2 mid+1...r rt*2 + 1
mid = (l + r) >> 1 # 这是除2的意思
# 下发之前的lazy add任务
self.pushDown(rt,mid - l + 1,r - mid)
# L ~ R
# 左孩子是否需要接到任务
if L <= mid:
self.add(L,R,C,l,mid,rt << 1)
# 右孩子是否需要接到任务
if R > mid:
self.add(L,R,C,mid + 1,r,rt << 1 | 1) # 除2+1
# 左右孩子做完任务后,我更新我的sum信息
self.pushUp(rt)
# 1~6 累加和是多少? 1~8 rt是哪个位置
def query(self,L,R,l,r,rt):
if L <= l and r <= R:
return self.sum[rt]
mid = (l + r) >> 1
self.pushDown(rt, mid - l + 1, r - mid)
ans = 0
if L <= mid:
ans += self.query(L, R, l, mid, rt << 1)
if R > mid:
ans += self.query(L, R, mid + 1, r, rt << 1 | 1)
return ans
# L ~ R 所有的值变成C
# l~r rt
def update(self,L,R,C,l,r,rt): # 更新方法
if L <= l and r <= R:
self.is_update[rt] = True # 有没有发生update操作,发生了就是True
self.change[rt] = C # 所有数
self.sum[rt] = C * (r - l + 1)
self.lazy[rt] = 0 # lazy 东西全取消
return
# 当前任务躲不掉,无法懒更新,要往下发
mid = (l + r) >> 1
self.pushDown(rt, mid - l + 1, r - mid)
if L <= mid: # 更新左边
self.update(L, R, C, l, mid, rt << 1)
if R > mid: # 更新右边
self.update(L, R, C, mid + 1, r, rt << 1 | 1)
self.pushUp(rt) # 汇总
# 之前的,所有懒增加,和懒更新,从父范围,发给左右两个子范围
# 分发策略是什么
# ln表示左子树元素结点个数,rn表示右子树结点个数
def pushDown(self,rt,ln,rn):
if self.is_update[rt]:
self.is_update[rt << 1] = True
self.is_update[rt << 1 | 1] = True
self.change[rt << 1] = self.change[rt] # 以父节点为准
self.change[rt << 1 | 1] = self.change[rt]
self.lazy[rt << 1] = 0 # 累加和全部清空
self.lazy[rt << 1 | 1] = 0
self.sum[rt << 1] = self.change[rt] * ln # 左侧累加和 = 左侧节点的个数*当前节点更新值
self.sum[rt << 1 | 1] = self.change[rt] * rn
self.is_update[rt] = False # 任务分发完
if self.lazy[rt] != 0:
# 累加是因为左右孩子还要懒信息
self.lazy[rt << 1] += self.lazy[rt]
self.sum[rt << 1] += self.lazy[rt] * ln
self.lazy[rt << 1 | 1] += self.lazy[rt]
self.sum[rt << 1 | 1] += self.lazy[rt] * rn # rn表示几个数
self.lazy[rt] = 0
origin = [2,1,1,2,3,4,5]
seg = SegmentTree(origin)
S = 1 # 整个区间的开始位置,规定从1开始,不从0开始 -> 固定
N = len(origin) # 整个区间的结束位置,规定能到N,不是N-1 -> 固定
root = 1 # 整棵树的头节点位置,规定是1,不是0 -> 固定
L = 1 # 操作区间的开始位置 -> 可变
R = 2 # 操作区间的结束位置 -> 可变
C = 5 # 要加的数字或者要更新的数字 -> 可变
# 区间生成,必须在[S,N]整个范围上build
seg.build(S, N, root)
# 区间修改,可以改变L、R和C的值,其他值不可改变
seg.add(L, R, C, S, N, root)
# 区间更新,可以改变L、R和C的值,其他值不可改变
seg.update(L, R, C, S, N, root)
# 区间查询,可以改变L和R的值,其他值不可改变
total_sum = seg.query(1,4, S, N, root)
print(total_sum)
1.4 实例应用
![在这里插入图片描述](https://img-blog.csdnimg.cn/341fadd885744c8582b1dfd4b80fd711.png)
max作为线段树
![在这里插入图片描述](https://img-blog.csdnimg.cn/527bea3640b84226a7922fa588c4e6e1.png)
![在这里插入图片描述](https://img-blog.csdnimg.cn/2f9a578ca9f042c9bbf7e79e79f6be50.png)
什么时候可以用线段树做题,在一个区间,想要增加值或者更新值,父亲节点信息可以由左数右树加工得到,而且这个信息不需要调用左右树状况情况下,所有这样的查询都可以用线段树,最大最小累加和都可以。
求出现次数最多的值,不能用线段树。
# positions表示一个一个落方块
# [2,7] 2位置开始7位置结束 -> 左边界是2,右边界是2+7 -1
# [3,10] 左边界3 ,右边界 3+10 -1
class SegmentTree:
def __init__(self,N):
N = N + 1
# 这里不需要有数组,1~N认为0就行
self.max = [0] * (N << 2) # 最大值范围
self.change = [0] * (N << 2) # 下面两个懒信息
self.is_update = [0] * (N << 2)
def pushUp(self,rt): # 汇总
self.max[rt] = max(self.max[rt << 1],self.max[rt << 1 | 1])
# ln表示左子树元素节点个数,rn表示右子树节点个数
def pushDown(self,rt,ln,rn):
if self.is_update[rt]: # 若父节点有懒信息
self.is_update[rt << 1] = True # 左孩子收到懒信息
self.is_update[rt << 1 | 1] = True # 右孩子收到懒信息
self.change[rt << 1] = self.change[rt] # 懒的东西和父一样
self.change[rt << 1 | 1] = self.change[rt]
# 左、右范围刷成此时的值
self.max[rt << 1] = self.change[rt]
self.max[rt << 1 | 1] = self.change[rt]
self.is_update[rt] = False # 父信息不懒了
def update(self,L,R,C,l,r,rt):
if L <= l and r <= R:
self.is_update[rt] = True
self.change[rt] = C
self.max[rt] = C
return
mid = (l + r) >> 1
self.pushDown(rt, mid - l + 1, r - mid)
if L <= mid:
self.update(L, R, C, l, mid, rt << 1)
if R > mid:
self.update(L, R, C, mid + 1, r, rt << 1 | 1)
self.pushUp(rt)
def query(self,L,R,l,r,rt):
if L <= l and r <= R:
return self.max[rt]
mid = (l + r) >> 1
self.pushDown(rt,mid - l +1,r - mid)
left = 0
right = 0
if L <= mid:
left = self.query(L,R,l,mid,rt << 1)
if R > mid:
right = self.query(L, R, mid + 1, r, rt << 1 | 1)
return max(left,right)
# positions是二维数组,[2,5] 表示在2位置下落的长方形,边长为5
def index(positions):
pos = []
for arr in positions:
pos.append(arr[0]) # 左边界
pos.append(arr[0] + arr[1] - 1) # 右边界
pos.sort() # 所有位置都在有序表内,从小到大排好序
map = {}
count = 0 # 线段树所对应的标号,标好 207位置标志1,208位置标志2,所有边界混在一起排序
for index in pos:
count += 1
map[index] = count
return map
def indexx(positions):
map = {} # 创建一个空字典
for i in range(len(positions)):
map[positions[i]] = i # 将 positions 列表中的元素作为键,索引作为值存入字典
return map
def fallingSquares(positions):
# 100 -> 1 306 -> 2 403 - > 3 位置对应编号
# [100,403] 这就是把 1~3范围上做修改,这就是标志作用
# 拿编号好的东西去修改
map = indexx(positions)
N = len(map)
segmentTree = SegmentTree(N)
is_max = 0
res = []
# 每落一个正方形,收集一下,所有东西组成的图像,最高高度是什么
for arr in positions: # 遇到的任何一个正方形
L = map.get(arr[0]) # 左边界,取出来对应的编号
R = map.get(arr[0] + arr[1] - 1) # 右边界
# L,R范围上最大值求出来,再加上该方块高度
height = segmentTree.query(L,R,1,N,1) + arr[1]
is_max = max(is_max,height) # 总高度pk
res.append(is_max) # 加入答案
# L,R 更新此时最高的高度
segmentTree.update(L,R,height,1,N,1)
return res
1.5 实例应用2
有1-N这么多房子,有17种颜色,想实现L…R刷成一种颜色,调用随机范围刷,还可以查询L…R颜色有多少种
2 一些问题讲解
2.1 线段重合问题
暴力解:
def maxCover1(lines):
mins = float("-inf")
maxs = float("inf")
for i in range(len(lines)):
mins = min(mins,lines[i][0]) # [2][6] 线段长度2-6
maxs = max(maxs,lines[i][i])
cover = 0
for p in range(mins + 0.5,maxs):
cur = 0
for i in range(len(lines)):
if lines[i][0] < p and lines[i][1] > p: # 有没有盖住p
cur += 1
cover = max(cover,cur)
return cover
线段树:
# 线段树
class Line:
def __init__(self,s,e):
self.start = s
self.end = e
def __lt__(self,other):
return self.start < other.start
def maxCover2(m):
lines = []
for i in range(len(m)):
lines.append(Line(m[i][0],m[i][1]))
lines.sort()
heap = [] # 优先级队列,结尾谁小谁在上面
maxs = 0
for i in range(len(lines)):
# 线段不为空 且 线段顶部的end <= 线段开始值,影响不到了,弹出
while heap and heap[0].end <= lines[i].start:
heap.pop(0)
heap.append(lines[i]) # 把自己的线段加入到小根堆里去
heap = sorted(lines, key=lambda x: x.end)
maxs = max(maxs,len(heap))
return maxs
2.2
哪些矩形重合在一起的多
写接口转成这么表达:
import bisect # 插入有序
class Rectangle:
def __init__(self,up,down,left,right):
# 最上边界,最下边界,最左边界,最右边界
self.up = up
self.down = down
self.left = left
self.right = right
def insert_sorted_by_right(item, param_list):
index = bisect.bisect_left([x.right for x in param_list], item.right)
param_list.insert(index, item)
def maxCover(recs): # resc 是矩形类型
if recs is None or len(recs) == 0:
return 0
# 根据down(底)排序,最小的down在上
recs = sorted(recs,key=lambda x:x.down) # 以down排序
# 可能会对当前底边的公共区域,产生影响矩形
leftOrdered = [] # 有序表表达,从左到右依次考虑边界
ans = 0
# O(N)
for i in range(len(recs)):
curDown = recs[i].down
index = i
while recs[index].down == curDown:
leftOrdered.append(recs[index]) # O(lonN)
index += 1
leftOrdered = sorted(leftOrdered,key=lambda x:x.left) # 以左排序
i = index
# O(N) list是不是有一些顶<=底的矩形,不会影响就移除
removeLowerOnCurDown(leftOrdered,curDown) # 最大线段重合问题
# 维持了右边界排序的容器
rightOrdered = [] # 有序表
for rec in leftOrdered: # O(N)
removeLeftOnCurLeft(rightOrdered,rec.left)
Rectangle.insert_sorted_by_right(rec,rightOrdered)
ans = max(ans,len(rightOrdered))
return ans
def removeLowerOnCurDown(set,curDown):
removes = []
for rec in set:
if rec.up <= curDown:
removes.append(rec)
for rec in removes:
set.remove(rec)
def removeLeftOnCurLeft(rightOrdered,curLef):
removes = []
for rec in rightOrdered:
if rec.right > curLef:
break
removes.append(rec)
for rec in removes:
rightOrdered.remove(rec)