蓝桥杯康复训练 Day4 (前缀和)(树状数组)(线段树)

昨天没状态摆了一天,今天复习一下各种区间问题


前缀和

常规遍历

区间求和复杂度 O(n)
单点修改复杂度 O(1)

前缀和

区间求和复杂度 O(1)
单点修改复杂度 O(n)

前缀和数组中每个值覆盖的是从开始到该点整个区间的和值
求 i ~ j 的区间和值可以通过 s [ j ] - s [ i - 1 ] 计算

可以扩展成二维三维的前缀和
在单点修改时需要对所有覆盖该点的值进行修改

在对区间求和复杂度要求高时使用

蓝桥杯–前缀和1


树状数组

对比前缀和复杂度

前缀和

区间求和复杂度 O(1)
单点修改复杂度 O(n)

树状数组

区间求和复杂度 O(logn)
单点修改复杂度 O(logn)

蓝桥杯–树状数组1

常用于综合考虑区间求和和单点修改的复杂度

与前缀和数组不同在于,树状数组的每个点覆盖的区间值是由它的下标决定的,如

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)

蓝桥杯–线段树1

线段树结构比较好理解,每个结点区间即是两个子节点的区间和

定义一个结点包含左右端点 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);
			}
		}
	}
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值