分块入门系列总结
数列分块入门 \(1\)
区间加法、单点查值
显然,这是一道分块的入门模板。对于每一个整块,我们维护一个加法标记,表示这个块被整体加上的值。对于被操作的区间,我们将其拆分成若干整块和至多两个不完整块,对于不完整块中的元素直接进行操作,对于完整块则直接修改加法标记即可。每次查询时,直接返回原本数列中的元素与其所属块的加法标记即可。
#include <cstdio>
#include <cmath>
using namespace std;
const int maxn = 5e5 + 5;
const int maxm = 2e3 + 5;
int n;
int a[maxn], st[maxm], ed[maxm];
int bel[maxn], lazy[maxm];
inline int read() {
int res = 0, flag = 1;
char ch = getchar();
while (ch < '0' || ch > '9') {
if (ch == '-')
flag = -1;
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
res = res * 10 + ch - '0';
ch = getchar();
}
return res * flag;
}
inline void update(int l, int r, int x) {
if (bel[l] == bel[r])
for (register int i = l; i <= r; i++)
a[i] += x;
else {
for (register int i = l; i <= ed[bel[l]]; i++)
a[i] += x;
for (register int i = st[bel[r]]; i <= r; i++)
a[i] += x;
for (register int i = bel[l] + 1; i < bel[r]; i++)
lazy[i] += x;
}
}
signed main() {
int opt, l, r, c;
n = read();
int block = sqrt(n);
int size = ceil(n * 1.0 / block);
for (register int i = 1; i <= size; i++) {
st[i] = (i - 1) * block + 1;
ed[i] = i * block;
}
ed[size] = n;
for (register int i = 1; i <= n; i++) {
a[i] = read();
bel[i] = (i - 1) / block + 1;
}
for (register int i = 1; i <= n; i++) {
opt = read();
l = read();
r = read();
c = read();
if (opt == 0)
update(l, r, c);
else
printf("%d\n", a[r] + lazy[bel[r]]);
}
return 0;
}
数列分块入门 \(2\)
区间加法、询问区间内小于某个值 \(x\) 的元素
这道题运用分块基本思想中的 二分 。假设现在有一个 升序序列 \(a\) ,要求出区间 \([l, r]\) 中小于值 \(x\) 的元素个数。显然,设第一个大于等于 \(x\) 的元素下标为 \(p\) ,第一个大于 \(x\) 的元素的下标为 \(q\) ,则 \(q - 1\) 就是最后一个等于 \(x\) 的元素的下标。所以等于 \(x\) 的元素个数为 \(q - 1 - p + 1\) ,也就是 \(q - p\) 。
由此,我们可以想到这道题的正解。同上一道题一样,对于每一个整块都维护一个加法标记。另外维护一个数组 \(b\) ,对于 \(b\) 来说,\(b\) 的元素与给定数组 \(a\) 的元素相同,每一个整块对应的区间都是有序的,但是整个数组不一定有序。
接着,区间加法操作按相同方法维护,区别在于在修改完序列 \(a\) 后,我们还需要把 \(a\) 赋值到 \(b\) ,同时维护序列 \(b\) 的有序性。每次查询时,如果左右两侧存在不完整的块,那么我们就暴力扫描一遍序列 \(a\) 中两侧不完整块的区间,统计小于 \(x\) 的元素个数。对于每一个完整的块,我们利用 lower_bound
和 upper_bound
函数求出上段中提到的信息,然后求出小于 \(x\) 的元素个数。注意,此时还未考虑到加法标记。因此,我们在二分的时候需要查询的值为 $x\ - $ 加法标记。
#include <cstdio>
#include <cmath>
#include <algorithm>
using namespace std;
const int maxn = 5e4 + 5;
const int maxm = 2e3 + 5;
int n;
int st[maxm], ed[maxm], bel[maxn];
long long a[maxn], b[maxn], lazy[maxm];
inline void update(int l, int r, long long x) {
if (bel[l] == bel[r]) {
for (register int i = l; i <= r; i++) {
a[i] += x;
}
for (register int i = st[bel[l]]; i <= ed[bel[l]]; i++) {
b[i] = a[i];
}
sort(b + st[bel[l]], b + ed[bel[l]] + 1);
} else {
for (register int i = l; i <= ed[bel[l]]; i++) {
a[i] += x;
}
for (register int i = st[bel[l]]; i <= ed[bel[l]]; i++) {
b[i] = a[i];
}
sort(b + st[bel[l]], b + ed[bel[l]] + 1);
for (register int i = st[bel[r]]; i <= r; i++) {
a[i] += x;
}
for (register int i = st[bel[r]]; i <= ed[bel[r]]; i++) {
b[i] = a[i];
}
sort(b + st[bel[r]], b + ed[bel[r]] + 1);
for (register int i = bel[l] + 1; i < bel[r]; i++) {
lazy[i] += x;
}
}
}
inline int query(int l, int r, long long x) {
int ans = 0, pos;
if (bel[l] == bel[r]) {
for (register int i = l; i <= r; i++) {
if (a[i] + lazy[bel[i]] < x) {
ans++;
}
}
} else {
for (register int i = l; i <= ed[bel[l]]; i++) {
if (a[i] + lazy[bel[i]] < x) {
ans++;
}
}
for (register int i = st[bel[r]]; i <= r; i++) {
if (a[i] + lazy[bel[i]] < x) {
ans++;
}
}
for (register int i = bel[l] + 1; i < bel[r]; i++) {
pos = lower_bound(b + st[i], b + ed[i] + 1, x - lazy[i]) - b;
ans += pos - st[i];
}
}
return ans;
}
int main() {
int opt, l, r;
long long c;
scanf("%d", &n);
int block = sqrt(n);
int cnt = ceil(n * 1.0 / block);
for (register int i = 1; i <= cnt; i++) {
st[i] = (i - 1) * block + 1;
ed[i] = i * block;
}
ed[cnt] = n;
for (register int i = 1; i <= n; i++) {
scanf("%lld", &a[i]);
b[i] = a[i];
bel[i] = (i - 1) / block + 1;
}
for (register int i = 1; i <= cnt; i++) {
sort(b + st[i], b + ed[i] + 1);
}
for (register int i = 1; i <= n; i++) {
scanf("%d%d%d%lld", &opt, &l, &r, &c);
if (opt == 0) {
update(l, r, c);
} else {
printf("%d\n", query(l, r, c * c));
}
}
return 0;
}
数列分块入门 \(3\)
区间加法、查询前驱
同样利用二分思想。维护加法标记同上题相同,具体差异在于查询时维护的是前驱的值,而且二分时取的下标是 lower_bound
得到的下标 \(- 1\) ,并且无论二分还是暴力都要考虑 加法标记 ,具体思路见代码。
#include <cstdio>
#include <cmath>
#include <algorithm>
using namespace std;
const int maxn = 1e5 + 5;
const int maxm = 1e3 + 5;
int n;
int st[maxm], ed[maxm], bel[maxn];
long long a[maxn], b[maxn], lazy[maxm];
void update(int l, int r, long long x) {
if (bel[l] == bel[r]) {
for (int i = l; i <= r; i++) {
a[i] += x;
}
for (int i = st[bel[l]]; i <= ed[bel[l]]; i++) {
b[i] = a[i];
}
sort(b + st[bel[l]], b + ed[bel[l]] + 1);
} else {
for (int i = l; i <= ed[bel[l]]; i++) {
a[i] += x;
}
for (int i = st[bel[l]]; i <= ed[bel[l]]; i++) {
b[i] = a[i];
}
sort(b + st[bel[l]], b + ed[bel[l]] + 1);
for (int i = st[bel[r]]; i <= r; i++) {
a[i] += x;
}
for (int i = st[bel[r]]; i <= ed[bel[r]]; i++) {
b[i] = a[i];
}
sort(b + st[bel[r]], b + ed[bel[r]] + 1);
for (int i = bel[l] + 1; i < bel[r]; i++) {
lazy[i] += x;
}
}
}
long long query(int l, int r, long long x) {
int pos;
long long val = -1;
if (bel[l] == bel[r]) {
for (int i = l; i <= r; i++) {
if (a[i] + lazy[bel[i]] < x) {
val = max(val, a[i] + lazy[bel[i]]);
}
}
} else {
for (int i = l; i <= ed[bel[l]]; i++) {
if (a[i] + lazy[bel[i]] < x) {
val = max(val, a[i] + lazy[bel[i]]);
}
}
for (int i = st[bel[r]]; i <= r; i++) {
if (a[i] + lazy[bel[i]] < x) {
val = max(val, a[i] + lazy[bel[i]]);
}
}
for (int i = bel[l] + 1; i < bel[r]; i++) {
if (b[st[i]] + lazy[i] >= x) {
continue;
}
pos = lower_bound(b + st[i], b + ed[i] + 1, x - lazy[i]) - b;
val = max(val, b[pos - 1] + lazy[i]);
}
}
return val;
}
int main() {
int opt, l, r;
long long c;
scanf("%d", &n);
int block = sqrt(n);
int cnt = ceil(n * 1.0 / block);
for (int i = 1; i <= cnt; i++) {
st[i] = (i - 1) * block + 1;
ed[i] = i * block;
}
ed[cnt] = n;
for (int i = 1; i <= n; i++) {
scanf("%lld", &a[i]);
bel[i] = (i - 1) / block + 1;
}
for (int i = 1; i <= cnt; i++) {
for (int j = st[i]; j <= ed[i]; j++) {
b[j] = a[j];
}
sort(b + st[i], b + ed[i] + 1);
}
for (int i = 1; i <= n; i++) {
scanf("%d%d%d%lld", &opt, &l, &r, &c);
if (opt == 0) {
update(l, r, c);
} else {
printf("%lld\n", query(l, r, c));
}
}
return 0;
}
数列分块入门 \(4\)
这道题使用了分块基本思想中的 维护 。我们不仅维护整块的加法标记,同时维护整块的元素之和。这样,就可以在 \(O(1)\) 的时间内求出整块的和。
考虑区间加法对于区间和的影响。显然,如果操作的是不完整块,我们直接在操作单个元素时维护整块和即可。如果操作一个完整块,显然区间和会增加区间长度 \(\times\) 被操作数,直接维护即可,注意取模。
#include <cstdio>
#include <cmath>
using namespace std;
const int maxn = 5e4 + 5;
const int maxm = 1e3 + 5;
int n;
int st[maxm], ed[maxm], bel[maxn];
long long a[maxn], sum[maxm], lazy[maxm];
void update(int l, int r, long long x) {
if (bel[l] == bel[r]) {
for (int i = l; i <= r; i++) {
a[i] += x;
sum[bel[i]] += x;
}
} else {
for (int i = l; i <= ed[bel[l]]; i++) {
a[i] += x;
sum[bel[i]] += x;
}
for (int i = st[bel[r]]; i <= r; i++) {
a[i] += x;
sum[bel[i]] += x;
}
for (int i = bel[l] + 1; i < bel[r]; i++) {
lazy[i] += x;
sum[i] += (ed[i] - st[i] + 1) * x;
}
}
}
long long query(int l, int r, long long mod) {
long long ans = 0;
mod++;
if (bel[l] == bel[r]) {
for (int i = l; i <= r; i++) {
ans = (ans + a[i] + lazy[bel[i]]) % mod;
}
} else {
for (int i = l; i <= ed[bel[l]]; i++) {
ans = (ans + a[i] + lazy[bel[i]]) % mod;
}
for (int i = st[bel[r]]; i <= r; i++) {
ans = (ans + a[i] + lazy[bel[i]]) % mod;
}
for (int i = bel[l] + 1; i < bel[r]; i++) {
ans = (ans + sum[i]) % mod;
}
}
return ans;
}
int main() {
int opt, l, r;
long long c;
scanf("%d", &n);
int block = sqrt(n);
int cnt = ceil(n * 1.0 / block);
for (int i = 1; i <= cnt; i++) {
st[i] = (i - 1) * block + 1;
ed[i] = i * block;
}
ed[cnt] = n;
for (int i = 1; i <= n; i++) {
scanf("%lld", &a[i]);
bel[i] = (i - 1) / block + 1;
sum[bel[i]] += a[i];
}
for (int i = 1; i <= n; i++) {
scanf("%d%d%d%lld", &opt, &l, &r, &c);
if (opt == 0) {
update(l, r, c);
} else {
printf("%lld\n", query(l, r, c));
}
}
return 0;
}
数组分块入门 \(5\)
区间开方、区间求和
这道题巧妙地运用了三大基本思想之一的 维护 。显然,假如待开方的数 \(\leq 1\) ,我们不需要对它进行开方。因此,我们可以维护一个块内最大值,假如块内最大元素 \(\leq 1\) ,我们不对这个块进行操作。否则,我们暴力地对整块中的每一个元素进行开方,同时重新维护整块最大值以及整块和。
严格意义上来说,这道题的时间复杂度十分玄学。但是,因为值域较小,相应的开方次数也会较小。因此,开了 \(O2\) 以后还是可以 卡进时限 \(AC\) 的。
#include <cstdio>
#include <cmath>
#include <algorithm>
using namespace std;
const int maxn = 5e4 + 5;
const int maxm = 1e3 + 5;
const long long inf = 1e16 + 5;
int n;
int st[maxm], ed[maxm], bel[maxn];
long long a[maxn], sum[maxm], val[maxm];
void update(int l, int r) {
if (bel[l] == bel[r]) {
if (val[bel[l]] <= 1) {
return;
}
for (int i = l; i <= r; i++) {
sum[bel[i]] = sum[bel[i]] - a[i] + sqrt(a[i]);
a[i] = sqrt(a[i]);
}
val[bel[l]] = -inf;
for (int i = st[bel[l]]; i <= ed[bel[l]]; i++) {
val[bel[l]] = max(val[bel[l]], a[i]);
}
} else {
if (val[bel[l]] > 1) {
for (int i = l; i <= ed[bel[l]]; i++) {
sum[bel[i]] = sum[bel[i]] - a[i] + sqrt(a[i]);
a[i] = sqrt(a[i]);
}
val[bel[l]] = -inf;
for (int i = st[bel[l]]; i <= ed[bel[l]]; i++) {
val[bel[l]] = max(val[bel[l]], a[i]);
}
}
if (val[bel[r]] > 1) {
for (int i = st[bel[r]]; i <= r; i++) {
sum[bel[i]] = sum[bel[i]] - a[i] + sqrt(a[i]);
a[i] = sqrt(a[i]);
}
val[bel[r]] = -inf;
for (int i = st[bel[r]]; i <= ed[bel[r]]; i++) {
val[bel[i]] = max(val[bel[i]], a[i]);
}
}
for (int i = bel[l] + 1; i < bel[r]; i++) {
if (val[i] > 1) {
for (int j = st[i]; j <= ed[i]; j++) {
sum[i] = sum[i] - a[j] + sqrt(a[j]);
a[j] = sqrt(a[j]);
}
val[i] = sqrt(val[i]);
}
}
}
}
long long query(int l, int r) {
long long ans = 0;
if (bel[l] == bel[r]) {
for (int i = l; i <= r; i++) {
ans += a[i];
}
} else {
for (int i = l; i <= ed[bel[l]]; i++) {
ans += a[i];
}
for (int i = st[bel[r]]; i <= r; i++) {
ans += a[i];
}
for (int i = bel[l] + 1; i < bel[r]; i++) {
ans += sum[i];
}
}
return ans;
}
int main() {
int opt, l, r, c;
scanf("%d", &n);
int block = sqrt(n);
int cnt = ceil(n * 1.0 / block);
for (int i = 1; i <= cnt; i++) {
st[i] = (i - 1) * block + 1;
ed[i] = i * block;
val[i] = -inf;
}
ed[cnt] = n;
for (int i = 1; i <= n; i++) {
scanf("%lld", &a[i]);
bel[i] = (i - 1) / block + 1;
sum[bel[i]] += a[i];
val[bel[i]] = max(val[bel[i]], a[i]);
}
for (int i = 1; i <= n; i++) {
scanf("%d%d%d%d", &opt, &l, &r, &c);
if (opt == 0) {
update(l, r);
} else {
printf("%lld\n", query(l, r));
}
}
return 0;
}
数组分块入门 \(6\)
单点插入、单点查询
因为数组的长度会变动,所以显然不可以用数组和传统分块来维护。自然想到用 vector
来维护每一个块,每次插入时直接暴力地使用 vector
的 insert
函数来插入。但是,如果任由数据插入的话,块长显然会失衡,分块也就没有了意义。因此,当块长失衡,也就是单块过长时,我们直接暴力重新分块。至于块长失衡的条件,这里设定成单块的块长超过了 \(20 \times \sqrt{n}\) ,亲测不会导致超时。
#include <cstdio>
#include <cmath>
#include <vector>
using namespace std;
const int maxn = 2e5 + 5;
const int maxm = 1e3 + 5;
int n, block, size;
int a[maxn], bel[maxn];
vector<int> v[maxm];
void rebuild()
{
int cnt = 0, sz;
for (int i = 1; i <= size; i++)
{
sz = v[i].size();
for (int j = 0; j < sz; j++)
a[++cnt] = v[i][j];
v[i].clear();
}
if (!cnt)
cnt = n;
block = sqrt(cnt);
size = ceil(cnt * 1.0 / block);
for (int i = 1; i <= cnt; i++)
bel[i] = (i - 1) / block + 1;
for (int i = 1; i <= cnt; i++)
v[bel[i]].push_back(a[i]);
}
void update(int pos, int val)
{
int x = 1;
while (x <= size && pos > v[x].size())
{
pos -= v[x].size();
x++;
}
v[x].insert(v[x].begin() + pos - 1, val);
if (v[x].size() > 5 * block)
rebuild();
}
int query(int pos)
{
int x = 1;
while (x <= size && pos > v[x].size())
{
pos -= v[x].size();
x++;
}
return v[x][pos - 1];
}
int main()
{
// freopen("a8.in", "r", stdin);
// freopen("a8.ans", "w", stdout);
int opt, l, r, c;
scanf("%d", &n);
for (int i = 1; i <= n; i++)
scanf("%d", &a[i]);
rebuild();
for (int i = 1; i <= n; i++)
{
scanf("%d%d%d%d", &opt, &l, &r, &c);
if (opt == 0)
update(l, r);
else
printf("%d\n", query(r));
}
return 0;
}
数组分块入门 \(7\)
区间乘法、区间加法、单点询问
显然,像线段树的模板一样,我们需要维护加法标记和乘法标记。相应的,两个标记的修改优先级需要讨论。当我们修改加法标记,原本的乘法标记不会有任何改变。但是,当我们修改乘法标记的时候,原本的序列值和加法标记都要相应地改变。
我们可以这样大致理解,设加法标记为 \(a\) ,乘法标记为 \(m\) ,原本的值为 \(x\) ,新乘的值为 \(n\) 。则修改前,这个单点的值为 \(x \times m + a\) ,修改后,这个值变成了 \(n \times (x \times m + a)\) ,乘法分配律后得到 \(n \times x \times m + n \times a\) 。
另外,每次对不完整块进行操作时,我们都需要先把原本的标记叠加到原序列上。这样做的原因是假如不先把乘法标记叠加在加法标记上,先加后乘的操作就会出错,也就是我们询问时就会访问到乘法之前的乘法标记。
#include <cstdio>
#include <cmath>
using namespace std;
const int maxn = 1e5 + 5;
const int maxm = 1e3 + 5;
const int mod = 10007;
int n;
int bel[maxn], st[maxm], ed[maxm];
long long a[maxn], add[maxm], mul[maxm];
void clear(int x)
{
for (int i = st[x]; i <= ed[x]; i++)
a[i] = ((a[i] * mul[x]) % mod + add[x]) % mod;
mul[x] = 1;
add[x] = 0;
}
void add_num(int l, int r, long long c)
{
if (bel[l] == bel[r])
{
clear(bel[l]);
for (int i = l; i <= r; i++)
a[i] = (a[i] + c) % mod;
}
else
{
clear(bel[l]);
for (int i = l; i <= ed[bel[l]]; i++)
a[i] = (a[i] + c) % mod;
clear(bel[r]);
for (int i = st[bel[r]]; i <= r; i++)
a[i] = (a[i] + c) % mod;
for (int i = bel[l] + 1; i < bel[r]; i++)
add[i] = (add[i] + c) % mod;
}
}
void mul_num(int l, int r, long long c)
{
if (bel[l] == bel[r])
{
clear(bel[l]);
for (int i = l; i <= r; i++)
a[i] = (a[i] * c) % mod;
}
else
{
clear(bel[l]);
for (int i = l; i <= ed[bel[l]]; i++)
a[i] = (a[i] * c) % mod;
clear(bel[r]);
for (int i = st[bel[r]]; i <= r; i++)
a[i] = (a[i] * c) % mod;
for (int i = bel[l] + 1; i < bel[r]; i++)
{
mul[i] = (mul[i] * c) % mod;
add[i] = (add[i] * c) % mod;
}
}
}
long long query(int pos)
{
return ((a[pos] * mul[bel[pos]]) % mod + add[bel[pos]]) % mod;
}
int main()
{
int opt, l, r;
long long c;
scanf("%d", &n);
int block = sqrt(n);
int size = ceil(n * 1.0 / block);
for (int i = 1; i <= size; i++)
{
st[i] = (i - 1) * block + 1;
ed[i] = i * block;
mul[i] = 1;
}
ed[size] = n;
for (int i = 1; i <= n; i++)
{
scanf("%lld", &a[i]);
bel[i] = (i - 1) / block + 1;
}
for (int i = 1; i <= n; i++)
{
scanf("%d%d%d%lld", &opt, &l, &r, &c);
if (opt == 0)
add_num(l, r, c);
else if (opt == 1)
mul_num(l, r, c);
else
printf("%lld\n", query(r));
}
return 0;
}
数列分块入门 \(8\)
区间赋值、查询与 \(x\) 相等的元素个数
这道题与分块入门 \(2\) 相似,但是使用相同的方法却会超时。这道题将询问和修改融合在一起,因此,我们可以将代码稍微进行修改,整合成一个整体的函数。
我们维护一个标记,含义是整块被赋值成的值,初始值为无穷大,代表没有被赋值过。对于每次查询,若左右两侧存在不完整块,则我们暴力扫描一遍,若当前元素与 \(x\) 相等,则相等元素个数 \(+ \ 1\) 。否则,我们把当前元素修改成 \(x\) 。
对于整块的查询类似。若当前块存在赋值标记,那么我们直接判断标记是否与 \(x\) 相等,若相等,说明整块都与 \(x\) 相等,计数器加上块长;否则,直接把当前块的标记修改成 \(x\) 。如果当前块没有赋值标记,我们再次暴力查询、修改,最后别忘了修改标记。
与上一道题相同,这道题也需要在每次操作前还原标记。hao
这道题的算法复杂度十分玄学,但是仔细一想,假如原本序列是有序的,每次操作至多令开头和结尾两个块失去有序性。也就是说,我们至少用 \(\sqrt{n}\) 个操作才能令序列失去有序性。并且 \(\sqrt{n}\) 个操作之后,我们的操作要么直接扫描的时间复杂度优于 \(O(\sqrt{n})\) ,要么就会无形中维护了序列的有序性。假如序列原本无序,那么我们的操作反而有可能帮助维护序列的有序性。这样一想,玄学的复杂度似乎也可以接受了。具体的证明,请前往开头提到的 \(hzwer\) 学长的博客。
#include <cstdio>
#include <cmath>
using namespace std;
#define int long long
const int maxn = 1e5 + 5;
const int maxm = 1e3 + 5;
const int inf = 1e18;
int n;
int a[maxn], st[maxm], ed[maxm];
int bel[maxn], lazy[maxm];
void clear(int x)
{
if (lazy[x] == inf)
return;
for (int i = st[x]; i <= ed[x]; i++)
a[i] = lazy[x];
lazy[x] = inf;
}
int solve(int l, int r, int c)
{
int cnt = 0;
if (bel[l] == bel[r])
{
clear(bel[l]);
for (int i = l; i <= r; i++)
{
if (a[i] == c)
cnt++;
else
a[i] = c;
}
}
else
{
clear(bel[l]);
for (int i = l; i <= ed[bel[l]]; i++)
{
if (a[i] == c)
cnt++;
else
a[i] = c;
}
clear(bel[r]);
for (int i = st[bel[r]]; i <= r; i++)
{
if (a[i] == c)
cnt++;
else
a[i] = c;
}
for (int i = bel[l] + 1; i < bel[r]; i++)
{
if (lazy[i] != inf)
{
if (lazy[i] == c)
cnt += (ed[i] - st[i] + 1);
else
lazy[i] = c;
}
else
{
for (int j = st[i]; j <= ed[i]; j++)
{
if (a[j] == c)
cnt++;
else
a[j] = c;
}
lazy[i] = c;
}
}
}
return cnt;
}
signed main()
{
int l, r, c;
scanf("%lld", &n);
int block = sqrt(n);
int size = ceil(n * 1.0 / block);
for (int i = 1; i <= size; i++)
{
st[i] = (i - 1) * block + 1;
ed[i] = i * block;
lazy[i] = inf;
}
ed[size] = n;
for (int i = 1; i <= n; i++)
{
scanf("%lld", &a[i]);
bel[i] = (i - 1) / block + 1;
}
for (int i = 1; i <= n; i++)
{
scanf("%lld%lld%lld", &l, &r, &c);
printf("%lld\n", solve(l, r, c));
}
return 0;
}
数列分块入门 \(9\)
静态查询区间最小众数
这道题是分块入门系列的压轴题,难度直接上天。在说明思路之前,先解释一个显然的现象:区间 \([l, r]\) 的最小众数,只有可能是左右两侧不完整块的最小众数和中间完整块的最小众数。所以,我们可以考虑暴力枚举这些众数,再一个个加以判断。
但是这样做的时间复杂度还是太坏了。我们考虑优化这个算法。注意到查询是 静态 的,这意味着我们可以 预处理 。设 \(f_{i, j}\) 为从块 \(i\) 到 块 \(j\) 的最小众数。我们在查询的时候可以直接调用预处理好的 \(f\) 来快速求出中间完整块的最小众数。
但是这样做的时间复杂度还是太坏了。我们在判断是否更新最小众数的时候需要扫一遍整个块,时间复杂度自然爆炸。期望在 \(O(logn)\) 的复杂度内更新最小众数,我们考虑一种乱搞做法。先将原序列 离散化 ,然后对于每一个排名,我们都用一个 vector
保存它出现过的下标。这样,我们就可以通过与分块入门 \(2\) 类似的思想来求出区间 \([l, r]\) 中排名 \(x\) 出现过的次数。
值得注意的是,我们需要维护两个映射:值到离散化排名、离散化排名到值。这里使用 map
实在没有必要,乱搞即可。以及这道题的块长十分玄学,使用 \(\sqrt{n}\) 会原地爆炸,亲测 \([150, 300]\) 是可以通过的区间。其他块长不保证可以通过。
#include <cstdio>
#include <cmath>
#include <cstring>
#include <map>
#include <vector>
#include <algorithm>
using namespace std;
const int maxn = 1e5 + 5;
const int maxm = 1e3 + 5;
int n;
int st[maxm], ed[maxm], f[maxm][maxm];
int a[maxn], val[maxn], cnt[maxn], bel[maxn];
map<int, int> mp;
vector<int> v[maxn];
int read()
{
int res = 0, flag = 1;
char ch = getchar();
while (ch < '0' || ch > '9')
{
if (ch == '-')
flag = -1;
ch = getchar();
}
while (ch >= '0' && ch <= '9')
{
res = res * 10 + ch - '0';
ch = getchar();
}
return res * flag;
}
int get_num(int l, int r, int x)
{
return upper_bound(v[x].begin(), v[x].end(), r) - lower_bound(v[x].begin(), v[x].end(), l);
}
void pre_work(int x)
{
int ans = 0, res = 0;
memset(cnt, 0, sizeof(cnt));
for (int i = st[x]; i <= n; i++)
{
cnt[a[i]]++;
if (cnt[a[i]] > res)
{
res = cnt[a[i]];
ans = a[i];
}
else if (cnt[a[i]] == res && val[a[i]] < val[ans])
ans = a[i];
f[x][bel[i]] = ans;
}
}
int query(int l, int r)
{
int ans = 0, res, max_res = 0;
if (bel[l] == bel[r])
{
for (int i = l; i <= r; i++)
{
res = get_num(l, r, a[i]);
if (res > max_res)
{
max_res = res;
ans = a[i];
}
else if (res == max_res && val[a[i]] < val[ans])
ans = a[i];
}
}
else
{
ans = f[bel[l] + 1][bel[r] - 1];
max_res = get_num(l, r, ans);
for (int i = l; i <= ed[bel[l]]; i++)
{
res = get_num(l, r, a[i]);
if (res > max_res)
{
max_res = res;
ans = a[i];
}
else if (res == max_res && val[a[i]] < val[ans])
ans = a[i];
}
for (int i = st[bel[r]]; i <= r; i++)
{
res = get_num(l, r, a[i]);
if (res > max_res)
{
max_res = res;
ans = a[i];
}
else if (res == max_res && val[a[i]] < val[ans])
ans = a[i];
}
}
return val[ans];
}
int main()
{
int l, r, tot = 0;
n = read();
int block = 150;
int size = ceil(n * 1.0 / block);
for (int i = 1; i <= size; i++)
{
st[i] = (i - 1) * block + 1;
ed[i] = i * block;
}
ed[size] = n;
for (int i = 1; i <= n; i++)
{
a[i] = read();
bel[i] = (i - 1) / block + 1;
if (!mp.count(a[i]))
{
mp[a[i]] = ++tot;
val[tot] = a[i];
}
a[i] = mp[a[i]];
v[a[i]].push_back(i);
}
for (int i = 1; i <= size; i++)
pre_work(i);
for (int i = 1; i <= n; i++)
{
l = read();
r = read();
printf("%d\n", query(l, r));
}
return 0;
}