算法训练营【7】线段树

线段树、IndexTree

线段树:

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)?

暴力方法略。。。

对时间复杂度要求高,其实就是想办法用空间来换时间。

用数组来代替二叉树存储这些元素。

相当于把满二叉树存放到数组中。

线段树的累加和用数据形式存储,数据长度为原来数组的4倍

为什么是4倍?

想办法让所有数字构建的二叉树都是满二叉树,即在不够2的某次方的情况下补上0,不影响总体结果

当数字刚好是 2^n +1 的个数时,数组的冗余空间最多,刚好是左半部分满足满二叉树,右半部分刚搞多一个数的情况。
在这里插入图片描述
叶节点所在的那层,左半部分为 n-1 个数,右半部分为1个数,
对于左侧,差不多是N的规模,右侧也需要补充到N个空间
从根节点到最底层的上一层需要2*n - 1 个元素空间
总共需要准备差不多4*N个空间

当元素个数刚好是 2^n 时,
最底层需要 n 个元素空间,从根节点到最底层的上一层需要n - 1 个元素空间
此时有2*n个空间是可以装下所有元素的

所以整体分配4*n个空间是可以满足最坏情况下的元素空间需求。

im
0节点不用,i节点的左孩子为 2 * i ,右孩子为2 * i+1
sum(i)=sum(2 * i )+sum(2 * i +1)

在这里插入图片描述

代码

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];
			}
      // 4倍空间的数组
      // 用来支持脑补概念中,某一个范围的累加和信息
			sum = new int[MAXN << 2]; 
      // 用来支持脑补概念中,某一个范围沒有往下傳遞的纍加任務
			lazy = new int[MAXN << 2]; 
      // 用来支持脑补概念中,某一个范围有没有更新操作的任务
			change = new int[MAXN << 2]; 
      // 用来支持脑补概念中,某一个范围更新任务,更新成了什么
			update = new boolean[MAXN << 2]; 
		}
		// 之前的,所有懒增加,和懒更新,从父范围,发给左右两个子范围
		// 分发策略是什么
		// 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;
			}
			if (lazy[rt] != 0) {
    
    
        //左孩子增加一个父的元素值
				lazy[rt << 1] += lazy[rt];
        // 左孩子累加和
				sum[rt << 1] += lazy[rt] * ln;
        //右孩子同理 
				lazy[rt << 1 | 1] += lazy[rt];
				sum[rt << 1 | 1] += lazy[rt] * rn;
				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;
			//  2 * i  等同于 rt<<1
			// 2*i + 1 等同于 rt<<1 | 1 
			build(l, mid, rt << 1);
			build(mid + 1, r, rt << 1 | 1);
      // 左半边和右半边都填好之后再累加
			pushUp(rt);
		}
   	// private 对内开放。对外封闭
		private void pushUp(int rt) {
    
    
      // rt << 1 | 1.  即 rt *2 + 1 
			sum[rt] = sum[rt << 1] + sum[rt << 1 | 1];
		}
		
		// L~R  所有的值变成C
		// l~r  rt
		public void update(int L, int R, int C, int l, int r, int rt) {
    
    
       // 更新操作之前,将所有lazy 清空,因为已经不需要他们做操作了
     
			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);
			}
      // 更新 sum 数组
			pushUp(rt);
		}
 
		// L~R, C 任务!
		// rt,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  mid = (l+r)/2
			int mid = (l + r) >> 1;
      //  下发之前所有攒的懒任务  操作
			pushDown(rt, mid - l + 1, r - mid);
			// L~R
			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);
		}
 
		// 1~6 累加和是多少? 1~8 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个积木依次掉落,
返回每一次掉落之后的最大高度
Leetcode题目:https://leetcode.com/problems/falling-squares/

class Solution {
    
    
	private int N;
	private int[] max;
	private Integer[] update;

