学习笔记-分块

分块——优雅的暴力。

分块永远是偏解。 ——@ny_fzx

但是,在面对一些区间问题的时候,不涉及SegTree、LCT等高级数据结构,编码简单的分块能拿到相当可观的分数。

根号分块

顾名思义,这是把长度为 $n$ 的数组分成 $\sqrt{n}$ 个区间。

核心思想:对于一个区间,将其不包含在整块的范围内直接暴力处理,这一段不超过 $\sqrt{n}$ 。而中间被整体包含的块就统一处理,也不会超过 $\sqrt n$ 个块。

因此整体时间为 $O(\sqrt{n})$ 。取 $\sqrt{n}$ 作块长的目的是为了平衡整块操作和非整块的暴力操作的时间。

经典例题:

NKOJ 2297

给出一列数{Ai}(1≤i≤n),总共有m次操作,操作分如下两种:

  1. ADD x y z 将x到y区间的所有数字加上z
  2. ASK x y 将x到y区间的最大一个数字输出。

我们沿袭上面的思想,在做加法的时候也将其不包含在整块的范围内直接暴力加上,整块的,类似于线段树,开一个 Lazy-tag 处理。

区间最大值呢?我们也可以使用 Lazy-tag 。因为对于整块的修改并不会影响块内最大值是哪个,在最后查询的时候直接使用最大值加上这块的总增量。

但是对于单点查询就不一样了。单点查询有可能会影响块内的最大值是哪个,因此直接对于这个块重新暴力找一遍 max ,复杂度也是 $O(\sqrt{n})$ 。

参考核心代码:

void modify(int x, int y, int val)
{
int k1 = (x - 1) / s + 1, k2 = (y - 1) / s + 1;
int ans = INT_MIN;
if (k1 == k2)
{
for (int i = x; i <= y; i++)
{
a[i] += val;
}
block[k1] = INT_MIN;
for (int i = (k1 - 1) * s + 1; i <= s * k1; i++)
{
block[k1] = max(block[k1], a[i]);
}
}
else
{
for (int i = x; i <= k1 * s; i++)
{
a[i] += val;
}
block[k1] = INT_MIN;
for (int i = (k1 - 1) * s + 1; i <= s * k1; i++)
{
block[k1] = max(block[k1], a[i]);
}
for (int i = (k2 - 1) * s + 1; i <= y; i++)
{
a[i] += val;
}
block[k2] = INT_MIN;
for (int i = (k2 - 1) * s + 1; i <= s * k2; i++)
{
block[k2] = max(block[k2], a[i]);
}
for (int i = k1 + 1; i < k2; i++)
{
lazy[i] += val;
}
}
}
int query(int x, int y)
{
int k1 = (x - 1) / s + 1, k2 = (y - 1) / s + 1;
int ans = INT_MIN;
if (k1 == k2)
{
for (int i = x; i <= y; i++)
{
ans = max(ans, a[i] + lazy[k1]);
}
return ans;
}
else
{
for (int i = x; i <= k1 * s; i++)
{
ans = max(ans, a[i] + lazy[k1]);
}
for (int i = (k2 - 1) * s + 1; i <= y; i++)
{
ans = max(ans, a[i] + lazy[k2]);
}
for (int i = k1 + 1; i < k2; i++)
{
ans = max(ans, block[i] + lazy[i]);
}
return ans;
}
return -1;
}

来看看稍有复杂的Lazy-tag处理。

NKOJ 8240

给出一个长为 n 的数列,以及 n 个操作,操作有区间开方,区间求和。

区间开方?这听起来不是 Lazy_tag 能做到的事情。

但是!在经过几次开方之后,该区间的所有数都会变成1,此时再开方就没有意义了。

经过估算,最多 7 次。

因此,Lazy_tag 可以记录区间开方的次数,如果 >=7 ,就没必要再开了,直接跳过。否则暴力。

参考代码:略。

NKOJ 8240

给出一个长为 n 的数列,以及 n 个操作,操作涉及区间加法,询问区间内小于某个值 x 的前驱(比其小的最大元素)。

这道题只有区间加法,很好处理。但是它的询问很奇怪。

求前驱当然不能开 Lazy。所以我们考虑把区间排序,这样便可以在 $\log n$ 的时间内求出来。

当进行非区间的暴力修改时需要重新排序

参考代码:略。

NKOJ 8242

给出一个长为 n 的数列,以及 n 个操作,操作有区间乘法,区间加法,单点询问。

区间乘法的处理原理和区间加法是一样的。但是要注意的是区间乘法与区间加法的关系。

在更新乘法的 Lazy_tag 时,也得顺带把加法的 Lazy 也乘上。当暴力修改时,一定记得将当前块的 Lazy_tag 下放。注意:因为加法的Lazy也做了乘法,所以要先乘后加

NKOJ 8242

给出一个长为 n 的数列,以及 n 个操作,操作有单点插入,单点询问,数据随机生成。

单点插入意味着这事没有那么简单。

如果直接暴力插入,数组后面的所有元素都要往后推一位,显然时间不能承受。

考虑用 vector 来分块,这样插入的时间复杂度就变成了 $\sqrt n$ ,查询也是$\sqrt n$。

但是考虑极限情况。如果每次查询都向同一个 vector 中插入,那么最后时间复杂度会趋近于 $O(n)$ 。

所以当某个 vector 的 size 过大的时候,我们需要重新分块,也就是俗称的拍平。

为 size 设置一个阈值,大概在 $8 \sqrt n$。

参考代码:

void rebuild()
{
int tmp = 0;
for (int i = 1; i <= cntblock; i++)
{
for (auto &j : block[i]) a[++tmp] = j;
}
for (int i = 1; i <= cntblock; i++) block[i].clear();
blocklen = sqrt(tmp);
for (int i = 1; i <= tmp; i++) block[(i - 1) / blocklen + 1].push_back(a[i]);
cntblock = (tmp - 1) / blocklen + 1;
}
void add(int pos, int val)
{
int now = 0, id = 0;
while (1)
{
id++;
if (now + block[id].size() >= pos) break;
else now += block[id].size();
}
block[id].insert(block[id].begin() + pos - now - 1, val);
if (block[id].size() > 8 * blocklen) rebuild();
}
int query(int pos)
{
int now = 0, id = 0;
while (1)
{
id++;
if (now + block[id].size() >= pos) break;
else now += block[id].size();
}
return block[id][pos - now - 1];
}

总之,分块的关键在于 区间如何统一处理

本文作者:aaaaaaqqqqqq

本文链接:https://www.cnblogs.com/aaaaaaqqqqqq/p/17976967

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   aaaaaaqqqqqq  阅读(8)  评论(0编辑  收藏  举报  
点击右上角即可分享
微信分享提示
💬
评论
📌
收藏
💗
关注
👍
推荐
🚀
回顶
收起
  1. 1 404 not found REOL
404 not found - REOL
00:00 / 00:00
An audio error has occurred.