前缀和 --- 初识计算机世界的"时空转换"

前缀和


导言

  • 前缀和 是一个很简单,而且很实用的算法技巧,一定要学会它!
  • 接下来我会引入一个问题,并逐步解决它,之后引出我们的主角 — 前缀和
  • 看到最后,小伙伴就知道标题的含义了。

问题

现在有一个数组A,求该数组在区间 [ l e f t , r i g h t ] [left, right] [left,right] 的总和。其中数组A长度在 [ 1 , 1 0 6 ] [1, 10^6] [1,106] 范围内,leftright都是合法的,且区间总和不会溢出。

例子: A = [1 2 3 4 5],left = 0,right = 3,则区间 [0, 3] 总和 sum = 1 + 2 + 3 + 4 = 10。

可能有的小伙伴觉得这题太简单了!直接使用一个 for 循环不就可以了吗?代码如下:

// 计算数组A,在区间[left, right]的和, 并返回。
int calculate(int* A,int left,int right){
	int sum = 0;
	for (int i=left;i<=right;i++){
		sum += A[i];
	}
	return sum;
}

的确,对于上面的问题,我们可以只用一个 for 循环解决。那我把问题升个级。

现在有一个数组 A,再给出一个查询次数 times,每一次查询都给出一个区间 [ l e f t , r i g h t ] [left, right] [left,right],求数组 A 在该区间的总和。其中数组 A 长度在 [ 1 , 1 0 6 ] [1, 10^6] [1,106] 范围内,times [ 1 , 1 0 6 ] [1, 10^6] [1,106] 范围内,leftright 都是合法的,且区间总和不会溢出。

思考 1 秒后 …

可能小伙伴会说,此时也可以使用 for 循环解决阿,代码如下。

// A是一个数组,times是需要查询的次数 
int calculate(int* A, int times){
	int left, right;
	while(times--){
		scanf("%d%d",&left,&right);	// 输入区间
		
		// 计算总和 
		int sum = 0;
		for (int i=left;i<=right;i++){
			sum += A[i];
		}
		
		printf("%d\n",sum);	// 输出结果 
	}
}

我想说可以,但很不幸的是,依照普通计算机的计算能力 (每秒执行 1 0 7 10^7 107 条指令),如果此时 t i m e s = 1 0 6 times=10^6 times=106,每次查询的区间都是 [ 0 , 1 0 6 − 1 ] [0, 10^6-1] [0,1061],这意味着计算机大约要执行 1 0 12 10^{12} 1012 条指令,执行耗时约为 1 0 5 10^5 105 秒,相当于 27.7 小时。显然,时间是宝贵的,不能这样浪费。

那我们还有什么办法来解决这个问题呢?

答案就是 前缀和。采用 前缀和,我们可以在 0.1 秒解决这个问题。

进入正题

前缀和的思想就是: 定义一个满足以下规则数组 prefixSum