	public List<Integer> fallingSquares(int[][] positions) {
    
    
		HashMap<Integer, Integer> map = index(positions);
		N = map.size();
//		N = this.arraySize(positions) + 1;// range()数组最大数
    // 线段树长度 == 数组长度 * 4
		int m = N << 2; 
		max = new int[m];// 最大值缓存结构
		update = new Integer[m]; // 更新缓存结构
		int start = 1; // 线段树最小索引
		int root = 1; // 线段树的根index
		List<Integer> ans = new ArrayList<>(); // 答案
		int max = 0; // 当前最大高度
		for (int i = 0; i < positions.length; i++) {
    
    
			int[] position = positions[i]; // 当前掉落的方块
			int L = map.get(position[0]); // 当前掉落左边界
			int R = map.get(position[0] + position[1] - 1); // 当前掉落右边界
//			int L = position[0]; // 当前掉落左边界
//			int R = position[0] + position[1] - 1; // 当前掉落右边界
			int height = this.query(L, R, start, N, root) + position[1]; // 掉落前这一段的最大高度 + 新增高度 == 这一段掉落后高度
			max = Math.max(max, height);// 掉落后的最大高度
			ans.add(max);
			this.update(L, R, height, start, N, root); // [L,R] 上都变成 height (下面有空的也没事)
		}
		return ans;
	}

	private void update(int L, int R, int H, int l, int r, int root) {
    
    
		if (L <= l && r <= R) {
    
    
			this.update[root] = H;// 当前范围属于更新范围内, 由root代表
			this.max[root] = H;
			return;
		}
		int mid = (l + r) >> 1;
		this.push(root);
		if (L <= mid) {
    
    
			this.update(L, R, H, l, mid, root << 1); 
		}
		if (R > mid) {
    
    
			this.update(L, R, H, mid + 1, r, (root << 1) | 1);
		}
		// 当前点最大值
		max[root] = Math.max(max[root << 1], max[(root << 1) | 1]);
	}

	private void push(int root) {
    
    
		if (this.update[root] != null) {
    
    
			int update = this.update[root];
			this.update[root << 1] = update;// 左子树
			this.update[(root << 1) | 1] = update;// 右子树
			this.max[root << 1] = update;
			this.max[(root << 1) | 1] = update;
			this.update[root] = null;
		}
	}

	private Integer query(int L, int R, int l, int r, int root) {
    
    
		if (L <= l && r <= R) {
    
    
			return this.max[root];
		}
		int mid = (l + r) >> 1;
		this.push(root);
		int maxLeft = 0;
		int maxRight = 0;
		if (L <= mid) {
    
    
			maxLeft = this.query(L, R, l, mid, root << 1);
		}
		if (R > mid) {
    
    
			maxRight = this.query(L, R, mid + 1, r, (root << 1) | 1);
		}
		return Math.max(maxLeft, maxRight);
	}

	private int arraySize(int[][] positions) {
    
    
		int maxNumber = 0;
		for (int[] position : positions) {
    
    
			maxNumber = Math.max(position[0] + position[1], maxNumber);
		}
		return maxNumber + 1; // 数组长度
	}
//离散化处理
	public HashMap<Integer, Integer> index(int[][] positions) {
    
    
		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;
	}
}

作者:wa-pian-d
链接:https://leetcode-cn.com/problems/falling-squares/solution/wa-pian-699-diao-luo-de-fang-kuai-java-z-ljxp/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

IndexTree 轻量化线段树

特点:

1)支持区间查询

2)没有线段树那么强,但是非常容易改成一维、二维、三维的结构

3)只支持单点更新

线段树可以实现,但是使用indexTree有比线段树更优的地方

indexTree不适于解决从L…R统一变成一个值的问题,将会非常慢

但是indexTree在处理单点更新上非常快,处理完单点更新后也能非常快速的查询一个范围的累加和

解决的问题:

indexTree只能解决单点更新完后怎么维护一个结构快速查询一个累加和快的问题

indexTree的help数组:1-1 2-1,2 3-3 4-1,2,3,4 …

help数组中index的二进制形式为010111000

管理了下面这些值

将最后一个1拆开的第一个数开始 ---->到该数本身

// 下标从1开始!
	public static class IndexTree {
    
    
 
		private int[] tree;
		private int N;
 
		// 0位置弃而不用!
		public IndexTree(int size) {
    
    
			N = size;
			tree = new int[N + 1];
		}
 
		// 1~index 累加和是多少?
		public int sum(int index) {
    
    
			int ret = 0;
			while (index > 0) {
    
    
				ret += tree[index];
				index -= index & -index;
			}
			return ret;
		}
 
		// index & -index : 提取出index最右侧的1出来
		// index :           0011001000
		// index & -index :  0000001000
		public void add(int index, int d) {
    
    
			while (index <= N) {
    
    
				tree[index] += d;
				index += index & -index;
			}
		}
	}	

