Loading

【算法】莫队

1. 算法简介

莫队算法有很多种:普通莫队,带修莫队,回滚莫队,树上莫队,二维莫队,莫队二次离线。

莫队算法主要用于解决支持快速插入,删除贡献的区间优化问题。

具体的,对于要求解贡献的区间 \([l,r]\) 来说,我们可以把以前求解过的区间 \([L,R]\) 的贡献保留下来,并通过移动 \(L,R\),同时插入,删除贡献的方式得到新区间 \([l,r]\) 的贡献。

比如要求解区间内出现次数最大的数。

可以先维护一个桶,记录每一次数出现的次数。然后对于询问区间 \([l,r]\),将指针 \(L,R\) 移动至 \([l,r]\) 处。同时将移动时摒弃掉的贡献删去,将新增的贡献插入。

2. 算法理论

可以发现如果直接按照上述操作进行的话,时间复杂度会退化至 \(O(n)\)。原因是每一次操作最坏会移动 \(n\) 步。

考虑如何优化,将原序列分块,所有询问离线存储。然后将区间左端点按其所在的块的编号作为第一关键字排序,按区间右端点为第二关键字排序。再进行上述操作,便可以做到 \(O((n+m)\sqrt n)\)

证明如下:
左端点要么在块内移动,要么每次跨越一个块。共进行 \(m\) 轮,时间复杂度为 \(O(m\sqrt n)\)
右端点在左端点的同一个块内只会向前移动,最多移动 \(n\) 步。时间复杂度 \(O(n\sqrt n)\)
综上,时间复杂度为 \(O((n+m)\sqrt n)\)。十分的巧妙。

小 trick:奇偶性排序,最坏情况下右端点每次跑完一遍就会回到最前面,这样极其浪费时间。不妨将区间左端点的块编按照奇偶分类。第二关键字排序时,奇数块的从小到大,偶数块从大到小。这样可以省去很多的时间。

以下是奇偶性排序的代码,其中 \(p_i\) 表示 \(i\) 号元素的块编:

bool cmp(Node x, Node y) {
  return p[x.l] == p[y.l] ? (p[x.l] & 1 ? x.r < y.r : x.r > y.r) : p[x.l] < p[y.l];
}

3. 各色莫队

3.1 普通莫队

3.1.1 P1972 [SDOI2009] HH的项链

P1972 [SDOI2009] HH的项链 为例。

经典数颜色。

维护一个桶,然后创建一个区间指针 \(l=0,r=1\) 分别指向当前区间左右端点。

然后进行区间转移:

while(l < q[i].l) del(l++);
while(l > q[i].l) add(--l);
while(r < q[i].r) add(++r);
while(r > q[i].r) del(r--);

然后是插入和删除函数:

void add(int x) {
  ans += 2 * cnt[a[x]] + 1;
  cnt[a[x]]++;
}

void del(int x) {
  ans += (-2 * cnt[a[x]]) + 1;
  cnt[a[x]]--;
}

然后就可以 \(O(n\sqrt n)\) 通过部分分数,原因是这道题卡了莫队的做法。

点击查看代码
#include <bits/stdc++.h>
#define int long long
#define H 19260817
#define rint register int
#define For(i,l,r) for(rint i=l;i<=r;++i)
#define FOR(i,r,l) for(rint i=r;i>=l;--i)
#define MOD 1000003
#define mod 1000000007
#define getchar()(p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<21,stdin),p1==p2)?EOF:*p1++)
char buf[1<<21],*p1=buf,*p2=buf;
using namespace std;

inline int read() {
  rint x=0,f=1;char ch=getchar();
  while(ch<'0'||ch>'9'){if (ch=='-') f=-1;ch=getchar();}
  while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
  return x*f;
}

void print(int x){
  if(x<0){putchar('-');x=-x;}
  if(x>9){print(x/10);putchar(x%10+'0');}
  else putchar(x+'0');
  return;
}

const int N = 1e6 + 10;

struct Node {
  int l, r, i;
} q[N];

int n, m, a[N], B, cnt[N], ans, Ans[N]; 

bool cmp(Node a, Node b) {
  return (a.l / B) ^ (b.l / B) ? a.l < b.l : ((a.l / B) & 1 ? a.r < b.r : a.r > b.r);
}

void add(int x) {
  if(!cnt[a[x]]) ans++;
  cnt[a[x]]++;
}

