声明
持续更新,因为博主也是正在学习分块的知识,我很菜的,菜的抠$jio$
写在前面
分块是个很暴力的算法,但却比暴力优秀的多,分块算法的时间复杂度一般是根号的,他的主要思想是将一个长度是$n$的数列分为$m$个块,在每个块上维护一些东西,询问的时候才会用到这些维护的东西,就像线段树中的懒标记一样。
Loj #6297. 数列分块入门1
这道题目是分块最基础的题目,仅需要支持区间加法和单点查询两个操作。
将这个数列分为$\sqrt{n}$块。同时可以确定每个块的左右端点。然后在于处理一个数组$in$,表示第$i$个元素在第$in[i]$块内。
再用一个$addtag$数组表示每个块的加法标记,到最后查询的时候会用到。
下面开始进行区间修改操作的讲解
假设给定的要修改序列的左右端点为$l$和$r$,那么可能会有三种情况出现,
- $l$和$r$在同一个块内,那么只需要暴力的修改l到r便可。
- $l$和$r$不在同一个块,那么要分别对$l$和$r$所在的块进行修改。
- 这一种是第$2$种的一个分支情况,就是$l$和$r$之间还有很多个块,因为这些块都是被整体修改,所以直接打到标记上就行。
下面给出这道题的代码
神呐,原谅我的码风放荡不羁
#include <iostream>
#include <cstdio>
#include <cmath>
typedef long long LL;
const int maxn = 50003;
int n, opt, a, b, c, w[maxn], cnt, in[maxn], addtag[maxn];
inline int read() {
int x = 0, f = 1; char ch = getchar();
while (ch < '0' || ch > '9') {if(ch == '-') f = -1;ch = getchar();}
while (ch <= '9' && ch >= '0') {x = x * 10 + ch - '0';ch = getchar();}
return x * f;
}
inline void add(int l, int r, int x) {
for(int i=l; i<=std::min(r, in[l]*cnt); i++)
w[i] += x;
if(in[l] != in[r])
for(int i=(in[r]-1)*cnt+1; i<=r; i++)
w[i] += x;
for(int i=in[l]+1; i<in[r]; i++)
addtag[i] += x;
}
inline int query(int x) {
return w[x] + addtag[in[x]];
}
int main() {
n = read();
cnt = std::sqrt(n);
for(int i=1; i<=n; i++) w[i] = read();
for(int i=1; i<=n; i++) in[i] = (i-1)/cnt+1;
for(int i=1; i<=n; i++) {
opt = read(), a = read(), b = read(), c = read();
if(opt == 0) add(a, b, c);
else printf("%d\n", query(b));
}
}
先到这里,明天续更QAQ
Loj #6298. 数列分块入门2
这次要维护的东西多了一个找区间内小于某个值的数的个数。但题目的做法和上面的非常的相似。
让每个块在查询的时候都变成有序的,我们就可以二分查找来找到第一个大于给定数值的数的位置,然后就可以算出一个块内有多少数是小于给定值的。
对于区间假发的操作和第一题的一样,唯一要加上的就是每次序列改变后都要进行排序,来保证序列是有序的。
代码看下面
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cmath>
#include <vector>
typedef long long LL;
const int maxn = 5e4+3;
std::vector <int> blo[508];
int n, w[maxn], opt, a, b, c, cnt, in[maxn], addtag[maxn];
inline LL read() {
LL x = 0, f = 1; char ch = getchar();
while (ch < '0' || ch > '9') {if(ch == '-') f = -1; ch = getchar();}
while (ch <= '9' && ch >= '0') {x = x*10 + ch-'0'; ch = getchar();}
return x * f;
}
inline void resort(int x) {
blo[x].clear();
for(int i=(x-1)*cnt+1; i<=x*cnt; i++)
blo[x].push_back(w[i]);
std::sort(blo[x].begin(), blo[x].end());
}
inline void add(int l, int r, int x) {
for(int i=l; i<=std::min(in[l]*cnt, r); i++)
w[i] += x;
resort(in[l]);
if(in[l] != in[r])
for(int i=(in[r]-1)*cnt+1; i<=r; i++)
w[i] += x;
resort(in[r]);
for(int i=in[l]+1; i<in[r]; i++)
addtag[i] += x;
}
inline int query(int l, int r, int x) {
int Ans = 0;
for(int i=l; i<=std::min(r, in[l]*cnt); i++)
if(w[i] + addtag[in[i]] < x) Ans ++;
if(in[l] != in[r])
for(int i=(in[r]-1)*cnt+1; i<=r; i++)
if(w[i] + addtag[in[i]] < x) Ans ++;
for(int i=in[l]+1; i<in[r]; i++){
int s = x-addtag[i];
Ans += std::lower_bound(blo[i].begin(), blo[i].end(), s)-blo[i].begin();
}
return Ans;
}
int main() {
n = read();
cnt = std::sqrt(n);
for(int i=1; i<=n; i++) w[i] = read();
for(int i=1; i<=n; i++) {
in[i] = (i-1)/cnt+1;
blo[in[i]].push_back(w[i]);
}
for(int i=1; i<=in[n]; i++)
std::sort(blo[i].begin(), blo[i].end());
for(int i=1; i<=n; i++) {
opt = read(), a = read(), b = read(), c = read();
if(opt == 0) add(a, b, c);
else printf("%d\n", query(a, b, c*c));
}
}
Loj #6279. 数列分块入门3
这一题在上一题的基础上稍作变化,将询问操作变为求每一个数在某一个区间内的前驱
一个数的前驱的定义为小于这个数的第一个数。这里要提到一点就是在每一个块中可以通过维护其他的数据结构来实现一些其他的操作
这里就维护了一个不可重集合$set$。
大部分操作和上题一样,只有查询稍有不同,说一下查询。
定义一个迭代器(指针)。$set$是有序的所以不需要排序,二分查找$x$。那么$x$前面的第一个数就是它的前驱。
显然,如果$x=l$,那$x$就没有前驱。
还是放上代码
#include <iostream>
#include <cstdio>
#include <cmath>
#include <set>
#include <algorithm>
const int maxn = 1e5+3;
typedef long long LL;
int n, w[maxn], addtag[1000], in[maxn], opt, a, b, c, cnt;
std::set <int> blo[1000];
inline LL read() {
LL x = 0, f = 1; char ch = getchar();
while (ch < '0' || ch > '9') {if(ch == '-') f = -1; ch = getchar();}
while (ch <= '9' && ch >= '0') {x = x*10 + ch-'0'; ch = getchar();}
return x * f;
}
inline void add(int l, int r, int x) {
for(int i=l; i<=std::min(r, in[l]*cnt); i++) {
blo[in[l]].erase(w[i]);
w[i] += x;
blo[in[l]].insert(w[i]);
}
if(in[l] != in[r])
for(int i=(in[r]-1)*cnt+1; i<=r; i++) {
blo[in[r]].erase(w[i]);
w[i] += x;
blo[in[r]].insert(w[i]);
}
for(int i=in[l]+1; i<in[r]; i++)
addtag[i] += x;
}
inline int query(int l, int r, int x) {
int ans = -1;
for(int i=l; i<=std::min(in[l]*cnt, r); i++)
if(w[i] + addtag[in[i]] < x)
ans = std::max(ans, w[i]+addtag[in[i]]);
if(in[l] != in[r])
for(int i=(in[r]-1)*cnt+1; i<=r; i++)
if(w[i] + addtag[in[i]] < x)
ans = std::max(ans, w[i]+addtag[in[i]]);
for(int i=in[l]+1; i<in[r]; i++) {
int s = x - addtag[i];
std::set<int>::iterator it = blo[i].lower_bound(s);
if(it == blo[i].begin()) continue;
--it;
ans = std::max(ans, *it+addtag[i]);
}
return ans;
}
int main() {
n = read();
cnt = 1000;
for(int i=1; i<=n; i++) w[i] = read();
for(int i=1; i<=n; i++) {
in[i] = (i-1)/cnt+1;
blo[in[i]].insert(w[i]);
}
for(int i=1; i<=n; i++){
opt = read(), a = read(), b = read(), c = read();
if(opt == 0) add(a, b, c);
else printf("%d\n", query(a, b, c));
}
}
Loj #6280. 数列分块入门4
现在又要求支持区间求和,其实不难,每一个块内都维护一个$sum$表示这个块内所有元素的和。
在进行修改区间两端的两个特殊的块的时候修改一个加一个。
到最后查询的时候$sum+addtag*cnt$,$cnt$表示块的大小。
代码长这个样子
#include <iostream>
#include <cstdio>
#include <cmath>
const int maxn = 5e4+3;
typedef long long LL;
int n, cnt, in[maxn], opt, l, r, k, arr[maxn], addtag[500], sum[500];
inline LL read() {
LL x = 0, f = 1; char c = getchar();
while (c < '0' || c > '9') {if(c == '-') f = -1; c = getchar();}
while (c <= '9' && c >= '0') {x = x*10 + c-'0'; c = getchar();}
return x * f;
}
inline void add(int l, int r, int k) {
for(int i=l; i<=std::min(r, in[l]*cnt); i++)
arr[i] += k, sum[in[i]] += k;
if(in[l] != in[r])
for(int i=(in[r]-1)*cnt+1; i<=r; i++)
arr[i] += k, sum[in[i]] += k;
for(int i=in[l]+1; i<in[r]; i++)
addtag[i] += k;
}
inline int query(int l, int r, int Mod) {
int ans = 0;
for(int i=l; i<=std::min(r, in[l]*cnt); i++)
ans = arr[i] % Mod + addtag[in[i]] % Mod + ans % Mod;
if(in[l] != in[r])
for(int i=(in[r]-1)*cnt+1; i<=r; i++)
ans = arr[i] % Mod + addtag[in[i]] % Mod + ans % Mod;
for(int i=in[l]+1; i<in[r]; i++)
ans = addtag[i] % Mod * cnt % Mod + sum[i] % Mod + ans % Mod;
return ans % Mod;
}
int main() {
n = read();
cnt = std::sqrt(n);
for(int i=1; i<=n; i++) {
arr[i] = read();
in[i] = (i-1)/cnt+1;
sum[in[i]] += arr[i];
}
for(int i=1; i<=n; i++) {
opt = read(), l = read(), r = read(), k = read();
if(opt == 0) add(l, r, k);
else printf("%d\n", query(l, r, k+1));
}
}
Loj #6281. 数列分块入门5
支持区间开方和区间查询。
显然区间开方是最难处理的地方,我去参考了一下黄学长的博客,得到了一种比较神奇的方法(原谅我见识少),一段区间进行至多$5$次区间开方就会变成$1$或者$0$。
那么我们只需要暴力的对区间进行修改,由于$1$和$0$开方后还是本身,所以对于已经全部变为$1$或者$0$的区间就不必再开方了
附上代码
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cmath>
const int maxn = 5e4+3;
int n, cnt, in[maxn], arr[maxn], sum[500], opt, l, r, k;
bool flag[500];
inline int read() {
int x = 0, f = 1; char c = getchar();
while (c < '0' || c > '9') {if(c == '-') f = -1; c = getchar();}
while (c <= '9' && c >= '0') {x = x*10 + c-'0'; c = getchar();}
return x * f;
}
inline void solve_sqrt(int x) {
if(flag[x]) return ;
flag[x] = 1;
sum[x] = 0;
for(int i=(x-1)*cnt+1; i<=x*cnt; i++) {
arr[i] = std::sqrt(arr[i]);
sum[x] += arr[i];
if(arr[i] > 1) flag[x] = 0;
}
}
inline void update(int l, int r) {
for(int i=l; i<=std::min(r, in[l]*cnt); i++) {
sum[in[i]] -= arr[i];
arr[i] = std::sqrt(arr[i]);
sum[in[i]] += arr[i];
}
if(in[l] != in[r]) {
for(int i=(in[r]-1)*cnt+1; i<=r; i++) {
sum[in[i]] -= arr[i];
arr[i] = std::sqrt(arr[i]);
sum[in[i]] += arr[i];
}
}
for(int i=in[l]+1; i<in[r]; i++)
solve_sqrt(i);
}
inline int query(int l, int r) {
int ans = 0;
for(int i=l; i<=std::min(in[l]*cnt, r); i++)
ans += arr[i];
if(in[r] != in[l])
for(int i=(in[r]-1)*cnt+1; i<=r; i++)
ans += arr[i];
for(int i=in[l]+1; i<in[r]; i++)
ans += sum[i];
return ans;
}
int main() {
n = read();
cnt = std::sqrt(n);
for(int i=1; i<=n; i++)
arr[i] = read(), in[i] = (i-1)/cnt + 1;
for(int i=1; i<=n; i++)
sum[in[i]] += arr[i];
for(int i=1; i<=n; i++) {
opt = read(), l = read(), r = read(), k = read();
if(opt == 0) update(l, r);
else printf("%d\n", query(l, r));
}
}
来源:oschina
链接:https://my.oschina.net/u/4382335/blog/3867821