求最多重合线段的个数

import java.util.Arrays;
import java.util.Comparator;
import java.util.PriorityQueue;

public class Code01_CoverMax {
    
    

	public static int maxCover1(int[][] lines) {
    
    
		int min = Integer.MAX_VALUE;
		int max = Integer.MIN_VALUE;
		for (int i = 0; i < lines.length; i++) {
    
    
			min = Math.min(min, lines[i][0]);
			max = Math.max(max, lines[i][1]);
		}
		int cover = 0;
		for (double p = min + 0.5; p < max; p += 1) {
    
    
			int cur = 0;
			for (int i = 0; i < lines.length; i++) {
    
    
				if (lines[i][0] < p && lines[i][1] > p) {
    
    
					cur++;
				}
			}
			cover = Math.max(cover, cur);
		}
		return cover;
	}

	public static int maxCover2(int[][] m) {
    
    
		Line[] lines = new Line[m.length];
		for (int i = 0; i < m.length; i++) {
    
    
			lines[i] = new Line(m[i][0], m[i][1]);
		}
		Arrays.sort(lines, new StartComparator());
		PriorityQueue<Line> heap = new PriorityQueue<>(new EndComparator());
		int max = 0;
		for (int i = 0; i < lines.length; i++) {
    
    
			while (!heap.isEmpty() && heap.peek().end <= lines[i].start) {
    
    
				heap.poll();
			}
			heap.add(lines[i]);
			max = Math.max(max, heap.size());
		}
		return max;
	}

	public static class Line {
    
    
		public int start;
		public int end;

		public Line(int s, int e) {
    
    
			start = s;
			end = e;
		}
	}

	public static class StartComparator implements Comparator<Line> {
    
    

		@Override
		public int compare(Line o1, Line o2) {
    
    
			return o1.start - o2.start;
		}

	}

	public static class EndComparator implements Comparator<Line> {
    
    

		@Override
		public int compare(Line o1, Line o2) {
    
    
			return o1.end - o2.end;
		}

	}

	// for test
	public static int[][] generateLines(int N, int L, int R) {
    
    
		int size = (int) (Math.random() * N) + 1;
		int[][] ans = new int[size][2];
		for (int i = 0; i < size; i++) {
    
    
			int a = L + (int) (Math.random() * (R - L + 1));
			int b = L + (int) (Math.random() * (R - L + 1));
			if (a == b) {
    
    
				b = a + 1;
			}
			ans[i][0] = Math.min(a, b);
			ans[i][1] = Math.max(a, b);
		}
		return ans;
	}

	public static void main(String[] args) {
    
    
		System.out.println("test begin");
		int N = 100;
		int L = 0;
		int R = 200;
		int testTimes = 200000;
		for (int i = 0; i < testTimes; i++) {
    
    
			int[][] lines = generateLines(N, L, R);
			int ans1 = maxCover1(lines);
			int ans2 = maxCover2(lines);
			if (ans1 != ans2) {
    
    
				System.out.println("Oops!");
			}
		}
		System.out.println("test end");
	}

}

求平面内最多矩形覆盖的面积的矩形个数

package class07;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.TreeSet;

public class Code04_CoverMax {
    
    

	public static class Rectangle {
    
    
		public int up;
		public int down;
		public int left;
		public int right;

		public Rectangle(int up, int down, int left, int right) {
    
    
			this.up = up;
			this.down = down;
			this.left = left;
			this.right = right;
		}

	}

	public static class DownComparator implements Comparator<Rectangle> {
    
    
		@Override
		public int compare(Rectangle o1, Rectangle o2) {
    
    
			return o1.down != o2.down ? (o1.down - o2.down) : o1.toString().compareTo(o2.toString());
		}
	}

	public static class LeftComparator implements Comparator<Rectangle> {
    
    
		@Override
		public int compare(Rectangle o1, Rectangle o2) {
    
    
			return o1.left != o2.left ? (o1.left - o2.left) : o1.toString().compareTo(o2.toString());
		}
	}

	public static class RightComparator implements Comparator<Rectangle> {
    
    
		@Override
		public int compare(Rectangle o1, Rectangle o2) {
    
    
			return o1.right != o2.right ? (o1.right - o2.right) : o1.toString().compareTo(o2.toString());
		}
	}

