算法与数据结构进阶课第七节笔记
作者:互联网
线段树
1,一种支持范围整体修改和范围整体查询的数据结构
2,解决的问题范畴:
大范围信息可以只由左、右两侧信息加工出,
而不必遍历左右两个子范围的具体状况
给定一个数组arr,用户希望你实现如下三个方法
1)void add(int L, int R, int V) : 让数组arr[L…R]上每个数都加上V
2)void update(int L, int R, int V) : 让数组arr[L…R]上每个数都变成V
3)int sum(int L, int R) :让返回arr[L…R]这个范围整体的累加和
怎么让这三个方法,时间复杂度都是O(logN)
因为需要扩充为满二叉树,N个数需要准备4N的空间,虽然浪费一些空间,但是绝对够用
懒更新机制,在更新时只下发到任务完全包住的位置,不直接下发到底层,在新任务到时,下发一层原本的值,自己变0,然后看新任务能不能包住下一层
public static class SegmentTree {
// arr[]为原序列的信息从0开始,但在arr里是从1开始的
// sum[]模拟线段树维护区间和
// lazy[]为累加懒惰标记
// change[]为更新的值
// update[]为更新慵懒标记
private int MAXN;
private int[] arr;
private int[] sum;
private int[] lazy;
private int[] change;
private boolean[] update;
public SegmentTree(int[] origin) {
MAXN = origin.length + 1;
arr = new int[MAXN]; // arr[0] 不用 从1开始使用
for (int i = 1; i < MAXN; i++) {
arr[i] = origin[i - 1];
}
sum = new int[MAXN << 2]; // 用来支持脑补概念中,某一个范围的累加和信息
lazy = new int[MAXN << 2]; // 用来支持脑补概念中,某一个范围沒有往下傳遞的纍加任務
change = new int[MAXN << 2]; // 用来支持脑补概念中,某一个范围有没有更新操作的任务
update = new boolean[MAXN << 2]; // 用来支持脑补概念中,某一个范围更新任务,更新成了什么
}
private void pushUp(int rt) {
sum[rt] = sum[rt << 1] + sum[rt << 1 | 1];
}
// ln表示左子树元素结点个数,rn表示右子树结点个数
// 之前的,所有懒增加和懒更新,从父范围,发给左右两个子范围,分发的策略
private void pushDown(int rt, int ln, int rn) {
if (update[rt]) {
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;
}
// 更新还没进行下发,更新后又来了一些add任务,所以需要在更新下发后,继续下发攒着的add任务
if (lazy[rt] != 0) {
// rt给,左孩子,右孩子收到下发的任务,懒住
lazy[rt << 1] += lazy[rt];
sum[rt << 1] += lazy[rt] * ln;
lazy[rt << 1 | 1] += lazy[rt];
sum[rt << 1 | 1] += lazy[rt] * rn;
// 把rt的任务清0,因为已经下发,实际值在他自己懒住时也已经累加
lazy[rt] = 0;
}
}
// 在初始化阶段,先把sum数组,填好
// 在arr[l~r]范围上,去build,1~N,
// rt : 这个范围在sum中的下标
public 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);
pushUp(rt);
}
public 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);
}
// L..R -> 任务范围 ,所有的值累加上C
// l,r -> 表达的范围
// rt 去哪找l,r范围上的信息
public void add(int L, int R, int C,
int l, int r,
int rt) {
// 任务的范围彻底覆盖了,当前表达的范围
if (L <= l && r <= R) {
// 实际值也加值,但是下面的结点实际值不变
sum[rt] += C * (r - l + 1);
lazy[rt] += C;
return;
}
// 要把任务往下发
// 任务 L, R 没有把本身表达范围 l,r 彻底包住
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);
}
// 左右孩子做完任务后,我更新我的sum信息
pushUp(rt);
}
// 查询累加和是多少
public 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 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;
}
}
例:想象一下标准的俄罗斯方块游戏,X轴是积木最终下落到底的轴线
下面是这个游戏的简化版:
1)只会下落正方形积木
2)[a,b] -> 代表一个边长为b的正方形积木,积木左边缘沿着X = a这条线从上方掉落
3)认为整个X轴都可能接住积木,也就是说简化版游戏是没有整体的左右边界的
4)没有整体的左右边界,所以简化版游戏不会消除积木,因为不会有哪一层被填满。
给定一个N*2的二维数组matrix,可以代表N个积木依次掉落,
返回每一次掉落之后的最大高度
[a,b] 的宽度是a+b,那么我们用线段树的 [a,a+b-1] 来表示,因为如果要用[a,b] 来表示,[b,c] 就落不下来了,因为 arr[b] 已经有值了
public static class SegmentTree {
private int[] max;
private int[] change;
private boolean[] update;
public SegmentTree(int size) {
int N = size + 1;
max = new int[N << 2];
change = new int[N << 2];
update = new boolean[N << 2];
}
// 根据题目要求,汇报最大值
private void pushUp(int rt) {
max[rt] = Math.max(max[rt << 1], max[rt << 1 | 1]);
}
// ln表示左子树元素结点个数,rn表示右子树结点个数
private void pushDown(int rt, int ln, int rn) {
if (update[rt]) {
update[rt << 1] = true;
update[rt << 1 | 1] = true;
change[rt << 1] = change[rt];
change[rt << 1 | 1] = change[rt];
max[rt << 1] = change[rt];
max[rt << 1 | 1] = change[rt];
update[rt] = false;
}
}
public 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;
max[rt] = C;
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);
}
public int query(int L, int R, int l, int r, int rt) {
if (L <= l && r <= R) {
return max[rt];
}
int mid = (l + r) >> 1;
pushDown(rt, mid - l + 1, r - mid);
int left = 0;
int right = 0;
if (L <= mid) {
left = query(L, R, l, mid, rt << 1);
}
if (R > mid) {
right = query(L, R, mid + 1, r, rt << 1 | 1);
}
return Math.max(left, right);
}
}
/**
* [2,7] -> 2,8
* [3,10] -> 3,12
* 离散化后
* 实际坐标 :2,3,8,12
* 对应坐标: 1,2,3,4
* @param positions
* @return
*/
public HashMap<Integer, Integer> index(int[][] positions) {
// 离散化坐标,将实际的坐标值计算出来,再从小到大认为他是1,2,3这样的下标
TreeSet<Integer> pos = new TreeSet<>();
for (int[] arr : positions) {
pos.add(arr[0]);
pos.add(arr[0] + arr[1] - 1);
}
HashMap<Integer, Integer> map = new HashMap<>();
int count = 0;
// 位置与数组下标对应的关系
for (Integer index : pos) {
map.put(index, ++count);
}
return map;
}
public List<Integer> fallingSquares(int[][] positions) {
HashMap<Integer, Integer> map = index(positions);
int N = map.size();
SegmentTree segmentTree = new SegmentTree(N);
int max = 0;
List<Integer> res = new ArrayList<>();
// 每落一个正方形,收集一下,所有东西组成的图像,最高高度是什么
for (int[] arr : positions) {
int L = map.get(arr[0]);
int R = map.get(arr[0] + arr[1] - 1); // 首先处理了块的右边界,所以左插,右插都没问题
int height = segmentTree.query(L, R, 1, N, 1) + arr[1];
max = Math.max(max, height);
res.add(max);
segmentTree.update(L, R, height, 1, N, 1);
}
return res;
}
想要查询的父值,能由左孩子和右孩子提供,就能使用线段树,累加和,最大最小值
例2:
有N个房子,另有17种颜色,想要实现[L,R] 范围上刷成一种颜色,还想查询[L,R] 上不同的颜色又多少种。
使用一个整数的二进制位表示刷上去的颜色
父上的颜色数量就是左孩子或右孩子
例3:
最大线段重合问题,有一组线段,看看重合的最多次是多少
和房子的高度一样,也要处理宽度[a,b]为[a,a+b-1],使用线段树
更容易的方法:
[1,10] [1,7] [2,5] [4,6]
1、首先用线段开始的问题排序
2、使用一个小根堆,首先看[1,10],堆里比小于等于1的数字都扔出,10入堆,此时堆的大小是1
[1,7] 小于等于1的扔出,7放入,那么堆顶就是7,堆大小就是2
如果处理的线段是[L,R] 就弹出所有小于等于L的数,加入R后,小根堆里有几个数就是要的答案
处理n个线段,记录每次的最大值,最后就是最大的重合次数
小根堆里存的是,之前的线段能对现在的产生影响,这些线段的结尾都存在堆里
public static class Line{
public int start;
public int end;
public Line(int s, int e){
this.start = s;
this.end = e;
}
public int getStart(){
return start;
}
public int getEnd(){
return end;
}
}
public static int maxCover(int[][] m){
Line[] lines = new Line[m.length];
for (int i = 0; i < lines.length; i++){
lines[i] = new Line(m[i][0],m[i][1]);
}
Arrays.sort(lines,Comparator.comparing(Line::getStart));
PriorityQueue<Line> heap = new PriorityQueue<>(Comparator.comparing(Line::getEnd));
int max = 0;
for (int i = 0; i < lines.length; i++){
while (!heap.isEmpty() && heap.peek().end <= lines[i].end){
heap.poll();
}
heap.add(lines[i]);
max = Math.max(max,heap.size());
}
return max;
}
例4:
有很多矩形,怎么看最多的盖住次数
1、矩形遍历顺序 底 从小到大
2、重合区域的底,必然是某个矩形的底,把每个底都都变成直线,求必须以这个底为底的重合区域数量
3、将高不高于这个底的矩阵全部扔掉,留下来的矩形的宽看成一个线段,转换为线段重合问题
public static int coverMax1(Rectangle[] resc){
if (resc == null || resc.length == 0){
return 0;
}
Arrays.sort(resc,Comparator.comparing(Rectangle::getDown));
// 可能回对当前底边的公共区域,产生影响的矩阵
TreeSet<Rectangle> leftOrdered = new TreeSet<>(Comparator.comparing(Rectangle::getLeft));
int ans = 0;
for (int i = 0; i < resc.length; i++){
int curDown = resc[i].getDown();
int index = i;
// 已经排好了序,所以不用担心越过某个矩形
while (resc[index].down == curDown){
leftOrdered.add(resc[index]);
index ++;
}
i = index;
// 将顶小于等于当前底的数去掉
removeLowerOnCurDown(leftOrdered,curDown);
TreeSet<Rectangle> rightOrdered = new TreeSet<>(Comparator.comparing(Rectangle::getRight));
for (Rectangle rec : leftOrdered){
removeLowerOnCurLeft(rightOrdered,rec.getLeft());
rightOrdered.add(rec);
ans = Math.max(ans,rightOrdered.size());
}
}
return ans;
}
public static void removeLowerOnCurDown(TreeSet<Rectangle> set, int curDown){
Iterator<Rectangle> iterator = set.iterator();
while (iterator.hasNext()){
Rectangle next = iterator.next();
if (next.up <= curDown){
set.remove(next);
}
}
}
public static void removeLowerOnCurLeft(TreeSet<Rectangle> set, int curLeft){
Iterator<Rectangle> iterator = set.iterator();
while (iterator.hasNext()){
Rectangle next = iterator.next();
if (next.right <= curLeft){
set.remove(next);
}
}
}
标签:rt,arr,进阶,int,线段,mid,第七节,数据结构,public 来源: https://blog.csdn.net/yshuoo/article/details/117265418