思路:
普通暴力肯定是过不了的,我们可以尝试优化一下:
优化1:每次枚举到一个数值num,增加出现次数时判断一下 c n t n u m cnt_num cntnum是否为0,如果为0,则这个数值之前没有出现过,现在出现了,那么结果+1。反之在区间中删除num后也判断一下 c n t n u m cnt_num cntnum是否为0,如果为0数值总数-1。
优化2:我们可以用两个指针l,r,每次询问不直接枚举,而是移动l,r指针到询问的区间,直到l,r与询问区间重合。在统计答案时,我们也只在指针处加减cnt,然后我们用优化1中的方法快速地统计答案。
优化3:我们可以考虑把所有查询区间按左端点排序,从而使左指针最多移动O(n)次。但这样的话右端点又是无序的,又让整体复杂度变回原来。莫队算法的核心是分块和排序。我们将大小为n的序列分为 s q r t n sqrt{n} sqrtn块,从1到 s q r t n sqrt{n} sqrtn编号,然后根据这个对查询区间进行排序。一种方法是把查询区间按照左端点所在块的序号排序,如果左端点所在块相同,再按右端点排序。排完序后我们再进行左右指针跳动操作。
关于复杂度:
区间排序:建个结构体,用sort排一遍,复杂度为 O ( n ∗ l o g n ) O(n*logn) O(n∗logn)
左指针的移动: O ( n ∗ n ) O(n*\sqrt{n}) O(n∗n );
右指针的移动: O ( n ∗ n ) O(n*\sqrt{n}) O(n∗n );
得出莫队的总时间复杂度为 O ( n ∗ n ) O(n*\sqrt{n}) O(n∗n );
关于排序:
int cmp(query a, query b) {
return belong[a.l] == belong[b.l] ? a.r < b.r : belong[a.l] < belong[b.l];
}
重点来了:莫队卡常技巧
1、#pragma GCC optimize(2)
开O2优化
2、莫队玄学奇偶性排序
主要操作:把查询区间的排序函数
int cmp(query a, query b) {
return belong[a.l] == belong[b.l] ? a.r < b.r : belong[a.l] < belong[b.l];
}
改为:
int cmp(query a, query b) {
return (belong[a.l] ^ belong[b.l]) ? belong[a.l] < belong[b.l] : ((belong[a.l] & 1) ? a.r < b.r : a.r > b.r);
}
3、移动指针的常数压缩
void add(int pos) {
if(!cnt[aa[pos]]) ++now;
++cnt[aa[pos]];
}
void del(int pos) {
--cnt[aa[pos]];
if(!cnt[aa[pos]]) --now;
}
int main()
{
while(l < ql) del(l++);
while(l > ql) add(--l);
while(r < qr) add(++r);
while(r > qr) del(r--);
}
压缩为
while(l < ql) now -= !--cnt[aa[l++]];
while(l > ql) now += !cnt[aa[--l]]++;
while(r < qr) now += !cnt[aa[++r]]++;
while(r > qr) now -= !--cnt[aa[r--]];
4、加快读、快输
本题的代码:
#include
using namespace std;
#pragma GCC optimize(2)
const int maxn = 1e6+10;
int aa[maxn],cnt[maxn], belong[maxn];
int n, m, size, bnum, now, ans[maxn];
struct query{
int l, r, id;
}q[maxn];
int cmp(query a, query b)
{
return ((belong[a.l] ^ belong[b.l]) ? belong[a.l] < belong[b.l] : (belong[a.l] & 1) ? a.r < b.r : a.r > b.r);
}
inline int read()
{
int s = 0 ,f = 1;
char ch = getchar();
while(ch < '0' || ch > '9') {
if(ch == '-')
f = -1;
ch = getchar();
}
while(ch >= '0' && ch <= '9') {
s = s * 10 + ch - '0';
ch = getchar();
}
return s*f;
}
inline void write(int x){
if(x < 0) {
putchar('-');
x = -x;
}
if(x > 9 ) {
write(x / 10);
}
putchar(x%10+'0');
}
int main()
{
n = read();
size = sqrt(n);
bnum = ceil((double)n / size);
for(int i = 1; i <= bnum; i++) {
for(int j = (i-1) * size + 1; j <= i * size; j++) {
belong[j] = i;
}
}
for(int i = 1; i <= n; ++i) {
aa[i] = read();
}
m = read();
for(int i = 1; i <= m; i++) {
q[i].l = read();
q[i].r = read();
q[i].id = i;
}
sort(q+1, q+m+1, cmp);
int l = 1, r = 0;
for(int i = 1; i <= m; i++) {
int ql = q[i].l, qr = q[i].r;
while(l < ql) {
now -= !--cnt[aa[l++]];
}
while(l > ql) {
now += !cnt[aa[--l]]++;
}
while(r > qr) {
now -= !--cnt[aa[r--]];
}
while(r < qr) {
now += !cnt[aa[++r]]++;
}
ans[q[i].id] = now;
}
for(int i = 1; i <= m; i++) {
write(ans[i]);
printf("\n");
}
return 0;
}
莫队算法的扩展——带修改的莫队
例题:BZOJ-2120: 数颜色
题意:
Q操作来查询从第L支画笔到第R支画笔中共有几种不同颜色的画笔。
R操作来把第P支画笔替换为颜色Col。
思路:
可以把莫队加上一维,变为带修莫队。
那么加上一维什么呢?具体怎么实现?我们的做法是把修改操作编号,称为"时间戳",而查询操作的时间戳沿用之前最近的修改操作的时间戳。跑主算法时定义当前时间戳为 tt ,对于每个查询操作,如果当前时间戳相对太大了,说明已进行的修改操作比要求的多,就把之前改的改回来,反之往后改。只有当当前区间和查询区间左右端点、时间戳均重合时,才认定区间完全重合,此时的答案才是本次查询的最终答案。
通俗地讲,就是再弄一指针,在修改操作上跳来跳去,如果当前修改多了就改回来,改少了就改过去,直到次数恰当为止。
这样,我们当前区间的移动方向从四个([l−1,r][l−1,r]、[l+1,r][l+1,r]、[l,r−1][l,r−1]、[l,r+1][l,r+1])变成了六个([l−1,r,t][l−1,r,t]、[l+1,r,t][l+1,r,t]、[l,r−1,t][l,r−1,t]、[l,r+1,t][l,r+1,t]、[l,r,t−1][l,r,t−1]、[l,r,t+1][l,r,t+1])
带修改莫队的排序:
int cmp(query a, query b) {
return (belong[a.l] ^ belong[b.l]) ? belong[a.l] < belong[b.l] : ((belong[a.r] ^ belong[b.r]) ? belong[a.r] < belong[b.r] : a.time < b.time);
}
分块大小和复杂度:
有的dalao证明了当块的大小设 n 2 / 3 n^{2/3} n2/3时理论复杂度达到最优.
代码:
#include
using namespace std;
const int maxn = 50500;
const int maxc = 1001000;
struct node{
int l, r, id, time;
}q[maxn];
struct node2{
int pos, color;
}c[maxn];
int n, m;
int a[maxn], cnt[maxc], belong[maxn], ans[maxn];
int tot = 0, tot2 = 0, size, bnum;
bool cmp(node xx, node yy)
{
return (belong[xx.l] ^ belong[yy.l]) ? belong[xx.l] < belong[yy.l] : ((belong[xx.r] ^ belong[yy.r]) ? belong[xx.r] < belong[yy.r] : xx.time < yy.time);
}
inline int read()
{
int s = 0, f = 1;
char ch = getchar();
while(ch < '0' || ch > '9') {
if(ch == '-')
f = -1;
ch = getchar();
}
while(ch >= '0' && ch <= '9') {
s = s * 10 + ch - '0';
ch = getchar();
}
return s * f;
}
inline void write(int x)
{
if(x < 0) {
putchar('-');
x = -x;
}
if(x > 9){
write(x/10);
}
putchar(x%10+'0');
}
int main()
{
n = read(); m = read();
size = pow(n, 2.0/3.0);
bnum = ceil((double)n/size);
for(int i = 1; i <= bnum; i++)
for(int j = (i-1)*size+1; j <= i*size; j++)
belong[j] = i;
for(int i = 1; i <= n; i++)
a[i] = read();
char ch[50];
int ll, rr;
for(int i = 1; i <= m; i++) {
scanf("%s",ch);
ll = read();
rr = read();
if(ch[0] == 'Q') {
q[++tot].l = ll;
q[tot].r = rr;
q[tot].id = tot;
q[tot].time = tot2;
}
else if(ch[0] == 'R') {
c[++tot2].pos = ll;
c[tot2].color = rr;
}
}
sort(q+1, q+tot+1, cmp);
int ql, qr, qt;
int l = 1, r = 0, time = 0, now = 0;
for(int i = 1; i <= tot; i++) {
ql = q[i].l, qr = q[i].r;
qt = q[i].time;
while(l < ql) now -= !--cnt[a[l++]];
while(l > ql) now += !cnt[a[--l]]++;
while(r < qr) now += !cnt[a[++r]]++;
while(r > qr) now -= !--cnt[a[r--]];
while(time < qt) {
++time;
if(ql <= c[time].pos && c[time].pos <= qr) now -= !--cnt[a[c[time].pos]] - !cnt[c[time].color]++;
swap(a[c[time].pos] , c[time].color);
}
while(time > qt) {
if(ql <= c[time].pos && c[time].pos <= qr) now -= !--cnt[a[c[time].pos]] - !cnt[c[time].color]++;
swap(a[c[time].pos] , c[time].color);
--time;
}
ans[q[i].id] = now;
}
for(int i = 1; i <= tot; i++) {
printf("%d\n", ans[i]);
}
return 0;
}