1.树状数组->「单点修改 & 区间查询」: 先输入一个长度为n的数组nums,有如下两种操作:
1.输入一个数m,输出数组中下标1~m的前缀和sum[1,m]
2.对某个指定下标的数进行值的修改
常规方法:前缀和 ,但是当单点修改的次数增多,前缀和更新耗时O(N),然后再求sum[1,m],总体时间复杂度为O(N^2)
进阶方法:树状数组和线段树 可以达到单次操作logN级别。平均时间复杂度O(NlogN)
7=0111=0100+0010+0001=lowBit(7)+lowBit(3)+lowBit(1)
前置知识 :二进制区间分解lowBit(x)=x^(-x)求出x中仅保留最低位的1的数值,lowBit(7)=0100=4
1 2 3 int lowbit (int x) { return x & -x; }
概念:树状数组就是一种基于二进制思想的数据结构,基本用途是维护序列的前缀和 。
对于给定的序列a,设树状数组为c,则c[x]保存序列a的区间[x-lowbit(x)+1,x]中所有数的和
主要有以下两个基本操作:
(1) update,单点修改,修改序列a中的某个元素;
(2) query,区间查询,查询序列a中区间[1,x]的所有数的和。
操作1:区间查询query
树状数组能够完成的是查询前缀和,相减即可得到区间和。
利用c[x]维护的是序列a中[x-lowbit(x)+1,x]的区间和,然后不断向前寻找即可,时间复杂度为O(logN)。
1 2 3 4 5 int query (int x) { int ans = 0 ; for (int i = x; i > 0 ; i -= lowbit(i)) ans += tr[i]; return ans; }
操作2:单点修改update
单点修改更准确的说是“单点增加”,给序列a中的一个数a[x]加上t,然后要更新树状数组c维护的前缀和,只需要不断向上维护c[x]的父亲结点即可,时间复杂度为O(logN)。
1 2 3 void add (int x, int u) { for (int i = x; i <= n; i += lowbit(i)) tr[i] += u; }
注意:这里都默认索引从1开始
思考:如何初始化树状数组?
方法一:输入序列a等价于对a进行单点修改,更新树状数组即可,时间复杂度为O(NlogN)。
1 2 3 for (int i = 0 ; i < nums.length; i++) { add(i, 1 ); }
方法二:考虑每个结点对父亲结点的贡献,时间复杂度为O(N)。
1 2 3 4 for (int i = 0 ; i < nums.length; i++) { tr[i] += nums[i]; if (i + lowBit(i) <= n) tr[i + lowBit(i)] += tr[i]; }
三叶姐树状数组模板:
一篇不错的图解:[树状数组] 详解树状数组, 包含更新查询图解, 秒懂lowbit含义(JAVA 65ms, 68.6MB)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 { int [] tree; int lowbit (int x) { return x & -x; } int query (int x) { int ans = 0 ; for (int i = x; i > 0 ; i -= lowbit(i)) ans += tree[i]; return ans; } void add (int x, int u) { for (int i = x; i <= n; i += lowbit(i)) tree[i] += u; } } { for (int i = 0 ; i < n; i++) add(i + 1 , nums[i]); } { void update (int i, int val) { add(i + 1 , val - nums[i]); nums[i] = val; } int sumRange (int l, int r) { return query(r + 1 ) - query(l); } }
2.线段树「单点修改、区间修改、单点查询、区间查询->但性能不高」 参考资料: https://mp.weixin.qq.com/s/T3Ds8Eb8mZ5f96NjRFr6WA
以下笔记均参考力扣题解(推荐): Range模块【线段树动态开点+线段树图解】
什么是线段树?
线段树其实是一种二叉搜索树,将一个大的区间划分为一个个单元区间。
内个单元区间表示成一个节点(单元区间->节点 ) 线段树中的线段,其实也是区间的意思,就是区间树
假设我们有一个数组为[1,2,3,4,5,6,7,8],我们表示为一个区间和线段树就是下图:
可见线段树的区间是按照区间的中点进行分叉,左子节点的区间必定小于右子节点的区间,同理左右子树均是BST
构建线段树代码(val基于区间求和):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 public class SegmentTree { Node[] tree; int [] data; public SegmentTree (int [] data) { this .data = data; this .tree = new Node [4 * data.length]; } static class Node { int left; int right; int lazy; int val; } public void build (int idx, int l, int r) { tree[idx] = new Node (); tree[idx].left = l; tree[idx].right = r; if (l == r) { tree[idx].val = data[r - 1 ]; } int mid = l + (r - l) / 2 ; build(idx * 2 , l, mid); build(idx * 2 + 1 , mid + 1 , r); tree[idx].val = tree[idx * 2 ].val + tree[idx * 2 + 1 ].val; } }
懒标记的引入:
如果是线段树,我们直接对根节点的值进行+(8-1+1)*1的操作就可以得到新的区间和,但是这样当我们查询中间某段区间的和时就会发现不对,因为这个+(8-1+1)*1没有涉及根节点的区间和操作!
于是就想可不可以在root处引入一个标记的量,在我们要下探到要求root子区间的区间和时可以把这个+1操作带下去?把子区间进行更新?于是就引入了lazy字段
注意: 我们只有在用到没有更新的区间时(也就是当前区间含有lazy),才会下传lazy,达到懒更新的目的。
也许我们还会更新[5,8]区间的值,我们除了设置lazy字段外,还需要将结果上传到他的父节点 !(很显然,下面变了,上面区间包含下面也要变)
更新与查询代码如下(非动态开点):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 public void update (int idx, int l, int r, int val) { if (tree[idx].left >= l && tree[idx].right <= r) { tree[idx].val += (tree[idx].right - tree[idx].left + 1 ) * val; tree[idx].lazy = val; return ; } if (tree[idx].lazy != 0 ) pushDown(idx); if (l <= tree[idx * 2 ].right) update(idx * 2 , l, r, val); if (r >= tree[idx * 2 + 1 ].left) update(idx * 2 + 1 , l, r, val); pushUp(idx); } public int query (int idx, int l, int r) { int res = 0 ; if (tree[idx].left >= l && tree[idx].right <= r) return tree[idx].val; if (tree[idx].lazy != 0 ) pushDown(idx); if (tree[idx * 2 ].right >= l) res += query(idx * 2 , l, r); if (tree[idx * 2 + 1 ].left <= r) res += query(idx * 2 + 1 , l, r); return res; } public void pushUp (int idx) { tree[idx].val = tree[idx * 2 ].val + tree[idx * 2 + 1 ].val; } public void pushDown (int idx) { tree[idx * 2 ].lazy += tree[idx].lazy; tree[idx * 2 + 1 ].lazy += tree[idx].lazy; int mid = tree[idx].left + (tree[idx].right - tree[idx].left) / 2 ; tree[idx * 2 ].val += tree[idx].lazy * (mid - tree[idx].left + 1 ); tree[idx * 2 + 1 ].val += tree[idx].lazy * (tree[idx].right - mid); tree[idx].lazy = 0 ; }
动态开点的引入:
上述代码只是针对不对区间长度进行修改,只能在固定的区间内查询和修改,并且用到了4n的空间,有些空间根本没有被使用,有的题目数据规模到了1e9,如果我们开4n的空间并不可行!
于是我们需要动态地进行节点创建,即我们不用idx * 2和idx* 2 + 1来表示节点的左右子节点,而是在Node里添加leftChild和rightChild两个引用,来找到左右节点。由于我们是在更新与查询中进行动态开点,所以不需要build树!
动态开点的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 public class SegmentTree_Dynamic { static class Node { int left, right; int val; int lazy; Node leftChild, rightChild; public Node (int left, int right) { this .left = left; this .right = right; } } public void update (Node root, int l, int r, int val) { if (r < root.left || l > root.right) return ; if (root.left >= l && root.right <= r) { root.lazy = val; root.val += (root.right - root.left + 1 ) * val; return ; } lazyCreate(root); pushDown(root); update(root.leftChild, l, r, val); update(root.rightChild, l, r, val); pushUp(root); } public int query (Node root, int l, int r) { if (root.left >= l && root.right <= r) return root.val; lazyCreate(root); pushDown(root); int mid = root.left + (root.right - root.left) / 2 ; if (r <= mid) { return query(root.leftChild, l, r); } else if (l > mid) { return query(root.rightChild, l, r); } else { return query(root.leftChild, l, mid) + query(root.rightChild, mid + 1 , r); } } public void pushUp (Node root) { root.val = root.leftChild.val + root.rightChild.val; } public void pushDown (Node root) { if (root.lazy != 0 ) { int v = root.lazy; root.leftChild.lazy += v; root.rightChild.lazy += v; root.leftChild.val += (root.leftChild.right - root.leftChild.left + 1 ) * v; root.rightChild.val += (root.rightChild.right - root.rightChild.left + 1 ) * v; root.lazy = 0 ; } } public void lazyCreate (Node root) { int mid = root.left + (root.right - root.left) / 2 ; if (root.leftChild == null ) root.leftChild = new Node (root.left, mid); if (root.rightChild == null ) root.leftChild = new Node (mid + 1 , root.right); }
数组表示的线段树(含懒标记+动态开点)可以参考三叶:
729. 我的日程安排表 I
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 package leetcode.SegmentTree;import org.junit.Test;public class Q729 { @Test public void test () { MyCalendar myCalendar = new MyCalendar (); System.out.println(myCalendar.book(10 , 20 )); System.out.println(myCalendar.book(15 , 25 )); System.out.println(myCalendar.book(20 , 30 )); } class MyCalendar { class Node { int ls, rs, add, val; } int N = (int ) 1e9 , M = 120010 , cnt = 1 ; Node[] tr = new Node [M]; public MyCalendar () { tr[0 ] = new Node (); } public void update (int u, int lc, int rc, int l, int r, int val) { if (lc >= l && rc <= r) { tr[u].val = (rc - lc + 1 ) * val; tr[u].add = val; return ; } lazyCreate(u); pushDown(u, rc - lc + 1 ); int mid = lc + (rc - lc) / 2 ; if (l <= mid) update(tr[u].ls, lc, mid, l, r, val); if (r > mid) update(tr[u].rs, mid + 1 , rc, l, r, val); pushUp(u); } public int query (int u, int lc, int rc, int l, int r) { if (lc >= l && rc <= r) return tr[u].val; lazyCreate(u); pushDown(u, rc - lc + 1 ); int mid = lc + (rc - lc) / 2 , res = 0 ; if (l <= mid) res = query(tr[u].ls, lc, mid, l, r); if (r > mid) res += query(tr[u].rs, mid + 1 , rc, l, r); return res; } public void pushUp (int u) { tr[u].val = tr[tr[u].ls].val + tr[tr[u].rs].val; } public void pushDown (int u, int len) { int v = tr[u].add; if (v != 0 ) { tr[tr[u].ls].add = v; tr[tr[u].rs].add = v; tr[tr[u].ls].val = (len - len / 2 ) * v; tr[tr[u].rs].val = (len / 2 ) * v; tr[u].add = 0 ; } } public void lazyCreate (int u) { if (tr[u].ls == 0 ) { tr[u].ls = cnt++; tr[tr[u].ls] = new Node (); } if (tr[u].rs == 0 ) { tr[u].rs = cnt++; tr[tr[u].rs] = new Node (); } } public boolean book (int start, int end) { if (query(0 , 0 , N, start, end - 1 ) > 0 ) { return false ; } update(0 , 0 , N, start, end - 1 , 1 ); return true ; } } }
731. 我的日程安排表 II (注意节点维护的是最大值)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 package leetcode.SegmentTree;import org.junit.Test;public class Q731 { @Test public void test () { MyCalendarTwo MyCalendar = new MyCalendarTwo (); System.out.println(MyCalendar.book(10 , 20 )); System.out.println(MyCalendar.book(50 , 60 )); System.out.println(MyCalendar.book(10 , 40 )); System.out.println(MyCalendar.book(5 , 15 )); System.out.println(MyCalendar.book(5 , 10 )); System.out.println(MyCalendar.book(25 , 55 )); } class MyCalendarTwo { class Node { int ls, rs, add, max; } int N = (int ) 1e9 , M = 120010 , cnt = 1 ; Node[] tr = new Node [M]; public MyCalendarTwo () { tr[0 ] = new Node (); } public void update (int u, int lc, int rc, int l, int r, int val) { if (lc >= l && rc <= r) { tr[u].add += val; tr[u].max += val; return ; } lazyCreate(u); pushDown(u); int mid = lc + (rc - lc) / 2 ; if (l <= mid) update(tr[u].ls, lc, mid, l, r, val); if (r > mid) update(tr[u].rs, mid + 1 , rc, l, r, val); pushUp(u); } public int query (int u, int lc, int rc, int l, int r) { if (lc >= l && rc <= r) return tr[u].max; lazyCreate(u); pushDown(u); int mid = lc + (rc - lc) / 2 , res = 0 ; if (l <= mid) res = query(tr[u].ls, lc, mid, l, r); if (r > mid) res = Math.max(res, query(tr[u].rs, mid + 1 , rc, l, r)); return res; } public void lazyCreate (int u) { if (tr[u].ls == 0 ) { tr[u].ls = cnt++; tr[tr[u].ls] = new Node (); } if (tr[u].rs == 0 ) { tr[u].rs = cnt++; tr[tr[u].rs] = new Node (); } } public void pushDown (int u) { int v = tr[u].add; if (v != 0 ) { tr[tr[u].ls].add += v; tr[tr[u].rs].add += v; tr[tr[u].ls].max += v; tr[tr[u].rs].max += v; tr[u].add = 0 ; } } public void pushUp (int u) { tr[u].max = Math.max(tr[tr[u].ls].max, tr[tr[u].rs].max); } public boolean book (int start, int end) { if (query(0 , 0 , N, start, end - 1 ) >= 2 ) return false ; update(0 , 0 , N, start, end - 1 , 1 ); return true ; } } }
做完这两题估计对线段树有了充分了解了!
3.差分数组->「区间修改 & 单点查询」: 差分区间求和:将每个区间覆盖信息转化为变化量 记录,最后从头开始统计变化量 就可以将总的变化量求出来
比喻成公交车:上车就表示该时刻t1->新区间加入乘客+1;下车就表示区间结束->下一个时刻(t2+1)乘客-1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Solution { public int [] corpFlightBookings(int [][] bookings, int n) { int [] counter = new int [n]; for (int [] booking : bookings) { counter[booking[0 ] - 1 ] += booking[2 ]; if (booking[1 ] < n) counter[booking[1 ]] -= booking[2 ]; } for (int i = 1 ; i < n; i++) { counter[i] += counter[i - 1 ]; } return counter; } }