分块算法入门

分块

1. 简介

对于区间信息的整理,我们往往会用树状数组或者线段树,但在维护一些较为复杂的信息时,例如不满足区间可加、可减时就显得非常的吃力,代码实现起来也不那么简单、直观。

分块算法的基本思想是,通过适当的划分,预处理一些信息保存下来,用空间换取时间,从而达到时空平衡。

分块的思想是把数据分成若干块,为了时间复杂度的均摊,块的大小通常设置为 sqrt(n) ,以块为单位维护区间的值。

对于单点更新,时间复杂度 O(1)

区间更新,若区间不跨越块,直接暴力处理,时间复杂度 O(sqrt(n)) ,若跨越块,则不能凑成一个整块的暴力处理,整块的直接更新,时间复杂度 O(sqrt(n))

区间查询,与区间更新一样,为O(sqrt(n))

2. 算法流程

2.1 建立分块 build

首先定义块操作的基本要素:

  1. 块的大小:用block表示。

  2. 块的数量:用num表示。

  3. 块的左右边界:用数组st[],en[],表示,st[i],en[i]就是第i块的起点与终点。

    st[i] = (i − 1) ∗ block + 1;
    en[i] = i ∗ block
    
  4. 每个元素所属的块:belong[i]表示第i个元素属于哪个块。

void build(){
    block = sqrt(n); //块的大小:每块有block个元素。
    num = n / block; //块的数量:共分为t块
    if (n % block)num++; // sqrt(n)的结果不是整数,最后加一小块
    for (int i = 1; i <= num; i++){ //遍历块
        st[i] = (i - 1) * block + 1;
        ed[i] = i * block;
    }
    ed[num] = n;				 // sqrt(n)的结果不是整数,最后一块较小
    for (int i = 1; i <= n; i++) //遍历所有元素的位置
        belong[i] = (i - 1) / block + 1; 
}

分块可能会有剩余的“小块”,对小块的处理是算法的关键。

2.2 区间修改 change

首先建立一个sum数组来辅助区间求和。O(n)

for (int i = 1; i <= num; i++)			 //遍历所有的块
	for (int j = st[i]; j <= ed[i]; j++) //遍历块i内的所有元素
		sum[i] += a[j];

以[L,R]每个数加上一个d为例。

  1. [L,R]在某一块中,即一个块的“碎片”,将a[L],a[L+1] …… a[R]全部加上一个d。此时sum[]已经失效需要维护,sum[i] = sum[i] + d * (R - L + 1)
  2. [L,R]跨越了多个块,对于完全被包含的块更新add[i] += d,对于两端未完全包含的块同上处理。
void change(int L, int R, int d){
	int p = belong[L], q = belong[R];
	if (p == q)	{ //情况1,计算次数是n / num
		for (int i = L; i <= R; i++) a[i] += d;
		sum[p] += d * (R - L + 1);
	}
	else{ //情况2
		for (int i = p + 1; i <= q - 1; i++)
			add[i] += d; //整块,有m=(q-1)-(p+1)+1个。计算m次
		for (int i = L; i <= ed[p]; i++)
			a[i] += d; //整块前面的碎片。计算n/num次
		sum[p] += d * (ed[p] - L + 1);
		for (int i = st[q]; i <= R; i++)
			a[i] += d; //整块后面的碎片。计算n/num次
		sum[q] += d * (R - st[q] + 1);
	}
}

2.3 区间查询 ask

以求[L,R]的和为例。

  1. [L, R]在某个i块之内。暴力加每个数,最后加上add[i],答案是ans = a[L] + a[L+1] + … + a[R] + (R - L + 1)*add[i]。计算次数约为n / num
  2. [L, R]跨越了多个块。在被[L, R]完全包含的那些块内(设有k个块),ans += sum[i] + add[i]*len[i],其中len[i]是第i段的长度,等于n / num。对于不能完全包含的那些碎片,按情况1处理,然后与ans累加。