void del(int x) {
  cnt[a[x]]--;
  if(!cnt[a[x]]) ans--;
}

signed main() {
  n = read();
  For(i,1,n) a[i] = read();
   m = read();
  B = n/sqrt(m*2/3);
  For(i,1,m) q[i] = (Node){read(), read(), i};
  sort(q + 1, q + m + 1, cmp);
  int l = 0, r = 0;
  For(i,1,m) {
    int ql = q[i].l, qr = q[i].r;
    while(ql < l) add(--l);
    while(ql > l) del(l++);
    while(qr < r) del(r--);
    while(qr > r) add(++r);
    Ans[q[i].i] = ans;
  }
  For(i,1,m) {
    cout << Ans[i] << '\n';
  }
  return 0;
}

3.1.2 P2709 小B的询问

考虑拆贡献:插入贡献时。\(c_i^2\ce{->}(c_i + 1)^2=c_i^2+2c_i+1\),于是原答案上更新 \(+2c_i+1\) 即可。删除贡献同理。

然后又变成数颜色的题了。

点击查看代码
#include<bits/stdc++.h>
#define ll long long
#define For(i,l,r) for(int i=l;i<=r;++i)
#define FOR(i,r,l) for(int i=r;i>=l;--i)

using namespace std;

const int N = 5e4 + 10;

struct Node {
  int l, r, id;
} q[N];

int n, m, k, cnt[N], a[N], p[N], L, ans, Ans[N];

bool cmp(Node x, Node y) {
  return p[x.l] == p[y.l] ? (p[x.l] & 1 ? x.r < y.r : x.r > y.r) : p[x.l] < p[y.l];
}

void add(int x) {
  ans += 2 * cnt[a[x]] + 1;
  cnt[a[x]]++;
}

void del(int x) {
  ans += (-2 * cnt[a[x]]) + 1;
  cnt[a[x]]--;
}

signed main() {
  ios::sync_with_stdio(0);
  cin.tie(0), cout.tie(0);
  cin >> n >> m >> k;
  L = sqrt(n);
  For(i,1,n) cin >> a[i], p[i] = (i - 1) / L + 1;
  For(i,1,m) cin >> q[i].l >> q[i].r, q[i].id = i;
  sort(q + 1, q + m + 1, cmp);
  int l = 1, r = 0;
  For(i,1,m) {
    while(l < q[i].l) del(l++);
    while(l > q[i].l) add(--l);
    while(r < q[i].r) add(++r);
    while(r > q[i].r) del(r--);
    Ans[q[i].id] = ans;
  }
  For(i,1,m) cout << Ans[i] << '\n';
  return 0;
}

3.2.3 P4462 [CQOI2018] 异或序列

一眼就很莫队。

先算出原数列的前缀异或和 \(sum_i\),记 \(cnt_x\) 表示 \(sum_i=x\) 的个数。当区间 \([l,r]\) 满足条件时,即为 \(sum_{r} \oplus sum_{l-1}=k\)。当加入一个位置 \(x\) 的贡献时,答案加上 \(cnt_{sum_x}\),桶 \(cnt_{sum_x}\) 贡献加一。删除位置同理。

细节就是区间指针初始化为 \(l=0,r=0\),询问区间左端点全部减一。原因是前缀和统计时的左端点下标值域为 \([0,n-1]\)

时间复杂度 \(O(n\sqrt n)\)

点击查看代码
#include<bits/stdc++.h>
#define int long long
#define For(i,l,r) for(int i=l;i<=r;++i)
#define FOR(i,r,l) for(int i=r;i>=l;--i)

using namespace std;

const int N = 1e5 + 10;

struct Node {
  int l, r, id;
} q[N];

int n, m, k, ans, L, a[N], sum[N], p[N], cnt[N], Ans[N];

bool cmp(Node x, Node y) {
  return (p[x.l] == p[y.l] ? (p[x.l] & 1 ? x.r < y.r : x.r > y.r) : p[x.l] < p[y.l]);
}

void add(int x) {
  ans += cnt[(sum[x] ^ k)];
  cnt[sum[x]]++;
}

void del(int x) {
  cnt[sum[x]]--;
  ans -= cnt[(sum[x] ^ k)];
}

