*【学习笔记】(9) 基础分块
分块思想
引用一下 oi-wiki 的话:
分块的基本思想是:通过对原数据的适当划分,并在划分后的每一个块上预处理部分信息,从而较一般的暴力算法取得更优的时间复杂度。
数列/序列分块
引入
#6280. 数列分块入门 4
给定一长度为
的数列,有 次操作。
操作分为两种:区间加,查询区间和。
。
划分
首先将序列按每
划分过程的常用模板:
len = T; //根据实际要求选择一个合适的大小。 cnt = (n - 1) / len + 1; for(int i = 1; i <= cnt; ++ i) { //分配块左右边界。 L[i] = R[i - 1] + 1; R[i] = min(n, L[i] + len - 1); } //分配元素所属的块编号。 for(int i = 1; i <= n; ++i) pos[i] = (i - 1) / len + 1;
查询
设查询区间
-
若 l,r 在同一块内,直接暴力求和即可。块长为
,单次查询复杂度上界 。 -
否则答案由三部分构成:
- 以
开头的不完整块的和。 - 以
结尾的不完整块的和。 - 中间的完整块的和。
- 以
对于
。
对于
单次查询总复杂度上界
修改
修改操作的过程与查询操作相同,设修改区间为
-
若
在同一块内,直接暴力修改,并更新区间和。块长为 ,单次修改复杂度上界 。 -
否则将修改区间划分成三部分:
- 以
开头的不完整块。 - 以
结尾的不完整块。 - 中间的完整块。
- 以
单次修改复杂度上界
复杂度分析
总复杂度为
由均值不等式,
此时块大小为
#include<bits/stdc++.h> #define int long long using namespace std; const int N = 5e4 + 5; inline int read(){ int x = 0, f = 1; char ch = getchar(); while(ch < '0' || ch > '9'){if(ch == '-') f = -f; ch = getchar();} while(ch >= '0' && ch <= '9'){x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar();} return x * f; } int n, len, cnt; int a[N], L[N], R[N], pos[N], del[N], sum[N]; void add(int l, int r, int c){ if(pos[l] == pos[r]){ for(int i = l; i <= r; ++i) a[i] += c, sum[pos[i]] += c; return ; } for(int i = l; i <= R[pos[l]]; ++i) a[i] += c, sum[pos[i]] += c; for(int i = pos[l] + 1; i < pos[r]; ++i) del[i] += c; for(int i = L[pos[r]]; i <= r; ++i) a[i] += c, sum[pos[i]] += c; } int ask(int l, int r, int c){ int res = 0; if(pos[l] == pos[r]){ for(int i = l; i <= r; ++i) (res += a[i] + del[pos[i]]) %= c; return res; } for(int i = l; i <= R[pos[l]]; ++i) (res += a[i] + del[pos[i]]) %= c; for(int i = pos[l] + 1; i < pos[r]; ++i) (res += sum[i] + del[i] * (R[i] - L[i] + 1)) %= c; for(int i = L[pos[r]]; i <= r; ++i) (res += a[i] + del[pos[i]]) %= c; return res; } signed main(){ n = read(); len = sqrt(n), cnt = (n - 1) / len + 1; for(int i = 1; i <= n; ++i) a[i] = read(), pos[i] = (i - 1) / len + 1, sum[pos[i]] += a[i]; for(int i = 1; i <= cnt; ++i) L[i] = R[i - 1] + 1, R[i] = min(n, L[i] + len - 1); while(n--){ int opt = read(), l = read(), r = read(), c = read(); if(!opt) add(l, r, c); else printf("%lld\n", ask(l, r, c + 1)); } return 0; }
练习
#6277. 数列分块入门 1
区间加,单点查询
直接暴力分块
完整块 修改永久懒标记
两端不完整块暴力修改元素值
单点查询值 = 元素值 + 懒标记
完整块数量不超过
, 两不完整块总长度不超过
总复杂度
#include<bits/stdc++.h> #define int long long using namespace std; const int N = 5e4 + 5; inline int read(){ int x = 0, f = 1; char ch = getchar(); while(ch < '0' || ch > '9'){if(ch == '-') f = -f; ch = getchar();} while(ch >= '0' && ch <= '9'){x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar();} return x * f; } int n, len, cnt; int a[N], L[N], R[N], pos[N], del[N]; void add(int l, int r, int c){ if(pos[l] == pos[r]){ for(int i = l; i <= r; ++i) a[i] += c; return ; } for(int i = l; i <= R[pos[l]]; ++i) a[i] += c; for(int i = pos[l] + 1; i < pos[r]; ++i) del[i] += c; for(int i = L[pos[r]]; i <= r; ++i) a[i] += c; } int ask(int l){ return a[l] + del[pos[l]]; } signed main(){ n = read(); len = sqrt(n), cnt = (n - 1) / len + 1; for(int i = 1; i <= n; ++i) a[i] = read(), pos[i] = (i - 1) / len + 1; for(int i = 1; i <= cnt; ++i) L[i] = R[i - 1] + 1, R[i] = min(n, L[i] + len - 1); while(n--){ int opt = read(), l = read(), r = read(), c = read(); if(!opt) add(l, r, c); else printf("%lld\n", ask(r)); } return 0; }
#6278. 数列分块入门 2
区间加,区间查询小于给定值 元素数
同
- 先不考虑 修改操作 :
不完整块 可直接暴力查询
对于完整块, 查询小于给定值元素数, 易联想到 lower_bound 操作
由于分块后单个块较小, 则可直接暴力排序, 再通过 lower_bound 求得元素数 - 再考虑 修改操作
同样, 不完整块 可直接进行暴力修改
对于完整块, 区间加后, 不影响它们之间的排名, 排序后 顺序不变
进行区间加 后 再进行区间查询 , 与 在区间加 之前, 区间查询 , 得到的结果相同
则可使用类似 的懒惰标记法 进行区间修改, 并根据懒标记调整查询的值
由上, 则需记录 未排序的数列 与 排序后的数列(需要进行 lower_bound)
可使用 vector 类, 来储存每一排序后的完整块
-
修改时 :
不完整块 暴力修改未排序数列, 将 原排序后的数列 重新赋值并排序 (为了便于 查询)
其总长度不超过 , 单次排序复杂度
完整块直接更新 懒标记即可, 其个数 不超过
则单次修改总复杂度为 -
查询时 :
不完整块 暴力查询未排序数列, 其总长度不超过
完整块 对排序后数列进行 lower_bound, 其个数 不超过 , 单次 lower_bound 复杂度为
则单次修改总复杂度也为
在预处理分块时 需要对整个数列进行排序预处理, 复杂度
则上述算法 总复杂度为
#include<bits/stdc++.h> #define int long long using namespace std; const int N = 5e4 + 5; inline int read(){ int x = 0, f = 1; char ch = getchar(); while(ch < '0' || ch > '9'){if(ch == '-') f = -f; ch = getchar();} while(ch >= '0' && ch <= '9'){x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar();} return x * f; } int n, len, cnt; int a[N], L[N], R[N], pos[N], del[N]; vector<int> E[505]; void reset(int x){ E[x].clear(); for(int i = L[x]; i <= R[x]; ++i) E[x].push_back(a[i]); sort(E[x].begin(), E[x].end()); } void change(int l, int r, int c){ if(pos[l] == pos[r]){ for(int i = l; i <= r; ++i) a[i] += c; reset(pos[l]); return ; } for(int i = l; i <= R[pos[l]]; ++i) a[i] += c; reset(pos[l]); for(int i = pos[l] + 1; i < pos[r]; ++i) del[i] += c; for(int i = L[pos[r]]; i <= r; ++i) a[i] += c; reset(pos[r]); } int ask(int l, int r, int c){ int res = 0; if(pos[l] == pos[r]){ for(int i = l; i <= r; ++i) res += (a[i] + del[pos[i]] < c); return res; } for(int i = l; i <= R[pos[l]]; ++i) res += (a[i] + del[pos[i]] < c); for(int i = pos[l] + 1; i < pos[r]; ++i) res += lower_bound(E[i].begin(), E[i].end(), c - del[i]) - E[i].begin(); for(int i = L[pos[r]]; i <= r; ++i) res += (a[i] + del[pos[i]] < c); return res; } signed main(){ n = read(); len = sqrt(n), cnt = (n - 1) / len + 1; for(int i = 1; i <= n; ++i) a[i] = read(), pos[i] = (i - 1) / len + 1; for(int i = 1; i <= n; ++i) E[pos[i]].push_back(a[i]); for(int i = 1; i <= cnt; ++i) sort(E[i].begin(), E[i].end()); for(int i = 1; i <= cnt; ++i) L[i] = R[i - 1] + 1, R[i] = min(n, L[i] + len - 1); while(n--){ int opt = read(), l = read(), r = read(), c = read(); if(!opt) change(l, r, c); else printf("%lld\n", ask(l, r, c * c)); } return 0; }
#6279. 数列分块入门 3
区间加,区间查询小于给定值 最大元素
-
算法1 :
套用 数列分块2 的做法, 在进行lower_bound时 求得每个块中 小于给定值 最大元素,
再将多个块的答案合并 取最大值.
总复杂度 -
算法2 :
查询前驱 ? 来发Splay
可以对每个块 都维护一个 set , 来代替算法 1 中的排序数组.
重赋值和插入操作 都会变得更方便 .
#include<bits/stdc++.h> #define int long long #define iter multiset<int>::iterator using namespace std; const int N = 1e5 + 5; inline int read(){ int x = 0, f = 1; char ch = getchar(); while(ch < '0' || ch > '9'){if(ch == '-') f = -f; ch = getchar();} while(ch >= '0' && ch <= '9'){x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar();} return x * f; } int n, len, cnt; int a[N], L[N], R[N], pos[N], del[N]; multiset<int> E[1010]; void change(int l, int r, int c){ if(pos[l] == pos[r]){ for(int i = l; i <= r; ++i) E[pos[l]].erase(a[i]), a[i] += c, E[pos[l]].insert(a[i]); return ; } for(int i = l; i <= R[pos[l]]; ++i) E[pos[l]].erase(a[i]), a[i] += c, E[pos[l]].insert(a[i]); for(int i = pos[l] + 1; i < pos[r]; ++i) del[i] += c; for(int i = L[pos[r]]; i <= r; ++i) E[pos[r]].erase(a[i]), a[i] += c, E[pos[r]].insert(a[i]); } int ask(int l, int r, int c){ int res = -1; if(pos[l] == pos[r]){ for(int i = l; i <= r; ++i) if(a[i] + del[pos[i]] < c) res = max(res, a[i] + del[pos[i]]); return res; } for(int i = l; i <= R[pos[l]]; ++i) if(a[i] + del[pos[i]] < c) res = max(res, a[i] + del[pos[i]]); for(int i = pos[l] + 1; i < pos[r]; ++i){ iter it = E[i].lower_bound(c - del[i]); if(it == E[i].begin()) continue; res = max(res, *(--it) + del[i]); } for(int i = L[pos[r]]; i <= r; ++i) if(a[i] + del[pos[i]] < c) res = max(res, a[i] + del[pos[i]]); return res; } signed main(){ n = read(); len = sqrt(n), cnt = (n - 1) / len + 1; for(int i = 1; i <= n; ++i) a[i] = read(), pos[i] = (i - 1) / len + 1; for(int i = 1; i <= n; ++i) E[pos[i]].insert(a[i]); for(int i = 1; i <= cnt; ++i) L[i] = R[i - 1] + 1, R[i] = min(n, L[i] + len - 1); while(n--){ int opt = read(), l = read(), r = read(), c = read(); if(!opt) change(l, r, c); else printf("%lld\n", ask(l, r, c)); } return 0; }
#6280. 数列分块入门 4
区间加, 区间求和
上面有了。
#6281. 数列分块入门 5
区间开方,区间求和
对于求和操作 :
同 T4 , 维护完整块元素和.
不完整块暴力查询, 完整块查询 整块元素和.
显然, 单次求和复杂度为
- 对于修改操作 :
不完整块 直接暴力开方 .
对于完整块 :
由于开方运算不满足 一系列运算律, 无法使用标记法.
怎么办? 考虑直接暴力.
众所周知,
.
对于一个数, 最多 5 次修改后 必然等于 1 或 0 .
对于一个块, 最多 5 次修改后 必然全部由 1 或 0 组成 .
而 , , 修改操作对其不会有影响 .
则可以标记一个完整块是否全为 1/0.
若全为 1/0, 则无论其修改次数, 元素和都不会改变, 不进行修改操作.
否则 直接暴力修改, 并在暴力修改时 完成对标记的更新.
由于每个完整块的修改次数 , 而不完整块的大小 .
则所有修改操作 总复杂度最大为 .
#include<bits/stdc++.h> #define int long long #define iter multiset<int>::iterator using namespace std; const int N = 1e5 + 5; inline int read(){ int x = 0, f = 1; char ch = getchar(); while(ch < '0' || ch > '9'){if(ch == '-') f = -f; ch = getchar();} while(ch >= '0' && ch <= '9'){x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar();} return x * f; } int n, len, cnt; int a[N], L[N], R[N], pos[N], del[N], sum[N]; bool flag[N]; void solve(int x){ if(flag[x]) return ; sum[x] = 0, flag[x] = 1; for(int i = L[x]; i <= R[x]; ++i){ a[i] = sqrt(a[i]), sum[x] += a[i]; if(a[i] > 1) flag[x] = 0; } } void change(int l, int r, int c){ if(pos[l] == pos[r]){ for(int i = l; i <= r; ++i) sum[pos[i]] -= a[i], a[i] = sqrt(a[i]), sum[pos[i]] += a[i]; return ; } for(int i = l; i <= R[pos[l]]; ++i) sum[pos[i]] -= a[i], a[i] = sqrt(a[i]), sum[pos[i]] += a[i]; for(int i = pos[l] + 1; i < pos[r]; ++i) solve(i); for(int i = L[pos[r]]; i <= r; ++i) sum[pos[i]] -= a[i], a[i] = sqrt(a[i]), sum[pos[i]] += a[i]; } int ask(int l, int r){ int res = 0; if(pos[l] == pos[r]){ for(int i = l; i <= r; ++i) res += a[i]; return res; } for(int i = l; i <= R[pos[l]]; ++i) res += a[i]; for(int i = pos[l] + 1; i < pos[r]; ++i) res += sum[i]; for(int i = L[pos[r]]; i <= r; ++i) res += a[i]; return res; } signed main(){ n = read(); len = sqrt(n), cnt = (n - 1) / len + 1; for(int i = 1; i <= n; ++i) a[i] = read(), pos[i] = (i - 1) / len + 1, sum[pos[i]] += a[i]; for(int i = 1; i <= cnt; ++i) L[i] = R[i - 1] + 1, R[i] = min(n, L[i] + len - 1); while(n--){ int opt = read(), l = read(), r = read(), c = read(); if(!opt) change(l, r, c); else printf("%lld\n", ask(l, r)); } return 0; }
#6282. 数列分块入门 6
单点插入,单点查询
众所周知 , vector 的 insert 操作, 复杂度与 vector 内元素个数 呈正比.
则可使用分块 来构建多个vector , 以降低插入复杂度 :
插入时 枚举所有块, 将元素插入指定位置所在块中, 单次复杂度近似
查询时 枚举所有块, 在指定位置所在块中查询 , 单次复杂度近似
总复杂度近似
上述复杂度分析 都是基于 随机数据 的前提下的.
如果出题人恶意构造类似下方的数据 :
100000 1 1 1 1 1 1 1 1 1 1 1 1 ...
上述做法会出现 不断插入同一块的情况 ,
单次插入会被卡到
考虑 如何才能使各块的大小相对平均, 避免产生上述问题.
直接再分一遍块不就行了 !
为块的大小设定上限 , 在插入同时判断块的大小是否达到上限.
若达到上限, 枚举所有元素, 重新进行分块, 以平均块的大小.
#include<bits/stdc++.h> #define int long long using namespace std; const int N = 1e5 + 5; inline int read(){ int x = 0, f = 1; char ch = getchar(); while(ch < '0' || ch > '9'){if(ch == '-') f = -f; ch = getchar();} while(ch >= '0' && ch <= '9'){x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar();} return x * f; } int n, len, cnt; int a[N], pos[N]; vector<int> E[N], tmp; void rebuild(){ tmp.clear(); for(int i = 1; i <= cnt; ++i){ int ll = E[i].size(); for(int j = 0; j < ll; ++j) tmp.push_back(E[i][j]); E[i].clear(); } len = sqrt(tmp.size()), cnt = (tmp.size() - 1) / len + 1; for(int i = 0; i < tmp.size(); ++i) E[i / len + 1].push_back(tmp[i]); return ; } void insert(int x, int val){ int pos1 = 1, pos2 = x; while(pos2 > E[pos1].size()) pos2 -= E[pos1].size(), ++pos1; E[pos1].insert(E[pos1].begin() + pos2 - 1, val); if(E[pos1].size() > 10 * len) rebuild(); return ; } int ask(int x){ int pos1 = 1, pos2 = x; while(pos2 > E[pos1].size()) pos2 -= E[pos1].size(), ++pos1; return E[pos1][pos2 - 1]; } signed main(){ n = read(); len = sqrt(n), cnt = (n - 1) / len + 1; for(int i = 1; i <= n; ++i) a[i] = read(), pos[i] = (i - 1) / len + 1; for(int i = 1; i <= n; ++i) E[pos[i]].push_back(a[i]); while(n--){ int opt = read(), l = read(), r = read(), c = read(); if(!opt) insert(l, r); else printf("%lld\n", ask(r)); } return 0; }
#6283. 数列分块入门 7
区间加,区间乘,单点查询
如果只有区间乘操作, 只需要将 T1 的 + 改为 * 即可.
但是 区间加区间乘 同时存在, 需记录 两懒标记, 就需要注意懒标记与 数列值之间的关系
(以下, 使用
对于完整块的两修改操作 :
-
当出现 先乘后加 时, 有 :
.
若只修改 数列值 x , 则会出现: . -
当出现 先加后乘 时, 有 :
.
若只修改 数列值, 则会出现: .
若只修改 乘法懒标记, 则会出现: .
进行修改操作时 要进行多方面的考虑, 需要进行分类讨论
吗, 当然是不需要的.
不完整块较小 , 可以直接暴力处理 .
先通过修改前的懒标记 求得整个块各元素值 , 并清空懒标记,
再暴力修改数列值 , 即可避免上述情况发生.
对于完整块的两修改操作 :
- 区间加时, 直接修改加法懒标记即可.
- 区间乘时, 根据 乘法结合律有 :
.
则将 加法标记 与 乘法标记同乘即可.
单点查询, 即输出
完整块数量不超过
综上, 总复杂度为
#include<bits/stdc++.h> #define int long long using namespace std; const int N = 1e5 + 5, mod = 1e4 + 7; inline int read(){ int x = 0, f = 1; char ch = getchar(); while(ch < '0' || ch > '9'){if(ch == '-') f = -f; ch = getchar();} while(ch >= '0' && ch <= '9'){x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar();} return x * f; } int n, len, cnt; int a[N], L[N], R[N], pos[N], del1[N], del2[N]; void reset(int x){ for(int i = L[x]; i <= R[x]; ++i) a[i] = (a[i] * del2[x] + del1[x]) % mod; del2[x] = 1, del1[x] = 0; } void add1(int l, int r, int c){ if(pos[l] == pos[r]){ reset(pos[l]); for(int i = l; i <= r; ++i) a[i] = (a[i] + c) % mod; return ; } reset(pos[l]); for(int i = l; i <= R[pos[l]]; ++i) a[i] = (a[i] + c) % mod; for(int i = pos[l] + 1; i < pos[r]; ++i) del1[i] = (del1[i] + c) % mod; reset(pos[r]); for(int i = L[pos[r]]; i <= r; ++i) a[i] = (a[i] + c) % mod; } void add2(int l, int r, int c){ if(pos[l] == pos[r]){ reset(pos[l]); for(int i = l; i <= r; ++i) a[i] = a[i] * c % mod; return ; } reset(pos[l]); for(int i = l; i <= R[pos[l]]; ++i) a[i] = a[i] * c % mod; for(int i = pos[l] + 1; i < pos[r]; ++i) del1[i] = del1[i] * c % mod, del2[i] = del2[i] * c % mod; reset(pos[r]); for(int i = L[pos[r]]; i <= r; ++i) a[i] = a[i] * c % mod; } int ask(int x){ return (a[x] * del2[pos[x]] % mod + del1[pos[x]]) % mod; } signed main(){ n = read(); len = sqrt(n), cnt = (n - 1) / len + 1; for(int i = 1; i <= n; ++i) a[i] = read(), pos[i] = (i - 1) / len + 1; for(int i = 1; i <= cnt; ++i) L[i] = R[i - 1] + 1, R[i] = min(n, L[i] + len - 1), del2[i] = 1; while(n--){ int opt = read(), l = read(), r = read(), c = read(); if(!opt) add1(l, r, c); else if(opt == 1) add2(l, r, c); else if(opt == 2) printf("%lld\n", ask(r)); } return 0; }
#6283. 数列分块入门 8
区间赋值,查询区间等于给定值元素数
同T6 , 还是先想暴力.
对于修改操作, 不完整块 直接暴力修改 , 完整块打懒标记.
单次修改操作 , 复杂度显然为
对于查询操作, 不完整块在暴力修改时 顺便暴力查询, 完整块分下列两种情况讨论 :
1. 有懒标记 , 则块内元素相同 , 直接判断标记与查询值 是否相同. 2. 无懒标记 , 没有什么好的处理方法 , 直接暴力查询.
若使一次查询, 其完整块查询操作 复杂度变为
而一次修改操作 , 只能使修改区间两端的 2 不完整块 懒标记清空,
若使一区间完整块懒标记全部清空, 需要先经过
单次查询操作, 处理不完整块复杂度为
而对于完整块的处理, 在无懒标记的情况下 可能会被卡到
最多只会有
故上述算法 总复杂度 近似
#include<bits/stdc++.h> #define int long long using namespace std; const int N = 1e5 + 5; inline int read(){ int x = 0, f = 1; char ch = getchar(); while(ch < '0' || ch > '9'){if(ch == '-') f = -f; ch = getchar();} while(ch >= '0' && ch <= '9'){x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar();} return x * f; } int n, len, cnt; int a[N], L[N], R[N], pos[N], same[N]; void reset(int x){ if(same[x] == -1) return ; for(int i = L[x]; i <= R[x]; ++i) a[i] = same[x]; same[x] = -1; } int solve(int l, int r, int c){ int res = 0; if(pos[l] == pos[r]){ reset(pos[l]); for(int i = l; i <= r; ++i) res += (a[i] == c), a[i] = c; return res; } reset(pos[l]); for(int i = l; i <= R[pos[l]]; ++i) res += (a[i] == c), a[i] = c; for(int i = pos[l] + 1; i < pos[r]; ++i){ if(same[i] != -1) res += (R[i] - L[i] + 1) * (same[i] == c); else for(int j = L[i]; j <= R[i]; ++j) res += (a[j] == c); same[i] = c; } reset(pos[r]); for(int i = L[pos[r]]; i <= r; ++i) res += (a[i] == c), a[i] = c; return res; } signed main(){ n = read(); len = sqrt(n), cnt = (n - 1) / len + 1; for(int i = 1; i <= n; ++i) a[i] = read(), pos[i] = (i - 1) / len + 1; for(int i = 1; i <= cnt; ++i) L[i] = R[i - 1] + 1, R[i] = min(n, L[i] + len - 1), same[i] = -1; while(n--){ int l = read(), r = read(), c = read(); printf("%lld\n", solve(l, r, c)); } return 0; }
#6283. 数列分块入门 9
查询区间最小众数
预处理出两个数组:
如何预处理
对于
对于
如果
如果
对于不完整块,直接暴力计算
对于完整块,就是之前预处理的
众数数量就是 码风有点不大一样,应该也是看得懂的。
#include<bits/stdc++.h> #define N 100005 using namespace std; void read(int &o){ int x=0,f=1; char ch=getchar(); while(ch<'0'||ch>'9'){if(ch=='-') f=-1;ch=getchar();} while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();} o=x*f; } int n,m,len,cnt; int a[N],b[N]; int l[N],r[N]; int sum[N]; int id[405][405],p[405][405],s[405][N]; //p[i][j] 表示块i 到 块j 的最小众数个数 s[i][j] 前i块 j 出现次数 int Get(int x){ int k=x/len; k+=(x%len?1:0); return k; } int main(){ read(n); len=sqrt(n); cnt=(n-1)/len+1; for(int i=1;i<=n;i++) read(a[i]),b[i]=a[i]; sort(b+1,b+1+n); int tot=unique(b+1,b+1+n)-b-1; for(int i=1;i<=cnt;i++) l[i]=r[i-1]+1,r[i]=(i==cnt?n:l[i]+len-1); for(int i=1;i<=n;i++) a[i]=lower_bound(b+1,b+1+tot,a[i])-b; for(int i=1;i<=cnt;i++){ int B[N]={0},ID=0,P=0; for(int j=i;j<=cnt;j++){ for(int k=l[j];k<=r[j];k++){ B[a[k]]++; if(B[a[k]]>P){ P=B[a[k]]; ID=a[k]; }else if(B[a[k]]==P) ID=min(a[k],ID); } id[i][j]=ID,p[i][j]=P; } } for(int i=1;i<=cnt;i++){ for(int j=1;j<=n;j++) s[i][a[j]]=s[i-1][a[j]]; for(int j=l[i];j<=r[i];j++) s[i][a[j]]++; } int x=0; while(n--){ int ll,rr,ans=0; read(ll),read(rr); memset(sum,0,sizeof(sum)); int ql=Get(ll),qr=Get(rr); if(qr-ql<=2){ for(int i=ll;i<=rr;i++){ sum[a[i]]++; if(sum[ans]<sum[a[i]]) ans=a[i]; else if(sum[a[i]]==sum[ans]) ans=min(ans,a[i]); } printf("%d\n",x=b[ans]); }else{ ans=id[ql+1][qr-1]; for(int i=ll;i<=r[ql];i++) sum[a[i]]++; for(int i=l[qr];i<=rr;i++) sum[a[i]]++; int maxnum=0,maxn=0; for(int i=ll;i<=r[ql];i++){ int val=s[qr-1][a[i]]-s[ql][a[i]]+sum[a[i]]; if(val>maxn){ maxn=val; maxnum=a[i]; }else if(val==maxn) maxnum=min(maxnum,a[i]); } for(int i=l[qr];i<=rr;i++){ int val=s[qr-1][a[i]]-s[ql][a[i]]+sum[a[i]]; if(val>maxn){ maxn=val; maxnum=a[i]; }else if(maxn==val) maxnum=min(maxnum,a[i]); } int ss=sum[ans]+p[ql+1][qr-1]; if(maxn>ss) ans=maxnum; else if(maxn==ss) ans=min(ans,maxnum); printf("%d\n",x=b[ans]); } } return 0; }
P4135 作诗
询问区间出现次数为正偶数的数的个数。
其实与区间众数那道题类似,实现预处理出块与块之间的答案,再根据散块内的值分类讨论进行修改。
记得位运算时要加括号来保证优先级,调了好久。
点击查看代码
#include<bits/stdc++.h> #define int long long using namespace std; const int N = 1e5 + 51; inline int read(){ int x = 0, f = 1; char ch = getchar(); while(ch < '0' || ch > '9'){if(ch == '-') f = -f; ch = getchar();} while(ch >= '0' && ch <= '9'){x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar();} return x * f; } int n, len, cnt, q, ans, c, top; int a[N], L[N], R[N], pos[N], num[351][N], f[351][351]; //num[i][j] 表示第 i 块到 n 中,j 的出现次数 f[i][j] 表示第 i 块到第 j 块的答案 int stk[N], cc[N]; signed main(){ n = read(), c = read(), q = read(); len = sqrt(n), cnt = (n - 1) / len + 1; for(int i = 1; i <= n; ++i) a[i] = read(), pos[i] = (i - 1) / len + 1; for(int i = 1; i <= cnt; ++i) L[i] = R[i - 1] + 1, R[i] = min(n, L[i] + len - 1); for(int i = 1; i <= cnt; ++i){ int t = 0; for(int j = L[i]; j <= n; ++j){ ++num[i][a[j]]; if(num[i][a[j]] > 1 && (num[i][a[j]] & 1)) --t; else if((num[i][a[j]] & 1) == 0) ++t; f[i][pos[j]] = t; } } while(q--){ int l = (read() + ans) % n + 1, r = (read() + ans) % n + 1; if(l > r) swap(l, r); ans = 0; if(pos[l] == pos[r]){ for(int i = l; i <= r; ++i) ++cc[a[i]], stk[++top] = a[i]; while(top){ if(cc[stk[top]]) ans += (cc[stk[top]] & 1) ^ 1; cc[stk[top--]] = 0; } printf("%d\n", ans); continue; } if(pos[l] + 1 <= pos[r] - 1) ans = f[pos[l] + 1][pos[r] - 1]; for(int i = l; i <= R[pos[l]]; ++i) ++cc[a[i]], stk[++top] = a[i]; for(int i = L[pos[r]]; i <= r; ++i) ++cc[a[i]], stk[++top] = a[i]; while(top){ int t = stk[top]; if(cc[t] != 0){ if(num[pos[l] + 1][t] - num[pos[r]][t] > 0 && ((num[pos[l] + 1][t] - num[pos[r]][t]) & 1) == 0 && (cc[t] & 1)) ans--; else if(num[pos[l] + 1][t] - num[pos[r]][t] > 0 && ((num[pos[l] + 1][t] - num[pos[r]][t]) & 1) && (cc[t] & 1)) ans++; else if(num[pos[l] + 1][t] - num[pos[r]][t] == 0 && ((cc[t] & 1) == 0)) ans++; } cc[stk[top--]] = 0; } printf("%d\n", ans); } return 0; }
值域分块
顾名思义,就是将值域分块,统计 值域为
P4396 [AHOI2013]作业
求区间
区间询问显然可以用莫队来实现,对于值域
点击查看代码
#include <bits/stdc++.h> using namespace std; const int N = 1e6 + 51; inline int read() { int x = 0, f = 1; char ch = getchar(); while (ch < '0' || ch > '9') { if (ch == '-') f = -f; ch = getchar(); } while (ch >= '0' && ch <= '9') { x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar(); } return x * f; } int n, m, len, cnt; int a[N], pos[N], L[N], R[N], ans[N], res[N]; // ans[i] 是不同数的种类 // res[i] 是数的数量 int sumr[N], sum[N], suma[N]; // sum[i] 表示 i 这个值的数量 // sumr[i] 表示 pos[i] 这个块的 res // suma[i] 表示 pos[i] 这个块的 ans struct Query { int id, l, r, a, b; bool operator<(const Query &A) const { return pos[l] ^ pos[A.l] ? pos[l] < pos[A.l] : r < A.r; } } q[N]; inline void add(int x) { ++sum[a[x]], ++sumr[pos[a[x]]]; if (sum[a[x]] <= 1) ++suma[pos[a[x]]]; } inline void del(int x) { --sum[a[x]], --sumr[pos[a[x]]]; if (sum[a[x]] <= 0) --suma[pos[a[x]]]; } inline int get_ans(int l, int r) { int ss = 0; if (l > r) return 0; if (pos[l] == pos[r]) { for (int i = l; i <= r; ++i) ss += (sum[i] >= 1); return ss; } for (int i = l; i <= R[pos[l]]; ++i) ss += (sum[i] >= 1); for (int i = pos[l] + 1; i <= pos[r] - 1; ++i) ss += suma[i]; for (int i = L[pos[r]]; i <= r; ++i) ss += (sum[i] >= 1); return ss; } inline int get_res(int l, int r) { int ss = 0; if (l > r) return 0; if (pos[l] == pos[r]) { for (int i = l; i <= r; ++i) ss += sum[i]; return ss; } for (int i = l; i <= R[pos[l]]; ++i) ss += sum[i]; for (int i = pos[l] + 1; i <= pos[r] - 1; ++i) ss += sumr[i]; for (int i = L[pos[r]]; i <= r; ++i) ss += sum[i]; return ss; } int main() { n = read(), m = read(); len = sqrt(n); for (int i = 1; i <= n; ++i) a[i] = read(), pos[i] = (i - 1) / len + 1; for (int i = 1; i <= m; ++i) q[i] = (Query){ i, read(), read(), read(), read() }; sort(q + 1, q + 1 + m); for (int i = 1; i <= n; ++i) L[i] = R[i - 1] + 1, R[i] = min(n, L[i] + len - 1); int l = 1, r = 0; for (int i = 1; i <= m; ++i) { while (r < q[i].r) add(++r); while (l > q[i].l) add(--l); while (l < q[i].l) del(l++); while (r > q[i].r) del(r--); res[q[i].id] = get_res(q[i].a, q[i].b); ans[q[i].id] = get_ans(q[i].a, q[i].b); } for (int i = 1; i <= m; ++i) printf("%d %d\n", res[i], ans[i]); return 0; }
参考资料: 「笔记」分块
本文作者:南风未起
本文链接:https://www.cnblogs.com/jiangchen4122/p/17417627.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步