	// 矩形数量是N
	// O(N*LogN)
	// +
	// O(N) * [ O(N) + O(N *LogN) ]
	public static int maxCover(Rectangle[] recs) {
    
    
		if (recs == null || recs.length == 0) {
    
    
			return 0;
		}
		// 根据down(底)排序
		Arrays.sort(recs, new DownComparator());
		// 可能会对当前底边的公共局域,产生影响的矩形
		// list -> treeSet(有序表表达)
		TreeSet<Rectangle> leftOrdered = new TreeSet<>(new LeftComparator());
		int ans = 0;
		// O(N)
		for (int i = 0; i < recs.length;) {
    
     // 依次考察每一个矩形的底边
			// 同样底边的矩形一批处理
			do {
    
    
				leftOrdered.add(recs[i++]);
			} while (i < recs.length && recs[i].down == recs[i - 1].down);
			// 清除顶<=当前底的矩形
			removeLowerOnCurDown(leftOrdered, recs[i - 1].down);
			// 维持了右边界排序的容器
			TreeSet<Rectangle> rightOrdered = new TreeSet<>(new RightComparator());
			for (Rectangle rec : leftOrdered) {
    
     // O(N)
				removeLeftOnCurLeft(rightOrdered, rec.left);
				rightOrdered.add(rec);// O(logN)
				ans = Math.max(ans, rightOrdered.size());
			}
		}
		return ans;
	}

	public static void removeLowerOnCurDown(TreeSet<Rectangle> set, int curDown) {
    
    
		List<Rectangle> removes = new ArrayList<>();
		for (Rectangle rec : set) {
    
    
			if (rec.up <= curDown) {
    
    
				removes.add(rec);
			}
		}
		for (Rectangle rec : removes) {
    
    
			set.remove(rec);
		}
	}

	public static void removeLeftOnCurLeft(TreeSet<Rectangle> rightOrdered, int curLeft) {
    
    
		List<Rectangle> removes = new ArrayList<>();
		for (Rectangle rec : rightOrdered) {
    
    
			if (rec.right > curLeft) {
    
    
				break;
			}
			removes.add(rec);
		}
		for (Rectangle rec : removes) {
    
    
			rightOrdered.remove(rec);
		}
	}

}

二维区域和检索

package class32;

// 测试链接:https://leetcode.com/problems/range-sum-query-2d-mutable
// 但这个题是付费题目
// 提交时把类名、构造函数名从Code02_IndexTree2D改成NumMatrix
public class Code02_IndexTree2D {
    
    
	private int[][] tree;
	private int[][] nums;
	private int N;
	private int M;

	public Code02_IndexTree2D(int[][] matrix) {
    
    
		if (matrix.length == 0 || matrix[0].length == 0) {
    
    
			return;
		}
		N = matrix.length;
		M = matrix[0].length;
		tree = new int[N + 1][M + 1];
		nums = new int[N][M];
		for (int i = 0; i < N; i++) {
    
    
			for (int j = 0; j < M; j++) {
    
    
				update(i, j, matrix[i][j]);
			}
		}
	}

	private int sum(int row, int col) {
    
    
		int sum = 0;
		for (int i = row + 1; i > 0; i -= i & (-i)) {
    
    
			for (int j = col + 1; j > 0; j -= j & (-j)) {
    
    
				sum += tree[i][j];
			}
		}
		return sum;
	}

	public void update(int row, int col, int val) {
    
    
		if (N == 0 || M == 0) {
    
    
			return;
		}
		int add = val - nums[row][col];
		nums[row][col] = val;
		for (int i = row + 1; i <= N; i += i & (-i)) {
    
    
			for (int j = col + 1; j <= M; j += j & (-j)) {
    
    
				tree[i][j] += add;
			}
		}
	}

	public int sumRegion(int row1, int col1, int row2, int col2) {
    
    
		if (N == 0 || M == 0) {
    
    
			return 0;
		}
		return sum(row2, col2) + sum(row1 - 1, col1 - 1) - sum(row1 - 1, col2) - sum(row2, col1 - 1);
	}

}

参考1

参考2

参考3

源代码

猜你喜欢

转载自blog.csdn.net/qq_41852212/article/details/124453106