Cho dãy và hàng trăm nghìn câu hỏi: đoạn có bao nhiêu giá trị khác nhau? Tổng tiền tố hay bảng thưa không trả lời được, vì không có phép gộp nào cho hai đoạn kề nhau. Nhưng nếu ta đã có đáp án của đoạn , thì thêm hay bớt một phần tử ở đầu mút để có đáp án đoạn bên cạnh rất dễ: chỉ cần một bảng đếm.
Khi mọi câu hỏi biết trước và dãy không đổi, thuật toán Mo sắp các câu hỏi theo một thứ tự khéo, rồi trượt một cửa sổ qua chúng. Tổng số lần trượt chỉ .
Cửa sổ trượt
Giữ một đoạn hiện tại và trạng thái của nó: bảng đếm cnt[x] số lần giá trị xuất hiện trong đoạn, cộng với đáp án đang có. Dời sang phải là thêm , dời sang trái là thêm , dời ngược lại là bỏ. Muốn trả lời câu hỏi thì dời hai biên tới đó. Với số giá trị khác nhau: thêm làm cnt[x] từ 0 lên 1 thì đáp án tăng, bỏ làm nó về 0 thì đáp án giảm.
Nên mở rộng trước, thu hẹp sau: dời ra phải và ra trái trước, rồi mới thu hai biên vào. Làm ngược lại có lúc , đoạn “âm” làm cnt âm. Với bảng đếm đơn giản kết quả cuối vẫn đúng, nhưng với trạng thái phức tạp hơn thì sai.
while (R < r) add(++R);
while (L > l) add(--L);
while (R > r) del(R--);
while (L < l) del(L++);
Hỏi nhanh
Cửa sổ đang là , câu hỏi tiếp theo là . Theo thứ tự “mở rộng trước, thu hẹp sau”, phần tử nào được thêm trước tiên?
nên không dời ra phải; nên thêm , rồi mới bỏ khi thu về 3. Thêm trước giữ cho cửa sổ luôn là một đoạn hợp lệ.
Ví dụGiá Trị Khác Nhau
Đề. Dãy số dương tới , câu hỏi: đếm số giá trị khác nhau trên .
Lời giải. Thuật toán Mo: giữ một cửa sổ cùng bảng đếm cnt theo giá trị và biến cur là số giá trị có cnt > 0. Sắp câu hỏi theo một thứ tự khéo, rồi dời hai biên của cửa sổ từ câu này sang câu kia. Thứ tự câu hỏi giải thích ở mục sau. Giá trị tới nên dùng mảng đếm trực tiếp, không cần nén.
Bước 1: thứ tự câu hỏi. Sắp theo khối của ; trong cùng khối thì theo , khối lẻ giảm, khối chẵn tăng (mẹo chẵn lẻ ở mục sau). Mỗi câu hỏi giữ id gốc để cuối cùng in đáp án theo thứ tự đầu vào.
bool cmp(query x, query y) {
if (x.l / B != y.l / B) return x.l < y.l;
return (x.l / B) & 1 ? x.r > y.r : x.r < y.r;
}
Bước 2: thêm và bỏ một phần tử. Giá trị lần đầu vào cửa sổ thì cur tăng; giá trị rời cửa sổ hẳn thì cur giảm. Cả hai .
void add(ll i) {
if (cnt[a[i]]++ == 0) cur++;
}
void del(ll i) {
if (--cnt[a[i]] == 0) cur--;
}
Bước 3: dời cửa sổ. Cửa sổ bắt đầu rỗng với L = 1, R = 0. Mỗi câu hỏi mở rộng hai biên trước rồi mới thu hẹp, để không lúc nào cửa sổ “âm” làm cnt bị trừ quá.
while (R < r) add(++R);
while (L > l) add(--L);
while (R > r) del(R--);
while (L < l) del(L++);
ans[qs[i].id] = cur;
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 3e4 + 5, Q = 2e5 + 5, V = 1e6 + 5;
ll n, q, B, cur, a[N], cnt[V], ans[Q];
struct query {
ll l, r, id;
} qs[Q];
bool cmp(query x, query y) {
if (x.l / B != y.l / B) return x.l < y.l;
return (x.l / B) & 1 ? x.r > y.r : x.r < y.r;
}
void add(ll i) {
if (cnt[a[i]]++ == 0) cur++;
}
void del(ll i) {
if (--cnt[a[i]] == 0) cur--;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n;
for (ll i = 1; i <= n; i++) cin >> a[i];
cin >> q;
for (ll i = 1; i <= q; i++) {
cin >> qs[i].l >> qs[i].r;
qs[i].id = i;
}
B = max(1LL, (ll)sqrt(n));
sort(qs + 1, qs + q + 1, cmp);
ll L = 1, R = 0;
for (ll i = 1; i <= q; i++) {
ll l = qs[i].l, r = qs[i].r;
while (R < r) add(++R);
while (L > l) add(--L);
while (R > r) del(R--);
while (L < l) del(L++);
ans[qs[i].id] = cur;
}
for (ll i = 1; i <= q; i++) cout << ans[i] << '\n';
}
7
4 4 1 2 1 4 3
4
1 7
2 5
3 3
4 7
4
3
1
4
Với khối , tổng số bước dời là , cộng để sắp.
Sắp theo khối
Trả lời theo thứ tự đầu vào có thể bắt cửa sổ chạy qua lại cả dãy mỗi câu, . Mo chia các vị trí thành khối dài , sắp câu hỏi theo khối của , trong cùng khối thì theo tăng dần. Đếm số bước dời:
- Biên phải. Trong một khối của , chỉ tăng nên đi tổng cộng ; có khối, tổng .
- Biên trái. Hai câu hỏi liên tiếp cùng khối có lệch nhau dưới ; sang khối mới lệch không quá . Tổng .
Tổng , nhỏ nhất khi , cho . Lấy cho , đủ tốt khi và cùng cỡ.
Mẹo chẵn lẻ: khối lẻ sắp giảm dần, khối chẵn sắp tăng dần, để không phải chạy từ cuối dãy về đầu mỗi khi đổi khối. Mẹo này chỉ bớt được chừng một phần mười thời gian, còn việc sắp theo khối thì quyết định tất cả:
Sắp kiểu Mo nhanh gấp khoảng 19 lần so với trả lời theo thứ tự đề.
Mỗi bước dời gọi một lần add hoặc del, nên hai hàm này phải , hoặc nhỏ như nếu chấp nhận thêm một thừa số. Bảng đếm theo giá trị là trạng thái điển hình; giá trị lớn thì nén trước.
Hỏi nhanh
Trong Mo với khối dài , vì sao biên phải chỉ đi tổng cộng bước trong mỗi khối của ?
Trong một khối của , đã sắp tăng nên chỉ tiến, nhiều nhất hết dãy một lần. Một khối có thể chứa rất nhiều câu hỏi và có thể ở bất cứ đâu; giới hạn chỉ đến từ việc không quay đầu.
Ví dụXác Suất Cùng Màu
Đề. Dãy màu, câu hỏi. Chọn ngẫu nhiên đều hai vị trí khác nhau trong ; in xác suất chúng cùng màu dưới dạng phân số tối giản, không có cặp cùng màu thì 0/1.
Lời giải. Số cặp cùng màu là , tổng số cặp là . Mo giữ tổng thứ nhất trong cur, cuối cùng chia cả tử và mẫu cho GCD. Với , ba màu 1 cho 3 cặp, hai màu 2 cho 1 cặp, trên 15 cặp.
Bước 1: thêm và bỏ. Phần tử màu mới vào tạo thêm cnt[x] cặp cùng màu với các bản đã có, rồi mới tăng cnt. Bỏ thì giảm trước rồi trừ, đúng là số cặp mất đi.
void add(ll i) {
cur += cnt[a[i]]++;
}
void del(ll i) {
cur -= --cnt[a[i]];
}
Bước 2: dời cửa sổ. Thứ tự câu hỏi và bốn vòng dời biên giống hệt ví dụ trước.
Bước 3: rút gọn phân số. tot là số cặp vị trí trong đoạn. Đoạn dài 1 có tot = 0 và cur = 0, rơi vào nhánh 0/1, nên không bao giờ chia cho 0.
ll len = r - l + 1, tot = len * (len - 1) / 2;
if (cur == 0) P[id] = 0, D[id] = 1;
else {
ll g = __gcd(cur, tot);
P[id] = cur / g, D[id] = tot / g;
}
Tử và mẫu tới , dùng long long.
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 5e4 + 5;
ll n, q, B, cur, a[N], cnt[N], P[N], D[N];
struct query {
ll l, r, id;
} qs[N];
bool cmp(query x, query y) {
if (x.l / B != y.l / B) return x.l < y.l;
return (x.l / B) & 1 ? x.r > y.r : x.r < y.r;
}
void add(ll i) {
cur += cnt[a[i]]++;
}
void del(ll i) {
cur -= --cnt[a[i]];
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> q;
for (ll i = 1; i <= n; i++) cin >> a[i];
for (ll i = 1; i <= q; i++) {
cin >> qs[i].l >> qs[i].r;
qs[i].id = i;
}
B = max(1LL, (ll)sqrt(n));
sort(qs + 1, qs + q + 1, cmp);
ll L = 1, R = 0;
for (ll i = 1; i <= q; i++) {
ll l = qs[i].l, r = qs[i].r, id = qs[i].id;
while (R < r) add(++R);
while (L > l) add(--L);
while (R > r) del(R--);
while (L < l) del(L++);
ll len = r - l + 1, tot = len * (len - 1) / 2;
if (cur == 0) P[id] = 0, D[id] = 1;
else {
ll g = __gcd(cur, tot);
P[id] = cur / g, D[id] = tot / g;
}
}
for (ll i = 1; i <= q; i++) cout << P[i] << '/' << D[i] << '\n';
}
6 4
1 2 1 1 2 3
1 6
2 5
3 3
4 6
4/15
1/3
0/1
0/1
bước dời cửa sổ.
Cặp tiền tố
Câu hỏi đếm các đoạn con của có một tính chất cộng tính, như tổng hay XOR bằng , trông không giống câu hỏi về các phần tử. Đặt và : đoạn con có XOR là . Vậy đếm đoạn con của có XOR bằng là đếm cặp chỉ số trong với , một câu hỏi về cặp phần tử của mảng tiền tố. Mo chạy trên mảng với đoạn ; thêm chỉ số làm tăng đáp án đúng cnt[p[y] ^ K].
Ví dụĐoạn Con XOR Bằng K
Đề. Dãy số nhỏ hơn , số , câu hỏi: đếm số cặp mà XOR của bằng .
Lời giải. Với XOR tiền tố , , XOR của bằng . Cặp trong ứng với cặp chỉ số trong có . Vậy chạy Mo trên mảng với đoạn , đếm số cặp chỉ số trong cửa sổ có XOR bằng .
Bước 1: tiền tố và đổi đoạn. p[i] tính tại chỗ khi đọc. Câu hỏi thành , nên cửa sổ rỗng ban đầu là L = 0, R = -1.
for (ll i = 1; i <= n; i++) {
cin >> p[i];
p[i] ^= p[i - 1];
}
Bước 2: thêm và bỏ. Chỉ số mới ghép được với mọi chỉ số cũ có bằng p[i] ^ k: cộng trước rồi mới tăng cnt[p[i]]. Bỏ thì làm ngược lại theo thứ tự ngược, giảm trước rồi trừ. Khi , p[i] ^ k là chính p[i], và thứ tự này bảo đảm một chỉ số không tự ghép với chính nó.
void add(ll i) {
cur += cnt[p[i] ^ k];
cnt[p[i]]++;
}
void del(ll i) {
cnt[p[i]]--;
cur -= cnt[p[i] ^ k];
}
Bảng cnt có ô vì mọi XOR tiền tố đều dưới . Thứ tự câu hỏi và vòng dời biên như ví dụ đầu.
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 1e5 + 5, V = 1 << 20;
ll n, q, k, B, cur, p[N], cnt[V], ans[N];
struct query {
ll l, r, id;
} qs[N];
bool cmp(query x, query y) {
if (x.l / B != y.l / B) return x.l < y.l;
return (x.l / B) & 1 ? x.r > y.r : x.r < y.r;
}
void add(ll i) {
cur += cnt[p[i] ^ k];
cnt[p[i]]++;
}
void del(ll i) {
cnt[p[i]]--;
cur -= cnt[p[i] ^ k];
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> q >> k;
for (ll i = 1; i <= n; i++) {
cin >> p[i];
p[i] ^= p[i - 1];
}
for (ll i = 1; i <= q; i++) {
cin >> qs[i].l >> qs[i].r;
qs[i].l--;
qs[i].id = i;
}
B = max(1LL, (ll)sqrt(n));
sort(qs + 1, qs + q + 1, cmp);
ll L = 0, R = -1;
for (ll i = 1; i <= q; i++) {
ll l = qs[i].l, r = qs[i].r;
while (R < r) add(++R);
while (L > l) add(--L);
while (R > r) del(R--);
while (L < l) del(L++);
ans[qs[i].id] = cur;
}
for (ll i = 1; i <= q; i++) cout << ans[i] << '\n';
}
5 4 3
1 2 3 1 2
1 5
1 3
2 5
4 5
4
2
2
1
bước dời cửa sổ.
Chỉ thêm, không bỏ
Có trạng thái thêm dễ mà bỏ khó: giá trị lớn nhất của một đại lượng chỉ tăng khi thêm, nhưng bỏ đi phần tử đang giữ kỷ lục thì không biết kỷ lục mới là bao nhiêu. Mo chỉ thêm (Mo quay lui, rollback) tránh hẳn thao tác bỏ:
- Xét lần lượt từng khối của . Câu hỏi có cùng một khối thì duyệt thẳng, .
- Các câu hỏi còn lại của khối, sắp theo tăng dần. Đặt là cuối khối; phần bên phải chỉ lớn dần, nên chỉ thêm.
- Với mỗi câu hỏi, thêm tạm phần trái vào trạng thái của phần phải, đọc đáp án, rồi xóa sạch những gì phần trái vừa ghi. Phần trái dài dưới .
Tổng chi phí vẫn . Cái giá là phải hoàn tác được phần trái, thường bằng cách ghi riêng nó vào mảng phụ rồi xóa đúng các ô đã ghi; không được xóa cả mảng bằng memset mỗi lần.
Ví dụKhoảng Cách Lặp Lại Xa Nhất
Đề. Dãy số, câu hỏi: trên , tìm lớn nhất với , không có cặp nào thì in 0.
Lời giải. Khoảng cách xa nhất không bỏ được dễ dàng: khi một phần tử rời cửa sổ, ta không biết đáp án lùi về bao nhiêu. Vì vậy dùng Mo chỉ thêm (rollback): với các câu hỏi có cùng khối, phần phải (với là cuối khối) chỉ được thêm vì tăng dần, còn phần trái ngắn hơn một khối được quét lại từ đầu cho mỗi câu hỏi rồi xóa đi.
Bước 1: nén giá trị và sắp câu hỏi. Giá trị nén về để dùng làm chỉ số mảng. Câu hỏi sắp theo khối của , trong khối theo tăng; không dùng mẹo chẵn lẻ vì phần phải chỉ được thêm.
sort(v + 1, v + n + 1);
for (ll i = 1; i <= n; i++) a[i] = lower_bound(v + 1, v + n + 1, a[i]) - v;
Bước 2: câu hỏi nằm gọn trong khối. Duyệt thẳng, mx[x] nhớ vị trí đầu tiên của ; xong thì xóa đúng các ô mx vừa ghi.
if (r <= b) {
for (ll t = l; t <= r; t++) {
if (!mx[a[t]]) mx[a[t]] = t;
res = max(res, t - mx[a[t]]);
}
for (ll t = l; t <= r; t++) mx[a[t]] = 0;
ans[qs[j].id] = res;
continue;
}
Bước 3: thêm vào phần phải. fst[x], lst[x] là vị trí đầu, cuối của trong phần phải, cur là đáp án của riêng phần phải; thêm chỉ có thể làm cur tăng.
while (R < r) {
R++;
if (!fst[a[R]]) fst[a[R]] = R;
lst[a[R]] = R;
cur = max(cur, R - fst[a[R]]);
}
Bước 4: quét phần trái. Quét từ lùi về : mx[x] là vị trí lớn nhất của trong phần trái, tức lần đầu gặp khi quét lùi. Vị trí ghép xa nhất với lst[x] nếu có ở phần phải, ngược lại với mx[x]. Quét xong xóa mx; hết khối thì xóa fst, lst trên đúng các vị trí đã thêm.
res = cur;
for (ll t = b; t >= l; t--) {
if (!mx[a[t]]) mx[a[t]] = t;
res = max(res, max(lst[a[t]], mx[a[t]]) - t);
}
for (ll t = b; t >= l; t--) mx[a[t]] = 0;
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 2e5 + 5;
ll n, q, B, a[N], v[N], fst[N], lst[N], mx[N], ans[N];
struct query {
ll l, r, id;
} qs[N];
bool cmp(query x, query y) {
if (x.l / B != y.l / B) return x.l < y.l;
return x.r < y.r;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n;
for (ll i = 1; i <= n; i++) {
cin >> a[i];
v[i] = a[i];
}
sort(v + 1, v + n + 1);
for (ll i = 1; i <= n; i++) a[i] = lower_bound(v + 1, v + n + 1, a[i]) - v;
cin >> q;
for (ll i = 1; i <= q; i++) {
cin >> qs[i].l >> qs[i].r;
qs[i].id = i;
}
B = max(1LL, (ll)sqrt(n));
sort(qs + 1, qs + q + 1, cmp);
for (ll i = 1, j; i <= q; i = j) {
ll blk = qs[i].l / B, b = min(n, (blk + 1) * B - 1), R = b, cur = 0;
for (j = i; j <= q && qs[j].l / B == blk; j++) {
ll l = qs[j].l, r = qs[j].r, res = 0;
if (r <= b) {
for (ll t = l; t <= r; t++) {
if (!mx[a[t]]) mx[a[t]] = t;
res = max(res, t - mx[a[t]]);
}
for (ll t = l; t <= r; t++) mx[a[t]] = 0;
ans[qs[j].id] = res;
continue;
}
while (R < r) {
R++;
if (!fst[a[R]]) fst[a[R]] = R;
lst[a[R]] = R;
cur = max(cur, R - fst[a[R]]);
}
res = cur;
for (ll t = b; t >= l; t--) {
if (!mx[a[t]]) mx[a[t]] = t;
res = max(res, max(lst[a[t]], mx[a[t]]) - t);
}
for (ll t = b; t >= l; t--) mx[a[t]] = 0;
ans[qs[j].id] = res;
}
for (ll t = b + 1; t <= R; t++) fst[a[t]] = lst[a[t]] = 0;
}
for (ll i = 1; i <= q; i++) cout << ans[i] << '\n';
}
8
5 1 5 2 1 5 2 3
5
1 8
2 5
4 8
6 8
3 6
5
3
3
0
3
Mỗi khối, phần phải đi tối đa bước; mỗi câu hỏi quét phần trái tối đa bước: tổng .
Thêm thời gian
Khi dãy bị sửa xen giữa các câu hỏi, thêm chiều thứ ba là thời gian : số lần sửa đã xảy ra trước câu hỏi. Trạng thái giờ là ; ngoài bốn vòng dời biên còn vòng dời , mỗi bước áp dụng hoặc hoàn tác một lần sửa. Một lần sửa ở vị trí nằm trong cửa sổ thì bỏ giá trị cũ và thêm giá trị mới; mẹo gọn là đổi chỗ giá trị trong dãy với giá trị lưu trong lần sửa, nên áp dụng và hoàn tác là cùng một hàm.
Sắp câu hỏi theo khối của , rồi khối của , rồi , với khối dài , tức căn bậc ba của (với là khoảng 2150). Biên trái và phải mỗi câu dời , còn chạy hết cho mỗi cặp khối, có cặp. Tổng , ở là khi và cùng cỡ; với khoảng bước đơn giản.
Ví dụMàu Sắc Theo Thời Gian
Đề. Dãy màu tới , thao tác cùng cỡ: Q l r đếm số màu khác nhau trên đoạn, R p c đổi màu ô thành .
Lời giải. Mo có thời gian: thêm chiều thứ ba là số lần sửa đã áp. Mỗi câu hỏi là bộ ba ; cửa sổ dời được theo cả ba chiều. cnt và cur như bài đếm giá trị khác nhau.
Bước 1: tách câu hỏi và lần sửa. Mỗi câu hỏi nhớ t, số lần sửa đứng trước nó; mỗi lần sửa lưu vị trí cp và màu cc.
if (op == 'Q') {
nq++;
qs[nq] = {x, y, nc, nq};
} else {
nc++;
cp[nc] = x, cc[nc] = y;
}
Bước 2: thứ tự ba khóa. Khối của , rồi khối của , rồi , mỗi khóa sau có mẹo chẵn lẻ. Khối dài , tính bằng cbrt(n * n).
bool cmp(query x, query y) {
if (x.l / B != y.l / B) return x.l < y.l;
if (x.r / B != y.r / B) return (x.l / B) & 1 ? x.r > y.r : x.r < y.r;
return (x.r / B) & 1 ? x.t > y.t : x.t < y.t;
}
Bước 3: áp hoặc hoàn tác một lần sửa. Nếu vị trí sửa nằm trong cửa sổ thì bỏ màu cũ, thêm màu mới. Rồi swap màu trong dãy với màu lưu trong lần sửa: gọi lần nữa thì đổi chỗ lại, tức hoàn tác, nên cùng một hàm dùng cho cả hai chiều.
void apply(ll t, ll l, ll r) {
ll p = cp[t];
if (l <= p && p <= r) {
del(a[p]);
add(cc[t]);
}
swap(a[p], cc[t]);
}
Bước 4: dời theo ba chiều. Dời , trước rồi mới dời , để apply kiểm vị trí sửa với đúng cửa sổ của câu hỏi.
while (R < r) add(a[++R]);
while (L > l) add(a[--L]);
while (R > r) del(a[R--]);
while (L < l) del(a[L++]);
while (T < t) apply(++T, L, R);
while (T > t) apply(T--, L, R);
cnt dùng int cho màu để tiết kiệm bộ nhớ.
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 1.4e5, V = 1e6 + 5;
ll n, m, B, cur, nq, nc, a[N], cp[N], cc[N], ans[N];
int cnt[V];
struct query {
ll l, r, t, id;
} qs[N];
bool cmp(query x, query y) {
if (x.l / B != y.l / B) return x.l < y.l;
if (x.r / B != y.r / B) return (x.l / B) & 1 ? x.r > y.r : x.r < y.r;
return (x.r / B) & 1 ? x.t > y.t : x.t < y.t;
}
void add(ll x) {
if (cnt[x]++ == 0) cur++;
}
void del(ll x) {
if (--cnt[x] == 0) cur--;
}
void apply(ll t, ll l, ll r) {
ll p = cp[t];
if (l <= p && p <= r) {
del(a[p]);
add(cc[t]);
}
swap(a[p], cc[t]);
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> m;
for (ll i = 1; i <= n; i++) cin >> a[i];
for (ll i = 1; i <= m; i++) {
char op;
ll x, y;
cin >> op >> x >> y;
if (op == 'Q') {
nq++;
qs[nq] = {x, y, nc, nq};
} else {
nc++;
cp[nc] = x, cc[nc] = y;
}
}
B = max(1LL, (ll)cbrt((double)n * n));
sort(qs + 1, qs + nq + 1, cmp);
ll L = 1, R = 0, T = 0;
for (ll i = 1; i <= nq; i++) {
ll l = qs[i].l, r = qs[i].r, t = qs[i].t;
while (R < r) add(a[++R]);
while (L > l) add(a[--L]);
while (R > r) del(a[R--]);
while (L < l) del(a[L++]);
while (T < t) apply(++T, L, R);
while (T > t) apply(T--, L, R);
ans[qs[i].id] = cur;
}
for (ll i = 1; i <= nq; i++) cout << ans[i] << '\n';
}
5 7
1 2 1 3 2
Q 1 5
R 3 2
Q 1 3
R 4 1
Q 2 5
R 1 2
Q 1 4
3
2
2
2
Với , tổng số bước dời là .
Khi đi thi
- Câu hỏi đoạn ngoại tuyến, dãy tĩnh, đáp án phụ thuộc vào bảng tần suất (số giá trị khác nhau, tổng theo tần suất, cặp cùng giá trị): Mo, với cỡ đến . Câu hỏi bắt buộc trả lời trực tuyến thì không dùng được.
addvàdelphải thật nhẹ: mảng thay chomap, nén giá trị trước, dùng mẹo chẵn lẻ khi sắp; đáp án tổng hay số cặp dùngll.- Bỏ phần tử khó thì Mo chỉ thêm; có sửa điểm thì Mo có cập nhật với khối ; trên cây thì trải cây thành dãy (bài 31) rồi chạy Mo trên đó.
Tóm tắt
- Cửa sổ dời từng bước, mỗi bước thêm hoặc bỏ một phần tử; mở rộng trước, thu hẹp sau.
- Sắp câu hỏi theo khối của rồi theo : , với là .
- Đoạn con có tổng hay XOR cho trước là cặp tiền tố: chạy Mo trên mảng tiền tố với đoạn .
- Mo chỉ thêm giữ phần phải chỉ lớn dần, thêm tạm phần trái rồi hoàn tác.
- Mo có cập nhật thêm chiều thời gian, áp dụng và hoàn tác lần sửa bằng đổi chỗ, khối .
Bài tập
Đọc thêm
- Mo's algorithmcp-algorithms
- 普通莫队算法OI Wiki
- Square Root DecompositionUSACO Guide
- Mo's algorithm on treesCodeforces blog