通常把长度为n的数列分成块
在进行区间操作前通常需要预处理以下数据:
- 数组l,l[i]表示第i块的最左边元素的位置
- 数组r,r[i]表示第i块的最右边元素的位置
- 数组pos,pos[i]表示第i个元素在第几个块内
注:若是有多余元素,则多余元素算入最后一个块内,如:有10个元素,则块数为下取整,也就是3个块,此时分块情况为{1,2,3},{4,5,6},{7,8,9,10}
分块算法的精髓在于,对于一次区间操作,对区间内部的整块进行整体的操作,对区间边缘的零散块单独暴力处理
区间加法,单点查值:数列分块入门 1
在对区间[left,right]进行加法操作时,我们可以枚举所有与该区间有交集的块,第i个块与修改区间应该有以下两种关系:
left <= l[i] and r[i] <= right,当前块完全包含于修改区间,我们使add[i] += c,表示第i个块全体元素都加上了c
当前块与修改区间有部分交集,我们直接对这块零散区间进行暴力修改即可,修改次数不会超过次
#include <bits/stdc++.h>
using namespace std;
int d[100005], pos[100005], l[333], r[333], add[333], n, m;
void change(int left, int right, int c) {
for (int i = pos[left]; i <= pos[right]; i++) {
if (left <= l[i] && r[i] <= right) {
add[i] += c;
} else {
for (int j = max(l[i], left); j <= min(r[i], right); j++) {
d[j] += c;
}
}
}
}
int main() {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> d[i];
}
m = sqrt(n);
for (int i = 1; i <= m; i++) {
l[i] = m * (i - 1) + 1;
r[i] = m * i;
}
r[m] = n;
for (int i = 1; i <= m; i++) {
for (int j = l[i]; j <= r[i]; j++) {
pos[j] = i;
}
}
while (n--) {
int op, l, r, c;
cin >> op >> l >> r >> c;
if (op == 0) {
change(l, r, c);
} else {
cout << d[r] + add[pos[r]] << endl;
}
}
return 0;
}
查询区间小于某个值的个数:数列分块入门 2
维护一个对块内元素进行排序的数组sd
进行区间加法操作时会出现以下两种情况:
- left <= l[i] and r[i] <= right,当前块完全包含于修改区间,我们使add[i] += c,表示第i个块全体元素都加上了c
当前块与修改区间有部分交集,我们直接对这块零散区间进行暴力修改,然后重新将它赋值到sd数组中,对sd数组中这个块所在位置重新排序
进行询问操作时,同样会出现两种情况:
- left <= l[i] and r[i] <= right,当前块完全包含于查询区间,我们在块内二分出小于查询元素的个数,并计入总计数中
- 当前块与查询区间有部分交集,我们直接对这块零散区间进行暴力计数,并计入总计数中
#include <bits/stdc++.h>
using namespace std;
int d[100005], sd[100005], pos[100005], l[333], r[333], add[333], n, m;
void change(int left, int right, int c) {
for (int i = pos[left]; i <= pos[right]; i++) {
if (left <= l[i] && r[i] <= right) {
add[i] += c;
} else {
for (int j = max(l[i], left); j <= min(r[i], right); j++) {
d[j] += c;
}
for (int j = l[i]; j <= r[i]; j++) {
sd[j] = d[j];
}
sort(sd + l[i], sd + r[i] + 1);
}
}
}
int ask(int left, int right, int c) {
int cnt = 0;
for (int i = pos[left]; i <= pos[right]; i++) {
if (left <= l[i] && r[i] <= right) {
cnt += lower_bound(sd + l[i], sd + r[i] + 1, c * c - add[i]) - (sd + l[i]);
} else {
for (int j = max(l[i], left); j <= min(r[i], right); j++) {
if (d[j] + add[i] < c * c) {
cnt++;
}
}
}
}
return cnt;
}
int main() {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> d[i];
sd[i] = d[i];
}
m = sqrt(n);
for (int i = 1; i <= m; i++) {
l[i] = m * (i - 1) + 1;
r[i] = m * i;
}
r[m] = n;
for (int i = 1; i <= m; i++) {
for (int j = l[i]; j <= r[i]; j++) {
pos[j] = i;
}
sort(sd + l[i], sd + r[i] + 1);
}
while (n--) {
int op, l, r, c;
cin >> op >> l >> r >> c;
if (op == 0) {
change(l, r, c);
} else {
cout << ask(l, r, c) << endl;
}
}
return 0;
}
区间查询某个值的前驱:数列分块入门 3
思路和上面第2题相差无几了,直接块内排序+二分
#include <bits/stdc++.h>
using namespace std;
int d[100005], sd[100005], pos[100005], l[333], r[333], add[333], n, m;
void change(int left, int right, int c) {
for (int i = pos[left]; i <= pos[right]; i++) {
if (left <= l[i] && r[i] <= right) {
add[i] += c;
} else {
for (int j = max(l[i], left); j <= min(r[i], right); j++) {
d[j] += c;
}
for (int j = l[i]; j <= r[i]; j++) {
sd[j] = d[j];
}
sort(sd + l[i], sd + r[i] + 1);
}
}
}
int ask(int left, int right, int c) {
int res = INT_MIN;
for (int i = pos[left]; i <= pos[right]; i++) {
if (left <= l[i] && r[i] <= right) {
int p = lower_bound(sd + l[i], sd + r[i] + 1, c - add[i]) - sd - 1;
if (left <= p && p <= right && sd[p] + add[i] < c) {
res = max(res, sd[p] + add[i]);
}
} else {
for (int j = max(l[i], left); j <= min(r[i], right); j++) {
if (d[j] + add[i] < c) {
res = max(res, d[j] + add[i]);
}
}
}
}
return res == INT_MIN ? -1 : res;
}
int main() {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> d[i];
sd[i] = d[i];
}
m = sqrt(n);
for (int i = 1; i <= m; i++) {
l[i] = m * (i - 1) + 1;
r[i] = m * i;
}
r[m] = n;
for (int i = 1; i <= m; i++) {
for (int j = l[i]; j <= r[i]; j++) {
pos[j] = i;
}
sort(sd + l[i], sd + r[i] + 1);
}
while (n--) {
int op, l, r, c;
cin >> op >> l >> r >> c;
if (op == 0) {
change(l, r, c);
} else {
cout << ask(l, r, c) << endl;
}
}
return 0;
}
区间加法,区间求和:数列分块入门 4
这个比上面2、3题还简单,只需要在第1题的基础上加一个sum数组,记录每个块内元素的总和,当第i个块完全包含于查询区间时,把sum[i]加到答案里。零散区间就直接暴力
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
ll d[100005], pos[100005], l[333], r[333], add[333], sum[333], n, m;
void change(ll left, ll right, ll c) {
for (ll i = pos[left]; i <= pos[right]; i++) {
if (left <= l[i] && r[i] <= right) {
add[i] += c;
sum[i] += c * (r[i] - l[i] + 1);
} else {
for (ll j = max(l[i], left); j <= min(r[i], right); j++) {
d[j] += c;
sum[i] += c;
}
}
}
}
ll ask(ll left, ll right) {
ll res = 0;
for (ll i = pos[left]; i <= pos[right]; i++) {
if (left <= l[i] && r[i] <= right) {
res += sum[i];
} else {
for (ll j = max(l[i], left); j <= min(r[i], right); j++) {
res += d[j] + add[i];
}
}
}
return res;
}
int main() {
cin >> n;
for (ll i = 1; i <= n; i++) {
cin >> d[i];
}
m = sqrt(n);
for (ll i = 1; i <= m; i++) {
l[i] = m * (i - 1) + 1;
r[i] = m * i;
}
r[m] = n;
for (ll i = 1; i <= m; i++) {
ll t = 0;
for (ll j = l[i]; j <= r[i]; j++) {
pos[j] = i;
t += d[j];
}
sum[i] = t;
}
while (n--) {
ll op, l, r, c;
cin >> op >> l >> r >> c;
if (op == 0) {
change(l, r, c);
} else {
cout << (ask(l, r) % (c + 1)) << endl;
}
}
return 0;
}
区间开方,区间求和:数列分块入门 5
首先需要注意到开方5次就会变成1,然后继续开方就没意义了
所以可以考虑设一个flag数组,记录块内元素是不是全部为0或1
对于区间开方:
- 若是对整块操作,则先看flag[i]是否为true,如果为true,那说明区间内元素都是0或1,对0或1开方等于没开方,所以此时直接跳过就行;如果flag[i]为false,那就暴力对区间所有数进行开方,同时看看是不是开方后能使所有数都为0或1,显然这个暴力过程不会进行太多次,因为一个数最多开方5次就会变成1
- 若是对零散块操作,直接暴力开方
对于区间求和:
- 若是对整块操作,把sum[i]加到答案里
- 若是对零散块操作,暴力
#include <bits/stdc++.h>
using namespace std;
int d[100005], pos[100005], l[333], r[333], flag[333], sum[333], n, m;
void change(int left, int right) {
for (int i = pos[left]; i <= pos[right]; i++) {
if (left <= l[i] && r[i] <= right) {
if (flag[i]) {
continue;
} else {
bool fa = true;
for (int j = l[i]; j <= r[i]; j++) {
int diff = d[j] - (int)sqrt(d[j]);
d[j] -= diff;
sum[i] -= diff;
if (d[j] > 1) fa = false;
}
if (fa) flag[i] = 1;
}
} else {
for (int j = max(l[i], left); j <= min(r[i], right); j++) {
int diff = d[j] - (int)sqrt(d[j]);
d[j] -= diff;
sum[i] -= diff;
}
}
}
}
int ask(int left, int right) {
int res = 0;
for (int i = pos[left]; i <= pos[right]; i++) {
if (left <= l[i] && r[i] <= right) {
res += sum[i];
} else {
for (int j = max(l[i], left); j <= min(r[i], right); j++) {
res += d[j];
}
}
}
return res;
}
int main() {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> d[i];
}
m = sqrt(n);
for (int i = 1; i <= m; i++) {
l[i] = m * (i - 1) + 1;
r[i] = m * i;
}
r[m] = n;
for (int i = 1; i <= m; i++) {
int t = 0;
for (int j = l[i]; j <= r[i]; j++) {
pos[j] = i;
t += d[j];
}
sum[i] = t;
}
while (n--) {
int op, l, r, c;
cin >> op >> l >> r >> c;
if (op == 0) {
change(l, r);
} else {
cout << ask(l, r) << endl;
}
}
return 0;
}
单点插入,单点查询:数列分块入门 6
这题数据是随机的,所以用纯vector实现也是可以过的
纯vector插入O(n),查询O(1)
现在说说vector + 分块的做法,复杂度方面插入O(),查询()。在这个方法中每个块的元素都用一个vector来存,对于每个插入操作,先找到插入位置所在的块,然后再暴力插入;对于每个查询操作也是一样,先找到在哪个块,再输出那个元素
但这方法有个漏洞,如果某个块有大量的单点插入,那这个方法就会退化成纯vector的方法了,所以这里要引入一个操作——重新分块,当块的大小大过某个值的时候,直接把那个块切成两半,引入重新分块后大概快了400ms吧
#include <bits/stdc++.h>
using namespace std;
int d[100005], l[333], r[333], n, m;
vector<vector<int>> v(333);
void rebuild(int pos) {
vector<int> t;
int n = v[pos].size();
for (int i = 0; i < n / 2; i++) {
t.push_back(v[pos].back());
v[pos].pop_back();
}
reverse(t.begin(), t.end());
v.insert(v.begin() + pos + 1, t);
}
void change(int pos, int c) {
int i = 1;
while (pos > v[i].size()) {
pos -= v[i].size();
i++;
}
v[i].insert(v[i].begin() + pos - 1, c);
if (v[i].size() > 10 * m) {
rebuild(i);
}
}
int ask(int pos) {
int i = 1;
while (pos > v[i].size()) {
pos -= v[i].size();
i++;
}
return v[i][pos - 1];
}
int main() {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> d[i];
}
m = sqrt(n);
for (int i = 1; i <= m; i++) {
l[i] = m * (i - 1) + 1;
r[i] = m * i;
}
r[m] = n;
for (int i = 1; i <= m; i++) {
for (int j = l[i]; j <= r[i]; j++) {
v[i].push_back(d[j]);
}
}
while (n--) {
int op, l, r, c;
cin >> op >> l >> r >> c;
if (op == 0) {
change(l, r);
} else {
cout << ask(r) << endl;
}
}
return 0;
}
区间乘法,区间加法,单点询问:数列分块入门 7
这题需要维护两个标记,一个乘法标记,一个加法标记
需要考虑两个标记的优先级,显然设置乘法比加法优先级高比较好思考
对于第i个元素的值 = d[i] * mul[pos[i]] + add[pos[i]]
区间加法:
- 对整块:加法标记加个c
- 对零散块:先把这整块的两个标记都下传,先传乘法标记,再传加法标记,然后将零散区间的每个元素加上c
区间乘法:
- 对整块:乘法标记和加法标记都乘上c
- 对零散块:先把这整块的两个标记都下传,先传乘法标记,再传加法标记,然后将零散区间的每个元素乘上c
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
ll d[100005], pos[100005], l[333], r[333], add[333], mul[333], n, m, mod = 10007;
void change_add(ll left, ll right, ll c) {
for (ll i = pos[left]; i <= pos[right]; i++) {
if (left <= l[i] && r[i] <= right) {
(add[i] += c) %= mod;
} else {
for (ll j = l[i]; j <= r[i]; j++) {
(d[j] *= mul[i]) %= mod;
(d[j] += add[i]) %= mod;
}
mul[i] = 1;
add[i] = 0;
for (ll j = max(l[i], left); j <= min(r[i], right); j++) {
(d[j] += c) %= mod;
}
}
}
}
void change_mul(ll left, ll right, ll c) {
for (ll i = pos[left]; i <= pos[right]; i++) {
if (left <= l[i] && r[i] <= right) {
(mul[i] *= c) %= mod;
(add[i] *= c) %= mod;
} else {
for (ll j = l[i]; j <= r[i]; j++) {
(d[j] *= mul[i]) %= mod;
(d[j] += add[i]) %= mod;
}
mul[i] = 1;
add[i] = 0;
for (ll j = max(l[i], left); j <= min(r[i], right); j++) {
(d[j] *= c) %= mod;
}
}
}
}
int main() {
cin >> n;
for (ll i = 1; i <= n; i++) {
cin >> d[i];
}
m = sqrt(n);
for (ll i = 1; i <= m; i++) {
l[i] = m * (i - 1) + 1;
r[i] = m * i;
}
r[m] = n;
for (ll i = 1; i <= m; i++) {
for (ll j = l[i]; j <= r[i]; j++) {
pos[j] = i;
}
mul[i] = 1;
}
while (n--) {
ll op, l, r, c;
cin >> op >> l >> r >> c;
if (op == 0) {
change_add(l, r, c);
} else if (op == 1) {
change_mul(l, r, c);
} else {
cout << (d[r] * mul[pos[r]] + add[pos[r]]) % mod << endl;
}
}
return 0;
}
区间询问等于c的元素个数,并将这个区间的所有元素改为c:数列分块入门 8
这题需要一个flag数组,用flag[i]表示第i个块里的元素全部是flag[i]。规定flag[i] == 1e18,表示这个数组里的元素不是同一个元素
对整块:
- 块内元素相同,且flag[i] == c,那么块里元素都是查询元素,则把块的长度加到答案里
- 块里元素不相同,则暴力查询块内元素有多少个是c,然后令flag[i] = c,表示将块内元素全部改为c
对零散块:
- 注意若是零散块所在块flag[i] != 1e18,那么要先将标记下传并清空,然后再暴力查询并修改
可能会有同学觉得对整块内暴力复杂度过高,但其实暴力一次过后,那个块就会全部变成同一个元素,之后对这个块的操作就基本没有暴力的情况,只有这个块被当成零散块操作,之后才可能再对它进行暴力,但每次区间操作最多产生两个零散块,这样综合来看复杂度应该是没问题的。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
ll d[100005], pos[100005], l[333], r[333], flag[333], n, m;
ll ask(ll left, ll right, ll c) {
ll res = 0;
for (ll i = pos[left]; i <= pos[right]; i++) {
if (left <= l[i] && r[i] <= right) {
if (flag[i] != 1e18) {
if (flag[i] == c) {
res += r[i] - l[i] + 1;
}
} else {
for (ll j = l[i]; j <= r[i]; j++) {
if (d[j] == c) {
res++;
}
}
}
flag[i] = c;
} else {
if (flag[i] != 1e18) {
for (ll j = l[i]; j <= r[i]; j++) {
d[j] = flag[i];
}
flag[i] = 1e18;
}
for (ll j = max(l[i], left); j <= min(r[i], right); j++) {
if (d[j] == c) {
res++;
}
d[j] = c;
}
}
}
return res;
}
int main() {
cin >> n;
for (ll i = 1; i <= n; i++) {
cin >> d[i];
}
m = sqrt(n);
for (ll i = 1; i <= m; i++) {
l[i] = m * (i - 1) + 1;
r[i] = m * i;
}
r[m] = n;
for (ll i = 1; i <= m; i++) {
for (ll j = l[i]; j <= r[i]; j++) {
pos[j] = i;
}
flag[i] = 1e18;
}
while (n--) {
ll l, r, c;
cin >> l >> r >> c;
cout << ask(l, r, c) << endl;
}
return 0;
}
区间的最小众数:数列分块入门 9
???心态炸裂 ???
本题数据挺强的,调了很久,终于不WA了,不过还是TLE,WA和TLE加起来有12页了,不想写了,有空再回来改吧
思路如下:
一个区间的最小众数可能是以下两种情况:
- 所有完整块的最小众数
- 零散块里的某个数
可以使用unordered_map<int, vector<int>> c,存放某个数出现的下标
这样要查询某个数出现的次数,直接在vector里二分就行
还要预处理二维数组f,f[i][j]表示块i到块j的最小众数
如果分块超时可以适当调整块长,调整方式见下方代码
#include <bits/stdc++.h>
using namespace std;
int d[100005], pos[100005], l[2333], r[2333], f[2333][2333], n, m;
unordered_map<int, vector<int>> c;
template <typename T> void inline read(T &x) {
int f = 1; x = 0; char s = getchar();
while (s < '0' || s > '9') { if (s == '-') f = -1; s = getchar(); }
while (s <= '9' && s >= '0') x = x * 10 + (s ^ 48), s = getchar();
x *= f;
}
template <typename T> void print(T x) {
if (x < 0) { putchar('-'); print(x); return ; }
if (x >= 10) print(x / 10);
putchar((x % 10) + '0');
}
int get(int left, int right, int v) {
return upper_bound(c[v].begin(), c[v].end(), right) - lower_bound(c[v].begin(), c[v].end(), left);
}
int ask(int left, int right) {
int le = (left == l[pos[left]] ? pos[left] : pos[left] + 1);
int ri = (right == r[pos[right]] ? pos[right] : pos[right] - 1);
int res = f[le][ri], max_cnt = get(left, right, res);
for (int i = left; i <= r[pos[left]]; i++) {
int cnt = get(left, right, d[i]);
if (cnt > max_cnt || (cnt == max_cnt && res > d[i])) {
res = d[i];
max_cnt = cnt;
}
}
for (int i = l[pos[right]]; i <= right; i++) {
int cnt = get(left, right, d[i]);
if (cnt > max_cnt || (cnt == max_cnt && res > d[i])) {
res = d[i];
max_cnt = cnt;
}
}
return res;
}
int main() {
read(n);
for (int i = 1; i <= n; i++) {
read(d[i]);
c[d[i]].push_back(i);
}
int block_len = 80;
int m = (n + block_len - 1) / block_len;
for (int i = 1; i <= m; i++) {
l[i] = block_len * (i - 1) + 1;
r[i] = block_len * i;
}
r[m] = n;
for (int i = 1; i <= m; i++) {
for (int j = l[i]; j <= r[i]; j++) {
pos[j] = i;
}
}
for (int i = 1; i <= m; i++) {
unordered_map<int, int> cn;
int num, max_cnt = 0;
for (int j = i; j <= m; j++) {
for (int k = l[j]; k <= r[j]; k++) {
cn[d[k]]++;
if (cn[d[k]] > max_cnt || (cn[d[k]] == max_cnt && num > d[k])) {
num = d[k];
max_cnt = cn[d[k]];
}
}
f[i][j] = num;
}
}
while (n--) {
int l, r;
read(l), read(r);
print(ask(l, r));
puts("");
}
return 0;
}