数列分块
\(\text{loj-6277}/\text{luogu-13976}\)
数列分块入门 \(1\)。
给出一个长为 \(n\) 的数列,以及 \(n\) 个操作,操作涉及区间加法,单点查值。
\(1 \le n \le 5 \times 10^4\),\(-2^{31} \le \text{others},\text{ans} \le 2^{31} - 1\)。
法一:
区间修改、单点查询很容易想到线段树,但实际上树状数组就可以维护,维护差分数组即可。
非常好写,也通俗易懂:
#include<iostream>
#include<cstdio>
using namespace std;
#define MAXN 50005
long long read() {
long long x = 0, f = 1;
char c = getchar();
while(c > 57 || c < 48) { if(c == 45) f = -1; c = getchar(); }
while(c >= 48 && c <= 57) { x = (x << 1) + (x << 3) + (c - 48); c = getchar(); }
return x * f;
}
long long n, op, l, r, k, t[MAXN];
long long lowbit(long long x) { return x & (-x); }
void add(long long x, long long k) {
while(x <= n) t[x] += k, x += lowbit(x);
return;
}
long long query(long long x) {
long long res = 0;
while(x) res += t[x], x -= lowbit(x);
return res;
}
int main() {
n = read();
for(int i = 1; i <= n; i ++) {
long long x = read();
add(i, x), add(i + 1, -x);
}
for(int i = 1; i <= n; i ++) {
op = read(), l = read(), r = read(), k = read();
if(op) cout << query(r) << "\n";
else add(l, k), add(r + 1, -k);
}
return 0;
}
法二:
数列分块的题显然有分块的做法,这道题算是分块的板子了。
首先需要记录一下每个点属于哪个块:
for(int i = 1; i <= n; i ++)
a[i] = read(), pos[i] = (i - 1) / len + 1;
接着是区间修改部分,逻辑很清晰:
void update(long long l, long long r, long long k) {
long long pl = pos[l], pr = pos[r];
if(pl == pr) {
for(int i = l; i <= r; i ++) a[i] += k;
return;
}
for(int i = pl + 1; i < pr; i ++) tg[i] += k;
for(int i = l; pos[i] == pl; i ++) a[i] += k;
for(int i = r; pos[i] == pr; i --) a[i] += k;
return;
}
查询也很简单:
long long query(long long x) { return a[x] + tg[pos[x]]; }
这样就写完了。
\(\text{loj-6278}/\text{luogu-13977}\)
数列分块入门 \(2\)。
给出一个长为 \(n\) 的数列,以及 \(n\) 个操作,操作涉及区间加法,询问区间内小于某个值 \(x\) 的元素个数。
\(1 \le n \le 5 \times 10^4\),\(-2^{31} \le \text{others},\text{ans} \le 2^{31} - 1\)。
其实稍微转化一下就好了,我们的要求是接近 \(O(1)\) 求出一个块中 \(< x\) 的个数。
显然没办法做到 \(O(1)\),因为这个东西完全没法预处理,但是考虑排序后二分,可以做到 \(O(\log \sqrt n)\)。
这样总时间复杂度是 \(O(n(T \log T + \frac{n}{T} \log T))\),在 \(T = \sqrt n\) 时取得最小值。
此时时间复杂度是 \(O(n \sqrt n \times \log n)\)。
#include<iostream>
#include<cstdio>
#include<vector>
#include<cmath>
#include<algorithm>
using namespace std;
#define MAXN 50005
long long read() {
long long x = 0, f = 1;
char c = getchar();
while(c > 57 || c < 48) { if(c == 45) f = -1; c = getchar(); }
while(c >= 48 && c <= 57) { x = (x << 1) + (x << 3) + (c - 48); c = getchar(); }
return x * f;
}
long long n, len, mk, a[MAXN], pos[MAXN], tg[MAXN], L[MAXN], R[MAXN];
vector<long long> v[MAXN];
void pushup(long long x) {
v[x].clear();
for(int i = L[x]; i <= R[x]; i ++) v[x].push_back(a[i]);
sort(v[x].begin(), v[x].end());
return;
}
void update(long long l, long long r, long long k) {
long long pl = pos[l], pr = pos[r];
if(pl == pr) {
for(int i = l; i <= r; i ++) a[i] += k;
pushup(pl); return;
}
for(int i = l; i <= R[pl]; i ++) a[i] += k;
for(int i = L[pr]; i <= r; i ++) a[i] += k;
for(int i = pl + 1; i < pr; i ++) tg[i] += k;
pushup(pl), pushup(pr);
return;
}
long long query(long long l, long long r, long long k) {
long long pl = pos[l], pr = pos[r], res = 0;
if(pl == pr) {
for(int i = l; i <= r; i ++) res += (a[i] + tg[pl] < k);
return res;
}
for(int i = l; i <= R[pl]; i ++) res += (a[i] + tg[pl] < k);
for(int i = L[pr]; i <= r; i ++) res += (a[i] + tg[pr] < k);
for(int i = pl + 1; i < pr; i ++) {
long long x = lower_bound(v[i].begin(), v[i].end(), k - tg[i]) - v[i].begin();
res += x;
}
return res;
}
int main() {
n = read(), len = sqrt(n);
for(int i = 1; i <= n; i ++)
a[i] = read(), pos[i] = (i - 1) / len + 1,
v[pos[i]].push_back(a[i]);
mk = pos[n];
for(int i = 1; i <= mk; i ++)
L[i] = (i - 1) * len + 1, R[i] = min(i * len, n),
sort(v[i].begin(), v[i].end());
for(int i = 1; i <= n; i ++) {
long long op = read(), l = read(), r = read(), k = read();
if(op) cout << query(l, r, k * k) << "\n";
else update(l, r, k);
}
return 0;
}
\(\text{loj-6279}/\text{luogu-13978}\)
数列分块入门 \(3\)。
给出一个长为 \(n\) 的数列,以及 \(n\) 个操作,操作涉及区间加法,询问区间内小于某个值 \(x\) 的前驱(比其小的最大元素)。
\(1 \le n \le 10^5\),\(-2^{31} \le \text{others}, \text{ans} \le 2^{31} - 1\)。
只需要在上一题的 \(\text{query}\) 稍微改一下就好了:
long long query(long long l, long long r, long long k) {
long long pl = pos[l], pr = pos[r], res = -INF;
if(pl == pr) {
for(int i = l; i <= r; i ++) if(a[i] + tg[pl] < k)
res = max(res, a[i] + tg[pl]);
if(res == -INF) return -1;
return res;
}
for(int i = l; i <= R[pl]; i ++) if(a[i] + tg[pl] < k)
res = max(res, a[i] + tg[pl]);
for(int i = L[pr]; i <= r; i ++) if(a[i] + tg[pr] < k)
res = max(res, a[i] + tg[pr]);
for(int i = pl + 1; i < pr; i ++) {
auto it = lower_bound(v[i].begin(), v[i].end(), k - tg[i]);
if(it != v[i].begin()) it --, res = max(res, *it + tg[i]);
}
if(res == -INF) return -1;
return res;
}
\(\text{loj-6280}/\text{luogu-13979}\)
数列分块入门 \(3\)。
给出一个长为 \(n\) 的数列,以及 \(n\) 个操作,操作涉及区间加法,区间求和。
\(1 \le n \le 5 \times 10^4\),\(-2^{31} \le \text{others}, \text{ans} \le 2^{31} - 1\)。
法一:
实际上就是分块入门 \(1\) 稍微改改就好了,我们多记录一个块中的和即可。
更新操作和查询操作都要改改:
#include<iostream>
#include<cstdio>
#include<cmath>
using namespace std;
#define MAXN 300005
long long read() {
long long x = 0, f = 1;
char c = getchar();
while(c > 57 || c < 48) { if(c == 45) f = -1; c = getchar(); }
while(c >= 48 && c <= 57) { x = (x << 1) + (x << 3) + (c - 48); c = getchar(); }
return x * f;
}
long long n, len, a[MAXN], pos[MAXN], b[MAXN], tg[MAXN], op, l, r, k;
void update(long long l, long long r, long long k) {
long long pl = pos[l], pr = pos[r];
if(pl == pr) {
for(int i = l; i <= r; i ++) a[i] += k, b[pl] += k;
return;
}
for(int i = pl + 1; i < pr; i ++) tg[i] += k;
for(int i = l; pos[i] == pl; i ++) a[i] += k, b[pl] += k;
for(int i = r; pos[i] == pr; i --) a[i] += k, b[pr] += k;
return;
}
long long query(long long l, long long r, long long MOD) {
long long pl = pos[l], pr = pos[r], res = 0;
if(pl == pr) {
for(int i = l; i <= r; i ++) (res += a[i] + tg[pl]) %= MOD;
return res;
}
for(int i = pl + 1; i < pr; i ++) (res += b[i] + tg[i] * len) %= MOD;
for(int i = l; pos[i] == pl; i ++) (res += a[i] + tg[pos[i]]) %= MOD;
for(int i = r; pos[i] == pr; i --) (res += a[i] + tg[pos[i]]) %= MOD;
return res;
}
int main() {
n = read(), len = sqrt(n);
for(int i = 1; i <= n; i ++)
a[i] = read(), pos[i] = (i - 1) / len + 1, b[pos[i]] += a[i];
for(int i = 1; i <= n; i ++) {
op = read(), l = read(), r = read(), k = read();
if(op) cout << query(l, r, k + 1) << "\n";
else update(l, r, k);
}
return 0;
}
但是这样交 \(\text{luogu}\) 上过不了,会被卡常。
一个非常有用的卡常技巧,众所周知取模的常数是很大的,于是只在最后取模就好了,本题过程中变量最大是 \(2 \times 10^{13}\) 左右,开 \(\text{long long}\) 即可。
法二:
#include<iostream>
#include<cstdio>
using namespace std;
#define MAXN 300005
long long read() {
long long x = 0, f = 1;
char c = getchar();
while(c > 57 || c < 48) { if(c == 45) f = -1; c = getchar(); }
while(c >= 48 && c <= 57) { x = (x << 1) + (x << 3) + (c - 48); c = getchar(); }
return x * f;
}
struct node { long long l, r, res, lz; } t[MAXN << 2];
long long n, a[MAXN], op, l, r, k;
void build(long long p, long long l, long long r) {
t[p].l = l, t[p].r = r;
if(l == r) { t[p].res = a[l]; return; }
long long mid = (l + r) >> 1;
build(p << 1, l, mid);
build(p << 1 | 1, mid + 1, r);
t[p].res = t[p << 1].res + t[p << 1 | 1].res;
return;
}
void pushdown(long long p) {
long long lz = t[p].lz; t[p].lz = 0;
t[p << 1].lz += lz, t[p << 1 | 1].lz += lz;
t[p << 1].res += lz * (t[p << 1].r - t[p << 1].l + 1);
t[p << 1 | 1].res += lz * (t[p << 1 | 1].r - t[p << 1 | 1].l + 1);
return;
}
void update(long long p, long long l, long long r, long long x) {
if(t[p].l >= l && t[p].r <= r) {
t[p].res += x * (t[p].r - t[p].l + 1);
t[p].lz += x; return;
}
long long mid = (t[p].l + t[p].r) >> 1;
if(t[p].lz) pushdown(p);
if(l <= mid) update(p << 1, l, r, x);
if(r > mid) update(p << 1 | 1, l, r, x);
t[p].res = t[p << 1].res + t[p << 1 | 1].res;
return;
}
long long query(long long p, long long l, long long r, const long long MOD) {
if(t[p].l >= l && t[p].r <= r) return t[p].res % MOD;
long long mid = (t[p].l + t[p].r) >> 1, res = 0;
if(t[p].lz) pushdown(p);
if(l <= mid) (res += query(p << 1, l, r, MOD)) %= MOD;
if(r > mid) (res += query(p << 1 | 1, l, r, MOD)) %= MOD;
return res;
}
int main() {
n = read();
for(int i = 1; i <= n; i ++) a[i] = read();
build(1, 1, n);
for(int i = 1; i <= n; i ++) {
op = read(), l = read(), r = read(), k = read();
if(op) cout << (query(1, l, r, k + 1) + k + 1) % (k + 1) << "\n";
else update(1, l, r, k);
}
return 0;
}
\(\text{loj-6281}/\text{luogu-13980}\)
数列分块入门 \(5\)。
给出一个长为 \(n\) 的数列 \(a_1 \ldots a_n\),以及 \(n\) 个操作,操作涉及区间开方,区间求和。
\(1 \le n \le 5 \times 10^4\),\(-2^{31} \le \text{others}, \text{ans} \le 2^{31} - 1\)。
法一:
对于一个数 \(v\),实际上开平方不用多少次就可以使它变成 \(1\),这个级别大概是 \(O(\log\log v)\) 的。
记块长为 \(T\)。这样一来我们考虑比较暴力的修改,对于散块每次 \(a_i \leftarrow \sqrt{a_i}\) 然后同步更新块内和 \(sum_{blk_i}\),复杂度 \(O(T\log\log \left|V\right|)\);对于整块,如果说块内元素均小于等于 \(1\),给该块打上标记下次就不处理了,复杂度 \(O(nT\log\log v)\)。
加上询问就是 \(O(nT\log\log v + n(\frac{n}{T} + T))\),在 \(T = \sqrt{\frac{n}{\log\log \left|V\right|}}\) 时最快。
#include<iostream>
#include<cstdio>
#include<cmath>
using namespace std;
#define MAXN 300005
long long read() {
long long x = 0, f = 1;
char c = getchar();
while(c > 57 || c < 48) { if(c == 45) f = -1; c = getchar(); }
while(c >= 48 && c <= 57) { x = (x << 1) + (x << 3) + (c - 48); c = getchar(); }
return x * f;
}
long long n, len, a[MAXN], b[MAXN], pos[MAXN], op, l, r, k;
bool vis[MAXN];
void update(long long l, long long r) {
long long pl = pos[l], pr = pos[r];
if(pl == pr) {
if(vis[pl]) return;
for(int i = l; i <= r; i ++)
b[pl] -= a[i], a[i] = sqrt(a[i]), b[pl] += a[i];
return;
}
for(int i = l; pos[i] == pl; i ++)
b[pl] -= a[i], a[i] = sqrt(a[i]), b[pl] += a[i];
for(int i = r; pos[i] == pr; i --)
b[pr] -= a[i], a[i] = sqrt(a[i]), b[pr] += a[i];
for(int i = pl + 1; i < pr; i ++) {
if(vis[i]) continue;
bool fg = true;
long long ls = (i - 1) * len + 1, rs = min(n, i * len);
for(int j = ls; j <= rs; j ++) if(a[j] > 1) {
b[i] -= a[j], a[j] = sqrt(a[j]), b[i] += a[j];
if(a[j] > 1) fg = false;
}
vis[i] = fg;
}
return;
}
long long query(long long l, long long r) {
long long pl = pos[l], pr = pos[r], res = 0;
if(pl == pr) {
for(int i = l; i <= r; i ++) res += a[i];
return res;
}
for(int i = l; pos[i] == pl; i ++) res += a[i];
for(int i = r; pos[i] == pr; i --) res += a[i];
for(int i = pl + 1; i < pr; i ++) res += b[i];
return res;
}
int main() {
n = read(), len = sqrt(n);
for(int i = 1; i <= n; i ++)
a[i] = read(), pos[i] = (i - 1) / len + 1,
b[pos[i]] += a[i];
for(int i = 1; i <= n; i ++) {
op = read(), l = read(), r = read(), k = read();
if(op) cout << query(l, r) << "\n";
else update(l, r);
}
return 0;
}
法二:
考虑用树状数组暴力单点修改、区间查询。
但是光这样显然过不了,可以用并查集跳过已经开成 \(1\) 的点,实际上跑的飞快。
#include<iostream>
#include<cstdio>
#include<cmath>
using namespace std;
#define MAXN 300005
long long read() {
long long x = 0, f = 1;
char c = getchar();
while(c < 48 || c > 57) { if(c == 45) f = -1; c = getchar(); }
while(c >= 48 && c <= 57) { x = (x << 3) + (x << 1) + (c - 48); c = getchar(); }
return x * f;
}
long long n, q, op, l, r, t[MAXN], a[MAXN], fa[MAXN];
long long lowbit(long long x) { return x & (-x); }
void add(long long x, long long p) {
for(; x <= n; x += lowbit(x)) t[x] += p;
return;
}
long long query(long long x) {
long long res = 0ll;
for(; x; x -= lowbit(x)) res += t[x];
return res;
}
long long find(long long x) { return fa[x] == x ? x : fa[x] = find(fa[x]); }
int main() {
q = n = read(), fa[n + 1] = n + 1;
for(int i = 1; i <= n; i ++) a[i] = read(), add(i, a[i]), fa[i] = i;
while(q --) {
op = read(), l = read(), r = read();
if(l > r) swap(l, r);
if(op) cout << query(r) - query(l - 1) << "\n";
else for(int i = l; i <= r; i = (find(i) == i) ? i + 1 : fa[i])
add(i, (int)sqrt(a[i]) - a[i]), a[i] = sqrt(a[i]),
fa[i] = (a[i] <= 1) ? i + 1 : i;
}
return 0;
}
\(\text{loj-6282}/\text{luogu-13981}\)
数列分块入门 \(6\)。
给出一个长为 \(n\) 的数列,以及 \(n\) 个操作,操作涉及单点插入,单点询问,数据随机生成。
\(1 \le n \le 10^5\),\(-2^{31} \le \text{others}, \text{ans} \le 2^{31} - 1\)。
记录一种 \(\text{rope}\) 的做法吧,这个题手写块状链表太麻烦了。
\(\text{rope}\) 介绍
\(\text{rope}\) 是 \(\text{pb_ds}\) 库中的一个分支,内部是一个块状链表,支持可持久化。
需要 #include <bits/extc++.h> 和 using namespace __gnu_cxx;。
\(\text{rope}\) 的使用
可以直接使用 rope<long long> s 来创建一个名为 s 的 \(\text{rope}\)。
s.append(x)可以在 \(\text{rope}\) 后面添加一个元素 \(x\),类似于vector。s[x]可以访问 \(\text{rope}\) 中下标为 \(x\) 的元素,默认下标从 \(0\) 开始。s.insert(x, y)可以在下标为 \(x\) 的位置前插入元素 \(y\)。
那么这道题就很好写了。
#include<iostream>
#include<cstdio>
#include<ext/rope>
using namespace std;
using namespace __gnu_cxx;
long long read() {
long long x = 0, f = 1;
char c = getchar();
while(c > 57 || c < 48) { if(c == 45) f = -1; c = getchar(); }
while(c >= 48 && c <= 57) { x = (x << 1) + (x << 3) + (c - 48); c = getchar(); }
return x * f;
}
long long n, op, l, r, x;
rope<long long> s;
int main() {
s.append(0ll), n = read();
for(int i = 1; i <= n; i ++) s.append(read());
while(n --)
if(read()) cout << s[read()] << "\n";
else l = read(), r = read(), s.insert(l, r);
return 0;
}
\(\text{loj-6283}/\text{luogu-13982}\)
数列分块入门 \(7\)。
给出一个长为 \(n\) 的数列,以及 \(n\) 个操作,操作涉及区间乘法,区间加法,单点询问。
\(1 \le n \le 10^5\),\(-2^{31} \le \text{others}、\text{ans} \le 2^{31} - 1\)。
其实可以用线段树写,但是数列分块题就记录一下分块写法。
在朴素分块下,需要记录两个懒标记,分别是乘法和加法。
需要分析一下运算优先级问题,所以也有跟线段树类似的 \(\text{pushdown}\) 操作。
#include<iostream>
#include<cstdio>
#include<cmath>
using namespace std;
#define MAXN 100005
#define MOD 10007
long long read() {
long long x = 0, f = 1;
char c = getchar();
while(c > 57 || c < 48) { if(c == 45) f = -1; c = getchar(); }
while(c >= 48 && c <= 57) { x = (x << 1) + (x << 3) + (c - 48); c = getchar(); }
return x * f;
}
long long n, len, a[MAXN], pos[MAXN], mpos, add[MAXN], mul[MAXN];
void pushdown(long long p) {
long long l = (p - 1) * len + 1, r = min(n, p * len);
for(int i = l; i <= r; i ++) a[i] = (a[i] * mul[p] % MOD + add[p]) % MOD;
mul[p] = 1, add[p] = 0; return;
}
void update1(long long l, long long r, long long c) {
long long pl = pos[l], pr = pos[r];
if(pl == pr) {
pushdown(pl);
for(int i = l; i <= r; i ++) (a[i] += c) %= MOD;
return;
}
for(int i = pl + 1; i < pr; i ++) (add[i] += c) %= MOD;
pushdown(pl), pushdown(pr);
for(int i = l; pos[i] == pl; i ++) (a[i] += c) %= MOD;
for(int i = r; pos[i] == pr; i --) (a[i] += c) %= MOD;
return;
}
void update2(long long l, long long r, long long c) {
long long pl = pos[l], pr = pos[r];
if(pl == pr) {
pushdown(pl);
for(int i = l; i <= r; i ++) (a[i] *= c) %= MOD;
return;
}
for(int i = pl + 1; i < pr; i ++)
(mul[i] *= c) %= MOD, (add[i] *= c) %= MOD;
pushdown(pl), pushdown(pr);
for(int i = l; pos[i] == pl; i ++) (a[i] *= c) %= MOD;
for(int i = r; pos[i] == pr; i --) (a[i] *= c) %= MOD;
return;
}
int main() {
n = read(), len = sqrt(n);
for(int i = 1; i <= n; i ++)
a[i] = read(), pos[i] = (i - 1) / len + 1;
mpos = pos[n];
for(int i = 1; i <= mpos; i ++) mul[i] = 1;
for(int i = 1; i <= n; i ++) {
long long op = read(), l = read(), r = read(), c = read();
if(!op) update1(l, r, c);
else if(op == 1) update2(l, r, c);
else cout << (a[r] * mul[pos[r]] % MOD + add[pos[r]]) % MOD << "\n";
}
return 0;
}
\(\text{loj-6284}/\text{luogu-13983}\)
数列分块入门 \(8\)。
给出一个长为 \(n\) 的数列,以及 \(n\) 个操作,操作涉及区间询问等于一个数 \(c\) 的元素,并将这个区间的所有元素改为 \(c\)。
\(1 \le n \le 10^5\),\(-2^{31} \le \text{others}、\text{ans} \le 2^{31} - 1\)。
考虑每个块记录一个覆盖标记,修改整块的时候更新当前块的标记,修改散块的时候需要下放标记将其所在的块都改成标记即可。
若当前散块没有标记就不用更新标记了,直接暴力修改 \(a_i \gets c\)。
查询的时候记得 \(\text{pushdown}\) 操作,时间复杂度为 \(O(n \sqrt n)\)。
#include<iostream>
#include<cstdio>
#include<cmath>
using namespace std;
#define MAXN 300005
long long read() {
long long x = 0, f = 1;
char c = getchar();
while(c > 57 || c < 48) { if(c == 45) f = -1; c = getchar(); }
while(c >= 48 && c <= 57) { x = (x << 1) + (x << 3) + (c - 48); c = getchar(); }
return x * f;
}
long long n, len, a[MAXN], pos[MAXN], lz[MAXN];
void pushdown(long long p) {
if(lz[p] == -1) return;
long long l = (p - 1) * len + 1, r = min(n, p * len);
for(int i = l; i <= r; i ++) a[i] = lz[p];
lz[p] = -1; return;
}
long long query(long long l, long long r, long long c) {
long long res = 0, pl = pos[l], pr = pos[r];
if(pl == pr) {
pushdown(pl);
for(int i = l; i <= r; i ++)
if(a[i] != c) a[i] = c;
else res ++;
return res;
}
pushdown(pl), pushdown(pr);
for(int i = pl + 1; i < pr; i ++)
if(lz[i] != -1)
if(lz[i] != c) lz[i] = c;
else res += len;
else {
for(int j = (i - 1) * len + 1; j <= min(n, i * len); j ++)
if(a[j] != c) a[j] = c;
else res ++;
lz[i] = c;
}
for(int i = l; pos[i] == pl; i ++)
if(a[i] != c) a[i] = c;
else res ++;
for(int i = r; pos[i] == pr; i --)
if(a[i] != c) a[i] = c;
else res ++;
return res;
}
int main() {
n = 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 <= pos[n]; i ++) lz[i] = -1;
for(int i = 1; i <= n; i ++) {
long long l = read(), r = read(), c = read();
cout << query(l, r, c) << "\n";
}
return 0;
}
\(\text{loj-6285}/\text{luogu-13984}\)
数列分块入门 \(9\)。
给出一个长为 \(n\) 的数列,以及 \(n\) 个操作,操作涉及询问区间的最小众数。
\(1 \le n \le 10^5\),\(-2^{31} \le \text{others}、\text{ans} \le 2^{31} - 1\)。
这道题就没有那么裸了。
考虑整块和散块的关联,其实整块的最小众数预处理出来就是散块的数会影响到整块。设块长为 \(T\)。
所以考虑预处理整块内的众数,记 \(num_{l,r}\) 表示块 \(l \sim r\) 的最小众数,直接枚举每个块右端点然后往 \(n\) 扫,这样复杂度是 \(O(\frac{n^2}{T})\)。
记录每个数出现的位置,因为我们需要知道每个数区间内出现次数,可以用二分解决,先找到第一个大于询问右端点的位置,再找到第一个大于等于询问左端点的位置,两者相减就是想要的出现次数。
每次查询再用散块更新答案,复杂度就是 \(O(T\log n)\)。
总复杂度 \(O(T\log n + \frac{n^2}{T})\),在 \(T = \sqrt{\frac{n}{\log n}}\) 取得最小值 \(O(n\sqrt{n\log n})\)。
#include<iostream>
#include<cstdio>
#include<cmath>
#include<vector>
#include<map>
#include<algorithm>
#include<cstring>
using namespace std;
#define MAXN 300005
#define MAXM 2005
long long read() {
long long x = 0, f = 1;
char c = getchar();
while(c > 57 || c < 48) { if(c == 45) f = -1; c = getchar(); }
while(c >= 48 && c <= 57) { x = (x << 1) + (x << 3) + (c - 48); c = getchar(); }
return x * f;
}
long long n, len, a[MAXN], pos[MAXN], p[MAXN], id, cnt[MAXN];
long long L[MAXN], R[MAXN], l, r, sum[MAXM][MAXM];
map<long long, long long> mp;
vector<long long> v[MAXN];
void init(long long x) {
long long l = L[x], r = R[x], mk = 0, res = 0;
memset(cnt, 0, sizeof(long long) * (n + 5));
for(int i = l; i <= n; i ++) {
long long px = pos[i]; cnt[a[i]] ++;
if(cnt[a[i]] > res || (cnt[a[i]] == res && p[a[i]] < p[mk]))
mk = a[i], res = cnt[a[i]];
sum[x][px] = mk;
}
return;
}
long long find(long long l, long long r, long long x) {
return upper_bound(v[x].begin(), v[x].end(), r)
- lower_bound(v[x].begin(), v[x].end(), l);
}
long long query(long long l, long long r) {
long long pl = pos[l], pr = pos[r], res = sum[pl + 1][pr - 1];
long long mk = find(l, r, res);
for(int i = l; i <= min(r, R[pl]); i ++) {
long long px = find(l, r, a[i]);
if(px > mk || (px == mk && p[a[i]] < p[res]))
res = a[i], mk = px;
}
if(pl ^ pr) for(int i = L[pr]; i <= r; i ++) {
long long px = find(l, r, a[i]);
if(px > mk || (px == mk && p[a[i]] < p[res]))
res = a[i], mk = px;
}
return res;
}
int main() {
n = read(), len = sqrt(n) * 0.5;
for(int i = 1; i <= n; i ++) {
a[i] = read(), pos[i] = (i - 1) / len + 1;
if(!mp[a[i]]) mp[a[i]] = (++ id), p[id] = a[i];
a[i] = mp[a[i]], v[a[i]].push_back(i);
}
for(int i = 1; i <= pos[n]; i ++)
L[i] = (i - 1) * len + 1, R[i] = min(n, i * len);
for(int i = 1; i <= pos[n]; i ++) init(i);
for(int i = 1; i <= n; i ++) {
long long l = read(), r = read();
if(l > r) swap(l, r);
cout << p[query(l, r)] << "\n";
}
return 0;
}
\(\text{luogu-2617}\)
给定一个含有 \(n\) 个数的序列 \(a_1,a_2 \dots a_n\),需要支持两种操作:
Q l r k表示查询下标在区间 \([l,r]\) 中的第 \(k\) 小的数;C x y表示将 \(a_x\) 改为 \(y\)。
\(1\le n,m \le 10^5\),\(1 \le l \le r \le n\),\(1 \le k \le r-l+1\),\(1\le x \le n\),\(0 \le a_i,y \le 10^9\)。
非常优美的分块题,实际上是树套树,但我偏不写。
考虑分为 \(\sqrt n\) 个块,块内排序,然后对于操作就有:
- 查询操作,进行值域二分,二分过程中查询区间小于等于 \(mid\) 的数的个数。
- 单点修改,直接暴力修改就好,更新块内值并排序。
时间复杂度为 \(O(q \sqrt n \log \sqrt n \log V)\),跑不满,且本题时限 \(\texttt{5s}\),可过。
#include<iostream>
#include<cstdio>
#include<vector>
#include<algorithm>
#include<cmath>
using namespace std;
#define MAXN 100005
#define ll long long
inline ll read() {
ll x = 0, f = 1;
char c = getchar();
while(c > 57 || c < 48) { if(c == 45) f = -1; c = getchar(); }
while(c >= 48 && c <= 57) { x = (x << 1) + (x << 3) + (c - 48); c = getchar(); }
return x * f;
}
ll n, q, mk, a[MAXN], L[MAXN], R[MAXN], pos[MAXN], len;
vector<ll> v[MAXN];
inline void upd(ll x, ll k) {
a[x] = k, x = pos[x], v[x].clear();
for(int i = L[x]; i <= R[x]; i ++) v[x].push_back(a[i]);
sort(v[x].begin(), v[x].end());
return;
}
inline ll qry(ll l, ll r, ll k) {
ll pl = pos[l], pr = pos[r], res = 0;
if(pl == pr) {
for(int i = l; i <= r; i ++) res += (a[i] <= k);
return res;
}
for(int i = l; i <= R[pl]; i ++) res += (a[i] <= k);
for(int i = L[pr]; i <= r; i ++) res += (a[i] <= k);
for(int i = pl + 1; i < pr; i ++)
res += upper_bound(v[i].begin(), v[i].end(), k) - v[i].begin();
return res;
}
int main() {
n = read(), q = read(), len = sqrt(n);
for(int i = 1; i <= n; i ++)
a[i] = read(), pos[i] = (i - 1) / len + 1,
v[pos[i]].push_back(a[i]);
mk = pos[n];
for(int i = 1; i <= mk; i ++)
L[i] = (i - 1) * len + 1, R[i] = min(i * len, n),
sort(v[i].begin(), v[i].end());
while(q --) {
char c; cin >> c; ll x, y, k;
if(c == 'Q') {
x = read(), y = read(), k = read();
ll l = 0, r = 1e9, res = 0;
while(l <= r) {
ll mid = (l + r) >> 1;
if(qry(x, y, mid) >= k) r = mid - 1, res = mid;
else l = mid + 1;
}
cout << res << "\n";
}
else x = read(), y = read(), upd(x, y);
}
return 0;
}
\(\text{luogu-4168}\)
在乡下的小路旁种着许多蒲公英,而我们的问题正是与这些蒲公英有关。
为了简化起见,我们把所有的蒲公英看成一个长度为 \(n\) 的序列 \(\{a_1,a_2..a_n\}\),其中 \(a_i\) 为一个正整数,表示第 \(i\) 棵蒲公英的种类编号。
而每次询问一个区间 \([l, r]\),你需要回答区间里出现次数最多的是哪种蒲公英,如果有若干种蒲公英出现次数相同,则输出种类编号最小的那个。
强制在线。令上次询问的结果为 \(lst\)(如果这是第一次询问,则 \(x = 0\))。
则 \(L=((l+lst-1)\bmod n) + 1\),\(R=((r+lst-1) \bmod n) + 1\)。如果 \(L > R\),则交换 \(L,R\)。 最终的询问区间为计算后的 \([L, R]\)。
\(1\le n \le 4 \times 10^4\),\(1\le m \le 5 \times 10^5\),\(1\le a_i \le 10^9\),\(1 \leq l, r \leq n\)。
实际上就是数列分块 \(9\)。
只不过强制在线而已,显然卡掉了莫队的写法,于是这题只能用分块写。
#include<iostream>
#include<cstdio>
#include<cmath>
#include<vector>
#include<map>
#include<algorithm>
#include<cstring>
using namespace std;
#define MAXN 300005
#define MAXM 2005
long long read() {
long long x = 0, f = 1;
char c = getchar();
while(c > 57 || c < 48) { if(c == 45) f = -1; c = getchar(); }
while(c >= 48 && c <= 57) { x = (x << 1) + (x << 3) + (c - 48); c = getchar(); }
return x * f;
}
long long n, m, lst, len, a[MAXN], pos[MAXN], p[MAXN], id, cnt[MAXN];
long long L[MAXN], R[MAXN], l, r, sum[MAXM][MAXM];
map<long long, long long> mp;
vector<long long> v[MAXN];
void init(long long x) {
long long l = L[x], r = R[x], mk = 0, res = 0;
memset(cnt, 0, sizeof(long long) * (n + 5));
for(int i = l; i <= n; i ++) {
long long px = pos[i]; cnt[a[i]] ++;
if(cnt[a[i]] > res || (cnt[a[i]] == res && p[a[i]] < p[mk]))
mk = a[i], res = cnt[a[i]];
sum[x][px] = mk;
}
return;
}
long long find(long long l, long long r, long long x) {
return upper_bound(v[x].begin(), v[x].end(), r)
- lower_bound(v[x].begin(), v[x].end(), l);
}
long long query(long long l, long long r) {
long long pl = pos[l], pr = pos[r], res = sum[pl + 1][pr - 1];
long long mk = find(l, r, res);
for(int i = l; i <= min(r, R[pl]); i ++) {
long long px = find(l, r, a[i]);
if(px > mk || (px == mk && p[a[i]] < p[res]))
res = a[i], mk = px;
}
if(pl ^ pr) for(int i = L[pr]; i <= r; i ++) {
long long px = find(l, r, a[i]);
if(px > mk || (px == mk && p[a[i]] < p[res]))
res = a[i], mk = px;
}
return res;
}
int main() {
n = read(), m = read(), len = sqrt(n) * 0.5;
for(int i = 1; i <= n; i ++) {
a[i] = read(), pos[i] = (i - 1) / len + 1;
if(!mp[a[i]]) mp[a[i]] = (++ id), p[id] = a[i];
a[i] = mp[a[i]], v[a[i]].push_back(i);
}
for(int i = 1; i <= pos[n]; i ++)
L[i] = (i - 1) * len + 1, R[i] = min(n, i * len);
for(int i = 1; i <= pos[n]; i ++) init(i);
while(m --) {
long long l = read(), r = read();
l = (l + lst - 1) % n + 1;
r = (r + lst - 1) % n + 1;
if(l > r) swap(l, r);
lst = p[query(l, r)];
cout << lst << "\n";
}
return 0;
}
本文来自博客园,作者:So_noSlack,转载请注明原文链接:https://www.cnblogs.com/So-noSlack/p/19254205

浙公网安备 33010602011771号