数列分块入门
Posted yjzoier
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数列分块入门相关的知识,希望对你有一定的参考价值。
分块是 莫队 算法的前置知识,也是一种十分 暴力 的数据结构。
分块的核心思想是把要操作的数列 (a_i) 分成若干长度相等的“块”;修改/查询时对于整一块都在指定区间 ([L,R]) 内的块整体修改/查询,对于只有块的一部分在指定区间内的暴力修改/查询。
由于不需要操作/查询具有 区间加法 等性质,分块比线段树、树状数组、ST表等数据结构具有更加灵活的应用。
先来看一道例题 数列分块入门 4,简而言之,就是要求实现区间加法&区间查询;线段树可以很轻松地实现这两个操作,但是我们尝试使用分块:
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
using namespace std;
const int maxn = 50007;
int n, a[maxn], bl[maxn], blo;
int sum[307], atag[307];
inline void add(int l, int r, int val){ //区间加法
for (int i = bl[l] + 1; i <= bl[r] - 1; ++i) //处理完整的块
atag[i] += val;
for (int i = l; i <= min(bl[l] * blo, r); ++i) //处理第一块
a[i] += val, sum[bl[l]] += val;
if (bl[l]!=bl[r])
for (int i = (bl[r]-1) * blo + 1; i <= r; ++i) //处理最后一块
a[i] += val, sum[bl[r]] += val;
}
inline long long query(int l, int r){ //区间查询,同区间加法
long long res = 0;
for (int i = bl[l] + 1; i <= bl[r] - 1; ++i)
res = res + sum[i] + (long long)atag[i] * blo;
for (int i = l; i <= min(bl[l] * blo, r); ++i)
res = res + a[i] + atag[bl[l]];
if (bl[l]!=bl[r])
for (int i = (bl[r]-1) * blo + 1; i <= r; ++i)
res = res + a[i] + atag[bl[r]];
return res;
}
int main(){
scanf("%d", &n);
blo = sqrt(n);
for (int i = 1; i <= n; ++i)
scanf("%d", &a[i]), bl[i] = (i-1) / blo + 1, sum[bl[i]] += a[i];
for (int i = 1; i <= n; ++i){
int opt, l, r, c;
scanf("%d%d%d%d", &opt, &l, &r, &c);
if (opt==0) add(l,r,c);
else printf("%d
", query(l,r)%(c+1));
}
return 0;
}
add
和 query
操作分别是 区间加法 和 区间查询,从这份代码可以看出分块操作的一般思路:
- 处理整块
- 处理非完整块
需要注意的是,处理非完整块和完整块的时候都需要注意处理标记,这和线段树是一样的;对于那些不便合并的标记,修改非完整块时可以先把标记作用到每个元素上再修改单个元素,这样处理起来会更加方便。
再来看一道有趣的题:数列分块入门8,简而言之,就是给出一个长为 (n) 的数列,以及 (n) 个操作,操作涉及区间询问等于一个数 (c) 的元素,并将这个区间的所有元素改为 (c)。
查询一个区间内有多少个元素等于 (c) 这个操作并不容易实现,我刚开始想到的方法是分块后对于每个块内排序,这样就可以通过二分实现查询某个元素的个数。这样时间复杂度是 (O(n sqrt n log n)). 会超时,不够优秀。
然后就有一个大(暴)胆(力)的算法,每次查询某个元素的个数的时候不用二分,而是直接暴力扫描,这样也不用排序,成功去掉了 (O(logn)),但是会有一个最大 (O(n)) 的额外时间复杂度。当然,如果一个块内都是同一个元素,就不用暴力扫描了。
但真的会每次有 (O(n)) 的额外时间复杂度吗?
我们可以把初始的数列看成全部都是同一个元素 + 暴力修改 (n) 个点,由于每次暴力修改的都是一段区间,最多会产生 (2) 个块内有不止一个元素。这样每次修改的额外花费就是 (O(sqrt n)),暴力扫描的时间复杂度被摊还分析掉了。所以总时间复杂度就是 (O(n sqrt n))。
利用相似的证明,可以得到线段树暴力修改也能得到正确的复杂度。
线段树暴力修改也是线段树的常用技巧,通常可以通过证明暴力修改的次数有限来确保时间复杂度。
(分块实现)
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cmath>
#include <algorithm>
using namespace std;
const int MAXN = 100007;
int n, blo, a[MAXN], bl[MAXN], tag[407];
void reset(int id){
if (tag[id]!=-1){
for (int i = (id-1)*blo+1; i <= min(id*blo,n); ++i)
a[i] = tag[id];
tag[id] = -1;
}
}
inline int read(){
int val = 0; bool f = false; char ch = getchar();
while ((ch < '0' || ch > '9') && (ch != '-')) ch = getchar();
if (ch == '-') f = true, ch = getchar();
while (ch >= '0' && ch <= '9')
val = (val<<3) + (val<<1) + ch - '0', ch = getchar();
return f ? (-val) : val;
}
int solve(int l, int r, int c){
int ans = 0;
reset(bl[l]);
for (int i = l; i <= min(bl[l] * blo, r); ++i)
if (a[i] == c) ++ans;
else a[i] = c;
for (int i = bl[l] + 1; i < bl[r]; ++i)
if (tag[i] != -1){
if (tag[i] == c) ans += blo;
else tag[i] = c;
}else{
for (int j = (i-1)*blo+1; j <= i * blo; ++j)
if (a[j] == c) ++ans;
else a[j] = c;
tag[i] = c;
}
if (bl[l] != bl[r]){
reset(bl[r]);
for (int i = (bl[r]-1)*blo+1; i <= r; ++i)
if (a[i] == c) ++ans;
else a[i] = c;
}
return ans;
}
int main(){
memset(tag, -1, sizeof(tag));
n = read(); blo = sqrt(n);
for (int i = 1; i <= n; ++i) a[i] = read();
for (int i = 1; i <= n; ++i) bl[i] = (i-1)/blo + 1;
for (int i = 1; i <= n; ++i){
int l = read(), r = read(), c = read();
printf("%d
", solve(l, r, c));
}
return 0;
}
(线段树实现)
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cmath>
#include <algorithm>
using namespace std;
const int MAXN = 100007;
int n, a[MAXN], tag[MAXN<<2];
inline void PushUp(int rt){
tag[rt] = (tag[rt<<1] == tag[rt<<1|1]) ? tag[rt<<1] : -1;
}
inline void PushDown(int rt){
if (tag[rt]==-1) return;
tag[rt<<1] = tag[rt];
tag[rt<<1|1] = tag[rt];
}
void build(int rt, int l, int r){
if (l == r){
tag[rt] = a[l];
return;
}
int m = (l + r) >> 1;
build(rt<<1, l, m);
build(rt<<1|1, m+1, r);
PushUp(rt);
}
int solve(int rt, int l, int r, int L, int R, int c){
if (L <= l && r <= R && tag[rt] != -1){
if (tag[rt] == c) return (r-l+1);
else {tag[rt]=c; return 0;}
}
int m = l + r >> 1, ans = 0;
PushDown(rt);
if (L <= m) ans += solve(rt<<1, l, m, L, R, c);
if (R > m) ans += solve(rt<<1|1, m+1, r, L, R, c);
PushUp(rt);
return ans;
}
inline int read(){
int val = 0; bool f = false; char ch = getchar();
while ((ch < '0' || ch > '9') && (ch != '-')) ch = getchar();
if (ch == '-') f = true, ch = getchar();
while (ch >= '0' && ch <= '9')
val = (val<<3) + (val<<1) + ch - '0', ch = getchar();
return f ? (-val) : val;
}
int main(){
n = read();
for (int i = 1; i <= n; ++i) a[i] = read();
build(1, 1, n);
for (int i = 1; i <= n; ++i){
int l = read(), r = read(), c = read();
printf("%d
", solve(1, 1, n, l, r, c));
}
return 0;
}
以上是关于数列分块入门的主要内容,如果未能解决你的问题,请参考以下文章