分块
分块是指把序列分成若干个块,使得对序列的各种操作的时间复杂度达到均衡,都不至于太高。一般情况下,都是把序列分成
n
\sqrt{n}
n块,每块的大小大致为
n
\sqrt{n}
n。n表示序列中元素的个数。
分块是一种思想,理解起来很简单,用代码实现也不难,在很多数据结构的题目中都能用上。
例1:
给一个长度为
n
n
n的数组,现在有两种操作:
操作1:区间修改
(
0
,
l
,
r
,
c
)
(0,l,r,c)
(0,l,r,c) 表示将区间
[
l
,
r
]
[l,r]
[l,r]整体加上一个数值
c
c
c。
操作2:单点查询$(1,l,r,c)
表
示
查
询
元
素
表示查询元素
表示查询元素r
的
值
。
此
时
忽
略
的值。此时忽略
的值。此时忽略l
和
和
和c$。
数据规模:
n
≤
500000.
n \leq 500000.
n≤500000.
分析:
区间修改和单点查询。用树状数组和线段树肯定可以做。
现在我们考虑用分块来做。
我们首先将数组分成若干块,每块的长度为
n
\sqrt{n}
n,最后的一块可能小一点,这没有关系。总的块数大概是根号级别的。
对于区间修改,比如
[
l
,
r
,
c
]
[l,r,c]
[l,r,c],我们可以计算区间
[
l
,
r
]
[l,r]
[l,r]包含了哪些块。一般情况下,
[
l
,
r
]
[l,r]
[l,r]这个区间,可能在左右两端包含了不完整的块,中间包含了若干个整块。对于部分块,我们可以暴力地去修改,即每个元素实时地去修改;对于整体块,我们只需要记录一个增量,表示这个块整体增加的数值。
对于单点查询,直接查询好了。注意要加上该区间的增量。
时间复杂度分析
那这个分块算法的时间复杂度为多少呢?
先看修改操作:对于每次修改操作,部分块最多两个,块长最多为
n
\sqrt{n}
n,这是暴力修改的,需要的时间复杂度为
n
\sqrt{n}
n , 对于中间的整体块,我们只需要记录增量,每个整体块花费的时间为
O
(
1
)
O(1)
O(1),最多有
n
\sqrt{n}
n个整体块。
所以,一次修改的时间复杂度为
O
(
n
)
O(\sqrt{n})
O(n).
再看查询操作:对于每次查询操作,部分块最多两个,块长最多为sqrt(n) ;中间的整体块,每个整体块只需要花费
O
(
1
)
O(1)
O(1)的时间,最多有
n
\sqrt{n}
n个整体块。所以,一次查询的时间复杂度为
O
(
n
)
O(\sqrt{n})
O(n)
这样,不管是修改还是查询,其时间复杂度均为
O
(
n
)
O(\sqrt{n})
O(n).
所以,总的时间复杂度为
O
(
n
)
O(\sqrt{n})
O(n).
在实现过程中,对于每一次操作,我们可以大体将操作区间分成三个部分,左端的不完整块,中间若干个完整块,右端的不完整块。但有时,这三个部分也不一定都存在。编程时稍加注意就可以了。对于部分块,我们使用暴力;中间的完整块,进行整体操作;当到了结尾的部分块,又转为暴力操作。
一个块的起点和终点,是可以通过下标计算出来的。
假设数组下标从
1
1
1开始,块的编号从
0
0
0开始计算。
块大小为
b
l
o
c
k
=
c
e
i
l
(
s
q
r
t
(
n
)
)
block=ceil(sqrt(n))
block=ceil(sqrt(n)).
则元素i所在的块的起点为:
(
i
−
1
)
/
b
l
o
c
k
∗
b
l
o
c
k
+
1
(i-1)/block*block+1
(i−1)/block∗block+1
元素i所在的块的终点为:
(
i
−
1
)
/
b
l
o
c
k
∗
b
l
o
c
k
+
b
l
o
c
k
(i-1)/block*block+block
(i−1)/block∗block+block
元素i所在的块的编号为:$(i-1)/block $
判断元素i是否为一个块的起点:
i
f
(
i
%
b
l
o
c
k
=
=
1
)
if(i\%block==1)
if(i%block==1)
也可以在将每个元素所属的块保存在数组中,以及每个块的起点和终点都提前保存在数组中。
具体实现时灵活处理即可。
#include <bits/stdc++.h>
using namespace std;
#define MAXN 50005
int arr[MAXN], delta[MAXN];
int n, op, l, r, c, ans;
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%d", &arr[i]);
int block = ceil(sqrt(n));
for (int i = 1; i <= n; i++) {
scanf("%d%d%d%d", &op, &l, &r, &c);
if (op == 0) {
if (l % block != 1) //左端有一个部分块,暴力加
{
int rt = (l - 1) / block * block + block; //部分块的右端点
while (l <= r && l <= rt) {
arr[l] += c;
l++;
}
}
while (l / block <= r / block && r - l + 1 >= block) {
delta[(l - 1) / block] += c; //整体块加上c
l = l + block;
}
while (l <= r) //右边的部分块,暴力加
{
arr[l] += c;
l++;
}
} else {
printf("%d\n", arr[r] + delta[(r - 1) / block]);
}
}
return 0;
}
例2.
给一个长度为
n
n
n的数组,有
n
n
n个操作,操作分为两种,一种是区间修改,即将一个区间
[
l
,
r
]
[l,r]
[l,r]整体加上一个整数
c
c
c;一种是区间求和,即查询区间
[
l
,
r
]
[l,r]
[l,r]的和。
$n \leq 50000 $
分析:
区间修改和上道题是一样的。
区间查询也很简单,分块以后,对两端的非完整块,暴力求和,中间的完整块依次统计。时间复杂度为
O
(
N
N
)
O(N\sqrt{N})
O(NN).
查询时,只要不要漏掉了区间的增量。
#include <bits/stdc++.h>
using namespace std;
#define MAXN 100005
#define LL long long int
int arr[MAXN];
LL sum[MAXN], delta[MAXN];
int n, block, blockcnt, blockid;
int opt, l, r, c;
LL ans;
int main() {
scanf("%d", &n);
block = ceil(sqrt(n));
for (int i = 1; i <= n; i++) {
scanf("%d", &arr[i]);
sum[(i - 1) / block] += arr[i];
}
for (int i = 1; i <= n; i++) {
scanf("%d%d%d%d", &opt, &l, &r, &c);
if (opt == 0) {
if (l % block != 1) {
blockid = (l - 1) / block;
int tmpr = min(r, blockid * block + block);
while (l <= tmpr) {
sum[blockid] += c;
arr[l] += c;
l++;
}
}
while (l / block <= r / block && r - l + 1 >= block) {
blockid = (l - 1) / block;
delta[blockid] += c;
l += block;
}
blockid = (l - 1) / block;
while (l <= r) {
sum[blockid] += c;
arr[l] += c;
l++;
}
} else {
ans = 0;
if (l % block != 1) {
blockid = (l - 1) / block;
int tmpr = min(r, blockid * block + block);
while (l <= tmpr) {
ans += delta[blockid];
ans = ans % (c + 1);
ans += arr[l];
l++;
}
}
while (l / block <= r / block && r - l + 1 >= block) {
blockid = (l - 1) / block;
ans += block * delta[blockid];
ans = ans % (c + 1);
ans += sum[blockid];
ans = ans % (c + 1);
l = l + block;
}
blockid = (l - 1) / block;
while (l <= r) {
ans += arr[l];
ans += delta[blockid];
ans = ans % (c + 1);
l++;
}
printf("%lld\n", ans % (c + 1));
}
}
return 0;
}
例3:
给出一个长为 n n n的数列,以及 n n n个操作,操作涉及区间加法,询问区间内小于某个值 x x x的元素个数。
输入格式
第一行输入一个数字
n
n
n。
第二行输入
n
n
n个数字,第i个数字为
a
i
a_i
ai,以空格隔开。
接下来输入
n
n
n行询问,每行输入四个数字
o
p
,
l
,
r
,
c
op,l,r,c
op,l,r,c,以空格隔开。
若
o
p
=
=
0
op==0
op==0,表示将位于
[
l
,
r
]
[l,r]
[l,r]的之间的数字都加上
c
c
c。
若
o
p
=
=
1
op==1
op==1,表示询问
[
l
,
r
]
[l,r]
[l,r]中,小于
c
∗
c
c*c
c∗c的数字的个数。
输出格式
对于每次询问,输出一行一个数字表示答案。
分析
仍然考虑用分块来做。
现在的查询要复杂一些了,查询的是区间中小于x的元素个数。很容易想到,可以将块中元素排序。
这样整体块中可以用二分查找。
那修改操作呢?如果整体块整体增加一个值,这个不影响排序,只需要记录增量即可。而如果是部分块,则暴力修改,并重新排序即可。
这样一个整体块的查询操作为
O
(
l
o
g
(
b
l
o
c
k
)
)
O(log(block))
O(log(block)),修改操作为
O
(
1
)
O(1)
O(1).部分块的查询操作为
O
(
b
l
o
c
k
)
O(block)
O(block),修改操作为
O
(
b
l
o
c
k
+
l
o
g
(
b
l
o
c
k
)
)
O(block+log(block))
O(block+log(block)).
参考代码:
#include <bits/stdc++.h>
using namespace std;
#define MAXN 100005
#define LL long long int
int arr[MAXN], delta[MAXN];
int sortarr[MAXN];
int n, op, l, r, c, block, blockcnt, ans;
void partmodify(int l, int r) {
int blockst = (l - 1) / block * block + 1,
blocked = min(n, (blockst + block - 1)), blockid = (l - 1) / block;
for (int i = blockst; i <= blocked; i++) {
arr[i] += delta[blockid];
if (i >= l && i <= r) arr[i] += c;
sortarr[i] = arr[i];
}
delta[blockid] = 0;
sort(sortarr + blockst, sortarr + blocked + 1);
}
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++) {
scanf("%d", &arr[i]);
sortarr[i] = arr[i];
}
block = ceil(0.5 * sqrt(n));
blockcnt = ceil(n / block);
for (int i = 0; i < blockcnt - 1; i++)
sort(sortarr + i * block + 1, sortarr + (i + 1) * block + 1);
sort(sortarr + (blockcnt - 1) * block + 1, sortarr + n + 1);
for (int i = 1; i <= n; i++) {
scanf("%d%d%d%d", &op, &l, &r, &c);
if (op == 0) {
if (l % block != 1) {
int tmpr = min(r, (l - 1) / block * block + block);
partmodify(l, tmpr);
l = tmpr + 1;
}
while (l / block <= r / block && r - l + 1 >= block) {
delta[(l - 1) / block] += c;
l = l + block;
}
if (l <= r) {
partmodify(l, r);
}
} else {
ans = 0;
int blockid = 0;
if (l % block != 1) {
int tmpr = min(r, (l - 1) / block * block + block);
blockid = (l - 1) / block;
while (l <= tmpr) {
if (arr[l] + delta[blockid] < 1ll * c * c) ans++;
l++;
}
}
while (l / block <= r / block && r - l + 1 >= block) {
blockid = (l - 1) / block;
ans += lower_bound(sortarr + l, sortarr + l + block,
1ll * c * c - delta[blockid]) -
(sortarr + l);
l = l + block;
}
blockid = (l - 1) / block;
while (l <= r) {
if (arr[l] + delta[blockid] < 1ll * c * c) ans++;
l++;
}
printf("%d\n", ans);
}
}
return 0;
}
例4:
题目描述
给出一个长为 n n n的数列,以及 n n n个操作,操作涉及区间加法,询问区间内小于某个值 x x x 的前驱(比其小的最大元素)。
输入格式
第一行输入一个数字n 。
第二行输入 n n n个数字,第 i i i个数字为 a i a_i ai,以空格隔开。
接下来输入 n n n行询问,每行输入四个数字 o p t 、 l 、 r 、 c opt、l、r、c opt、l、r、c,以空格隔开。
若 o p t = = 0 opt==0 opt==0,表示将位于 [ l , r ] [l,r] [l,r]的之间的数字都加 c c c。
若 o p t = = 1 opt==1 opt==1,表示询问 [ l , r ] [l,r] [l,r]中, c c c的前驱。
输出格式
对于每次询问,输出一行一个数字表示答案。
分析
我们将原数组复制一份,然后进行分块。原数组和新数组采用同样的分块方式。新数组中需要对每个块中的元素进行排序。
原数组主要处理区间修改操作,新数组主要处理查询操作。
具体来讲,区间修改时,在原数组中,对于不完整的块(最多两个),对其中每个元素进行实时修改,并将这两个块同步到新数组中——块复制并重新排序;对整体块,直接记录整理增量即可。排序没有变化,所以不需要更新到新数组中。
一次修改的时间复杂度为
O
(
n
+
n
∗
l
o
g
n
)
O(\sqrt{n}+\sqrt{n}*log\sqrt{n})
O(n+n∗logn)
区间查询时,对非完整块,逐个元素比较;在整体块中,采用二分查找。一次查询时间复杂度为
O
(
n
+
n
∗
l
o
g
n
)
O(\sqrt{n}+\sqrt{n}*log\sqrt{n})
O(n+n∗logn)
#include<bits/stdc++.h>
#include<algorithm>
using namespace std;
#define MAXN 100005
#define LL long long int
int arr[MAXN],delta[MAXN];
int sortarr[MAXN];
int n,op,l,r,c,block,blockcnt,ans;
void partmodify(int l,int r)
{
int blockst=(l-1)/block*block+1,blocked=min(n,(blockst+block-1)),blockid=(l-1)/block;
for(int i=blockst;i<=blocked;i++)
{
arr[i]+=delta[blockid];
if(i>=l&&i<=r)arr[i]+=c;
sortarr[i]=arr[i];
}
delta[blockid]=0;
sort(sortarr+blockst,sortarr+blocked+1);
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%d",&arr[i]);
sortarr[i]=arr[i];
}
block=ceil(sqrt(n));
blockcnt=ceil(n/block);
for(int i=0;i<blockcnt-1;i++)
sort(sortarr+i*block+1,sortarr+(i+1)*block+1);
sort(sortarr+(blockcnt-1)*block+1,sortarr+n+1);
for(int i=1;i<=n;i++)
{
scanf("%d%d%d%d",&op,&l,&r,&c);
if(op==0)
{
if(l%block!=1)
{
int tmpr=min(r,(l-1)/block*block+block);
partmodify(l,tmpr);
l=tmpr+1;
}
while(l/block<=r/block && r-l+1>=block)
{
delta[(l-1)/block]+=c;
l=l+block;
}
if(l<=r)
{
partmodify(l,r);
}
}
else
{
ans=1<<31;
int blockid=0;
if(l%block!=1)
{
int tmpr=min(r,(l-1)/block*block+block);
blockid=(l-1)/block;
while(l<=tmpr)
{
if(arr[l]+delta[blockid]<c)
{
if(arr[l]+delta[blockid]>ans)
ans=arr[l]+delta[blockid];
}
l++;
}
}
while(l/block<=r/block&&r-l+1>=block)
{
blockid=(l-1)/block;
int pos=lower_bound(sortarr+l,sortarr+l+block,c-delta[blockid])-(sortarr+l);
if(pos!=0)
{
if(sortarr[l+pos-1]+delta[blockid]>ans)
ans=sortarr[l+pos-1]+delta[blockid];
}
l=l+block;
}
blockid=(l-1)/block;
while(l<=r)
{
if(arr[l]+delta[blockid]<c)
{
ans=max(ans,arr[l]+delta[blockid]);
}
l++;
}
if(ans==(1<<31))
printf("-1\n");
else printf("%d\n",ans);
}
}
return 0;
}
例5:
给出一个长为n的数列 ,以及n个操作,操作涉及区间开方,区间求和。
输入格式
第一行输入一个数字n。
第二行输入n个数字,第i个数字为ai,以空格隔开。
接下来输入n行询问,每行输入四个数字 op,l,r,c,以空格隔开。
若 op==0,表示将位于[l,r]之间的数字都开方。对于区间中每个数ai,ai变成sqrt(ai)
若 op==1,表示询问位于[l,r]的所有数字的和。
输出格式
对于每次询问,输出一行一个数字表示答案。
分析:
一个正整数
n
n
n,经过若干次开方操作,就会变成1.这个过程是很快的。
1
0
8
10^8
108经过
5
5
5次开方操作就可以变成1.
我们对数组进行分块,对每个块记录它是否全为1.如果全为1,则开方操作可以省略了。否则,对块中每个元素进行开方。
看似非常暴力,但其实非常优秀,我们分析一下修改的时间复杂度。
每次修改最多包含两个不完整区间,假设每次都是不全为1,需要全部修改,花费
n
\sqrt{n}
n次开方;对于完整区间,每个区间最多经过
5
5
5次左右开方就变成全
1
1
1了,累计下来最多是
5
∗
n
5*n
5∗n次开方操作。所以,修改的总时间复杂度为
O
(
n
∗
n
+
5
∗
n
)
=
O
(
n
∗
n
)
O(n*\sqrt{n}+5*n)=O(n*\sqrt{n})
O(n∗n+5∗n)=O(n∗n)
查询操作:遇到全1的,直接计算;遇到不全为1的,逐个元素累加。那如果连续很多次查询操作,可能会超时。怎么办?预处理处每个块的和,在修改时更新每个分块的和。这样,查询时遇到完整块,就可以对完整块整体操作了。
#include <bits/stdc++.h>
using namespace std;
#define MAXN 100005
#define LL long long int
int arr[MAXN], block, blockid, n, opt, l, r, c;
LL ans, sum[400];
int cntone[400], bsz[400];
int main() {
scanf("%d", &n);
assert(n > 0);
block = ceil(sqrt(n));
for (int i = 1; i <= n; i++) {
blockid = (i - 1) / block;
scanf("%d", &arr[i]);
cntone[blockid] += (arr[i] == 1);
}
for (int i = 0; i < (n - 1) / block; i++) {
bsz[i] = block;
}
bsz[(n - 1) / block] = (n - 1) % block + 1;
for (int i = 1; i <= n; i++) {
scanf("%d%d%d%d", &opt, &l, &r, &c);
if (opt == 0)
while (l <= r) {
blockid = (l - 1) / block;
if (cntone[blockid] == bsz[blockid]) {
l = (blockid + 1) * block + 1;
} else {
if (arr[l] > 1) {
arr[l] = int(sqrt(arr[l]));
if (arr[l] == 1) cntone[(l - 1) / block]++;
}
l++;
}
}
else {
ans = 0;
while (l <= r) {
blockid = (l - 1) / block;
if (cntone[blockid] == bsz[blockid]) {
int tmpr = min(r, blockid * block + block);
ans += (tmpr - l + 1);
l = tmpr + 1;
} else {
ans += arr[l];
l++;
}
}
printf("%lld\n", ans);
}
}
return 0;
}
例6.
题目描述
给出一个长为n的数列,以及n个操作,操作涉及单点插入,单点询问,数据随机生成。
输入格式
第一行输入一个数字n。
第二行输入 n 个数字,第i个数字为ai,以空格隔开。
接下来输入n行询问,每行输入四个数字 opt、l、r、c,以空格隔开。
若opt==0,表示在第l个数字前插入数字 r( c忽略)。
若opt==1 ,表示询问ar的值(l和c忽略)。
分析:
分块以后,每个块用一个
v
e
c
t
o
r
vector
vector保存,插入时直接在
v
e
c
t
o
r
vector
vector中操作,同时修改块大小。
每次定位时,对块的大小求前缀和,先定位到块,然后在块中继续定位即可。
本题数据是随机生成的,插入的元素会平均分布在每个块中,这样做没有问题。
如果数据不是随机生成的,这样做就可能超时。只要构造这样的数据,使得插入都发生在一个块中,时间复杂度就变成了
O
(
n
2
)
O(n^2)
O(n2)了。
那如何解决呢?
我们可以设一个阈值,当块大小变为标准块的两倍时,就把这个块裂开,变成两个块。
vector不好用了,我们可以使用链表操作。
#include <bits/stdc++.h>
using namespace std;
#define MAXN 210005
struct node {
int val;
node *next;
} arr[MAXN];
struct dnode {
int cnt;
node *head;
dnode *nxtlist;
} lst[600];
int block, ans, n, opt, l, r, c;
int main() {
int a;
scanf("%d", &n);
block = ceil(sqrt(2 * n));
int lstid = 0, nodeid = 0;
node *last = NULL;
for (int i = 1, j = 1; i <= n; i++, j++) {
scanf("%d", &a);
arr[++nodeid].val = a;
if (last == NULL) {
lst[++lstid].head = &arr[nodeid];
last = &arr[nodeid];
} else {
last->next = (&arr[nodeid]);
last = &arr[nodeid];
}
if (j == block) {
lst[lstid].nxtlist = &lst[lstid + 1];
lst[lstid].cnt = block;
last = NULL;
j = 0;
}
}
if (n % block != 0) lst[lstid].nxtlist = 0, lst[lstid].cnt = (n % block);
int tot = n;
for (int i = 1; i <= n; i++) {
scanf("%d%d%d%d", &opt, &l, &r, &c);
if (opt == 0) {
dnode *p = &lst[1];
int sum = 0;
while (p->nxtlist != NULL && sum + (p->cnt) < l) {
sum += p->cnt;
p = p->nxtlist;
}
node *q = p->head;
sum++;
if (sum == l) {
arr[++nodeid].val = r;
arr[nodeid].next = q;
p->head = &arr[nodeid];
p->cnt++;
} else {
node *last = q;
while (q && sum < l) {
last = q;
q = q->next;
sum++;
}
arr[++nodeid].val = r;
arr[nodeid].next = q;
last->next = &arr[nodeid];
p->cnt++;
}
if (p->cnt > 2 * block) {
q = p->head;
int pos = 0;
while (pos < block) {
pos++;
q = q->next;
}
lst[++lstid].head = q->next;
lst[lstid].cnt = p->cnt - pos - 1;
q->next = 0;
lst[lstid].nxtlist = p->nxtlist;
p->nxtlist = &lst[lstid];
p->cnt = pos + 1;
}
}
else {
int sum = 0;
dnode *p = &lst[1];
while (p->nxtlist && sum + p->cnt < r) {
sum = sum + p->cnt;
p = p->nxtlist;
}
node *q = p->head;
sum++;
while (q->next && sum < r) {
q = q->next;
sum++;
}
printf("%d\n", q->val);
}
}
return 0;
}
例7.
题目描述
给出一个长为 n n n的数列,以及 n n n个操作,操作涉及区间乘法,区间加法,单点询问。
输入格式
第一行输入一个数字n。
第二行输入n个数字,第 i 个数字为ai,以空格隔开。
接下来输入n行询问,每行输入四个数字 o p t 、 l 、 r 、 c opt、l、r、c opt、l、r、c,以空格隔开。
若 o p t = = 0 opt==0 opt==0,表示将位于 [ l , r ] [l,r] [l,r]的之间的数字都加 c c c 。
若$opt==1 , 表 示 将 位 于 ,表示将位于 ,表示将位于[l,r] 的 之 间 的 数 字 都 乘 的之间的数字都乘 的之间的数字都乘c$。
若$opt==2 , 表 示 询 问 ,表示询问 ,表示询问a_r 的 值 模 10007 ( 的值模10007( 的值模10007(l 和 和 和c$忽略)
输出格式
对于每次询问,输出一行一个数字表示答案。
分析:
对数组进行分开,对每个块,用delta记录它整体加上的值,用multi记录它整体乘上的值。当delta和multi都有值时,表示先乘multi后再加上delta。
遇到完整块,
对于加法操作,则
d
e
l
t
a
=
d
e
l
t
a
+
c
delta=delta+c
delta=delta+c。
对于乘法操作,则$multi=multic, delta=deltac $
遇到不完整块:
对于加法操作:先将
m
u
l
t
i
multi
multi乘到块中每个元素上,并把
m
u
l
t
i
multi
multi置为
1
1
1,然后再对操作区间内的数进行加
c
c
c的操作;
对于乘法操作:可以先将
m
u
l
t
i
multi
multi和
d
e
l
t
a
delta
delta更新到每个元素上,再将区间中的元素乘
c
c
c。
#include <bits/stdc++.h>
using namespace std;
#define MAXN 100005
#define MOD 10007
int arr[MAXN], add[MAXN], mul[MAXN];
int n, op, l, r, c, block;
void modify(int be, int ed, bool flg) // flg==0 add, flg==1 mul
{
int blockst = (be - 1) / block * block + 1,
blocked = min(n, (be - 1) / block * block + block),
blockid = (be - 1) / block;
for (int i = blockst; i <= blocked; i++) {
arr[i] = (arr[i] * mul[blockid] + add[blockid]) % MOD;
if (i >= be && i <= ed) {
if (flg == 0)
arr[i] = (arr[i] + c) % MOD;
else
arr[i] = arr[i] * c % MOD;
}
}
mul[blockid] = 1, add[blockid] = 0;
}
int main() {
scanf("%d", &n);
block = ceil(sqrt(n));
for (int i = 1; i <= n; i++) scanf("%d", &arr[i]);
int blockid = 0;
for (int i = 0; i <= 500; i++) mul[i] = 1, add[i] = 0;
for (int i = 1; i <= n; i++) {
scanf("%d%d%d%d", &op, &l, &r, &c);
c = c % MOD;
if (op <= 1) // add c or multi c;
{
if (l % block != 1) {
int tmpr = min(r, (l - 1) / block * block + block);
modify(l, tmpr, op);
l = tmpr + 1;
}
while (l / block <= r / block && r - l + 1 >= block) {
blockid = (l - 1) / block;
if (op == 0)
add[blockid] = (add[blockid] + c) % MOD;
else {
mul[blockid] = mul[blockid] * c % MOD;
add[blockid] = add[blockid] * c % MOD;
}
l = l + block;
}
if (l <= r) {
modify(l, r, op);
}
} else {
blockid = (r - 1) / block;
printf("%d\n", (arr[r] * mul[blockid] + add[blockid]) % MOD);
}
}
return 0;
}
例8.
给出一个长为n的数列,以及n个操作,操作涉及区间询问等于一个数x的元素个数,并将这个区间的所有元素改为x。
输入格式
第一行输入一个数字n。
第二行输入n个数字,第 i 个数字为ai,以空格隔开。
接下来输入n行询问,每行输入三个数字$ l、r、c$,以空格隔开。
表示先查询位于[l,r]的数字有多少个是c,再把位于[l,r]的数字都改为c。
输出格式
对于每次询问,输出一行一个数字表示答案。
分析:
对数组分块,然后对每个分块,记录其内部是否全部相同,如果全部相同,记录该块的值
b
l
o
c
k
v
a
l
blockval
blockval。
每次操作
[
l
,
r
,
c
]
[l,r,c]
[l,r,c],查找它包含的所有块。
对于非完整块,暴力操作;
对于完整块,如果块中全同,则比较
b
l
o
c
k
v
a
l
blockval
blockval是否等于
c
c
c,可以快速统计和修改;如果块中不全同,则暴力操作。
看似暴力操作比较多,但实际上复杂度并不高。
可以这样分析:每次操作时最多有两个非完整区间,这两个区间需要暴力;其他的完整区间如果是块中不全同才需要暴力,然后会变成全同,这个完整块的暴力操作的费用可以提前计算,即在一个完整块由全同变为不全同时,我们将它后面变为全同的费用提前支付。于是每次操作,最多两个区间需要支付暴力的费用,每个区间支付最多两次暴力费用(由全同变为不全同时才支付两次)。一次暴力的费用是
O
(
n
)
O(\sqrt{n})
O(n).
所以时间复杂度为
O
(
n
n
)
O(n\sqrt{n})
O(nn)
#include <bits/stdc++.h>
using namespace std;
#define MAXN 100005
int arr[MAXN], val[400];
int n, l, r, c, block, ans, blockid;
bool same[400];
int modify(int be, int ed) {
int res = 0;
blockid = (be - 1) / block;
int blockst = blockid * block + 1, blocked = min(n, blockid * block + block);
if (same[blockid] == 1) {
if (val[blockid] == c)
res = (ed - be) + 1;
else {
res = 0;
for (int i = blockst; i <= blocked; i++) {
arr[i] = val[blockid];
if (i >= be && i <= ed) arr[i] = c;
}
same[blockid] = 0;
}
} else {
for (int i = be; i <= ed; i++) {
if (arr[i] == c) res++;
arr[i] = c;
}
}
return res;
}
int main() {
scanf("%d", &n);
block = ceil(sqrt(n));
for (int i = 1; i <= n; i++) scanf("%d", &arr[i]);
for (int i = 1; i <= n; i++) {
scanf("%d%d%d", &l, &r, &c);
ans = 0;
if (l % block != 1) {
int tmpr = min(r, (l - 1) / block * block + block);
ans += modify(l, tmpr);
l = tmpr + 1;
}
while (l / block <= r / block && r - l + 1 >= block) {
blockid = (l - 1) / block;
int tmpr = min(r, blockid * block + block);
if (same[blockid] == 1) {
if (val[blockid] == c) ans += block;
} else {
for (int j = l; j <= tmpr; j++)
if (arr[j] == c) ans++;
}
same[blockid] = 1;
val[blockid] = c;
l += block;
}
if (l <= r) {
ans += modify(l, r);
}
printf("%d\n", ans);
}
return 0;
}
例9.
题目描述
给出一个长为 n n n的数列,以及 n n n个操作,操作涉及询问区间的最小众数。所谓众数,是指数量最多的数。
输入格式
第一行输入一个数字n。
第二行输入
n
n
n个数字,第
i
i
i个数字为
a
i
a_i
ai,以空格隔开。
接下来输入
n
n
n行询问,每行输入两个数字
l
、
r
l、r
l、r,以空格隔开。
表示查询位于
[
l
,
r
]
[l,r]
[l,r]的数字的众数。
输出格式
对于每次询问,输出一行一个数字表示答案。
分析:
这是一个经典难题,来自2013年的国家集训队互测试题。
考虑用分块来做。首先预处理出
c
o
m
m
o
n
n
u
m
[
i
]
[
j
]
commonnum[i][j]
commonnum[i][j],表示分块i到分块j这个区间内的众数。
这样,一次查询[l,r],众数要么是非完整区间内的数,要么完整区间中的
c
o
m
m
o
n
n
u
m
commonnum
commonnum中的。非完整区间的数可以遍历,最多
n
\sqrt{n}
n;完整块中的数可以O(1)得到,然后对这些可能的众数进行检测,以确定真正的众数。
如果求
c
o
m
m
o
n
n
u
m
[
i
]
[
j
]
commonnum[i][j]
commonnum[i][j]呢?如何检测可能的众数是否是真正的众数呢?这都需要先求出每个数在前缀块中出现的次数——
p
r
e
c
n
t
[
v
a
l
]
[
i
]
precnt[val][i]
precnt[val][i],它表示
v
a
l
val
val在前面的
i
i
i个块中出现的次数。有了它,一切就好办了。
#include <bits/stdc++.h>
using namespace std;
#define MAXN 100005
#define MAXC 355
int ans, anscnt;
int sz, n, m, l, r, arr[MAXN], num[MAXN], tmpcnt[MAXN], blockcnt[MAXN][MAXC];
int commonnum[MAXC][MAXC], commoncnt[MAXC][MAXC], precnt[MAXN][MAXC];
void getint(int &t){
t = 0;
char c;
int flg = 1;
while((c = getchar()) > '9' || c < '0')if(c == '-')flg = -flg;
while(c >= '0' && c <= '9')t = t * 10 + c - '0', c = getchar();
t *= flg;
}
int main(){
getint(n);
for(int i = 1; i <= n; i++){
getint(arr[i]);
num[i] = arr[i];
}
sort(num + 1, num + n + 1);
int cnt = unique(num + 1, num + n + 1) - num - 1;
for(int i = 1; i <= n; i++){
arr[i] = lower_bound(num + 1, num + cnt + 1, arr[i]) - num;
}
sz = floor(sqrt(n));
m = ceil(1.0 * n / sz);
int id;
for(int i = 1; i <= n; i++){
id = (i - 1) / sz + 1;
blockcnt[arr[i]][id]++;
if(commoncnt[id][id] < blockcnt[arr[i]][id] || (commoncnt[id][id] == blockcnt[arr[i]][id] && arr[i] < commonnum[id][id])){
commoncnt[id][id] = blockcnt[arr[i]][id];
commonnum[id][id] = arr[i];
}
}
for(int i = 1; i <= cnt; i++){
for(int j = 1; j <= m; j++)
precnt[i][j] = precnt[i][j - 1] + blockcnt[i][j];
}
for(int i = 1; i <= m; i++){
for(int j = i + 1; j <= m; j++){
int l1 = j * sz -sz + 1, r1 = min(n, j * sz);
commoncnt[i][j] = commoncnt[i][j - 1], commonnum[i][j] = commonnum[i][j - 1];
for(int k = l1; k <= r1; k++){
if(precnt[arr[k]][j] - precnt[arr[k]][i - 1] > commoncnt[i][j] || (precnt[arr[k]][j] - precnt[arr[k]][i - 1] == commoncnt[i][j] && arr[k] < commonnum[i][j])){
commoncnt[i][j] = precnt[arr[k]][j] - precnt[arr[k]][i - 1];
commonnum[i][j] = arr[k];
}
}
}
}
for(int i = 1; i <= n; i++){
getint(l), getint(r);
int ll = min(r, (l - 1) / sz * sz + sz );
for(int k = l; k <= ll; k++) tmpcnt[arr[k]]++;
if((r - 1) / sz != (l - 1) / sz){
for(int k = (r - 1) / sz * sz + 1; k <= r; k++){
tmpcnt[arr[k]]++;
}
}
int block_start = (l - 1) / sz + 2, block_end = (r - 1) / sz;
ans = 0, anscnt = 0;
if(block_start <= block_end){
ans = commonnum[block_start][block_end], anscnt = commoncnt[block_start][block_end];
for(int k = l; k <=ll; k++){
int tmp = tmpcnt[arr[k]] + precnt[arr[k]][block_end] - precnt[arr[k]][block_start - 1];
if(tmp > anscnt || (tmp == anscnt && arr[k] < ans)){
anscnt = tmpcnt[arr[k]] + precnt[arr[k]][block_end] - precnt[arr[k]][block_start - 1], ans = arr[k];
}
tmpcnt[arr[k]] = 0;
}
if((r - 1) / sz != (l - 1) / sz){
for(int k = (r - 1) / sz * sz + 1; k <= r; k++){
int tmp = tmpcnt[arr[k]] + precnt[arr[k]][block_end] - precnt[arr[k]][block_start - 1];
if(tmp > anscnt || (tmp == anscnt && arr[k] < ans)){
anscnt = tmp, ans = arr[k];
}
tmpcnt[arr[k]] = 0;
}
}
}
else{
for(int k = l; k <=ll; k++){
if(tmpcnt[arr[k]] > anscnt ||(tmpcnt[arr[k]] == anscnt && arr[k] < ans)){
anscnt = tmpcnt[arr[k]] , ans = arr[k];
}
tmpcnt[arr[k]] = 0;
}
if((r - 1) / sz != (l - 1) / sz){
for(int k = (r - 1) / sz * sz + 1; k <= r; k++){
if(tmpcnt[arr[k]] > anscnt ||(tmpcnt[arr[k]] == anscnt && arr[k] < ans)){
anscnt = tmpcnt[arr[k]], ans = arr[k];
}
tmpcnt[arr[k]] = 0;
}
}
}
printf("%lld\n", num[ans]);
}
return 0;
}