昨天没状态摆了一天,今天复习一下各种区间问题
前缀和
常规遍历
区间求和复杂度 O(n)
单点修改复杂度 O(1)
前缀和
区间求和复杂度 O(1)
单点修改复杂度 O(n)
前缀和数组中每个值覆盖的是从开始到该点整个区间的和值
求 i ~ j 的区间和值可以通过 s [ j ] - s [ i - 1 ] 计算
可以扩展成二维三维的前缀和
在单点修改时需要对所有覆盖该点的值进行修改
在对区间求和复杂度要求高时使用
树状数组
对比前缀和复杂度
前缀和
区间求和复杂度 O(1)
单点修改复杂度 O(n)
树状数组
区间求和复杂度 O(logn)
单点修改复杂度 O(logn)
常用于综合考虑区间求和和单点修改的复杂度
与前缀和数组不同在于,树状数组的每个点覆盖的区间值是由它的下标决定的,如
tr [ 8 ] 覆盖前8个值的和,即 w [ 1 ] ~ w [ 8 ]
tr [ 9 ] 覆盖前1个值的和,即 w [ 9 ] ~ w [ 9 ]
tr [ 10 ] 覆盖前2个值的和,即 w [ 9 ] ~ w [ 10 ]
tr [ 11 ] 覆盖前1个值的和,即 w [ 11 ] ~ w [ 11 ]
tr [ 12 ] 覆盖前4个值的和,即 w [ 9 ] ~ w [ 12 ]
覆盖长度可以通过对下标的计算得来,定义这个计算操作lowbit
static int lowbit(int x) {
return x & -x;
}
计算涉及到二进制知识不详述,总结就是 x 可以被一个最大的 2k 整除,那么 x 即可覆盖 2k 个值,lowbit返回值即是这个 2k
如 lowbit(12) = 22 = 4,因此 tr [ 12 ] 覆盖 w [ 9 ] ~ w [ 12 ] 共4个值的和,即
tr[ x ] = ( w [ x - lowbit( x ) ],w [ x ] ]
(左端点不覆盖,因此左开右闭,该左端点即是左边下一个区间的右端点)
由此可以得出计算前缀和的方法query,每次使用lowbit得到左侧下一区间的右端点,递推直到最左端
可以通过两前缀和相减来实现区间求和
static int query(int x) {
int res = 0;
for (int i = x; i >= 1; i -= lowbit(i)) {
res += tr[i];
}
return res;
}
同理也可以得出单点修改的方法modify,由于覆盖该点区间在右侧,并且右侧下一区间的 tr 坐标即是该坐标加上lowbit,因此可以使用lowbit向右递推直到最右端
树状数组的初始化也可以通过该函数实现
static void modify(int x, int v) {
for (int i = x; i <= n; i += lowbit(i)) {
tr[i] += v;
}
}
有些时候只要求区间求和用前缀和就好,没必要树状数组很麻烦
(比如去年国赛😑)
模板题
Acwing 动态求连续区间和
题意:区间求和及单点修改,树状数组模板题
代码如下
import java.io.*;
import java.util.*;
public class Main {
static Scanner tab = new Scanner(System.in);
static BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
static BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
static int N = 100010;
static int tr[] = new int[N]; // 树状数组
static int w[] = new int[N]; // 原数组
static int n, m;
// lowbit操作
static int lowbit(int x) {
return x & -x;
}
// 单点修改
static void modify(int x, int v) {
for (int i = x; i <= n; i += lowbit(i)) {
tr[i] += v;
}
}
// 前缀和
static int query(int x) {
int res = 0;
for (int i = x; i >= 1; i -= lowbit(i)) {
res += tr[i];
}
return res;
}
public static void main(String[] args) throws IOException {
n = tab.nextInt();
m = tab.nextInt();
for (int i = 1; i <= n; i++) {
w[i] = tab.nextInt();
}
for (int i = 1; i <= n; i++) {
modify(i, w[i]);
}
while (m-- > 0) {
int k = tab.nextInt();
int a = tab.nextInt();
int b = tab.nextInt();
if (k == 0) {
System.out.println(query(b) - query(a - 1));
} else {
modify(a, b);
}
}
}
}
线段树
复杂度等同树状数组
区间求和复杂度 O(logn)
单点修改复杂度 O(logn)
线段树结构比较好理解,每个结点区间即是两个子节点的区间和
定义一个结点包含左右端点 l , r 及区间和值sum
static class node {
public int l, r, sum;
public node(int l, int r, int sum) {
super();
this.l = l;
this.r = r;
this.sum = sum;
}
}
需要四种操作,更新、建树、区间求和、单点修改
(1)更新操作pushup,传参更新的结点u
只需要根据两个子节点更新当前结点数据即可
static void pushup(int u) {
tr[u].sum = tr[u * 2].sum + tr[u * 2 + 1].sum;
}
(2)建树操作build,传参结点u,结点属性 l 和 r
新建当前结点,再通过中值mid递归建立左右两个子节点,之后更新该节点
递归直至结点长度为1即 l == r 时结束,也就是叶结点,使用原数组 w 赋值 sum 属性
static void build(int u, int l, int r) {
if (l == r) {
tr[u] = new node(l, r, w[l]);
return;
}
tr[u] = new node(l, r, 0);
int mid = (l + r) / 2;
build(u * 2, l, mid);
build(u * 2 + 1, mid + 1, r);
pushup(u);
}
(3)区间求和操作query,传参结点u,区间端点 l r
通过中值mid判断左右两个子节点是否与区间存在交集,若存在则向子节点递归求和并加上该值
递归直到该结点完全被区间覆盖时结束
static int query(int u, int l, int r) {
if (l <= tr[u].l && r >= tr[u].r) {
return tr[u].sum;
}
int mid = (tr[u].l + tr[u].r) / 2;
int sum = 0;
if (l <= mid) {
sum += query(u * 2, l, r);
}
if (r >= mid + 1) {
sum += query(u * 2 + 1, l, r);
}
return sum;
}
(4)单点修改操作modify,传参结点u,修改单点x,权值v
通过中值mid向左右子节点递归查找修改单点的位置,修改后更新该结点
递归直至结点的长度为1即 tr [ u ] . l == tr [ u ] . r 时结束
static void modify(int u, int x, int v) {
if (tr[u].l == tr[u].r) {
tr[u].sum += v;
return ;
}
int mid = (tr[u].l + tr[u].r) / 2;
if (x <= mid) {
modify(u * 2, x, v);
} else {
modify(u * 2 + 1, x, v);
}
pushup(u);
}
线段树和树状数组的应用方向比较相似,树状数组更加简便,但树状数组应用性更强,如计算区间Max、Min等属性时使用线段树才能解决
简单的区间问题使用前缀和或树状数组更加方便
模板题
Acwing 动态求连续区间和
题意:区间求和及单点修改,线段树写法
代码如下
import java.io.*;
import java.util.*;
public class Main {
static Scanner tab = new Scanner(System.in);
static BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
static BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
static int N = 100010;
static node tr[] = new node[4 * N]; // 线段树需4倍于原数组长度
static int w[] = new int[N];
static int n, m;
// 定义树结点
static class node {
public int l, r, sum;
public node(int l, int r, int sum) {
super();
this.l = l;
this.r = r;
this.sum = sum;
}
}
// 更新
static void pushup(int u) {
tr[u].sum = tr[u * 2].sum + tr[u * 2 + 1].sum;
}
// 建树
static void build(int u, int l, int r) {
if (l == r) {
tr[u] = new node(l, r, w[l]);
return;
}
tr[u] = new node(l, r, 0);
int mid = (l + r) / 2;
build(u * 2, l, mid);
build(u * 2 + 1, mid + 1, r);
pushup(u);
}
// 区间求和
static int query(int u, int l, int r) {
if (l <= tr[u].l && r >= tr[u].r) {
return tr[u].sum;
}
int mid = (tr[u].l + tr[u].r) / 2;
int sum = 0;
if (l <= mid) {
sum += query(u * 2, l, r);
}
if (r >= mid + 1) {
sum += query(u * 2 + 1, l, r);
}
return sum;
}
// 单点修改
static void modify(int u, int x, int v) {
if (tr[u].l == tr[u].r) {
tr[u].sum += v;
return;
}
int mid = (tr[u].l + tr[u].r) / 2;
if (x <= mid) {
modify(u * 2, x, v);
} else {
modify(u * 2 + 1, x, v);
}
pushup(u);
}
public static void main(String[] args) throws IOException {
n = tab.nextInt();
m = tab.nextInt();
for (int i = 1; i <= n; i++) {
w[i] = tab.nextInt();
}
build(1, 1, n);
while (m-- > 0) {
int k = tab.nextInt();
int a = tab.nextInt();
int b = tab.nextInt();
if (k == 0) {
System.out.println(query(1, a, b));
} else {
modify(1, a, b);
}
}
}
}