分块、莫队
分块思想
- 其实,分块是一种思想,而不是一种数据结构
- 分块的基本思想是,通过对原数据的适当划分,并在划分后的每一个块上预处理部分信息,从而较一般的暴力算法取得更优的时间复杂度
- 分块的时间复杂度主要取决于分块的块长B,一般可以通过均值不等式求出某个问题下的最优块长,以及相应的时间复杂度
- 常见的块长B为\(\sqrt n + 1、\frac{n}{\sqrt {log\ q}} + 1、\sqrt{\frac{n}{log\ q}} + 1\)
- 分块\(Debug\)的小技巧,可以选择块长\(B = 1\ or\ 2\)等一些小的块长去试一下看一下答案是不是正确
- 分块的常见应用:计算满足某个性质的元素个数等,包括离线算法的莫队也是基于分块思想实现的
模板
const int B = sqrt(N) + 1;
int n;
int a[N];
int id[N], L[N], R[N];
void build() // 分块
{
for (int i = 1; i <= n; ++i)
id[i] = (i - 1) / B + 1;
for (int i = 1; i <= id[n]; ++i)
{
L[i] = (i - 1) * B + 1; // 每个块的左端点
R[i] = min(i * B, n); // 每个块的右端点,注意最后一个块可能不完整
}
}
void bf_modify(int l, int r, int x) // 暴力修改
{
}
void modify(int l, int r, int x) // 修改
{
if (id[l] == id[r]) // 如果在左右端点在同一个块内,直接暴力修改
{
bf_modify(l, r, x);
return;
}
bf_modify(l, R[id[l]], x);
bf_modify(L[id[r]], r, x);
for (int i = id[l] + 1; i <= id[r] - 1; ++i) // 注意+1 和 -1
{
}
}
int bf_query(int l, int r, int x) // 暴力查询
{
}
int query(int l, int r, int x) // 查询
{
int res = 0;
if (id[l] == id[r]) // 如果在一个同一个块内直接暴力查询
{
return bf_query(l, r, x);
}
res += bf_query(l, R[id[l]], x);
res += bf_query(L[id[r]], r, x);
for (int i = id[l] + 1; i <= id[r] - 1; ++i)
{
}
return res;
}
例1·区间求和
题解
- 对于查询操作:
- 如果\(l,r\)在同一个块内,直接暴力求和即可,复杂度\(O(B)\)
- 如果\(l,r\)不在用一个块内,对散块(不完整块)暴力求和,对整块利用已经预处理的整块和\(B_i\)求和即可,复杂度 \(O(B + \frac{n}{B})\)
- 对于修改操作:
- 如果l,r在同一块内,直接暴力修改即可,复杂度\(O(B)\)
- 如果l,r不在同一个块内,对散块暴力修改(但别忘了更新整块和\(B_i\)),对于整块直接修改\(B_i\)即可,复杂度为\(O(B + \frac{n}{B})\)
- 利用均值不等式可知,当\(B = \frac{n}{B}\)时,即\(B = \sqrt n\)时,时间复杂度最优,且为\(O(\sqrt n)\)
const int N = 1e5 + 10, M = 4e5 + 10;
const int B = sqrt(1e5) + 1;
int n, m;
int a[N];
int tag[N];
int sum[N];
int id[N];
int L[N], R[N];
void init()
{
for (int i = 1; i <= n; ++i)
{
id[i] = (i - 1) / B + 1;
if (id[i] != id[i - 1])
{
L[id[i]] = i;
R[id[i]] = min(n, i + B - 1);
}
tag[id[i]] = 0;
}
for (int i = 1; i <= n / B + 1; ++i)
{
for (int j = L[i]; j <= R[i]; ++j)
sum[i] += a[j];
}
}
void bf_modify(int l, int r, int x)
{
for (int i = l; i <= r; ++i)
a[i] += x;
sum[id[l]] += x * (r - l + 1);
}
void modify(int l, int r, int x)
{
if (id[l] == id[r])
{
bf_modify(l, r, x);
return;
}
bf_modify(l, R[id[l]], x);
bf_modify(L[id[r]], r, x);
for (int i = id[l] + 1; i <= id[r] - 1; ++i)
tag[i] += x;
}
int bf_query(int l, int r)
{
int res = 0;
for (int i = l; i <= r; ++i)
res += a[i];
return res;
}
int query(int l, int r)
{
int res = 0;
if (id[l] == id[r])
{
return bf_query(l, r) + tag[id[l]] * (r - l + 1);
}
res += bf_query(l, R[id[l]]) + tag[id[l]] * (R[id[l]] - l + 1);
res += bf_query(L[id[r]], r) + tag[id[r]] * (r - L[id[r]] + 1);
for (int i = id[l] + 1; i <= id[r] - 1; ++i)
{
res += sum[i] + tag[i] * (R[i] - L[i] + 1);
}
return res;
}
void solve()
{
cin >> n >> m;
for (int i = 1; i <= n; ++i)
cin >> a[i];
init();
while (m--)
{
int op, x, y, k;
cin >> op;
if (op == 1)
{
cin >> x >> y >> k;
modify(x, y, k);
}
else
{
cin >> x >> y;
cout << query(x, y) << endl;
}
}
}
例2·LibreOJ - 6278
\(1 \leq n \leq 5e4\)
题解
- 首先我们应该对于每个块开一个\(vector\),并排序
- 对于修改操作:
- 如果\(l,r\)在同一个块中,我们直接暴力修改,复杂度\(O(B)\)
- 如果\(l,r\)不在同一个块中,对于散块我们暴力修改,然后我们将\(vector\)清空后重新把该块的元素加入后重新排序(重构),复杂度\(O(BlogB)\);对于整块我们更新懒标记\(tag\)即可,复杂度\(O(B)\)
- 那么修改的大致复杂度为\(O(B + BlogB)\)
- 对于查询操作:
- 如果\(l,r\)在同一个块中,我们直接暴力在序列\(a\)的\([l,r]\)中查询即可,复杂度\(O(B)\)
- 如果\(l,r\)不在同一个块中,对于散块我们直接暴力查询,复杂度\(O(B)\);对于整块我们二分求出个数,复杂度\(O(\frac{n}{B}\times logB)\)
- 那么修改的大致复杂度为\(O(B + \frac{n}{B}\times logB)\)
const int N = 5e4 + 10, M = 5e4;
const int B = sqrt(M / log(M)) + 1;
int n;
int a[N];
int id[N], tag[N], L[N], R[N];
vector<int> g[N];
void init()
{
for (int i = 1; i <= n; ++i)
{
id[i] = (i - 1) / B + 1;
g[id[i]].push_back(a[i]);
}
for (int i = 1; i <= id[n]; ++i)
{
L[i] = (i - 1) * B + 1;
R[i] = min(i * B, n);
sort(all(g[i]));
}
}
void bf_modify(int l, int r, int x)
{
for (int i = l; i <= r; ++i)
a[i] += x;
g[id[l]].clear();
for (int i = L[id[l]]; i <= R[id[l]]; ++i)
g[id[l]].push_back(a[i]);
sort(all(g[id[l]]));
}
void modify(int l, int r, int x)
{
if (id[l] == id[r])
{
bf_modify(l, r, x);
return;
}
bf_modify(l, R[id[l]], x);
bf_modify(L[id[r]], r, x);
for (int i = id[l] + 1; i <= id[r] - 1; ++i)
tag[i] += x;
}
int bf_query(int l, int r, int x)
{
int res = 0;
for (int i = l; i <= r; ++i)
if (a[i] + tag[id[i]] < x)
res++;
return res;
}
int query(int l, int r, int x)
{
int res = 0;
if (id[l] == id[r])
{
return bf_query(l, r, x);
}
res += bf_query(l, R[id[l]], x);
res += bf_query(L[id[r]], r, x);
for (int i = id[l] + 1; i <= id[r] - 1; ++i)
{
res += lower_bound(all(g[i]), x - tag[i]) - g[i].begin();
}
return res;
}
void solve()
{
cin >> n;
for (int i = 1; i <= n; ++i)
cin >> a[i];
init();
for (int i = 1; i <= n; ++i)
{
int op, l, r, c;
cin >> op >> l >> r >> c;
if (op == 0)
modify(l, r, c);
else
cout << query(l, r, c * c) << endl;
}
}
例3·求区间最小众数
给定一个长度为\(n\)的序列\(a\),其中\(a_i>0\),每次询问一个区间\([l,r]\)中的出现次数最多且最小的数,即最小众数
\(1 \leq n \leq 10^4\)
\(1 \leq a_i \leq 10^9\)
题解
- 我们在分块的时候预处理一个\(pair\)对的\(ans[i][j]\),代表第\(i\)个块到第\(j\)个块之间最小众数和该众数在这些块中的出现次数,预处理复杂度\(O(\frac{n}{B}\times n)\)
- 对于查询操作:
- 如果\(l,r\)在同一个块中,我们直接暴力查询,我们不妨对于每个数开一个\(vector\),在里面记录这个数在序列\(a\)中出现的位置,显然我们需要提前离散化,那么我们可以枚举\([l,r]\)区间中的每一个数,然后在\(vector\)中二分求出\(l\)和\(r\)即可求出每一个数在该区间中的出现次数,复杂度\(O(B\times logn)\),然后知道出现次数,显然最小众数已然浮出水面
- 如果\(l,r\)不在同一个块中,我们对于散块直接暴力查询散块中所有数在\([l,r]\)中出现次数,如果散块中某个数\(x\)出现的次数比整块\(ans[id[l] + 1][id[r] - 1]\)的来的大,那么\(x\)为最小众数,复杂度\(O(B \times logn)\)
const int N = 1e5 + 10;
const int B = 50;
const int M = 1000;
int n, q;
int a[N];
int id[N], tag[N], L[N], R[N];
pii ans[M][M];
vector<int> g[N];
map<int, int> mp;
void init()
{
for (int i = 1; i <= n; ++i)
id[i] = (i - 1) / B + 1;
for (int i = 1; i <= id[n]; ++i)
{
L[i] = (i - 1) * B + 1;
R[i] = min(i * B, n);
}
for (int i = 1; i <= id[n]; ++i)
{
int mx = 0, res = INF;
vector<int> cnt(n + 10);
for (int j = L[i]; j <= n; ++j)
{
cnt[a[j]]++;
if (cnt[a[j]] > mx)
{
mx = cnt[a[j]];
res = mp[a[j]];
}
else if (cnt[a[j]] == mx)
res = min(res, mp[a[j]]);
ans[i][(j - 1) / B + 1] = {res, mx};
}
}
}
inline pii Min(pii t1, pii t2)
{
pii c;
if (t1.second < t2.second)
{
c.second = t2.second;
c.first = t2.first;
}
else if (t1.second > t2.second)
{
c.second = t1.second;
c.first = t1.first;
}
else
{
if (t1.first <= t2.first)
{
c.first = t1.first;
c.second = t1.second;
}
else
{
c.second = t2.second;
c.first = t2.first;
}
}
return c;
}
pii bf_query(int l, int r, int ql, int qr)
{
int mx = 0, res = INF;
for (int i = l; i <= r; ++i)
{
int x = lower_bound(all(g[a[i]]), ql) - g[a[i]].begin();
int y = upper_bound(all(g[a[i]]), qr) - g[a[i]].begin() - 1;
if (mx < y - x + 1)
{
res = mp[a[i]];
mx = y - x + 1;
}
else if (mx == y - x + 1)
res = min(res, mp[a[i]]);
}
return mpk(res, mx);
}
pii query(int l, int r)
{
pii res = {INF, 0};
if (id[l] == id[r])
return bf_query(l, r, l, r);
res = Min(res, bf_query(l, R[id[l]], l, r));
res = Min(res, bf_query(L[id[r]], r, l, r));
res = Min(res, ans[id[l] + 1][id[r] - 1]);
return res;
}
void solve()
{
cin >> n >> q;
vector<int> vec;
for (int i = 1; i <= n; ++i)
{
cin >> a[i];
vec.push_back(a[i]);
}
sort(all(vec));
vec.erase(unique(all(vec)), vec.end());
for (int i = 1; i <= n; ++i)
{
int t = lower_bound(all(vec), a[i]) - vec.begin() + 1;
mp[t] = a[i];
a[i] = t;
g[a[i]].push_back(i);
}
init();
int x = 0;
while (q--)
{
int l, r;
cin >> l >> r;
l = (l + x - 1) % n + 1;
r = (r + x - 1) % n + 1;
if (l > r)
swap(l, r);
x = query(l, r).first;
cout << x << endl;
}
}
例4·黑暗爆炸 - 2002
\(1 \leq n \leq 2e5\)
题解
- 我们分块后\(dp\)预处理出\(pair\)对\(ans[i]\),代表从第\(i\)个位置跳出当前块需要的步数和跳出后的落点位置
- 如何\(dp\)预处理呢?首先我们知道在位置\(n\)一定是只要跳一步就可以跳出当前块 ,\(ans[n]=\{1,n + a_n\}\)
- 那么我们不妨考虑从后往前\(dp\),即可预处理出\(ans[i]\),预处理复杂度\(O(n)\)
- 对于单点修改操作:
- 我们发现单点修改后只会影响这个点所在块内的所有信息,所以只需要这个块的右端点从后往前\(dp\)更新这个块中的\(ans\)即可,时间复杂度\(O(B)\)
- 对于查询操作:
- 我们利用维护的\(ans\)信息直接从查询点开始往后跳即可,复杂度为\(O(\frac{n}{B})\)
#include <bits/stdc++.h>
#define Zeoy std::ios::sync_with_stdio(false), std::cin.tie(0), std::cout.tie(0)
#define all(x) (x).begin(), (x).end()
#define rson id << 1 | 1
#define lson id << 1
#define int long long
#define mpk make_pair
#define endl '\n'
using namespace std;
typedef unsigned long long ULL;
typedef long long ll;
typedef pair<int, int> pii;
typedef pair<double, double> pdd;
const int inf = 0x3f3f3f3f;
const ll INF = 0x3f3f3f3f3f3f3f3f;
const int mod = 1e9 + 7;
const double eps = 1e-9;
const int N = 3e5 + 10, M = 4e5 + 10;
const int B = 388;
int n, m;
int a[N];
int id[N], L[N], R[N];
pii ans[N]; // ans[i]代表从当前所属的块跳几步能够跳出当前块,且落在下一个块的落点位置
void build()
{
for (int i = 1; i <= n; ++i)
id[i] = (i - 1) / B + 1;
for (int i = 1; i <= id[n]; ++i)
L[i] = (i - 1) * B + 1, R[i] = min(n, i * B);
for (int i = n; i >= 1; --i)
{
int step = 0, pos = 0;
if (i + a[i] > R[id[i]])
{
step = 1;
pos = i + a[i];
}
else
{
step = ans[i + a[i]].first + 1;
pos = ans[i + a[i]].second;
}
ans[i] = mpk(step, pos);
}
}
// 单点修改:重新更新块内ans信息 O(B)
void modify(int x, int val)
{
a[x] = val;
for (int i = x; i >= L[id[x]]; --i)
{
int step = 0, pos = 0;
if (i + a[i] > R[id[i]])
{
step = 1;
pos = i + a[i];
}
else
{
step = ans[i + a[i]].first + 1;
pos = ans[i + a[i]].second;
}
ans[i] = mpk(step, pos);
}
}
int query(int x)
{
int res = 0;
for (int i = x; i <= n; i = ans[i].second)
res += ans[i].first;
return res;
}
void solve()
{
cin >> n;
for (int i = 1; i <= n; ++i)
cin >> a[i];
build();
cin >> m;
while (m--)
{
int op, x, k;
cin >> op;
if (op == 1)
{
cin >> x;
cout << query(x + 1) << endl;
}
else
{
cin >> x >> k;
modify(x + 1, k);
}
}
}
莫队算法
- 莫队算法可以解决一类离线区间询问问题,适用性极为广泛。同时将其加以扩展,便能轻松处理树上路径询问以及支持修改操作
- 莫队算法需要用到分块的思想
- 莫队算法实际上就是将查询离线后排序,顺序处理每个询问,暴力从上一个区间的答案转移到下一个区间答案(一步一步移动即可)
- 查询的排序方法:对于区间\([l,r]\),以\(l\)所在块的编号为第一关键词升序排列,以\(r\)所在的块为第二关键词进行奇偶排序(块编号为奇数升序排列,块编号为偶数降序排列)
- 时间复杂度\(O(n \times \sqrt{n})\)
普通莫队模板
const int N = 1e5 + 10, M = 4e5 + 10;
int n, q;
int a[N];
int id[N];
int ans[N];
int preAns;
struct node
{
int l, r, idx;
bool operator<(const node &t) const
{
if (id[l] == id[t.l])
{
if (id[l] & 1)
return r < t.r;
else
return r > t.r;
}
else
return l < t.l;
}
} qry[N];
void del(int x)
{
}
void add(int x)
{
}
void solve()
{
cin >> n >> q;
int B = n / sqrt(q) + 1;
for (int i = 1; i <= n; ++i)
cin >> a[i];
for (int i = 1; i <= q; ++i)
{
cin >> qry[i].l >> qry[i].r;
qry[i].idx = i;
}
for (int i = 1; i <= n; ++i) // 分块
id[i] = (i - 1) / B + 1;
sort(qry + 1, qry + q + 1); // 对查询进行排序
for (int i = 1, l = 1, r = 0; i <= q; ++i)
{
while (l > qry[i].l)
add(a[--l]);
while (r < qry[i].r)
add(a[++r]);
while (l < qry[i].l)
del(a[l++]);
while (r > qry[i].r)
del(a[r--]);
ans[qry[i].idx] = preAns;
}
for (int i = 1; i <= q; ++i)
cout << ans[i] << endl;
}
例1·洛谷 - P1494
小 Z 把这 \(N\) 只袜子从 \(1\) 到 \(N\) 编号,然后从编号 \(L\) 到 \(R\) (\(L\) 尽管小 Z 并不在意两只袜子是不是完整的一双,甚至不在意两只袜子是否一左一右,他却很在意袜子的颜色,毕竟穿两只不同色的袜子会很尴尬。
你的任务便是告诉小 Z,他有多大的概率抽到两只颜色相同的袜子。当然,小 Z 希望这个概率尽量高,所以他可能会询问多个 \((L,R)\) 以方便自己选择。
然而数据中有 \(L=R\) 的情况,请特判这种情况,输出
0/1
。
题解
- 设询问区间长度为\(len = r - l + 1\),那么抽两只袜子的总方案数为\(C_{len}^{2}\)
- 抽到两只颜色相同的袜子的方案数为,对于每一种袜子数量\(cnt\)超过2只的,其方案数为\(C_{cnt}^{2}\),那么抽到两只颜色相同的袜子的总方案数为所有袜子数量超过2只的方案数之和
- 我们考虑莫队中的删除和添加操作:
- 减去这只袜子原有的方案数
- 更新袜子数量\(cnt\)
- 加上更新后的方案数
const int N = 5e4 + 10, M = 5e4;
const int B = (5e4) / sqrt(M) + 1;
int n, q, k;
int a[N];
int id[N];
pii ans[N];
int preAns;
int mp[N];
int gcd(int a, int b)
{
return b ? gcd(b, a % b) : a;
}
void print(int a, int b)
{
if (a == 0 || b == 0)
{
cout << 0 << "/" << 1 << endl;
return;
}
int d = gcd(a, b);
a /= d;
b /= d;
cout << a << "/" << b << endl;
}
struct node
{
int l, r, idx;
bool operator<(const node &t) const
{
if (id[l] == id[t.l])
{
if (id[l] % 2)
return r < t.r;
else
return r > t.r;
}
else
return l < t.l;
}
} qry[N];
void del(int x)
{
if (mp[x] >= 2)
preAns -= (mp[x] * (mp[x] - 1)) / 2;
mp[x]--;
if (mp[x] >= 2)
preAns += (mp[x] * (mp[x] - 1)) / 2;
}
void add(int x)
{
if (mp[x] >= 2)
preAns -= (mp[x] * (mp[x] - 1)) / 2;
mp[x]++;
if (mp[x] >= 2)
preAns += (mp[x] * (mp[x] - 1)) / 2;
}
void solve()
{
cin >> n >> q;
for (int i = 1; i <= n; ++i)
cin >> a[i];
for (int i = 1; i <= q; ++i)
{
cin >> qry[i].l >> qry[i].r;
qry[i].idx = i;
}
for (int i = 1; i <= n; ++i)
id[i] = (i - 1) / B + 1;
sort(qry + 1, qry + q + 1);
for (int i = 1, l = 1, r = 0; i <= q; ++i)
{
while (l > qry[i].l)
add(a[--l]);
while (r < qry[i].r)
add(a[++r]);
while (l < qry[i].l)
del(a[l++]);
while (r > qry[i].r)
del(a[r--]);
if (qry[i].l == qry[i].r)
{
ans[qry[i].idx] = {0, 1};
continue;
}
int t = qry[i].r - qry[i].l + 1;
ans[qry[i].idx] = {preAns, (t * (t - 1)) / 2};
}
for (int i = 1; i <= q; ++i)
print(ans[i].first, ans[i].second);
}
例2·洛谷 - P2709
小B 有一个长为 \(n\) 的整数序列 \(a\),值域为 \([1,k]\)。
他一共有 \(m\) 个询问,每个询问给定一个区间 \([l,r]\),求: $$\sum\limits_{i=1}^k c_i^2$$其中 \(c_i\) 表示数字 \(i\) 在 \([l,r]\) 中的出现次数
题解
我们考虑莫队中的删除和添加操作:设\(i\)的出现次数为\(cnt\)
减去原有的贡献:\(cnt \times cnt\)
更新\(cnt:=cnt + 1\ \ or\ \ cnt - 1\)
加上更新后的贡献:\(cnt \times cnt\)
const int N = 5e4 + 10, M = 5e4;
const int B = sqrt(M) + 1;
int n, q, k;
int a[N];
int id[N];
int ans[N];
int preAns;
map<int, int> mp;
struct node
{
int l, r, idx;
bool operator<(const node &t) const
{
if (id[l] == id[t.l])
{
if (id[l] % 2)
return r < t.r;
else
return r > t.r;
}
else
return l < t.l;
}
} qry[N];
void del(int x)
{
preAns -= mp[x] * mp[x];
mp[x]--;
preAns += mp[x] * mp[x];
}
void add(int x)
{
preAns -= mp[x] * mp[x];
mp[x]++;
preAns += mp[x] * mp[x];
}
void solve()
{
cin >> n >> q >> k;
for (int i = 1; i <= n; ++i)
cin >> a[i];
for (int i = 1; i <= q; ++i)
{
cin >> qry[i].l >> qry[i].r;
qry[i].idx = i;
}
for (int i = 1; i <= n; ++i)
id[i] = (i - 1) / B + 1;
sort(qry + 1, qry + q + 1);
for (int i = 1, l = 1, r = 0; i <= q; ++i)
{
while (l > qry[i].l)
add(a[--l]);
while (r < qry[i].r)
add(a[++r]);
while (l < qry[i].l)
del(a[l++]);
while (r > qry[i].r)
del(a[r--]);
ans[qry[i].idx] = preAns;
}
for (int i = 1; i <= q; ++i)
cout << ans[i] << endl;
}
例3·CodeForces - 1514D Cut and Stick
给定一个长度为 \(n\) 的序列 \((n\le3\times10^5)\),可以对其进行三种操作:
- 把一段区间片段中所有的数从原来的区间里剪下来
- 把这些数按照在原来序列里的排列顺序重新拼接成序列
- 最后形成一个或者多个片段的序列,使最开始的序列中每一个数都属于某一个片段。
要求:给定 \(q\) 个询问 \((q\le3\times10^5)\) ,每次给出一个区间的左右端点,将这个区间分成若干个片段,使得每个片段内任意元素出现的次数不严格大于 \(\lceil\frac{x}{2}\rceil\) (\(x\) 为该片段长度)。求可以分成的最少片段数目
题解:莫队求区间众数 + 思维
题意将一个区间中的所有元素分成几个序列,使得任意元素出现的次数不严格大于 \(\lceil\frac{x}{2}\rceil\) (\(x\) 为该片段长度),求可以分成的最少序列数
容易发现如果区间\([l,r]\)中的众数的出现次数\(cnt \leq \lceil\frac{r - l + 1}{2}\rceil\),那么不用分,答案为1
否则我们需要将区间众数分给不是众数的数,让其形成一个序列,显然该序列是合法的
然后将多余的众数单独成为一个序列,因为如果不单独成为一个序列就一定不满足题目要求
例如将\([1,1,1,1,1,2]=>[1],[1],[1],[1],[1,2]\)
设众数个数为\(ans\),那么最少序列数目为\(ans - (r - l + 1 - ans) + 1\)
所以题目就转化为求区间众数的出现次数,我们可以利用分块和莫队求区间众数,这里选择莫队:
我们维护\(cnt[i]\):\(i\)出现的次数,\(num[i]\):出现\(i\)次的数的个数,我们考虑莫队的删除操作
num[cnt[x]]--; cnt[x]--; num[cnt[x]]++;
因为删除可能会导致众数改变,所以我们需要维护一下当前删除\(x\)后,设原有的众数的出现次数为\(cnt\),如果删除后\(num[cnt]=0\),说明众数已经改变,我们更新众数的出现次数
对于添加操作,直接维护每个数出现次数的最大值即可
const int N = 3e5 + 10, M = 4e5 + 10;
int n, q;
int a[N];
int id[N];
int ans[N];
int preAns;
int cnt[N]; // cnt[i]记录数字i在区间内出现的次数
int num[N]; // num[i]记录出现在区间内出现i次的数字的个数
struct node
{
int l, r, idx;
bool operator<(const node &t) const
{
if (id[l] == id[t.l])
{
if (id[l] % 2)
return r < t.r;
else
return r > t.r;
}
else
return l < t.l;
}
} qry[N];
void del(int x)
{
num[cnt[x]]--;
num[--cnt[x]]++;
if (cnt[x] + 1 == preAns && num[preAns] == 0)
preAns--;
}
void add(int x)
{
num[cnt[x]]--;
num[++cnt[x]]++;
preAns = max(preAns, cnt[x]);
}
void solve()
{
cin >> n >> q;
int B = n / sqrt(q) + 1;
for (int i = 1; i <= n; ++i)
cin >> a[i];
for (int i = 1; i <= q; ++i)
{
cin >> qry[i].l >> qry[i].r;
qry[i].idx = i;
}
for (int i = 1; i <= n; ++i)
id[i] = (i - 1) / B + 1;
sort(qry + 1, qry + q + 1);
for (int i = 1, l = 1, r = 0; i <= q; ++i)
{
while (l > qry[i].l)
add(a[--l]);
while (r < qry[i].r)
add(a[++r]);
while (l < qry[i].l)
del(a[l++]);
while (r > qry[i].r)
del(a[r--]);
if (preAns <= (r - l + 1 + 1) / 2)
ans[qry[i].idx] = 1;
else
ans[qry[i].idx] = 2 * preAns - (r - l + 1);
}
for (int i = 1; i <= q; ++i)
cout << ans[i] << endl;
}
例4·求区间内不同元素的数量
题解
- 对于莫队的删除操作:
- 如果删除完后\(cnt[x]= 0\),说明少了一个不同元素,答案贡献 \(-1\)
- 对于莫队的添加操作:
- 如果添加完后\(cnt[x] = 1\),说明多了一个不同元素,答案贡献 \(+ 1\)
const int N = 2e5 + 10, M = 2e5;
const int B = (3e4) / sqrt(M) + 1;
int n, q, k;
int a[N];
int id[N];
int ans[N];
int preAns;
map<int, int> mp;
struct node
{
int l, r, idx;
bool operator<(const node &t) const
{
if (id[l] == id[t.l])
{
if (id[l] % 2)
return r < t.r;
else
return r > t.r;
}
else
return l < t.l;
}
} qry[N];
void del(int x)
{
if (mp[x] == 1)
preAns--;
mp[x]--;
}
void add(int x)
{
if (mp[x] == 0)
preAns++;
mp[x]++;
}
void solve()
{
cin >> n;
for (int i = 1; i <= n; ++i)
cin >> a[i];
cin >> q;
for (int i = 1; i <= q; ++i)
{
cin >> qry[i].l >> qry[i].r;
qry[i].idx = i;
}
for (int i = 1; i <= n; ++i)
id[i] = (i - 1) / B + 1;
sort(qry + 1, qry + q + 1);
for (int i = 1, l = 1, r = 0; i <= q; ++i)
{
while (l > qry[i].l)
add(a[--l]);
while (r < qry[i].r)
add(a[++r]);
while (l < qry[i].l)
del(a[l++]);
while (r > qry[i].r)
del(a[r--]);
ans[qry[i].idx] = preAns;
}
for (int i = 1; i <= q; ++i)
cout << ans[i] << endl;
}