【ACWing】2492. HH的项链

题目地址:

https://www.acwing.com/problem/content/description/2494/

HH有一串由各种漂亮的贝壳组成的项链。HH相信不同的贝壳会带来好运,所以每次散步完后,他都会随意取出一段贝壳,思考它们所表达的含义。HH不断地收集新的贝壳,因此他的项链变得越来越长。有一天,他突然提出了一个问题:某一段贝壳中,包含了多少种不同的贝壳?这个问题很难回答,因为项链实在是太长了。于是,他只好求助睿智的你,来解决这个问题。

输入格式:
第一行:一个整数 N N N,表示项链的长度。
第二行: N N N个整数,表示依次表示项链中贝壳的编号(编号为 0 0 0 1000000 1000000 1000000之间的整数)。
第三行:一个整数 M M M,表示HH询问的个数。
接下来 M M M行:每行两个整数, L L L R R R,表示询问的区间。

输出格式:
M M M行,每行一个整数,依次表示询问对应的答案。

数据范围:
1 ≤ N ≤ 50000 1≤N≤50000 1N50000
1 ≤ M ≤ 2 × 1 0 5 1≤M≤2×10^5 1M2×105
1 ≤ L ≤ R ≤ N 1≤L≤R≤N 1LRN

改一下记号,设 n n n为项链总长度,询问数为 m m m

处理离线区间询问的问题。如果我们可以将所有询问区间的左端点从小到大排序,使得右端点也恰好从小到大排好序,则可以按这种顺序处理询问,用双指针的方式,利用上一次询问的结果来求当前询问的结果,总时间是 O ( n ) O(n) O(n)。显然询问不总能如此排序。可以用莫队优化。

莫队的思想其实是分块 + 对离线询问排序 + 双指针。先用分块思想,对整个区间做分块,每个块的长度设为 t t t,将所有询问的区间按这样的规则排序:如果左端点在不同分块中,则按分块的下标从小到大排序;如果在同一分块中,则奇数分块(此为玄学优化,下面会解释)。将询问排好序之后,开两个指针 i i i j j j,每次处理某个询问的时候,设询问是 { l , r } \{l,r\} {l,r},那么我们将 i i i r r r移动, i i i指针如果向右,则是处理增量,即遇到新的颜色要计数加 1 1 1,如果向左,则是处理减量,即某个颜色不存在了,计数要减 1 1 1;接下来将 j j j l l l移动, j j j指针如果向右,是处理减量,否则是处理增量。得到答案之后记录之,再处理下一个询问。

我们计算一下两个指针的总移动次数。设块数为 s = n / t s=n/t s=n/t,当 j j j在某块内移动的时候, i i i移动 n n n次(因为 i i i是单调移动的),一共 s s s个分块,所以 i i i移动次数为 s n sn sn;对于某个分块来说, j j j指针在块内移动的时候只会最多移动 m i t m_it mit次( m i m_i mi是左端点在当前块内的询问次数),从而总共的块内移动次数为 m t mt mt,当处理到下一个分块的询问的时候, j j j最多移动 2 t s = 2 n 2ts=2n 2ts=2n次。所以总次数为 n 2 t + m t + 2 n \frac{n^2}{t}+mt+2n tn2+mt+2n,如果按普通分块来做, t = n t=\sqrt n t=n ,则时间复杂度为 O ( m n ) O(m\sqrt n) O(mn ),即平均每次询问时间复杂度为 O ( n ) O(\sqrt n) O(n ),优化了许多。由均值不等式,可以取 t = n 2 m t=\sqrt {n^2m} t=n2m 取得最少步数。

接下来解释一下奇数块右端点递增排序,偶数块右端点递减排序的原因。在跨分块的时候,由于处理完了上个分块的询问,假设上一个分块的右端点是递增的,那么右指针很大概率会停留在靠右的位置。如果这样排序,那么处理下一个分块的询问的时候,下一个分块右端点递减,从而右指针是回滚的;而如果仍然按右端点递减排序,则右指针在处理第一个询问的时候会回滚一大段,然后在向右滚动,这一大段回滚在上面的排序方式中是不会出现的。

代码如下:

#include <iostream>
#include <algorithm>
#include <cmath>
using namespace std;

const int N = 50010, M = 200010, S = 1000010;
int n, m, len;
int w[N], res[M], ans;
int cnt[S];
struct Query {
  int id, l, r;
} q[M];

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

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

int main() {
  scanf("%d", &n);
  for (int i = 1; i <= n; i++) scanf("%d", &w[i]);
  scanf("%d", &m);
  // 这里块长后面加1是为了应对n * n / m = 0的情况
  len = sqrt(double(n) * n / m) + 1;
  for (int i = 0; i < m; i++) {
    scanf("%d%d", &q[i].l, &q[i].r);
    q[i].id = i;
  }
  
  // 对询问按上面规则排序
  sort(q, q + m, [](const Query &a, const Query &b) {
    int i = a.l / len, j = b.l / len;
    if (i != j) return i < j;
    if (i & 1) return a.r < b.r;
    else return a.r > b.r;
  });

  // i向r靠近,j向l靠近,i走到r的时候会将w[r]计入,j会将走到w[l]之前的改变计入
  for (int k = 0, i = 0, j = 1; k < m; k++) {
    int id = q[k].id, l = q[k].l, r = q[k].r;
    while (i < r) add(w[++i]);
    while (i > r) del(w[i--]);
    while (j < l) del(w[j++]);
    while (j > l) add(w[--j]);
    res[id] = ans;
  }

  for (int i = 0; i < m; i++) printf("%d\n", res[i]);
}

预处理时间复杂度 O ( m log ⁡ m ) O(m\log m) O(mlogm),每次询问 O ( n ) O(\sqrt n) O(n ),空间 O ( m + S ) O(m+S) O(m+S) S S S为不同颜色数量。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值