C++算法 - 分块思想初步

例题

题目描述

给出一个长为n的序列,以及n个操作,操作涉及区间加法,单点查值。

输入格式

第一行输入一个数字n

第二行输入n个数字,第i个数字为a_i,以空格隔开。

接下来输入n次询问,每行输入四个数字optlrc,以空格隔开。

opt=0,表示将位于[l,r]的区间的数字都加c

opt=1,表示询问a_r的值(lc忽略)。

输出格式

对于每次询问,输出一行一个数字表示答案。

样例

样例输入

4

1 2 2 3

0 1 3 1

1 0 1 0

0 1 2 2

1 0 2 0

 样例输出

2

5

数据范围与提示

对于100%的数据,1\leqslant n\leqslant 50000,-2^{31}\leqslant others,ans\leqslant 2^{31}-1

题目分析

暴力

此方法较为直接,代码较短,但是由于其时间复杂度过高,难免会TLE,在此不多做讲解,直接贴代码:

#include <bits/stdc++.h>
using namespace std;
int a[50005];

int main() {
	int n, opt, l, r, c;
	scanf("%d", &n);
	for (int i=1; i<=n; i++) {
		scanf("%d", &a[i]);
	}
	for (int i=1; i<=n; i++) {
		scanf("%d%d%d%d", &opt, &l, &r, &c);
		if (opt == 0) {
			for (int j=l; j<=r; j++) {
				a[j] += c;
			}
		}
		else /*if (opt == 1)*/ {
			printf("%d\n", a[r]);
		}
	}
	return 0;
}

进入正题——分块(AC)

首先我们来了解一下分块思想:

分块的基本思想是,通过对原数据的适当划分,并在划分后的每一个块上预处理部分信息,从而较一般的暴力算法取得更优的时间复杂度。

分块的时间复杂度主要取决于分块的块长,一般可以通过均值不等式求出某个问题下的最优块长,以及相应的时间复杂度。

分块是一种很灵活的思想,相较于树状数组和线段树,分块的优点是通用性更好,可以维护很多树状数组和线段树无法维护的信息。

当然,分块的缺点是渐进意义的复杂度,相较于线段树和树状数组不够好。

不过在大多数问题上,分块仍然是解决这些问题的一个不错选择。

——OI Wiki

那么我们该如何实现呢?

对于暴力来说,区间修改和查询所需要的时间复杂度都是O(N),我们可以通过前缀和的方法将查询的时间复杂度降低到O(1),也可以用差分将区间修改的复杂度降低到O(1)。对于这种既要进行区间修改,也要进行查询的问题,如果要直接修改数组中的数值,O(N)的时间复杂度是无法避免的,于是我们引入了“懒标记(lazy tag)”。

分块的实现

                 
第1段第2段第3段第4段

第5段

如上图,我们先预处理出每一个“块”的区间和,对于每一个“块”,我们对它添加一个标记(初始值为0),如果整个块的数值都被加上一个数c,那么我们就将这个数加到标记上,在需要查询的时候再将标记的数值与数组中的数值相加,这样就大大减少了区间修改的时间复杂度。同样地,对于查询区间和,我们也可以用整块的区间和进行运算,同样减少了运算的时间复杂度。一举两得!(大喜)

我们将原数组每\left \lfloor \sqrt{n} \right \rfloor个元素作为一个块(n指原数组的长度),本人是使用结构体进行分块,当然也可以使用多个数组,代码如下:

 

struct node {
	int l, r; 
	ll sum, lt;
} block[M];

区间修改和查询的思路也比较简单,可以结合代码中的注释进行理解,基本思路已经明确了,完整代码如下:

#include <bits/stdc++.h>
using namespace std;

typedef long long ll;
const int M = 50005;  // maxium num of n
int n, len, opt, l, r, c, tail; 
/*
 * n: length of array 'a'; 
 * len: length of a block ([sqrt n])
 * opt, l, r, c: input variable from the problem
 */ 
int a[M], id[M];
/*
 * a: main array
 * id: the location(block) of each item in array 'a'
 */

struct node {
	int l, r; 
	ll sum, lt;
	/*
	 * sum: sum of items from [l, r] in array 'a'
	 * lt: lazy tag (+x)
	 */
} block[M];

void change(int ll, int rr, int cc) {
	int L = id[ll], R = id[rr];
	if (L == R) { // change the items directly if [ll, rr] are in a same block
		for(int i=ll; i<=rr; i++) {
			a[i] += cc;
			block[L].sum += cc;
		}
	}
	else {
		for (int i=L+1; i<=R-1; i++) { // add lazy tags of whole blocks first
			block[i].lt += cc;
		}
		for (int i=ll; i<=block[L].r; i++) { // change the items directly in parts of a block
			a[i] += cc;
			block[L].sum += cc;
		}
		for (int i=block[R].l; i<=rr; i++) {
			a[i] += cc;
			block[R].sum += cc;
		}
	}
}

ll ask(int rr) {
	return (ll)a[rr] + block[id[rr]].lt;
}

int main() {
	//freopen("sample.in", "r", stdin);
	scanf("%d", &n);
	len = (int)sqrt(n);
	for (int i=1; i<=n; i++) {
		scanf("%d", &a[i]);
		id[i] = (i-1)/len+1;
		block[id[i]].sum += a[i]; // preprocess the sum of each block
		if ((i-1)%len == 0) {
			block[tail].r = i-1;
			block[++tail].l = i;
		}
	}
	for (int i=1; i<=n; i++) {
		scanf("%d%d%d%d", &opt, &l, &r, &c);
		if (opt == 0) {
			change(l, r, c);
		}
		else /*(opt == 1)*/ {
			printf("%lld\n", ask(r));
		}
	}
	return 0;
}

怎么样,是不是很简单?

再啰嗦一句,分块的区间查询可以利用完整的块的区间和与区间两边多余的元素数值进行相加,原理与上面代码中的change函数类似,代码如下:

long long ask(int ll, int rr) {
	long long ans = 0L;
	int L = id[ll], R = id[rr];
	if (L == R) {
		for(int i=ll; i<=rr; i++) {
			ans += (long long)a[i];
		}
	}
	else {
		for (int i=L+1; i<=R-1; i++) {
			ans += (long long)block[i].sum;
		}
		for (int i=ll; i<=block[L].r; i++) {
			ans += (long long)a[i];
		}
		for (int i=block[R].l; i<=rr; i++) {
			ans += (long long)a[i];
		}
	}
	return ans;
}

感谢完整地读完我这篇枯燥的博客,还请点个赞再走叭~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值