转自:http://hzwer.com/8053.html
很好的分块知识讲解。
可能涉及的几个词语解释:
区间:数列中连续一段的元素
区间操作:将某个区间[a,b]的所有元素进行某种改动的操作
块:我们将数列划分成若干个不相交的区间,每个区间称为一个块
整块:在一个区间操作时,完整包含于区间的块
不完整的块:在一个区间操作时,只有部分包含于区间的块,即区间左右端点所在的两个块
例题1:
给出一个长为n的数列,以及n个操作,操作涉及区间加法,单点查值。
这是一道能用许多数据结构优化的经典题,可以用于不同数据结构训练。
数列分块就是把数列中每m个元素打包起来,达到优化算法的目的。
以此题为例,如果我们把每m个元素分为一块,共有n/m块,每次区间加的操作会涉及O(n/m)个整块,以及区间两侧两个不完整的块中至多2m个元素。
我们给每个块设置一个加法标记(就是记录这个块中元素一起加了多少),每次操作对每个整块直接O(1)标记,而不完整的块由于元素比较少,暴力修改元素的值。
每次询问时返回元素的值加上其所在块的加法标记。
这样每次操作的复杂度是O(n/m)+O(m),根据均值不等式,当m取√n时总复杂度最低,为了方便,我们都默认下文的分块大小为√n。
#include<cstdio>
#include<algorithm>
using namespace std;
int n, blo;
int v[50005], bl[50005], atag[50005];
void add(int a, int b, int c)
{
for (int i = a; i <= min(bl[a] * blo, b); i++)
v[i] += c;
if (bl[a] != bl[b])
for (int i = (bl[b] - 1)*blo + 1; i <= b; i++)
v[i] += c;
for (int i = bl[a] + 1; i <= bl[b] - 1; i++)
atag[i] += c;
}
int main()
{
scanf("%d", &n); blo = sqrt(n);
int tmp;
for (int i = 1; i <= n; i++) {
scanf("%d", &tmp);
v[i] = tmp;
}
for (int i = 1; i <= n; i++)bl[i] = (i - 1) / blo + 1;
for (int i = 1; i <= n; i++)
{
int opt, a, b, c;
scanf("%d%d%d%d", &opt, &a, &b, &c);
if (opt == 0) add(a, b, c);
if (opt == 1) printf("%d
", v[b] + atag[bl[b]]);
}
return 0;
}
例题2:
给出一个长为n的数列,以及n个操作,操作涉及区间加法,询问区间内小于某个值x的前驱(比其小的最大元素)。
n<=100000其实是为了区分暴力和一些常数较大的写法。
不过这题其实想表达:可以在块内维护其它结构使其更具有拓展性,比如放一个 set ,这样如果还有插入、删除元素的操作,会更加的方便。
分块的调试检测技巧:可以生成一些大数据,然后用两份分块大小不同的代码来对拍,还可以根据运行时间尝试调整分块大小,减小常数。
#include<set>
#include<cmath>
#include<cstdio>
#include<cmath>
#include<algorithm>
using namespace std;
int n, blo;
const int maxn = 100000 + 5;
int v[maxn], bl[maxn], atag[maxn];
set<int>st[maxn];
void add(int a, int b, int c)
{
for (int i = a; i <= min(bl[a] * blo, b); i++)
{
st[bl[a]].erase(v[i]);
v[i] += c;
st[bl[a]].insert(v[i]);
}
if (bl[a] != bl[b])
{
for (int i = (bl[b] - 1)*blo + 1; i <= b; i++)
{
st[bl[b]].erase(v[i]);
v[i] += c;
st[bl[b]].insert(v[i]);
}
}
for (int i = bl[a] + 1; i <= bl[b] - 1; i++)
atag[i] += c;
}
int query(int a, int b, int c)
{
int ans = -1;
for (int i = a; i <= min(bl[a] * blo, b); i++)
{
int val = v[i] + atag[bl[a]];
if (val<c)ans = max(val, ans);
}
if (bl[a] != bl[b])
for (int i = (bl[b] - 1)*blo + 1; i <= b; i++)
{
int val = v[i] + atag[bl[b]];
if (val<c)ans = max(val, ans);
}
for (int i = bl[a] + 1; i <= bl[b] - 1; i++)
{
int x = c - atag[i];
set<int>::iterator it = st[i].lower_bound(x);
if (it == st[i].begin())continue;
--it;
ans = max(ans, *it + atag[i]);
}
return ans;
}
int main()
{
scanf("%d", &n);
blo = 1000;
for (int i = 1; i <= n; i++) {
int tmp;
scanf("%d", &tmp);
v[i] = tmp;
}
for (int i = 1; i <= n; i++)
{
bl[i] = (i - 1) / blo + 1;
st[bl[i]].insert(v[i]);
}
for (int i = 1; i <= n; i++)
{
int opt, a, b, c;
scanf("%d%d%d%d", &opt, &a, &b, &c);
if (opt == 0)add(a, b, c);
if (opt == 1)printf("%d
", query(a, b, c));
}
return 0;
}
例题3:
给出一个长为n的数列,以及n个操作,操作涉及区间开方,区间求和。
稍作思考可以发现,开方操作比较棘手,主要是对于整块开方时,必须要知道每一个元素,才能知道他们开方后的和,也就是说,难以快速对一个块信息进行更新。
看来我们要另辟蹊径。不难发现,这题的修改就只有下取整开方,而一个数经过几次开方之后,它的值就会变成 0 或者 1。
如果每次区间开方只不涉及完整的块,意味着不超过2√n个元素,直接暴力即可。
如果涉及了一些完整的块,这些块经过几次操作以后就会都变成 0 / 1,于是我们采取一种分块优化的暴力做法,只要每个整块暴力开方后,记录一下元素是否都变成了 0 / 1,区间修改时跳过那些全为 0 / 1 的块即可。
这样每个元素至多被开方不超过4次,显然复杂度没有问题
#include<cmath>
#include<cstdio>
#include<iostream>
#include<algorithm>
using namespace std;
int n, blo;
const int maxn = 50000 + 5;
int bl[maxn];
int v[maxn], sum[maxn], flag[maxn];
void solve_sqrt(int x)
{
if (flag[x])return;
flag[x] = 1;
sum[x] = 0;
for (int i = (x - 1)*blo + 1; i <= x*blo; i++)
{
v[i] = sqrt(v[i]), sum[x] += v[i];
if (v[i]>1)flag[x] = 0;
}
}
void add(int a, int b, int c)
{
for (int i = a; i <= min(bl[a] * blo, b); i++)
{
sum[bl[a]] -= v[i];
v[i] = sqrt(v[i]);
sum[bl[a]] += v[i];
}
if (bl[a] != bl[b])
for (int i = (bl[b] - 1)*blo + 1; i <= b; i++)
{
sum[bl[b]] -= v[i];
v[i] = sqrt(v[i]);
sum[bl[b]] += v[i];
}
for (int i = bl[a] + 1; i <= bl[b] - 1; i++)
solve_sqrt(i);
}
int query(int a, int b)
{
int ans = 0;
for (int i = a; i <= min(bl[a] * blo, b); i++)
ans += v[i];
if (bl[a] != bl[b])
for (int i = (bl[b] - 1)*blo + 1; i <= b; i++)
ans += v[i];
for (int i = bl[a] + 1; i <= bl[b] - 1; i++)
ans += sum[i];
return ans;
}
int main()
{
scanf("%d", &n);
blo = sqrt(n);
int tmp;
for (int i = 1; i <= n; i++) {
scanf("%d", &tmp);
v[i] = tmp;
}
for (int i = 1; i <= n; i++)
{
bl[i] = (i - 1) / blo + 1;
sum[bl[i]] += v[i];
}
for (int i = 1; i <= n; i++)
{
int opt, a, b, c;
scanf("%d%d%d%d", &opt, &a, &b, &c);
if (opt == 0) add(a, b, c);
if (opt == 1) printf("%d
", query(a, b));
}
return 0;
}