分块小记
普通分块
分块思想是什么?(引自 OI-wiki)
分块的时间复杂度主要取决于分块的块长,一般可以通过均值不等式求出某个问题下的最优块长,以及相应的时间复杂度。
分块是一种很灵活的思想,相较于树状数组和线段树,分块的优点是通用性更好,可以维护很多树状数组和线段树无法维护的信息。
当然,分块的缺点是渐进意义的复杂度,相较于线段树和树状数组不够好。
例题
1.守墓人
题目大意
原题见 Luogu P2357 守墓人
有一个长为 \(n\) 的序列,一共有 \(m\) 此操作,
每次操作会输入 \(opt\),对应操作如下:
1.\(opt = 1\),将 \([l,r]\) 这个区间所有数的值增加 \(k\)。
2.\(opt = 2\), 将 \([1,1]\) 值增加 \(k\)
3.\(opt = 3\),将 \([1,1]\) 值减少 \(k\)
4.\(opt = 4\),统计 \([l,r]\) 的区间和
5.\(opt = 5\),求 \([1,1]\) 的区间和
代码(思路见注释)
点击查看代码
#include <bits/stdc++.h>
#define PII pair <int, int>
#define LL long long
#define DB double
#define ST string
using namespace std;
const int N = 200010;
int n, m;
int idx[N], klen;
// 所在位置公式(下表为 i):(i - 1) / len + 1(len 为块长)
int opt, l, r;
LL a[N], tag[N], sum[N], x; // 记得开 long long
// 计算一个块的左端点
int L(int x)
{ return klen * (x - 1) + 1; }
// 计算一个块的右端点
int R(int x)
{
return min(n, x * klen);
// 记得取 min,因为最后一个块不一定是整个
}
void add(int l, int r, int x)
{
// 如果左右段点均在同一个块中,就暴力处理
if(idx[l] == idx[r])
{
for(int i = l; i <= r; i ++ )
a[i] += x, sum[idx[i]] += x;
}
else
{
// 将左边的散块暴力处理
for(int i = l; i <= R(idx[l]); i ++ )
a[i] += x, sum[idx[i]] += x;
// 统一处理中间的整块
for(int i = idx[l] + 1; i <= idx[r] - 1; i ++ )
{
tag[i] += x; // 懒惰标记,统计散块和的时候加上
sum[i] += (R(i) - L(i) + 1) * x;
}
// 将右边的散块暴力处理
for(int i = L(idx[r]); i <= r; i ++ )
a[i] += x, sum[idx[i]] += x;
}
}
LL count(int l, int r)
{
LL res = 0;
if(idx[l] == idx[r])
{
for(int i = l; i <= r; i ++ )
res += a[i] + tag[idx[i]]; // 记得加上懒惰标记中的值
}
else
{
for(int i = l; i <= R(idx[l]); i ++ )
res += a[i] + tag[idx[i]];
for(int i = idx[l] + 1; i <= idx[r] - 1; i ++ )
res += sum[i];
for(int i = L(idx[r]); i <= r; i ++ )
res += a[i] + tag[idx[i]];
}
return res;
}
signed main()
{
scanf("%d%d", &n, &m);
// 预处理出块长
klen = sqrt(n);
for(int i = 1; i <= n; i ++ )
{
scanf("%lld", &a[i]);
idx[i] = (i - 1) / klen + 1;
sum[idx[i]] += a[i];
}
for(int i = 1; i <= m; i ++ )
{
scanf("%d", &opt);
if(opt == 1)
{
scanf("%d%d%d", &l, &r, &x);
add(l, r, x);
}
if(opt == 2)
scanf("%d", &x), add(1, 1, x);
if(opt == 3)
scanf("%d", &x), add(1, 1, -x);
if(opt == 4)
{
scanf("%d%d", &l, &r);
printf("%lld\n", count(l, r));
}
if(opt == 5)
printf("%lld\n", count(1, 1));
}
return 0;
}
2.蒲公英
题目大意
做题思路
用 \(s[p][x]\) 记录第 \(1~p\) 块中数 \(x\) 出现的次数
(tip:因为 \(x\) 比较大,所以要将 \(x\) 离散化)。
所以数 \(x\) 在 \(p~q\) 块中出现次数即为 \(s[p][x]-s[q-1][x]\)。
再用 \(f[p][q]\) 维护在 \(p~q\) 块中的众数。
对于一段(\(l~r\)),若 \(l\) 和 \(r\) 在同一块,就暴力记录数出现的次数。
否则枚举左边散数和右边散数,如果这一个数是第一次出现,
那就把它在整块中出现的次数加上去(即 \(s[id[r]-1][x]-s[id[l]][x]\)),
最后把它与整快中的众数(即 \(f[id[l]+1][id[r]-1]\))作比较即可。
最后记得输出离散前的答案。
复杂度:\(O(N\sqrt{N})\)。
代码
点击查看代码
#include <bits/stdc++.h>
#define PII pair <int, int>
#define LL long long
#define DB double
#define ST string
#define endl '\n'
using namespace std;
const int N = 40010, M = 310;
int n, m, len, tot, a[N];
int id[N], s[M][N], f[M][M];
vector <int> mp1;
int l, r, lst;
int num, cnt, numx, cntx, c[N];
unordered_map <int, int> mp2;
int L(int x)
{ return (x - 1) * len + 1; }
int R(int x)
{ return min(n, x * len); }
int query(int l, int r)
{
int num, cnt = 0;
if(id[l] == id[r])
{
for(int i = l; i <= r; i ++ )
{
c[a[i]] ++ ;
if(c[a[i]] > cnt || (c[a[i]] == cnt && a[i] < num))
cnt = c[a[i]], num = a[i];
}
for(int i = l; i <= r; i ++ )
c[a[i]] = 0;
}
else
{
num = f[id[l] + 1][id[r] - 1];
cnt = s[id[r] - 1][num] - s[id[l]][num];
for(int i = l; i <= R(id[l]); i ++ )
{
if(c[a[i]] == 0)
c[a[i]] += s[id[r] - 1][a[i]] - s[id[l]][a[i]];
c[a[i]] ++ ;
if(c[a[i]] > cnt || (c[a[i]] == cnt && a[i] < num))
cnt = c[a[i]], num = a[i];
}
for(int i = L(id[r]); i <= r; i ++ )
{
if(c[a[i]] == 0)
c[a[i]] += s[id[r] - 1][a[i]] - s[id[l]][a[i]];
c[a[i]] ++ ;
if(c[a[i]] > cnt || (c[a[i]] == cnt && a[i] < num))
cnt = c[a[i]], num = a[i];
}
for(int i = l; i <= R(id[l]); i ++ )
c[a[i]] = 0;
for(int i = L(id[r]); i <= r; i ++ )
c[a[i]] = 0;
}
return mp1[num];
}
signed main()
{
scanf("%d%d", &n, &m);
len = sqrt(n);
for(int i = 1; i <= n; i ++ )
{
scanf("%d", &a[i]);
mp1.push_back(a[i]);
id[i] = (i - 1) / len + 1;
}
sort(mp1.begin(), mp1.end());
tot = unique(mp1.begin(), mp1.end()) - mp1.begin();
for(int i = 0; i < tot; i ++ )
mp2[mp1[i]] = i;
for(int i = 1; i <= n; i ++ )
a[i] = mp2[a[i]];
for(int i = 1; i <= id[n]; i ++ )
for(int j = L(i); j <= R(i); j ++ )
for(int k = i; k <= id[n]; k ++ )
s[k][a[j]] ++ ;
for(int i = 1; i <= id[n]; i ++ )
{
for(int j = i; j <= id[n]; j ++ )
{
num = f[i][j - 1];
cnt = s[j][num] - s[i - 1][num];
for(int k = L(j); k <= R(j); k ++ )
{
numx = a[k];
cntx = s[j][numx] - s[i - 1][numx];
if(cntx > cnt || (cntx == cnt && numx < num))
num = numx, cnt = cntx;
}
f[i][j] = num;
}
}
while(m -- )
{
scanf("%d%d", &l, &r);
l = ((l + lst - 1) % n) + 1;
r = ((r + lst - 1) % n) + 1;
if(l > r) swap(l, r);
lst = query(l, r);
printf("%d\n", lst);
}
return 0;
}
3.磁力块
题目大意
做题思路
发现:可以将 \(x\) 和 \(y\) 转化为与 \(x0\) 和 \(y0\) 的距离,
但是为了避免精度问题,可以不对距离取平方根,而将所有的磁吸半径取平方(!!!)。
所以可以先将每一个磁力块按它与原点(即小取酒的位置)的距离先按从低到高排序,
对于每一块(块长根号N)再按它内部的磁块重量从小到大排序,并记录这个段中的最重磁块重量。
处理:
可以使用 BFS,在队列中存储未被使用的磁力块,每一次取出队头的磁力块属性。
接着从前到后枚举所有的块,若这个块的最大质量已经超过了当前磁力块的磁性,
那么就退出循环,进行散块操作
(即从前到后遍历整个块中的磁力块,若可以取,则标记为已取并加入磁力块队列),
否则就从上一次去到的磁力块的后面(记录在 \(lst[i]\) 中)的块遍历,
若该块的质量大于当前磁力块的磁力,那么退出循环(记得边做边更改 \(lst[i]\))。
tip:判断 这个块的最大质量已经超过了当前磁力块的磁性 需要放在操作之前。
复杂度:\(O(N\sqrt{N})\)。
代码
点击查看代码
#include <bits/stdc++.h>
#define PII pair <int, int>
#define int long long
#define DB double
#define y0 zzxdalao
using namespace std;
const int N = 250010, M = 510;
int n, t, k, tot;
int x0, y0;
int mx[N], lst[N];
bool used[N];
struct Node
{
int w, m, p, r;
}a[N];
int L(int x)
{ return (x - 1) * t + 1; }
int R(int x)
{ return min(x * t, n); }
signed main()
{
scanf("%lld%lld", &x0, &y0);
scanf("%lld%lld%lld", &a[0].p, &a[0].r, &n);
// 记得平方
a[0].r = a[0].r * a[0].r;
t = sqrt(n), k = (n - 1) / t + 1;
for(int i = 1, x, y; i <= n; i ++ )
{
scanf("%lld%lld", &x, &y);
// 将 x 和 y 转化为与小取酒的距离
a[i].w = pow(x - x0, 2) + pow(y - y0, 2);
scanf("%lld%lld%lld", &a[i].m, &a[i].p, &a[i].r);
// 记得平方
a[i].r = a[i].r * a[i].r;
}
// 按距离排序
sort(a + 1, a + 1 + n, [&](Node x, Node y){ return x.w < y.w; });
// 初始化
for(int i = 1; i <= k; i ++ )
{
lst[i] = L(i) - 1;
mx[i] = a[R(i)].w;
sort(a + L(i), a + 1 + R(i), [&](Node x, Node y){ return x.m < y.m; });
}
queue <Node> q;
q.push(a[0]);
while(q.size())
{
Node frn = q.front(); q.pop();
int i;
// 整块处理
for(i = 1; i <= k; i ++ )
{
// 记得将这句话放在 for 循环之前
if(mx[i] > frn.r) break;
for(int j = lst[i] + 1; j <= R(i); j ++ )
{
if(a[j].m > frn.p) break;
lst[i] = j;
if(used[j]) continue;
used[j] = 1;
q.push(a[j]);
tot ++ ;
}
}
// 散块处理
for(int j = lst[i] + 1; j <= R(i); j ++ )
{
if(a[j].m <= frn.p && !used[j] && a[j].w <= frn.r)
used[j] = 1, tot ++ , q.push(a[j]);
}
}
printf("%lld\n", tot);
return 0;
}
莫队
莫队是什么?
我们令 \(f(l, r)\) 表示题目中给出的通过区间 \([l, r]\) 中算出的值(满足 \(f(l, r)\) 可以由 \(f(l, r - 1)\) \(f(l + 1, r)\) \(f(l - 1, r)\) \(f(l, r +1)\) 计算出来)
但是一定要记录非常多的值才能计算出 \(f(l, r)\),此时若分别记录在每一个块中,整块处理会十分的麻烦(甚至可能会退化)。
所以就可以将双指针与分块结合起来,即沿用上一次处理出的结果进行处理得出这一次处理的结果。
我们先用 \(l\) 和 \(r\) 记录上一次计算出的区间,对于这一次处理区间的左端点和右端点进行移动,移动结束后记录答案。
为了防止退化,我们应该将询问区间进行双关键字排序,即:
若两区间左端点处于同一块中,按右端点升序排序;反之按左端点(其实就是左端点所处的块)排序。
例题
1.小Z的袜子
题目思路
做题思路
先将所有的询问按左端点排序,接着将左端点在同一个块中的询问按右端点排序。
用 \(l\) 和 \(r\) 记录当前覆盖区间的左端点和右端点,再用 \(cur\) 记录现在的分子(分母可以直接计算)
接着一个一个块处理,对于每一个块中的询问的左端点和右端点进行对 \(l\) 和 \(r\) 的移动,
边移动 \(l\) 和 \(r\),边计算 \(cur\) 的值(见前置),移动完成后赋值分子的值
(此处需要特判 \(cur\) 是否等于 0,若等于 0,则要讲分母改为 1)。
最终按提前记录的 \(id\) 升序排序,输出即可。
tip:
1.\(\frac{x(x + 1)}{2} - \frac{x(x - 1)}{2} = \frac{x(x + 1 - x + 1)}{2} = \frac{2x}{2} = x\)
2.记录 \(l\) 和 \(r\) 的时候需要用 long long 记录,因为计算 \(\frac{len \times (len - 1)}{2}\) 的时候,\(len \times (len - 1)\) 最大为 25 亿,会爆 int。
复杂度:\(O(N\sqrt{N})\)。
代码
点击查看代码
#include <bits/stdc++.h>
#define PII pair <int, int>
#define LL long long
#define x first
#define y second
using namespace std;
const int N = 100010;
int n, m, a[N];
int L[N], R[N];
LL c[N];
struct Node
{
LL l, r, p, q;
int id;
}q[N];
LL gcd(LL x, LL y)
{ return y ? gcd(y, x % y) : x; }
signed main()
{
scanf("%d%d", &n, &m);
int len = sqrt(n);
int k = (n - 1) / len + 1;
for(int i = 1; i <= n; i ++ )
scanf("%d", &a[i]);
for(int i = 1; i <= m; i ++ )
{
q[i].id = i;
scanf("%lld%lld", &q[i].l, &q[i].r);
q[i].q = (q[i].r - q[i].l + 1ll) * (q[i].r - q[i].l) / 2ll;
}
sort(q + 1, q + 1 + m, [&](Node x, Node y){ return x.l < y.l; });
for(int i = 1; i <= k; i ++ )
{
L[i] = R[i - 1] + 1;
R[i] = R[i - 1];
while(R[i] < m && q[R[i] + 1].l <= i * len) R[i] ++ ;
sort(q + L[i], q + 1 + R[i], [&](Node x, Node y){ return x.r < y.r; });
}
c[0] ++ ;
for(int i = 1, l = 0, r = 0, cur = 0; i <= k; i ++ )
{
for(int j = L[i]; j <= R[i]; j ++ )
{
while(r < q[j].r) cur += c[a[ ++ r]], c[a[r]] ++ ;
while(l > q[j].l) cur += c[a[ -- l]], c[a[l]] ++ ;
while(r > q[j].r) c[a[r]] -- , cur -= c[a[r -- ]];
while(l < q[j].l) c[a[l]] -- , cur -= c[a[l ++ ]];
if(!cur) q[j].q = 1;
else
{ int d = gcd(cur, q[j].q); q[j].p = cur / d, q[j].q /= d; }
}
}
sort(q + 1, q + 1 + m, [&](Node x, Node y){ return x.id < y.id; });
for(int i = 1; i <= m; i ++ )
printf("%lld/%lld\n", q[i].p, q[i].q);
return 0;
}