p r e f i x S u m ( i ) = { 0 , i  = 0 A [ 0 ] + A [ 1 ] + . . . . + A [ i − 1 ] , i  > 0 prefixSum(i) = \begin{cases} 0, & \text{$i$ = 0} \\ A[0] + A[1] + .... + A[i - 1], & \text{$i$ > 0} \end{cases} prefixSum(i)={0,A[0]+A[1]+....+A[i1],i = 0i > 0

于是:
A 在 [ l e f t , r i g h t ] 区 间 的 总 和 = p r e f i x S u m [ r i g h t + 1 ] − p r e f i x S u m [ l e f t ] A 在 [left, right] 区间的总和 = prefixSum[right + 1] - prefixSum[left] A[left,right]=prefixSum[right+1]prefixSum[left]

下面是推导过程。(不复杂,不要跳过哈)

① 由前缀和定义,我们可以得出:
p r e f i x S u m [ r i g h t + 1 ] = A [ 0 ] + A [ 1 ] + . . . + A [ l e f t − 1 ] + A [ l e f t ] + . . . + A [ r i g h t ] p r e f i x S u m [ l e f t ] = A [ 0 ] + A [ 1 ] + . . . + A [ l e f t − 1 ] \begin{aligned} prefixSum[right + 1] &= A[0] + A[1] + ... + A[left - 1] + A[left] + ... + A[right]\\ \\ prefixSum[left] &= A[0] + A[1] + ... + A[left - 1]\\ \end{aligned} prefixSum[right+1]prefixSum[left]=A[0]+A[1]+...+A[left1]+A[left]+...+A[right]=A[0]+A[1]+...+A[left1]
② 二者相减,可得:
p r e f i x S u m [ r i g h t + 1 ] − p r e f i x S u m [ l e f t ] = A [ l e f t ] + . . . . + A [ r i g h t ] prefixSum[right + 1] - prefixSum[left] = A[left] + .... + A[right] prefixSum[right+1]prefixSum[left]=A[left]+....+A[right]

明白了上面的推导后。接下来我们进入下一个问题:如何获取 prefixSum 数组呢?

其实也不难,先来看推导。(不复杂,不要跳过哈)

① 由前缀和定义,我们可以得出:
p r e f i x S u m [ i − 1 ] = A [ 0 ] + A [ 1 ] + . . . + A [ i − 2 ] p r e f i x S u m [ i ] = A [ 0 ] + A [ 1 ] + . . . + A [ i − 2 ] + A [ i − 1 ] \begin{aligned} prefixSum[i - 1] &= A[0] + A[1] + ... + A[i - 2]\\ \\ prefixSum[i] &= A[0] + A[1] + ... + A[i - 2] + A[i - 1] \\ \end{aligned} prefixSum[i1]prefixSum[i]=A[0]+A[1]+...+A[i2]=A[0]+A[1]+...+A[i2]+A[i1]
② 于是,我们可以得出递推式:
p r e f i x S u m [ i ] = p r e f i x S u m [ i − 1 ] + A [ i − 1 ] , i  > 0 \begin{aligned} prefixSum[i] &= prefixSum[i - 1] + A[i - 1], & \text{$i$ > 0} \end{aligned} prefixSum[i]=prefixSum[i1]+A[i1],i > 0

于是,求前缀和的代码出来了。

// A: 源数组
// length: A的长度 
int* getPrefixSum(int* A,int length){
	int* prefixSum = new int[length+1];	// 注意要 +1,因为prefixSum数组比A数组多一个元素。
	prefixSum[0] = 0;	// 依照定义,prefixSum[0] = 0。
	for (int i=1;i<=length;i++){
		prefixSum[i] = prefixSum[i-1] + A[i-1];
	} 
	return prefixSum;
} 

接下来,我们把代码拼接一下,得到求取区间总和的最终代码。

// A: 源数组 
// length: 数组A的长度 
// times: 需要查询的次数 
// 这个函数用于求取区间总和
int calculate(int* A,int length,int times){
	int left, right;
	int* prefixSum = getPrefixSum(A, length); 	// 获取前缀和数组 
	while(times--){
		scanf("%d%d",&left,&right);	// 输入区间
		
		// 计算总和 (与上面相比,求取总和的代码变了)
		int sum = prefixSum[right+1] - prefixSum[left];
		
		printf("%d\n",sum);	// 输出结果 
	}
}

// A: 源数组
// length: A的长度 
// 这个函数用于求取前缀和
int* getPrefixSum(int* A,int length){
	int* prefixSum = new int[length+1];	// 注意要 +1,因为prefixSum数组比A数组多一个元素。
	prefixSum[0] = 0;
	for (int i=1;i<=length;i++){
		prefixSum[i] = prefixSum[i-1] + A[i-1];
	} 
	return prefixSum;
} 

此时我们只用 1 条指令就能求取区间 [ l e f t , r i g h t ] [left,right] [left,right] 的总和,所以即使 n = 1 0 6 n = 10^6 n=106,每次查询的区间都是 [ 0 , 1 0 6 − 1 ] [0, 10^6-1] [0,1061],为完成这个任务,计算机只需执行 1 0 6 10^6 106 条指令,总耗时才 0.1 秒!

拓展

  • 前缀和 采用了 空间换时间 的思想。所谓 空间换时间 ,就是通过开辟一些新的存储空间,用以存储计算的结果,使得再次遇到相同的问题时,计算机只需要到对应的存储空间拿取结果,而不需要再次计算,大大节省了时间。

    例子: 这就好像你背了 99 乘法表后,因为你已经记住了 9 × 9 9×9 9×9 的结果,所以就不需要再到草稿纸上计算了。

  • 空间换时间 的思想很重要,很多算法都用到了这个思想,比如 散列、动态规划…

最后

以上如果有任何错误,欢迎大家指出哈。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值