long long ask(int L, int R){
	int p = belong[L], q = belong[R];
	long long ans = 0;
	if (p == q){ //情况1
		for (int i = L; i <= R; i++) ans += a[i];
		ans += add[p] * (R - L + 1);
	}
	else{ //情况2
		for (int i = p + 1; i <= q - 1; i++)
			ans += sum[i] + add[i] * (ed[i] - st[i] + 1); //整块
		for (int i = L; i <= ed[p]; i++)
			ans += a[i]; //整块前面的碎片
		ans += add[p] * (ed[p] - L + 1);
		for (int i = st[q]; i <= R; i++)
			ans += a[i]; //整块后面的碎片
		ans += add[q] * (R - st[q] + 1);
	}
	return ans;
}

3. 例题讲解

【模板】线段树 1

题目描述
如题,已知一个数列,你需要进行下面两种操作:

将某区间每一个数加上 k。
求出某区间每一个数的和。

输入格式
第一行包含两个整数 n, m,分别表示该数列数字的个数和操作的总个数。
第二行包含 n 个用空格分隔的整数,其中第 i 个数字表示数列第 i 项的初始值。
接下来 m 行每行包含 3 或 4 个整数,表示一个操作,具体如下:
1 x y k:将区间 [x, y] 内每个数加上 k 。
2 x y:输出区间 [x, y] 内每个数的和。

【参考代码】

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll maxn = 1e5 + 7;
int n, m;
int a[maxn], block, num, sum[maxn] = {0}, add[maxn] = {0};
int st[maxn], ed[maxn], belong[maxn];
void build()
{
	block = sqrt(n);
	num = n / block;
	if (n % block) num++;
	for (int i = 1; i <= num; i++){
		st[i] = (i - 1) * block + 1;
		ed[i] = i * block;
	}
	ed[num] = n;
	for (int i = 1; i <= n; i++)
		belong[i] = (i - 1) / block + 1;
}
void change(int l, int r, int d)
{
	int p = belong[l], q = belong[r];
	if (p == q){
		for (int i = l; i <= r; i++) a[i] += d;
		sum[p] += d * (r - l + 1);
	}
	else{
		for (int i = p + 1; i <= q - 1; i++)
			add[i] += d;
		for (int i = l; i <= ed[p]; i++)
			a[i] += d;
		sum[p] += d * (ed[p] - l + 1);
		for (int i = st[q]; i <= r; i++)
			a[i] += d;
		sum[q] += d * (r - st[q] + 1);
	}
}
ll ask(int l, int r)
{
	int p = belong[l], q = belong[r];
	ll ans = 0;
	if (p == q){
		for (int i = l; i <= r; i++) ans += a[i];
		ans += add[p] * (r - l  + 1);
	}
	else{
		for (int i = p + 1; i <= q - 1; i++)
			ans += sum[i] + add[i] * (ed[i] - st[i] + 1);
		for (int i = l; i <= ed[p]; i++)
			ans += a[i];
		ans += add[p] * (ed[p] - l + 1);
		for (int i = st[q]; i <= r; i++)
			ans += a[i];
		ans += add[q] * (r - st[q] + 1);
	}
	return ans;
}
signed main()
{
	scanf("%d%d", &n, &m);
	build();
	for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
	for (int i = 1; i <= num; i++)
		for (int j = st[i]; j <= ed[i]; j++)
			sum[i] += a[j];
	while (m--)
	{
		int op, x, y, k;
		scanf("%d%d%d", &op, &x, &y);
		if (op == 1) {
			scanf("%d", &k);
			change(x, y, k);
		}
		else printf("%lld\n", ask(x, y));
	}
	return 0;
}
//__asm__(R"(xxxxxxxxxxxx)");
题目序号题目出处题目难度
1A Simple Problem with Integers
2蒲公英⭐⭐⭐
3磁力⭐⭐⭐
4小Z的袜子(hose)⭐⭐
5开关⭐⭐⭐
6守墓人⭐⭐
7Balanced Lineup G⭐⭐⭐
8XOR的艺术⭐⭐⭐
9质量检测⭐⭐⭐
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

TUStarry

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值