signed main() {
  ios::sync_with_stdio(0);
  cin.tie(0), cout.tie(0);
  cin >> n >> m >> k;
  L = sqrt(n);
  For(i,1,n) cin >> a[i], p[i] = (i - 1) / L + 1, sum[i] = sum[i-1] ^ a[i];
  cnt[0] = 1;
  For(i,1,m) {
    cin >> q[i].l >> q[i].r;
    q[i].l--, q[i].id = i;
  }
  sort(q + 1, q + m + 1, cmp);
  int l = 0, r = 0;
  For(i,1,m) {
    while(l < q[i].l) del(l++);
    while(l > q[i].l) add(--l);
    while(r < q[i].r) add(++r);
    while(r > q[i].r) del(r--);
    Ans[q[i].id] = ans;
  }
  For(i,1,m) cout << Ans[i] << '\n';
  return 0;
}

3.2 带修莫队

在普通莫队上增加了一个修改操作。

处理方法同普通莫队一样,将修改操作的时间戳记下,当成莫队的一维。这样,每一次修改的情况就有六种:

  1. \([l,r-1,t]\)
  2. \([l,r+1,t]\)
  3. \([l+1,r,t]\)
  4. \([l-1,r,t]\)
  5. \([l,r,t+1]\)
  6. \([l,r,t-1]\)

操作的排序是按照左端点所在块为第一关键字,右端点所在块为第二关键字,时间戳为第三关键字。

修改操作的细节:只有修改在本次操作区间的贡献才会对当前答案造成影响,但是同样会影响到原序列,所以每一次修改要判断修改的贡献是否在操作区间中,同时修改原系列的答案。

注意,块长设为 \(n^{\frac{2}{3}}\) 时,时间复杂度最优。

3.2.1 P1903 [国家集训队] 数颜色 / 维护队列

带修莫队板子。

同样是维护一个桶,没什么细节,时间也非常宽裕。

点击查看代码
#include<bits/stdc++.h>
#define int long long
#define rint register int
#define For(i,l,r) for(rint i=l;i<=r;++i)
#define FOR(i,r,l) for(rint i=r;i>=l;--i)

using namespace std;

const int N = 1e6 + 10;

struct Node {
  int l, r, t, id;
} q[N];

struct node {
  int x, y;
} upd[N];

int n, m, Q, a[N], L, p[N], T, Ans[N], cnt[N], ans;

bool cmp(Node x, Node y) {
  return (p[x.l] == p[y.l] ? (p[x.r] == p[y.r] ? x.t < y.t : p[x.r] < p[y.r]) : p[x.l] < p[y.l]);
}

void add(int x) {
  if(!cnt[x]) ans++;
  cnt[x]++;
}

void del(int x) {
  cnt[x]--;
  if(!cnt[x]) ans--;
}

signed main() {
  ios::sync_with_stdio(0);
  cin.tie(0), cout.tie(0);
  cin >> n >> m;
  L = pow(n, 2.0 / 3.0);
  For(i,1,n) cin >> a[i], p[i] = (i-1) / L + 1;
  For(i,1,m) {
    char op; int x, y;
    cin >> op >> x >> y;
    if(op == 'Q') {
      q[++Q] = (Node){x, y, T, Q};
    } else {
      upd[++T] = (node){x, y};
    }
  }
  sort(q + 1, q + Q + 1, cmp);
  int l = 1, r = 0, t = 0;
  For(i,1,Q) {
    while(l > q[i].l) add(a[--l]);
    while(l < q[i].l) del(a[l++]);
    while(r < q[i].r) add(a[++r]);
    while(r > q[i].r) del(a[r--]);
    while(t < q[i].t) {
      ++t;
      if(q[i].l <= upd[t].x && upd[t].x <= q[i].r) {
        del(a[upd[t].x]);
        add(upd[t].y);
      }
      swap(a[upd[t].x], upd[t].y);
    }
    while(t > q[i].t) {
      if(q[i].l <= upd[t].x && upd[t].x <= q[i].r) {
        del(a[upd[t].x]);
        add(upd[t].y);
      }
      swap(a[upd[t].x], upd[t].y);
      --t;
    }
    Ans[q[i].id] = ans;
  } 
  For(i,1,Q) cout << Ans[i] << '\n';
  return 0;
}
posted @ 2024-10-13 20:18  Daniel_yzy  阅读(112)  评论(0编辑  收藏  举报