分块
分块
前言 | Preface
最近学了分块,所以写了一篇总结
题单:分块
普通分块
引入
分块是一种思想
既然单独的一些元素处理起区间操作来的复杂度很高,那么为什么不将它们看做一个整体呢?
假设一个“整体”的大小是 \(S\),总的元素个数是 \(n\) 个,查询的 \([l,r]\) 区间,这个时候两边不完整的块至多大 \(2 S - 2\),而中间的都是完整的整体
所以易知复杂度可以表示为:\(T(n) = \frac{n}{S} - 2 + 2S - 2 = \mathcal O(\frac{n}{S}+S)\)
由均值不等式易知 \(T(n)\) 最小为 \(S = \sqrt n\) 时,\(T(n) = \mathcal O(\sqrt n)\)
这里涉及到了另外的一个概念————平衡规划,不过很少有人讲,我在后面应该会讲的
实现
考虑将序列分成长度为 \(\lfloor \sqrt n \rfloor\) 的小块,最后一块会不足 \(\lfloor \sqrt{n} \rfloor\),总共会有 \(\lceil \sqrt{n} \rceil\) 个块
对于区间操作 \([l, r]\),考虑两种情况:
- \(l\) 和 \(r\) 在同一个块内,这个时候直接暴力,时间复杂度 \(\mathcal O(\sqrt{n})\)
- \(l\) 和 \(r\) 不在同一个块内,不完整的两个端点的块直接暴力,剩余的打上
tag
例题
P13976 数列分块入门 1
就是一道版子
View Code
struct __block{
ll tag;
int start, end;
} block[maxn];
ll a[maxn];
int id[maxn];
void solve() {
int n;
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i];
int block_size = sqrt(n);
int block_count = (n + block_size - 1) / block_size;
for (int i = 1; i <= n; i++) id[i] = (i - 1) / block_size + 1;
for (int i = 1; i <= block_count; i++) {
block[i].start = (i - 1) * block_size + 1;
block[i].end = i * block_size;
}
block[block_count].end = n;
for (int i = 1; i <= n; i++) {
int opt, l, r;
ll c;
cin >> opt >> l >> r >> c;
if (opt == 0) {
if (id[l] == id[r]) for (int j = l; j <= r; j++) a[j] += c;
else {
for (int j = l; j <= block[id[l]].end; j++) a[j] += c;
for (int j = id[l] + 1; j <= id[r] - 1; j++) block[j].tag += c;
for (int j = block[id[r]].start; j <= r; j++) a[j] += c;
}
} else
cout << a[r] + block[id[r]].tag << "\n";
}
}
[数列分块入门 2-8]
链接见题单
数列分块入门 2
对每一个散块,操作之后进行重建,然后排序,而区间操作的整块则不用,因为加同一个数不会改变相对大小
复杂度 \(\mathcal O(n\sqrt{n}\log \sqrt n)\)
数列分块入门 3
将 2 的操作由求个数改成求最大值即可
数列分块入门 4
维护一个区间和就是了
数列分块入门 5
和这道题 P4145 上帝造题的七分钟 2 / 花神游历各国 有些类似
这道题涉及到了一个叫做势能的概念,它就相当你在银行中存储的钱,会有用完的时候
观察发现,一个数 \(n\) 顶多开方 \(\log\log n\) 次,一个 int 范围内的数就顶多开方 \(5\) 次
基于此,我们可以考虑仅对势能没有用尽、还能够开方的数进行操作
View Code
struct __block{
ll tag;
int start, end;
ll sum;
bool vis;
} block[maxn];
ll a[maxn];
int id[maxn];
void solve() {
int n;
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i];
int block_size = sqrt(n);
int block_count = (n + block_size - 1) / block_size;
for (int i = 1; i <= n; i++)
id[i] = (i - 1) / block_size + 1;
for (int i = 1; i <= block_count; i++) {
block[i].start = (i - 1) * block_size + 1;
block[i].end = min(i * block_size, n);
block[i].tag = 0;
block[i].sum = 0;
block[i].vis = false;
for (int j = block[i].start; j <= block[i].end; j++)
block[i].sum += a[j];
}
for (int i = 1; i <= n; i++) {
int opt, l, r;
cin >> opt >> l >> r;
if (opt == 0) {
if (id[l] == id[r]) {
if (!block[id[l]].vis) {
for (int j = l; j <= r; j++) {
block[id[l]].sum -= a[j];
a[j] = sqrt(a[j]);
block[id[l]].sum += a[j];
}
}
} else {
if (!block[id[l]].vis) {
for (int j = l; j <= block[id[l]].end; j++) {
block[id[l]].sum -= a[j];
a[j] = sqrt(a[j]);
block[id[l]].sum += a[j];
}
}
for (int j = id[l] + 1; j <= id[r] - 1; j++) {
if (block[j].vis) continue;
bool flag = true;
for (int k = block[j].start; k <= block[j].end; k++) {
block[j].sum -= a[k];
a[k] = sqrt(a[k]);
block[j].sum += a[k];
if (a[k] > 1) flag = false;
}
if (flag) block[j].vis = true; // 全是 1,势能已经用完了
}
if (!block[id[r]].vis) {
for (int j = block[id[r]].start; j <= r; j++) {
block[id[r]].sum -= a[j];
a[j] = sqrt(a[j]);
block[id[r]].sum += a[j];
}
}
}
} else {
ll answer = 0;
if (id[l] == id[r]) {
for (int j = l; j <= r; j++)
answer += a[j];
} else {
for (int j = l; j <= block[id[l]].end; j++)
answer += a[j];
for (int j = id[l] + 1; j <= id[r] - 1; j++)
answer += block[j].sum;
for (int j = block[id[r]].start; j <= r; j++)
answer += a[j];
}
cout << answer << "\n";
}
}
}
这样子虽然看起来时间复杂度有足足 \(\mathcal O(n^2)\),可是由于 int 范围内的数最多进行 \(5\) 次开方操作,所以均摊下来还是 \(\mathcal O(n\sqrt n)\)
数列分块入门 7
维护两个 tag,一个加 add,一个乘 mul 就可以了
这里需要注意,在区间乘操作的时候,此时的 tag 更新是这样的:\((\text{mul} \times a_i + \text{add}) \times t = (\text{mul}\times t) \times a_i + \text{add} \times t\),也就是两个 tag 都要相乘
注意取模就行了
数列分块入门 8
记录一下区间赋值的操作,然后对于处理的散块,将 tag 下传,然后统计答案,最后附上 tag
对于整块,如果有区间的赋值标记,则看这个标记是否是当前这个数,如果是,统计答案,不是则赋值跳过
如果没有,则遍历统计答案,赋值
P2801 教主的魔法
其实就是数列分块入门 2,只用稍微改一下代码就可以了
CF455D Serega and Fun
我觉得这道题和它的名字一样有意思
发现值域范围不大,可以使用桶来存出现次数
至于右移,STL 里面有一个十分好用的 deque(双端队列)
所以我们考虑每一个块对应一个双端队列,剩下的就是模拟的事了
View Code
int last = 0;
for (int i = 1; i <= q; i++) {
int opt;
cin >> opt;
if (opt == 1) {
int l, r;
cin >> l >> r;
l = (l + last - 1) % n + 1;
r = (r + last - 1) % n + 1;
if (l > r) swap(l, r);
if (id[l] == id[r]) {
int value = dq[id[l]][r - block[id[l]].start];
dq[id[l]].erase(dq[id[l]].begin() + (r - block[id[l]].start));
dq[id[l]].insert(dq[id[l]].begin() + (l - block[id[l]].start), value);
} else {
dq[id[l]].insert(dq[id[l]].begin() + (l - block[id[l]].start), dq[id[r]][r - block[id[r]].start]);
cnt[id[l]][dq[id[r]][r - block[id[r]].start]]++;
cnt[id[r]][dq[id[r]][r - block[id[r]].start]]--;
dq[id[r]].erase(dq[id[r]].begin() + (r - block[id[r]].start));
for (int j = id[l] + 1; j <= id[r]; j++) {
int temp = dq[j - 1].back();
dq[j].push_front(temp);
++cnt[j][temp];
dq[j - 1].pop_back();
--cnt[j - 1][temp];
}
}
} else {
int l, r, k;
cin >> l >> r >> k;
l = (l + last - 1) % n + 1;
r = (r + last - 1) % n + 1;
k = (k + last - 1) % n + 1;
if (l > r) swap(l, r);
int answer = 0;
if (id[l] == id[r]) {
for (int j = l; j <= r; j++)
if (dq[id[l]][j - block[id[l]].start] == k)
answer++;
} else {
for (int j = l; j <= block[id[l]].end; j++) if (dq[id[l]][j - block[id[l]].start] == k) answer++;
for (int j = block[id[r]].start; j <= r; j++) if (dq[id[r]][j - block[id[r]].start] == k) answer++;
for (int j = id[l] + 1; j < id[r]; j++) answer += cnt[j][k];
}
cout << answer << "\n";
last = answer;
}
}
哦对了,deque 支持随机访问(下标访问)
块状链表
如果我们要支持 insert 和 erase 操作,该怎么办?
很简单,我们将分出来的块看做一个数组就行了,然后像链表那般连起来(实际上不会连的)
但是如果一个块内插入了很多元素,近乎退化成了普通数组怎么办?
当然是拼夕夕砍一刀
其实砍一刀的思路是对的,当一个块的大小大于 \(2B\) (\(B\) 是给定的阈值)时,将它裂成两段,这样子每一段都维持在了 \(\B \sim 2B\) 的范围内了
这就是分裂操作了
但是如果删除操作太多了,退化成链表了怎么办?
当然是拼起来
当然,这个合并操作很少有人写,毕竟删除多了,自然 \(n\) 也就小了
模版就如下了
vector<vector<ll>> block;
int B;
void update(int position, ll value) {
int current = 0;
int k = position - 1;
while (current < block.size() - 1 and k >= block[current].size()) {
k -= block[current].size();
current++;
}
block[current].insert(block[current].begin() + k, value);
if (block[current].size() > 2 * B) {
vector<ll> new_block(block[current].begin() + B, block[current].end());
block[current].erase(block[current].begin() + B, block[current].end());
block.insert(block.begin() + current + 1, new_block);
}
}
ll query(int position) {
int current = 0;
int k = position - 1;
while (current < block.size() - 1 and k >= block[current].size()) {
k -= block[current].size();
current++;
}
return block[current][k];
}
例题
P13981 数列分块入门 6
版子,使用上面的模板就可以通过
平衡规划
由于没有什么好的文章,所以问了一下 AI,以下是我自己的理解,可能有误
之前香槟说要讲一道平衡规划的题,可是一直没有讲,现在看到分块才想起来
引入
首先,我们需要明白,平衡规划的定义是什么
定义 1
平衡规划指的是在给定的资源约束(输入规模 \(N\))下,构造一个包含可调参数 \(B\) 的复杂度函数 \(T(N, B)\),通过求解 \(\min\limits_B T(N, B)\) ,找到使得系统整体效率达到帕累托最优 Pareto Optimality的参数配置
帕累托最优 Pareto Optimality
在不损害其他目标的前提下,无法再改进任何一个目标的一个解
这个定义就是找到一个在给定的规模下的最优解,根据选取的 \(B\) 的含义的不同,可能会有不同,而根据具体的规模定义,也会有不同
为什么一定要满足帕累托最优
假设算法 A 是我们平衡规划出来的,而 B 是满足在这个规模下的帕累托最优的算法
这样则有:
\(T_A(n) \le T_B(n)\),\(S_A(n) \le S_B(n)\)(\(S\) 是空间复杂度)
并且这之中一定有一个是 \(\lt\),那么算法 A 就不是这个规模下的最优解
解耦
通常来讲,耦合在一起的步骤十分的难以操作,经常是 \(\mathcal O(f(n) \cdot g(n))\) 的时间复杂度
通过解耦操作,我们可以降成 \(\mathcal O(w(n)(f'(n) + g'(n)))\) 的形式,其中 \(w(n)\) 是复杂度低于 \(f(n)\)、\(g(n)\) 的函数
解耦后的复杂度通常与解耦的操作有关
以分块为例
其实我们发现,分块的主要时间复杂度优化在于懒标记,所以我们可以考虑从懒标记出发
最早学到懒标记,是在线段树中的,对应的是一整块的区间
所以我们会想到将序列分成很多份,但这还不是解耦,因为在修改之后,还是必须立马操作
这个时候,懒标记的作用很明显了,它对时间进行了解耦,将原本应该立即执行的操作延后到询问,正是这种延后,使得不用每一个元素都访问
所以时间复杂度有 \(\mathcal O(n ^ 2)\) 变成了 \(\mathcal O(\frac{n}{B} + B)\)
很神奇
总结
所以所平衡规划的主要概念就是 平衡 和 解耦,平衡让我们找到最优解,解耦则是方法,由于规模的定义可以不同,随意根据我们想要的,可以自定义,然后找出合适的解
找个合适的时间写一篇博客吧
根号分治
与其说是算法,不如说是一种思想
引入
其实是对于平衡规划的应用
对应的概念叫做根号平衡
这里有一篇总结的十分全的文章:对祂使用根号平衡吧(十二重分块法)
思路
对于一道数据规模 \(n\),查询 \(q\) 的题目,朴素算法的复杂度一般是 \(\mathcal O(nq)\)
但是 \(n\) 和 \(q\) 是耦合在一起的,两者非正交,所以在不引入外界条件的情况下无法分成 \(\mathcal O(f(n) + g(q))\)
所以我们现在引入一个阈值 \(B\),小于 \(B\) 的直接暴力修改,\(\mathcal O(B)\)
大于 \(B\) 的,分成很多个块加上预处理修改,\(\mathcal O(\frac{n}{B})\)
所以总复杂度降为 \(T(n,q) = \mathcal O(nB + \frac{nq}{B}))\)
由均值不等式得到,当 \(B = \sqrt n\) 的时候 \(T(n,q)\) 最小,最小为 \(T(n,q) = \mathcal O((n + q)\sqrt n)\)
例题
P3396 哈希冲突
用 ans[mod][rest] 表示模数为 mod,余数为 rest 的所有数的和
有两种思路:
- 纯暴力(计算
ans数组 - 以模数为步长,一步一步地跳
我们会发现而更适合模数大的情况,而一则适合模数小的情况
考虑设置一个阈值 \(B\),则低于阈值的使用方案2,大于则使用方案1
这样子复杂度变为 \(\mathcal O(nB + \frac{nq}{B})\)
取 \(B = \sqrt n\),复杂度为 \(T(n) = \mathcal O(q\sqrt n)\)
View Code
using ll = long long;
const int maxn = 1.5e5 + 5;
const int max_block = 405;
int answer[max_block][max_block];
int n, m, B;
int arr[maxn];
void solve() {
cin >> n >> m;
B = sqrt(n);
for (int i = 1; i <= n; i++) {
cin >> arr[i];
for (int mod = 1; mod <= B; mod++) answer[mod][i % mod] += arr[i];
}
while (m--) {
char opt;
int x, y;
cin >> opt >> x >> y;
if (opt == 'C') {
int delta = y - arr[x];
arr[x] = y;
for (int mod = 1; mod <= B; mod++) answer[mod][x % mod] += delta;
} else if (opt == 'A'){
if (x <= B) cout << answer[x][y] << "\n";
else {
int sum = 0;
for (int i = (y == 0 ? x : y); i <= n; i += x) sum += arr[i];
cout << sum << "\n";
}
}
}
}
其余
[P5309 [Ynoi] 初始化]
不对,怎么会有区间查询呢?
首先要维护一个区间和 sum
然后发现这个修改有一点阴
对于大的块,直接按照步长 \(x (x \gt B)\) 跳跃,暴力修改 a 和 sum
对于小的块,如果直接按照步长 \(x (x \le B)\) 跳的话,时间复杂度可能退化为 \(\mathcal O(nq)\),所以考虑存起来
用 prefix[x][r] 表示 步长为 \(x\) 时,所有修改 \(y \le r\) 的贡献
用 suffix[x][r] 表示 步长为 \(x\) 时,所有修改 \(y \ge r\) 的贡献
这样子询问的时候就可以用原数组加上修改的累计贡献快速求出了
时间复杂度 \(\mathcal O((n + q)\sqrt n)\)
View Code
const int maxn = 2e5 + 5;
const ll mod = 1e9 + 7;
struct __block{
ll sum;
int start, end;
} block[maxn];
ll a[maxn];
ll prefix[maxn][500], suffix[maxn][500];
int id[maxn];
int n, m;
int block_size;
ll Summation_of_Block(int l, int r) {
int xb = id[l], yb = id[r];
ll result = 0;
if (xb == yb)
for (int i = l; i <= r; i++) result = (result + a[i]) % mod;
else {
for (int i = l; i <= block[xb].end; i++) result = (result + a[i]) % mod;
for (int i = xb + 1; i < yb; i++) result = (result + block[i].sum) % mod;
for (int i = block[yb].start; i <= r; i++) result = (result + a[i]) % mod;
}
return result;
}
void update(int x, int y, int delta) {
if (x >= block_size) {
for (int i = y; i <= n; i += x) {
a[i] = (a[i] + delta) % mod;
block[id[i]].sum = (block[id[i]].sum + delta);
}
} else {
for (int i = y; i <= x; i++) prefix[x][i] = (prefix[x][i] + delta) % mod;
for (int i = 1; i <= y; i++) suffix[x][i] = (suffix[x][i] + delta) % mod;
}
}
ll query(int x, int y) {
ll result = Summation_of_Block(x, y);
for (int i = 1; i <= block_size; i++) {
ll X = (x - 1) / i + 1, Y = (y - 1) / i + 1;
ll length = Y - X - 1;
if (X == Y) result = (result - prefix[i][(x - 1) % i] + prefix[i][(y - 1) % i + 1]) % mod;
else result = (result + 1ll * length * prefix[i][i] + prefix[i][(y - 1) % i + 1] + suffix[i][(x - 1) % i + 1]) % mod;
}
return (result + mod) % mod;
}
void solve() {
cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> a[i];
block_size = sqrt(n);
int block_count = (n + block_size - 1) / block_size;
for (int i = 1; i <= n; i++) id[i] = (i - 1) / block_size + 1;
for (int i = 1; i <= block_count; i++) {
block[i].start = (i - 1) * block_size + 1;
block[i].end = min(i * block_size, n);
}
for (int i = 1; i <= n; i++)
block[id[i]].sum = (block[id[i]].sum + a[i]) % mod;
while (m--) {
int opt;
cin >> opt;
if (opt == 1) {
int x, y, z;
cin >> x >> y >> z;
update(x, y, z);
} else {
int x, y;
cin >> x >> y;
cout << query(x, y) << "\n";
}
}
}

浙公网安备 33010602011771号