0x44
分块前两节中,我们探讨了树状数组和线段树两种数据结构。树状数组基于二进制划分和倍增思想,线段树基于分治思想。它们之所以能够高效地在一个序列上执行指令并统计信息,就是因为它们把序列中的元素聚合成大大小小的“段”,花费额外的代价对这些“段”进行维护,从而使得每个区间的信息可以快速由几个已有的“段”结合而成。
当然树状数组与线段树也有其缺点。比如在维护较为复杂的信息(尤其是不满足区间可加、可减性的信息)时显得吃力,代码维护也不是那么简单、直观,需要深入理解并注意许多细节。在本节中,我们介绍分块算法。分块的基本思想是通过适当的划分,预处理一部分信息并保存下来,用空间换取时间,达到时空平衡。事实上,分块更接近于“朴素”,效率往往比不上树状数组和线段树,但是它更加通用、容易实现。我们通过几道例题详细讨论各种形式的分块算法及其应用。
给定长度为 N ( N ≤ 1 0 5 ) N(N\leq 10^5) N(N≤105)的数列 A A A,然后输入 Q ( Q ≤ 1 0 5 ) Q(Q\leq 10^5) Q(Q≤105)行操作指令。
第一类指令形如“ C l r d C\ l\ r\ d C l r d”,表示把数列中第 l ∼ r l\sim r l∼r个数都加上 d d d。
第二类指令形如“ Q l r Q\ l\ r Q l r”,表示询问数列中第 l ∼ r l\sim r l∼r个数的和。
我们已经用树状数组和线段树在 O ( ( N + Q ) l o g N ) O((N+Q)logN) O((N+Q)logN)的时间内解决过该问题。现在我们用分块来再次求解。
把数列 A A A分成若干个长度不超过 ⌊ N ⌋ \lfloor \sqrt{N} \rfloor ⌊N⌋的段,其中第 i i i段左端点为 ( i − 1 ) ⌊ N ⌋ + 1 (i-1)\lfloor \sqrt{N} \rfloor+1 (i−1)⌊N⌋+1,右端点为 m i n ( i ⌊ N ⌋ ) min(i\lfloor \sqrt{N} \rfloor) min(i⌊N⌋),如下图所示。
另外,预处理出数组 s u m sum sum,其中 s u m [ i ] sum[i] sum[i]表示第 i i i段的区间和。设 a d d [ i ] add[i] add[i]表示第 i i i段的“增量标记”,起初 a d d [ i ] = 0 add[i]=0 add[i]=0。
对于指令“ C l r d C\ l\ r\ d C l r d”:
1.若 l l l与 r r r同时位于第 i i i段内,则直接把 A [ l ] , A [ l + 1 ] , . . . , A [ r ] A[l],A[l+1],...,A[r] A[l],A[l+1],...,A[r]都加 d d d,同时令 s u m [ i ] + = d ∗ ( r − l + 1 ) sum[i]+=d*(r-l+1) sum[i]+=d∗(r−l+1)。
2.否则,设 l l l处于第 p p p段, r r r处于第 q q q段。
(1)对于 i ∈ [ p + 1 , q − 1 ] i\in [p+1,q-1] i∈[p+1,q−1],令 a d d [ i ] + = d add[i]+=d add[i]+=d。
(2)对于开头、结尾不足一整段的两部分,按照与第1种情况相同的方法朴素地更新。
该指令的操作方法如下图所示。
对于指令“ Q l r Q\ l\ r Q l r”:
1.若 l l l与 r r r同时处于第 i i i段内,则 ( A [ l ] + A [ l + 1 ] + . . . + A [ r ] ) + ( r − l + 1 ) ∗ a d d [ i ] (A[l]+A[l+1]+...+A[r])+(r-l+1)*add[i] (A[l]+A[l+1]+...+A[r])+(r−l+1)∗add[i]就是答案。
2.否则,设 l l l处于第 p p p段, r r r处于第 q q q段,初始化 a n s = 0 ans=0 ans=0。
(1)对于 i ∈ [ p + 1 , q − 1 ] i\in [p+1,q-1] i∈[p+1,q−1],令 a n s + = s u m [ i ] + a d d [ i ] ∗ l e n [ i ] ans+=sum[i]+add[i]*len[i] ans+=sum[i]+add[i]∗len[i],其中 l e n [ i ] len[i] len[i]表示第 i i i段的长度。
(2)对于开头、结尾不足一整段的两部分,按照与第1种情况相同的方法朴素地更新。
这种分块算法对于整段的修改使用标记 a d d add add记录,对于不足整段的修改采用朴素算法。因为段数和段长都是 O ( N ) O(\sqrt{N}) O(N),所以整个算法的时间复杂度为 O ( ( N + Q ) ∗ N ) O((N+Q)*\sqrt{N}) O((N+Q)∗N)。大部分常见的分块思想都可以用“大段维护、局部朴素”来形容。
long long a[100010],sum[100010],add[100010];
int L[100010],R[100010]; //每段左右端点
int pos[100010]; //每个位置属于哪一段
int n,m,t;
void change(int l,int r,long long d)
{
int p=pos[l],q=pos[r];
if(p==q)
{
for(int i=l;i<=r;++i)
a[i]+=d;
sum[p]+=d*(r-l+1);
}
else
{
for(int i=p+1;i<=q-1;++i)
add[i]+=d;
for(int i=l;i<=R[p];++i)
a[i]+=d;
sum[p]+=d*(R[p]-l+1);
for(int i=L[q];i<=r;++i)
a[i]+=d;
sum[q]+=d*(r-L[p]+1);
}
}
long long ask(int l,int r)
{
int p=pos[l],q=pos[r];
long long ans=0;
if(p==q)
{
for(int i=l;i<=r;++i)
ans+=a[i];
ans+=add[p]*(r-l+1);
}
else
{
for(int i=p+1;i<=q-1;++i)
ans+=sum[i]+add[i]*(R[i]-L[i]+1);
for(int i=l;i<=R[p];++i)
ans+=a[i];
ans+=add[p]*(R[p]-l+1);
for(int i=L[q];i<=r;++i)
ans+=a[i];
ans+=add[q]*(r-L[q]+1);
}
return ans;
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;++i)
scanf("%lld",&a[i]);
t=sqrt(n);
for(int i=1;i<=t;++i)
{
L[i]=(i-1)*sqrt(n)+1;
R[i]=i*sqrt(n);
}
if(R[t]<n)
{
t++;
L[t]=R[t-1]+1;
R[t]=n;
}
//预处理
for(int i=1;i<=t;++i)
{
for(int j=L[i];j<=R[i];++j)
{
pos[j]=i;
sum[i]+=a[j];
}
}
//指令
while(m--)
{
char op[3];
int l,r,d;
scanf("%s%d%d",op,&l,&r);
if(op[0]=='C')
{
scanf("%d",&d);
change(l,r,d);
}
else
printf("%lld\n",ask(l,r));
}
return 0;
}
方法 | 复杂度 | 时间 | 内存 | 代码 | 优劣 |
---|---|---|---|---|---|
树状数组 | O ( ( N + Q ) l o g N ) O((N+Q)logN) O((N+Q)logN) | 1.0s |
3MB |
850B |
效率高、代码短、不易扩展、不太直观 |
线段树 | O ( ( N + Q ) l o g N ) O((N+Q)logN) O((N+Q)logN) | 1.5s |
7MB |
1700B |
效率较高、扩展性好、代码较长、直观性一般 |
分块 | O ( ( N + Q ) N ) O((N+Q)\sqrt{N}) O((N+Q)N) | 1.9s |
1.5MB |
1500B |
通用、直观、效率偏低、码长一般 |
朴素 | O ( ( N + Q ) ∗ N ) O((N+Q)*N) O((N+Q)∗N) | TLE |
1MB |
500B |
略 |
分块时一定要注意边界值,如若分成 T T T块,每块长度为 l e n len len,若 T ∗ l e n < N T*len
在乡下的小路旁种着许多蒲公英,而我们的问题正是与这些蒲公英有关。
为了简化起见,我们把所有的蒲公英看成一个长度为 n n n 的序列 { a 1 , a 2 . . a n } \{a_1,a_2..a_n\} {a1,a2..an},其中 a i a_i ai 为一个正整数,表示第 i i i 棵蒲公英的种类编号。
而每次询问一个区间 [ l , r ] [l, r] [l,r],你需要回答区间里出现次数最多的是哪种蒲公英,如果有若干种蒲公英出现次数相同,则输出种类编号最小的那个。
注意,你的算法必须是在线的。第一行有两个整数,分别表示蒲公英的数量 n n n 和询问次数 m m m。
第二行有 n n n 个整数,第 i i i 个整数表示第 i i i 棵蒲公英的种类 a i a_i ai。
接下来 m m m 行,每行两个整数 l 0 , r 0 l_0, r_0 l0,r0,表示一次询问。输入是加密的,解密方法如下:
令上次询问的结果为 x x x(如果这是第一次询问,则 x = 0 x = 0 x=0),设 l = ( ( l 0 + x − 1 ) m o d n ) + 1 , r = ( ( r 0 + x − 1 ) m o d n ) + 1 l=((l_0+x-1)\bmod n) + 1,r=((r_0+x-1) \bmod n) + 1 l=((l0+x−1)modn)+1,r=((r0+x−1)modn)+1。如果 l > r l > r l>r,则交换 l , r l, r l,r。
最终的询问区间为计算后的 [ l , r ] [l, r] [l,r]。对于 100 % 100\% 100% 的数据,保证 1 ≤ n ≤ 40000 1\le n \le 40000 1≤n≤40000, 1 ≤ m ≤ 50000 1\le m \le 50000 1≤m≤50000, 1 ≤ a i ≤ 1 0 9 1\le a_i \le 10^9 1≤ai≤109, 1 ≤ l 0 , r 0 ≤ n 1 \leq l_0, r_0 \leq n 1≤l0,r0≤n。
本题是经典的在线求众数问题。因为众数不具有“区间可加性”(已知序列 a a a中区间 [ x , y ] [x,y] [x,y]的众数和区间 [ y + 1 , z ] [y+1,z] [y+1,z]的众数,不能直接得到区间 [ x , z ] [x,z] [x,z]的众数),所以有树状数组或线段树维护就十分困难。下面我们介绍两种常见的分块做法。
若把序列 a a a分成 T T T块,则每块的长度 L = N / T L=N/T L=N/T,我们稍后会讨论 T T T的取值。
对于每个询问 [ l , r ] [l,r] [l,r],设 l l l处于第 p p p块, r r r处于第 q q q块。我们把询问区间 [ l , r ] [l,r] [l,r]分成三部分:
1.开头不足一整段的 [ l , L ) [l,L) [l,L)。
2.第 p + 1 ∼ q − 1 p+1\sim q-1 p+1∼q−1块构成的区间 [ L , R ] [L,R] [L,R]。
3.结尾不足一整段的 ( R , r ] (R,r] (R,r]。
显然 a a a序列在区间 [ l , r ] [l,r] [l,r]中的众数只可能来自一下两种情况:
1.区间 [ L , R ] [L,R] [L,R]的众数。
2.出现在 [ l , L ) [l,L) [l,L)与 ( R , r ] (R,r] (R,r]之间的数。
预处理出所有以“段边界”为端点的区间 [ L , R ] [L,R] [L,R]中每个数出现的次数,以及区间众数。这样的区间总共有 O ( T 2 ) O(T^2) O(T2)个,并且保存每个数出现的次数需要长度为 O ( N ) O(N) O(N)的数组(记为 c n t L , R cnt_{L,R} cntL,R)。
对于每个询问中的 [ l , L ) [l,L) [l,L)与 ( R , r ] (R,r] (R,r],可以通过朴素扫描,在数组 c n t L , R cnt_{L,R} cntL,R的基础上累加次数,从而更新答案。回答询问后在进行一次朴素扫描,从而 c n t L , R cnt_{L,R} cntL,R中减少次数,把数组复原。
这个算法的时间为 O ( N T 2 + M N / T ) O(NT^2+MN/T ) O(NT2+MN/T),空间为 O ( N T 2 ) O(NT^2 ) O(NT2)。通过方程可解得 T = M / 2 3 T=\sqrt[3]{M/2} T=3M/2。但我们一般让 T T T与 N N N有关,而不与 M M M有关(因为 T T T是让 N N N个数据分成 T T T块,如果令 T = M / 2 3 T=\sqrt[3]{M/2} T=3M/2,有可能出现 M M M过大,造成 T T T很大,给的 N N N过小,不足以分成 T T T块)。不妨设 N , M N,M N,M为相同数量级,令 T = N 3 T=\sqrt[3]{N} T=3N,此时算法复杂度在 O ( N 5 3 ) O(N^{ \frac{5}{3} }) O(N35)级别。
#include
using namespace std;
int N, M, Q, T, len, l, r, ans; // T最大为30
int a[40005], b[40005], disc[40005];
int cnt[35][35][40005];
int f[35][35];
int L[35], R[35];
int pos[40005];
int c[40005];
int main()
{
scanf("%d%d", &N, &M);
for (int i = 1; i <= N; ++i)
{
scanf("%d", &a[i]);
b[i] = a[i];
}
T = max(1.0, pow(1.0 * N / 2, 1.0 / 3));
len = N / T;
for (int i = 1; i <= T; ++i)
{
L[i] = (i - 1) * len + 1;
R[i] = i * len;
}
if (R[T] < N)
{
T++;
L[T] = R[T - 1] + 1;
R[T] = N;
}
for (int i = 1; i <= T; ++i)
for (int j = L[i]; j <= R[i]; ++j)
pos[j] = i;
sort(b + 1, b + N + 1);
for (int i = 1; i <= N; ++i)
if (i == 1 || b[i] != b[i - 1])
disc[++Q] = b[i];
for (int i = 1; i <= N; ++i)
{
int id = lower_bound(disc + 1, disc + Q + 1, a[i]) - disc;
b[i] = id;
}
for (int i = 1; i <= T; ++i)
{
for (int j = i; j <= T; ++j)
{
for (int k = L[i]; k <= R[j]; ++k)
{
cnt[i][j][b[k]]++;
if (cnt[i][j][f[i][j]] < cnt[i][j][b[k]] || (cnt[i][j][f[i][j]] == cnt[i][j][b[k]] && f[i][j] > b[k]))
f[i][j] = b[k];
}
}
}
while (M--)
{
scanf("%d%d", &l, &r);
l = ((l + ans - 1) % N) + 1;
r = ((r + ans - 1) % N) + 1;
if (l > r)
swap(l, r);
int p = pos[l], q = pos[r];
ans = 0;
if (p == q)
{
memset(c, 0, sizeof(c));
for (int i = l; i <= r; ++i)
{
c[b[i]]++;
if (c[ans] < c[b[i]] || (c[ans] == c[b[i]] && ans > b[i]))
ans = b[i];
}
}
else
{
ans = f[p + 1][q - 1];
for (int i = l; i <= R[p]; ++i)
{
cnt[p + 1][q - 1][b[i]]++;
if (cnt[p + 1][q - 1][ans] < cnt[p + 1][q - 1][b[i]] || (cnt[p + 1][q - 1][ans] == cnt[p + 1][q - 1][b[i]] && ans > b[i]))
ans = b[i];
}
for (int i = L[q]; i <= r; ++i)
{
cnt[p + 1][q - 1][b[i]]++;
if (cnt[p + 1][q - 1][ans] < cnt[p + 1][q - 1][b[i]] || (cnt[p + 1][q - 1][ans] == cnt[p + 1][q - 1][b[i]] && ans > b[i]))
ans = b[i];
}
for (int i = l; i <= R[p]; ++i)
cnt[p + 1][q - 1][b[i]]--;
for (int i = L[q]; i <= r; ++i)
cnt[p + 1][q - 1][b[i]]--;
}
ans = disc[ans];
printf("%d\n", ans);
}
return 0;
}
在预处理时,只保存所有以“段边界”为端点的区间 [ L , R ] [L,R] [L,R]的众数。另外,对每一个数值建立 S T L v e c t o r STL\ vector STL vector,按顺序保存该数值在序列 a a a每次出现的位置。
对于每个询问,扫描 [ l , L ) [l,L) [l,L)与 ( R , r ] (R,r] (R,r]中的每个数 x x x,在对应的 v e c t o r vector vector中二分查找即可得到 x x x在 [ l , r ] [l,r] [l,r]中出现的次数,从而更新答案。
这个算法的时间为 O ( N T + M N / T ∗ l o g N ) O(NT+MN/T*logN) O(NT+MN/T∗logN),空间为 O ( T 2 ) O(T^2) O(T2)。应取 T = M l o g N T=\sqrt{MlogN} T=MlogN,取 T = N l o g N T=\sqrt{NlogN} T=NlogN,此时整个算法的复杂度在 O ( N N l o g N ) O(N\sqrt{NlogN} ) O(NNlogN)级别。
#include
using namespace std;
int N, M, Q, T, len, l, r, ans, cnt; // T最大为875
int a[40005], b[40005], disc[40005];
int f[900][900];
vector<int> v[40005];
int L[900], R[900];
int pos[40005];
int c[40005];
int find_up(int L, int num)
{
int l = 0, r = v[num].size() - 1;
int ans = -1;
while (l <= r)
{
int mid = (l + r) / 2;
if (v[num][mid] >= L)
{
ans = mid;
r = mid - 1;
}
else
l = mid + 1;
}
return ans;
}
int find_down(int R, int num)
{
int l = 0, r = v[num].size() - 1;
int ans = -2;
while (l <= r)
{
int mid = (l + r) / 2;
if (v[num][mid] <= R)
{
ans = mid;
l = mid + 1;
}
else
r = mid - 1;
}
return ans;
}
int main()
{
scanf("%d%d", &N, &M);
for (int i = 1; i <= N; ++i)
{
scanf("%d", &a[i]);
b[i] = a[i];
}
sort(b + 1, b + N + 1);
for (int i = 1; i <= N; ++i)
if (i == 1 || b[i] != b[i - 1])
disc[++Q] = b[i];
for (int i = 1; i <= N; ++i)
{
int id = lower_bound(disc + 1, disc + Q + 1, a[i]) - disc;
b[i] = id;
}
T = max(1.0, sqrt(1.0 * M * log(N) / log(2)));
len = N / T;
for (int i = 1; i <= T; ++i)
{
L[i] = (i - 1) * len + 1;
R[i] = i * len;
}
if (R[T] < N)
{
T++;
L[T] = R[T - 1] + 1;
R[T] = N;
}
for (int i = 1; i <= T; ++i)
for (int j = L[i]; j <= R[i]; ++j)
pos[j] = i;
for (int i = 1; i <= N; ++i)
v[b[i]].push_back(i);
for (int i = 1; i <= T; ++i)
{
memset(c, 0, sizeof(c));
ans = 0, cnt = 0;
for (int j = L[i]; j <= N; ++j)
{
c[b[j]]++;
if (cnt < c[b[j]] || (cnt == c[b[j]] && ans > b[j]))
{
ans = b[j];
cnt = c[b[j]];
}
f[i][pos[j]] = ans;
}
}
ans = 0;
while (M--)
{
scanf("%d%d", &l, &r);
l = ((l + ans - 1) % N) + 1;
r = ((r + ans - 1) % N) + 1;
if (l > r)
swap(l, r);
int p = pos[l];
int q = pos[r];
ans = 0;
cnt = 0;
if (p == q)
{
for (int i = l; i <= r; ++i)
{
int tmp = find_down(r, b[i]) - find_up(l, b[i]) + 1;
if (cnt < tmp || (cnt == tmp && ans > b[i]))
{
ans = b[i];
cnt = tmp;
}
}
}
else
{
if (p + 1 <= q - 1)
{
ans = f[p + 1][q - 1];
cnt = find_down(r, ans) - find_up(l, ans) + 1;
}
for (int i = l; i <= R[p]; ++i)
{
int tmp = find_down(r, b[i]) - find_up(l, b[i]) + 1;
if (cnt < tmp || (cnt == tmp && ans > b[i]))
{
ans = b[i];
cnt = tmp;
}
}
for (int i = L[q]; i <= r; ++i)
{
int tmp = find_down(r, b[i]) - find_up(l, b[i]) + 1;
if (cnt < tmp || (cnt == tmp && ans > b[i]))
{
ans = b[i];
cnt = tmp;
}
}
}
ans = disc[ans];
printf("%d\n", ans);
}
return 0;
}
H 有一串由各种漂亮的贝壳组成的项链。H 相信不同的贝壳会带来好运,所以每次散步完后,他都会随意取出一段贝壳,思考它们所表达的含义。H 不断地收集新的贝壳,因此,他的项链变得越来越长。
有一天,他突然提出了一个问题:某一段贝壳中,包含了多少种不同的贝壳?这个问题很难回答…… 因为项链实在是太长了。于是,他只好求助睿智的你,来解决这个问题。
n n n 个正整数 a i a_i ai,表示项链中第 i i i 个贝壳的种类。m个询问。
1 ≤ n , m , a i ≤ 1 0 6 1\le n,m,a_i \leq 10^6 1≤n,m,ai≤106, 1 ≤ l ≤ r ≤ n 1\le l \le r \le n 1≤l≤r≤n。
在本题中,我们将介绍分块算法的一种重要形式——对“询问”进行分块。这是一种离线做法,又称为莫队算法。
题目到手,我们开始分析本题的算法。这题最简单做法无非暴力——用一个cnt
数组记录每个数值出现的次数,再暴力枚举l
到r
统计次数,最后再扫一遍cnt
数组,统计cnt
不为零的数值个数,输出答案即可。设最大数值为s
,那么这样做的复杂度为 O ( m ( n + s ) ) O(m(n+s)) O(m(n+s))。
然后优化一下,每次枚举到一个数值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。这样我们优化掉了一个 O ( m s ) O(ms) O(ms)。
然后再优化,我们弄两个指针 l
、r
,每次询问不直接枚举,而是移动l
、r
指针到询问的区间,直到 [ l , r ] [l,r] [l,r]与询问区间重合。在统计答案时,我们也只在两个指针处加减cnt
。
那么增添删除函数就可以写出来了:
inline void add(int p) // 添数,p为下标
{
if (cnt[A[p]] == 0)
cur++;
cnt[A[p]]++;
}
inline void del(int p) // 删数
{
cnt[A[p]]--;
if (cnt[A[p]] == 0)
cur--;
}
那么从一个区间到另一个区间,只需写:
while (l > Q[i].l)
add(--l);
while (l < Q[i].l)
del(l++);
while (r < Q[i].r)
add(++r);
while (r > Q[i].r)
del(r--);
注意++和–的位置。删数是先删后移,添数是先移后添。初始化时,要先令l=1,r=0。
现在我们可以从一个区间的答案转移到另一个区间了,但是,如果直接在线查询,很有可能在序列两头“左右横跳”,到头来还不如朴素算法。但是,我们可以把查询离线下来(记录下来),然后,排个序。
那么我们该如何排序呢?我们很容易想到以l
为第一关键词,r
为第二关键词排下序,这样 l l l在 1 ∼ N 1\sim N 1∼N范围内不断递增,l
得到控制,但r
在 1 ∼ N 1\sim N 1∼N范围不断来回横跳,例如前一个范围是 [ 1 , 2 ] [1,2] [1,2],下一个范围是 [ 2 , N ] [2,N] [2,N],然后下一个范围是 [ 3 , 5 ] [3,5] [3,5],这样遍历一个询问的最坏时间依旧是 O ( N ) O(N) O(N),效果并不好。
我们可以使用分块的思想,把这些询问按照左端点递增排序,然后分成 N \sqrt{N} N块,每块内部再按右端点递增排序。这样左端点的变化范围是 N \sqrt{N} N,而右端点是递增的,左右端点都得到了控制。如果我们以上一次询问的回答为基础,那么每次只需要花费 O ( N ) O(\sqrt{N}) O(N)的时间处理左端多出或减少的部分。而整块中右端点增长的范围之和为 O ( N ) O(N) O(N),所以在 O ( N N ) O(N\sqrt{N}) O(NN)的时间内即可求解本题。
实际排序的过程中,我们可以先完成分块,然后排序,左端点属于较小块的排在前面,左端点属于同一块的,右端点小的排在前面。
但在此之上,我们还可以进行常数优化:奇偶化排序。意为:如果pos[l]
是奇数,则将r
顺序排序,否则将r
逆序排序。这样可以在每个块间转移时,降低 r r r的移动次数。
#include
using namespace std;
struct query{
int l,r;
int id;
}q[1000005];
int N,M,cur,T,len;
int a[1000005];
int cnt[1000005];
int ans[1000005];
int pos[1000005];
inline int read()
{
int 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*10+ch-'0';
ch=getchar();
}
return x*f;
}
inline void write(int x)
{
if(x<0)
putchar('-'),x=-x;
if(x>9)
write(x/10);
putchar(x%10+'0');
}
void add(int p)
{
if(cnt[a[p]]==0)
++cur;
++cnt[a[p]];
}
void del(int p)
{
--cnt[a[p]];
if(cnt[a[p]]==0)
--cur;
}
int main()
{
N=read();
for(int i=1;i<=N;++i)
a[i]=read();
M=read();
for(int i=1;i<=M;++i)
q[i].id=i,q[i].l=read(),q[i].r=read();
T=sqrt(M);
len=max(1,N/T);
for(int i=1;i<=T;++i)
for(int j=(i-1)*len+1;j<=i*len;++j)
pos[j]=i;
if(T*len<N)
{
T++;
for(int i=(T-1)*len+1;i<=N;++i)
pos[i]=T;
}
sort(q+1,q+M+1,[](query a,query b){
//return pos[a.l]==pos[b.l]?a.r
return pos[a.l]==pos[b.l]?((pos[a.l]&1)?a.r<b.r:a.r>b.r):pos[a.l]<pos[b.l]; //奇偶化处理
});
int l=1,r=0;
for(int i=1;i<=M;++i)
{
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]=cur;
}
for(int i=1;i<=M;++i)
{
write(ans[i]);
printf("\n");
}
return 0;
}
我们来分析一下时间复杂度。记块长为 N N N,询问数为 M M M,设块长为 S S S,则有 N S \frac{N}{S} SN个块。每个左端点每次最多移动 S S S的长度,一个块中右端点最多能移动 N N N的长度,时间复杂度为:
M ∗ S + N ∗ N S = M S + N 2 S M*S+N*\frac{N}{S}=MS+\frac{N^2}{S} M∗S+N∗SN=MS+SN2
易知当 S = N M S=\frac{N}{\sqrt{M}} S=MN时,即取 M \sqrt{M} M块时,取最小值为 2 N M 2N\sqrt{M} 2NM,所以实际时间复杂度为 O ( N M ) O(N\sqrt{M}) O(NM),当 N N N和 M M M同数量级时,可以看做 O ( N N ) O(N\sqrt{N}) O(NN)。然而实际上我们并不会取 M \sqrt{M} M块,而是取 N \sqrt{N} N块,因为若 M M M远大于 N N N, M \sqrt{M} M大于 N N N则无法分出 M \sqrt{M} M块。