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
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)。
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)。
void add (int x, int u) { for (int i = x; i <= n; i += lowbit(i)) tr[i] += u; }
注意:这里都默认索引从1开始
思考:如何初始化树状数组?
方法一:输入序列a等价于对a进行单点修改,更新树状数组即可,时间复杂度为O(NlogN)。
for (int i = 0 ; i < nums.length; i++) { add(i, 1 ); }
方法二:考虑每个结点对父亲结点的贡献,时间复杂度为O(N)。
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)
{ 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基于区间求和):
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字段外,还需要将结果上传到他的父节点 !(很显然,下面变了,上面区间包含下面也要变)
更新与查询代码如下(非动态开点):
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树!
动态开点的代码如下:
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
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 (注意节点维护的是最大值)
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
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; } }