分块与莫队
分块
简介
对原数据适当划分,并在划分后的每一个块上预处理部分信息,从而取得一个较优时间复杂度,即为分块的思想。
分块的时间复杂度取决于块长,一般可以通过均值不等式求出每个问题下的最优块长,以及相应的时间复杂度。
分块较树状数组、线段树的优点为通用性强,可以解决很多后者不能解决的问题,缺点就是时间复杂度不优。
例题
我们取块长 \(len=\sqrt{n}\),虽然最后一个块可能不完整,但不影响。我们令 \(b\) 数组表示块内和,\(laz\) 数组为一个懒标记数组,具体用途看下文解释。
对于修改操作:
-
\(l,r\) 在同一个块内,则暴力修改 \(a\) 数组。
-
不在同一个块内,则对两端不完整的快依然暴力修改;对于完整块,则直接修改 \(b_i=b_i+c\times len\) 与 \(laz_i=laz_i+c\)。
对于查询操作:
-
\(l,r\) 在同一个块内,依然暴力计算和,但注意每次加和加上当前块上懒标记存的和。
-
不在同一个块,同理。
时间复杂度 \(O(n\sqrt{n})\)。可以结合代码理解。
惶惶世人,可知警钟已在怒雷中长鸣!
#include <bits/stdc++.h>
#define N 50004
#define int long long
using namespace std;
int n,a[N],block[N],len,b[N],laz[N];
struct T{int l,r;}t[N];
void Input(){
scanf("%lld",&n);for(int i = 1;i <= n;i ++) scanf("%lld",&a[i]);len = sqrt(n);int k = n / len;
for(int i = 1;i <= k;i ++) t[i].l = (i - 1) * len + 1,t[i].r = t[i].l + len - 1;t[k].r = n;
for(int i = 1;i <= k;i ++) for(int j = t[i].l;j <= t[i].r;j ++) block[j] = i,b[i] += a[j];
}
void update(int l,int r,int c){
int bl = block[l],br = block[r];
if(bl == br){
for(int i = l;i <= r;i ++) a[i] += c,b[bl] += c;
return ;
}
for(int i = l;i <= t[bl].r;i ++) a[i] += c,b[bl] += c;
for(int i = t[br].l;i <= r;i ++) a[i] += c,b[br] += c;
for(int i = bl + 1;i <= br - 1;i ++) b[i] += c * len,laz[i] += c;
}
int query(int l,int r,int MOD){
int bl = block[l],br = block[r],res = 0;
if(bl == br){
for(int i = l;i <= r;i ++) res += a[i] + laz[bl];
return res % MOD;
}
for(int i = l;i <= t[bl].r;i ++) res += a[i] + laz[bl];
for(int i = t[br].l;i <= r;i ++) res += a[i] + laz[br];
for(int i = bl + 1;i <= br - 1;i ++) res += b[i];
return res % MOD;
}
void work(){
int opt,l,r,c;while(n --){
scanf("%lld%lld%lld%lld",&opt,&l,&r,&c);
if(opt == 0) update(l,r,c);
if(opt == 1) printf("%lld\n",query(l,r,c + 1));
}
}
signed main(){Input();work();return 0;}
习题
\(1.\) LibreOJ-6277 数列分块入门 1
例题弱化版。
迈向光明之路,注定荆棘丛生。
#include <bits/stdc++.h>
#define N 50004
#define int long long
using namespace std;
int n,a[N],len,laz[N],block[N];
struct T{int l,r;}t[N];
void Input(){
scanf("%lld",&n);for(int i = 1;i <= n;i ++) scanf("%lld",&a[i]);len = sqrt(n);int k = n / len;
for(int i = 1;i <= k;i ++) t[i].l = (i - 1) * len + 1,t[i].r = t[i].l + len - 1;t[k].r = n;
for(int i = 1;i <= k;i ++) for(int j = t[i].l;j <= t[i].r;j ++) block[j] = i;
}
void update(int l,int r,int c){
int bl = block[l],br = block[r];
if(bl == br){
for(int i = l;i <= r;i ++) a[i] += c;
return ;
}
for(int i = l;i <= t[bl].r;i ++) a[i] += c;
for(int i = t[br].l;i <= r;i ++) a[i] += c;
for(int i = bl + 1;i <= br - 1;i ++) laz[i] += c;
}
int query(int x) {return a[x] + laz[block[x]];}
void work(){
int opt,l,r,c;while(n --){
scanf("%lld%lld%lld%lld",&opt,&l,&r,&c);
if(opt == 0) update(l,r,c);
if(opt == 1) printf("%lld\n",query(r));
}
}
signed main(){Input();work();return 0;}
\(2.\) LibreOJ-6278 数列分块入门 2
使用一新数组 \(b\) 为 \(a\) 块内排序后的数组,这样每次查询整个块内小于给定值得数的个数用二分查找即可,其他非完整块暴力查询以及修改。
我的道路没有岔口,因为未来早已笃定!
#include <bits/stdc++.h>
#define N 50004
#define int long long
using namespace std;
int n,a[N],b[N],len,laz[N],block[N];
struct T{int l,r;}t[N];
void Input(){
scanf("%lld",&n);for(int i = 1;i <= n;i ++) scanf("%lld",&a[i]);len = sqrt(n);int k = n / len;
for(int i = 1;i <= k;i ++) t[i].l = (i - 1) * len + 1,t[i].r = t[i].l + len - 1;t[k].r = n;
for(int i = 1;i <= k;i ++) for(int j = t[i].l;j <= t[i].r;j ++) block[j] = i,b[j] = a[j];
for(int i = 1;i <= k;i ++) sort(b + t[i].l,b + t[i].r + 1);
}
void update(int l,int r,int c){
int bl = block[l],br = block[r];
if(bl == br){
for(int i = l;i <= r;i ++) a[i] += c;
for(int i = t[bl].l;i <= t[bl].r;i ++) b[i] = a[i];
sort(b + t[bl].l,b + t[bl].r + 1);return ;
}
for(int i = l;i <= t[bl].r;i ++) a[i] += c;
for(int i = t[bl].l;i <= t[bl].r;i ++) b[i] = a[i];
sort(b + t[bl].l,b + t[bl].r + 1);
for(int i = t[br].l;i <= r;i ++) a[i] += c;
for(int i = t[br].l;i <= t[br].r;i ++) b[i] = a[i];
sort(b + t[br].l,b + t[br].r + 1);
for(int i = bl + 1;i <= br - 1;i ++) laz[i] += c;
}
int query(int l,int r,int c){
int bl = block[l],br = block[r],res = 0;
if(bl == br){
for(int i = l;i <= r;i ++) if(a[i] + laz[bl] < c) res ++;
return res;
}
for(int i = l;i <= t[bl].r;i ++) if(a[i] + laz[bl] < c) res ++;
for(int i = t[br].l;i <= r;i ++) if(a[i] + laz[br] < c) res ++;
for(int i = bl + 1;i <= br - 1;i ++) res += lower_bound(b + t[i].l,b + t[i].r + 1,c - laz[i]) - b - t[i].l;
return res;
}
void work(){
int opt,l,r,c;while(n --){
scanf("%lld%lld%lld%lld",&opt,&l,&r,&c);
if(opt == 0) update(l,r,c);
if(opt == 1) printf("%lld\n",query(l,r,c * c));
}
}
signed main(){Input();work();return 0;}
四倍经验:UVA12003 Array Transformer \(\&\) SP18185 GIVEAWAY - Give Away \(\&\) P2801 教主的魔法
世间强大如我,亦有无法战胜之事。
#include <bits/stdc++.h>
#define N 300005
#define int long long
using namespace std;
int n,m,u,a[N],b[N],block[N],len;
struct T{int l,r;}t[N];
void Input(){
scanf("%lld%lld%lld",&n,&m,&u);len = sqrt(n);int k = n / len;
for(int i = 1;i <= n;i ++) scanf("%lld",&a[i]);
for(int i = 1;i <= k;i ++) t[i].l = (i - 1) * len + 1,t[i].r = t[i].l + len - 1;t[k].r = n;
for(int i = 1;i <= k;i ++) for(int j = t[i].l;j <= t[i].r;j ++) block[j] = i,b[j] = a[j];
for(int i = 1;i <= k;i ++) sort(b + t[i].l,b + t[i].r + 1);
}
int query(int l,int r,int c){
int bl = block[l],br = block[r],res = 0;
if(bl == br){
for(int i = l;i <= r;i ++) if(a[i] < c) res ++;
return res;
}
for(int i = l;i <= t[bl].r;i ++) if(a[i] < c) res ++;
for(int i = t[br].l;i <= r;i ++) if(a[i] < c) res ++;
for(int i = bl + 1;i <= br - 1;i ++) res += lower_bound(b + t[i].l,b + t[i].r + 1,c) - b - t[i].l;
return res;
}
void update(int x,int c){
a[x] = c;int bx = block[x];
for(int i = t[bx].l;i <= t[bx].r;i ++) b[i] = a[i];
sort(b + t[bx].l,b + t[bx].r + 1);
}
void work(){
int l,r,v,p;while(m --){
scanf("%lld%lld%lld%lld",&l,&r,&v,&p);
update(p,u * query(l,r,v) / (r - l + 1));
}
for(int i = 1;i <= n;i ++) printf("%lld\n",a[i]);
}
signed main(){Input();work();return 0;}
寄希望于神明,不如亲手撕碎这黑夜!
#include <bits/stdc++.h>
#define N 1000006
using namespace std;
struct A{int l,r;}A[N];
int n,Q,a[N],b[N],len,block[N];
void Input(){
scanf("%d",&n);
for(int i = 1;i <= n;i ++) scanf("%d",&a[i]),b[i] = a[i];
len = sqrt(n);int k = n / len;
for(int i = 1;i <= k;i ++) A[i].l = (i - 1) * len + 1,A[i].r = A[i].l + len - 1;A[k].r = n;
for(int i = 1;i <= k;i ++) for(int j = A[i].l;j <= A[i].r;j ++) block[j] = i;
for(int i = 1;i <= k;i ++) sort(b + A[i].l,b + A[i].r + 1);
}
void update(int l,int x){
int bl = block[l];
a[l] = x;
for(int i = A[bl].l;i <= A[bl].r;i ++) b[i] = a[i];
sort(b + A[bl].l,b + A[bl].r + 1);
return ;
}
int query(int l,int r,int c){
int bl = block[l],br = block[r],num = 0;
if(bl == br){
for(int i = l;i <= r;i ++) if(a[i] >= c) num ++;
return num;
}
for(int i = l;i <= A[bl].r;i ++) if(a[i] >= c) num ++;
for(int i = A[br].l;i <= r;i ++) if(a[i] >= c) num ++;
for(int i = bl + 1;i <= br - 1;i ++) num += A[i].r + 1 - (lower_bound(b + A[i].l,b + A[i].r + 1,c) - b);
return num;
}
void work(){
int opt,a,b,c;scanf("%d",&Q);while(Q --){
scanf("%d",&opt);
if(opt == 0) scanf("%d%d%d",&a,&b,&c),printf("%d\n",query(a,b,c));
if(opt == 1) scanf("%d%d",&a,&b),update(a,b);
}
}
int main(){
Input();
work();
return 0;
}
任何无法击垮我们的,只会令我们更加强大。
#include <bits/stdc++.h>
#define N 1000006
using namespace std;
struct A{int l,r;}A[N];
int n,Q,a[N],d[N],laz[N],len,block[N];
bool cmp(int a,int b){return a > b;}
void Input(){
scanf("%d%d",&n,&Q);
for(int i = 1;i <= n;i ++) scanf("%d",&a[i]),d[i] = a[i];
len = sqrt(n);int k = n / len;
for(int i = 1;i <= k;i ++) A[i].l = (i - 1) * len + 1,A[i].r = A[i].l + len - 1;A[k].r = n;
for(int i = 1;i <= k;i ++) for(int j = A[i].l;j <= A[i].r;j ++) block[j] = i;
for(int i = 1;i <= k;i ++) sort(d + A[i].l,d + A[i].r + 1);
}
void update(int l,int r,int c){
int bl = block[l],br = block[r];
if(bl == br){
for(int i = l;i <= r;i ++) a[i] += c;
for(int i = A[bl].l;i <= A[bl].r;i ++) d[i] = a[i];
sort(d + A[bl].l,d + A[bl].r + 1);
return ;
}
for(int i = l;i <= A[bl].r;i ++) a[i] += c;
for(int i = A[bl].l;i <= A[bl].r;i ++) d[i] = a[i];
sort(d + A[bl].l,d + A[bl].r + 1);
for(int i = bl + 1;i <= br - 1;i ++) laz[i] += c;
for(int i = A[br].l;i <= r;i ++) a[i] += c;
for(int i = A[br].l;i <= A[br].r;i ++) d[i] = a[i];
sort(d + A[br].l,d + A[br].r + 1);
}
int query(int l,int r,int c){
int bl = block[l],br = block[r],num = 0;
if(bl == br){
for(int i = l;i <= r;i ++) if(a[i] + laz[bl] >= c) num ++;
return num;
}
for(int i = l;i <= A[bl].r;i ++) if(a[i] + laz[bl] >= c) num ++;
for(int i = A[br].l;i <= r;i ++) if(a[i] + laz[br] >= c) num ++;
for(int i = bl + 1;i <= br - 1;i ++) num += A[i].r - (lower_bound(d + A[i].l,d + A[i].r + 1,c - laz[i]) - d) + 1;
return num;
}
void work(){
char opt;int l,r,c;while(Q --){
cin >> opt;scanf("%d%d%d",&l,&r,&c);
if(opt == 'M') update(l,r,c);
if(opt == 'A') printf("%d\n",query(l,r,c));
}
}
int main(){
Input();
work();
return 0;
}
\(3.\) P3203 [HNOI2010] 弹飞绵羊
讲原数组分块,对于块内每个元素预处理出它跳出自己所在的块需要的步数以及跳出当前块后到达的第一个块的位置。修改暴力修改其所在块内它和它之前的元素,查询就一直往后跳即可。若块长取 \(\sqrt{n}\),则二者复杂度上限都为 \(O(\sqrt{n})\)。可以通过此题。
注意装置编号从 \(0\sim n-1\)。
我掌中的不只是力量,也是打破桎梏的钥匙!
#include <bits/stdc++.h>
#define N 200005
using namespace std;
int n,m,a[N],b[N],p[N],block[N],len;
struct T{int l,r;}t[N];
void Input(){
scanf("%d",&n);for(int i = 1;i <= n;i ++) scanf("%d",&a[i]);len = sqrt(n);int k = n / len;
for(int i = 1;i <= k;i ++) t[i].l = (i - 1) * len + 1,t[i].r = t[i].l + len - 1;t[k].r = n;
for(int i = 1;i <= k;i ++)
for(int j = t[i].r;j >= t[i].l;j --){
block[j] = i;
if(j + a[j] > t[i].r) b[j] = 1,p[j] = j + a[j];
else b[j] = 1 + b[j + a[j]],p[j] = p[j + a[j]];
}
}
int query(int x){int res = 0;while(x <= n) res += b[x],x = p[x];return res;}
void update(int x,int c){
a[x] = c;int bx = block[x];
for(int i = x;i >= t[bx].l;i --){
if(i + a[i] > t[bx].r) b[i] = 1,p[i] = i + a[i];
else b[i] = 1 + b[i + a[i]],p[i] = p[i + a[i]];
}
}
void work(){
int opt,j,k;scanf("%d",&m);while(m --){
scanf("%d",&opt);
if(opt == 1) scanf("%d",&j),printf("%d\n",query(j + 1));
if(opt == 2) scanf("%d%d",&j,&k),update(j + 1,k);
}
}
int main(){Input();work();return 0;}
双倍经验:CF13E Holes
注意装置编号从 \(1\sim n\)。
无尽征程漫漫,风暴如影随形。
#include <bits/stdc++.h>
#define N 200005
using namespace std;
int n,m,a[N],b[N],p[N],block[N],len;
struct T{int l,r;}t[N];
void Input(){
scanf("%d%d",&n,&m);for(int i = 1;i <= n;i ++) scanf("%d",&a[i]);len = sqrt(n);int k = n / len;
for(int i = 1;i <= k;i ++) t[i].l = (i - 1) * len + 1,t[i].r = t[i].l + len - 1;t[k].r = n;
for(int i = 1;i <= k;i ++)
for(int j = t[i].r;j >= t[i].l;j --){
block[j] = i;
if(j + a[j] > t[i].r) b[j] = 1,p[j] = j + a[j];
else b[j] = 1 + b[j + a[j]],p[j] = p[j + a[j]];
}
}
void query(int x){
int res = 0,ans;
while(x <= n){
if(p[x] > n) ans = x;
res += b[x];x = p[x];
}
while(ans + a[ans] <= n) ans += a[ans];
printf("%d %d\n",ans,res);
}
void update(int x,int c){
a[x] = c;int bx = block[x];
for(int i = x;i >= t[bx].l;i --){
if(i + a[i] > t[bx].r) b[i] = 1,p[i] = i + a[i];
else b[i] = 1 + b[i + a[i]],p[i] = p[i + a[i]];
}
}
void work(){
int opt,j,k;while(m --){scanf("%d",&opt);
if(opt == 1) scanf("%d",&j),query(j);
if(opt == 0) scanf("%d%d",&j,&k),update(j,k);
}
}
int main(){Input();work();return 0;}
\(4.\) P4168 [Violet] 蒲公英
首先讲 \(a_i\) 离散化。然后预处理两个数组,\(f_{i,j}\) 表示前 \(i\) 个块内 \(j\) 出现的次数,\(p_{i,j}\) 表示块 \(i\sim j\) 中的最小的众数。若块长取 \(\sqrt{n}\),则二者的时间复杂度均为 \(O(n\sqrt{n})\)。
仍有,对于 \(l,r\) 在同一个块内,暴力查询。否则,暴力查询在两端不完整的块内元素在 \(l,r\) 区间内出现的次数,取其最小的众数,然后,与中间完整的块内的众数比较取最优值。稍加思考,易得其正确性显然。单次询问复杂度 \(O(\sqrt{n})\)。
为了让未来回到正确的路上而战。
#include <bits/stdc++.h>
#define N 40004
using namespace std;
int n,Q,a[N],tmp[N],arr[N],block[N],m,p[205][205],f[205][N],len;
struct T{int l,r;}t[N];
void Input(){
scanf("%d%d",&n,&Q);len = sqrt(n);int k = n / len;
for(int i = 1;i <= n;i ++) scanf("%d",&a[i]),tmp[i] = a[i];
for(int i = 1;i <= k;i ++) t[i].l = (i - 1) * len + 1,t[i].r = i * len;t[k].r = n;
for(int i = 1;i <= k;i ++) for(int j = t[i].l;j <= t[i].r;j ++) block[j] = i;
sort(tmp + 1,tmp + n + 1);int m = unique(tmp + 1,tmp + n + 1) - tmp - 1;
for(int i = 1;i <= n;i ++) a[i] = lower_bound(tmp + 1,tmp + m + 1,a[i]) - tmp;
for(int i = 1;i <= k;i ++){
for(int j = t[i].l;j <= t[i].r;j ++) f[i][a[j]] ++;
for(int j = 1;j <= m;j ++) f[i][j] += f[i - 1][j];
}
for(int i = 1;i <= k;i ++){
for(int j = i;j <= k;j ++){
int flag = p[i][j - 1];
for(int o = t[j].l;o <= t[j].r;o ++){
if(f[j][a[o]] - f[i - 1][a[o]] > f[j][flag] - f[i - 1][flag]) flag = a[o];
else if(f[j][a[o]] - f[i - 1][a[o]] == f[j][flag] - f[i - 1][flag]) flag = min(flag,a[o]);
}
p[i][j] = flag;
}
}
}
void work(){
int x = 0,l,r;while(Q --){
scanf("%d%d",&l,&r);l = (l + x - 1) % n + 1;
r = (r + x - 1) % n + 1;if(l > r) swap(l,r);
int bl = block[l],br = block[r],flag = 0;
if(bl == br){
for(int i = l;i <= r;i ++) arr[a[i]] ++;
for(int i = l;i <= r;i ++){
if(arr[a[i]] > arr[flag]) flag = a[i];
else if(arr[a[i]] == arr[flag]) flag = min(flag,a[i]);
}
for(int i = l;i <= r;i ++) arr[a[i]] = 0;
x = tmp[flag];printf("%d\n",x);continue;
}
for(int i = l;i <= t[bl].r;i ++) arr[a[i]] ++;
for(int i = t[br].l;i <= r;i ++) arr[a[i]] ++;
flag = p[bl + 1][br - 1];
for(int i = l;i <= t[bl].r;i ++){
if(arr[a[i]] + f[br - 1][a[i]] - f[bl][a[i]] > arr[flag] + f[br - 1][flag] - f[bl][flag]) flag = a[i];
else if(arr[a[i]] + f[br - 1][a[i]] - f[bl][a[i]] == arr[flag] + f[br - 1][flag] - f[bl][flag]) flag = min(flag,a[i]);
}
for(int i = t[br].l;i <= r;i ++){
if(arr[a[i]] + f[br - 1][a[i]] - f[bl][a[i]] > arr[flag] + f[br - 1][flag] - f[bl][flag]) flag = a[i];
else if(arr[a[i]] + f[br - 1][a[i]] - f[bl][a[i]] == arr[flag] + f[br - 1][flag] - f[bl][flag]) flag = min(flag,a[i]);
}
for(int i = l;i <= t[bl].r;i ++) arr[a[i]] = 0;
for(int i = t[br].l;i <= r;i ++) arr[a[i]] = 0;
x = tmp[flag];printf("%d\n",x);
}
}
int main(){Input();work();return 0;}
\(5.\) AT_joisc2014_c 歴史の研究
类蒲公英,不再详细展开。
弱小并不可怕,可怕的是懦弱。
#include <bits/stdc++.h>
#define N 100005
#define M 320
#define int long long
using namespace std;
struct T{int l,r;}t[M];
int n,m,Q,len,a[N],b[N],block[N],p[M][M],f[M][N],tmp[N];
void Input(){
scanf("%lld%lld",&n,&Q);len = sqrt(n);int k = n / len;
for(int i = 1;i <= k;i ++) t[i].l = (i - 1) * len + 1,t[i].r = i * len;t[k].r = n;
for(int i = 1;i <= k;i ++) for(int j = t[i].l;j <= t[i].r;j ++) block[j] = i;
for(int i = 1;i <= n;i ++) scanf("%lld",&a[i]),b[i] = a[i];
sort(b + 1,b + n + 1);m = unique(b + 1,b + n + 1) - b - 1;
for(int i = 1;i <= n;i ++) a[i] = lower_bound(b + 1,b + m + 1,a[i]) - b;
for(int i = 1;i <= k;i ++){
for(int j = t[i].l;j <= t[i].r;j ++) f[i][a[j]] += b[a[j]];
for(int j = 1;j <= m;j ++) f[i][j] += f[i - 1][j];
}
for(int i = 1;i <= k;i ++){
int MAX = 0;
for(int j = 1;j <= k;j ++){
for(int l = t[j].l;l <= t[j].r;l ++){
if(f[j][a[l]] - f[i - 1][a[l]] >= f[j][MAX] - f[i - 1][MAX]) MAX = a[l];
}
p[i][j] = f[j][MAX] - f[i - 1][MAX];
}
}
}
void work(){
int l,r;while(Q --){
scanf("%lld%lld",&l,&r);
int bl = block[l],br = block[r],MAX = 0;
if(br - bl <= 1){
for(int i = l;i <= r;i ++) tmp[a[i]] += b[a[i]],MAX = max(MAX,tmp[a[i]]);
for(int i = l;i <= r;i ++) tmp[a[i]] = 0;printf("%lld\n",MAX);continue;
}
for(int i = l;i <= t[bl].r;i ++) tmp[a[i]] += b[a[i]];
for(int i = t[br].l;i <= r;i ++) tmp[a[i]] += b[a[i]];
for(int i = l;i <= t[bl].r;i ++) if(tmp[a[i]] + f[br - 1][a[i]] - f[bl][a[i]] >= tmp[MAX] + f[br - 1][MAX] - f[bl][MAX]) MAX = a[i];
for(int i = t[br].l;i <= r;i ++) if(tmp[a[i]] + f[br - 1][a[i]] - f[bl][a[i]] >= tmp[MAX] + f[br - 1][MAX] - f[bl][MAX]) MAX = a[i];
printf("%lld\n",max(tmp[MAX] + f[br - 1][MAX] - f[bl][MAX],p[bl + 1][br - 1]));
for(int i = l;i <= t[bl].r;i ++) tmp[a[i]] = 0;
for(int i = t[br].l;i <= r;i ++) tmp[a[i]] = 0;
}
}
signed main(){
Input();work();return 0;
}
莫队
简介
由莫涛提出。是一个离线算法,核心思想是讲所有的询问按照分块进行排序后,对于每个询问可以通过双指针增删数据来达到总体较低的复杂度。
普通莫队
例题
之前一般都拿 P1972 [SDOI2009] HH的项链 做例题,但是后来它的数据加强了,不能用莫队去做了(但是好像吸吸氧卡卡常还是可以)。不过这道题是它的弱化版,裸的莫队可以过去。
我们大体的思路是这样的:定义一个双指针 \(ql,qr\),每次到了一个询问之后,我们依次将二者移到当前询问对应的 \(l,r\) 并根据上一个状态来计算当前答案。下面给出代码,\(sum_i\) 数组表示双指针范围内数字 \(i\) 出现的次数。
纠结对错,善恶,那毫无意义。
void add(int x) {if(!sum[a[x]]) num ++;sum[a[x]] ++;}
void del(int x) {sum[a[x]] --;if(!sum[a[x]]) num --;}
int l = 1,r = 0;
for(int i = 1;i <= m;i ++){
int ql = t[i].l,qr = t[i].r;
while(l < ql) del(l ++);while(l > ql) add(-- l);
while(r < qr) add(++ r);while(r > qr) del(r --);
}
但是这样的话复杂度还是原地起飞的,考虑优化。
我们按某种方式排序,让它减少挪动次数,这样会变快一些。这种方法就是离线,所以普通不能支持修改。
一种就是按照左端点进行排序,但是每次右端点的最坏情况还是从一端到另一端,极端复杂度还是原地起飞。
我们排序是让左右端点挪动次数的总和尽可能小,所以这里就有一种排序方法:
将序列分成块长为 \(b\) 的各个块,若左端点在一个块内,那我们就按右端点排序。
踟蹰,必将失败。
bool cmp(T a,T b) {return (a.l / len != b.l / len) ? a.l < b.l : a.r < b.r;}
我们来解释一下为什么这样跑的快。
首先对于左端点。对于每一个块,左端点在一起的询问都排序到了一起,所以单次询问左端点最多移动 \(b\) 次,总共 \(m\) 个询问,因此总复杂度为 \(O(mb)\)。
其次对于右端点。对于每一个块,右端点是单调递增的,因此只会向右移动,最多移动 \(n\) 次。由于有 \(\frac{n}{b}\) 个块,所以无论多少询问,它的总体复杂度是 \(O(\frac{n^2}{b})\) 的。虽然每次左端点从一个块到另一个块之间右端点会向左移动,但是这也是小常数,每次极限最多也就是 \(2n\) 次,所以我们忽略不计。
所以总体复杂度就是 \(O(mb+\frac{n^2}{b})\)。由于 \(m\) 与 \(n\) 是已知量,根据基本不等式得,\(mb+\frac{n^2}{b}\ge2\sqrt{mb\times\frac{n^2}{b}}=2n\sqrt{m}\),当且仅当 \(mb=\frac{n^2}{b}\),即 \(b=\frac{n}{\sqrt{m}}\) 时等式成立,所以它也就成为了我们经常取的块长。
我们发现,当 \(n,m\) 同阶时,\(b=\sqrt{n}\),所以它也是我们经常取的块长。
然而在数据随机的情况下,询问的区间大小期望是是 \(\frac{2}{3}n\),因此此时块大小是 \(\frac{n}{\sqrt{\frac{2}{3}m}}\) 反而会快些,所以它也是我们经常取的块长。
另外的卡常技巧:
上文中我们提到,每次右端点其实在左端点变快之后会向左移动一些距离,因此我们可以用另一种排序方法:奇偶性排序。
不难发现,我们右端点排序只要单调就行了,即单调递增和单调递减是一致的。
所以我们让左端点所在块编号为奇数的右端点按升序,左端点所在块编号为偶数的右端点降序排列。
但是这只是一个小优化,不能降低时间复杂度。这是代码:
站在最高处,窥见最全貌。
bool cmp(T a,T b) {return pos[a.l] ^ pos[b.l] ? pos[a.l] < pos[b.l] : pos[a.l] & 1 ? a.r < b.r : a.r > b.r;}
然后这个题就结束了,可以参照完整代码取理解一下:
怜悯,这世间最大的奢侈。
#include <bits/stdc++.h>
#define N 1000006
using namespace std;
struct T{int l,r,id;}t[N];
int n,m,a[N],sum[N],num,len,ans[N];
bool cmp(T a,T b) {return (a.l / len != b.l / len) ? a.l < b.l : a.r < b.r;}
void Input(){
scanf("%d",&n);for(int i = 1;i <= n;i ++) scanf("%d",&a[i]);
scanf("%d",&m);len = n / sqrt(m * 2 / 3);
for(int i = 1;i <= m;i ++) scanf("%d%d",&t[i].l,&t[i].r),t[i].id = i;
sort(t + 1,t + m + 1,cmp);
}
void add(int x) {if(!sum[a[x]]) num ++;sum[a[x]] ++;}
void del(int x) {sum[a[x]] --;if(!sum[a[x]]) num --;}
void work(){
int l = 1,r = 0;
for(int i = 1;i <= m;i ++){
int ql = t[i].l,qr = t[i].r;
while(l < ql) del(l ++);while(l > ql) add(-- l);
while(r < qr) add(++ r);while(r > qr) del(r --);
ans[t[i].id] = num;
}
for(int i = 1;i <= m;i ++) printf("%d\n",ans[i]);
}
int main(){Input();work();return 0;}
习题
\(1.\) P2709 小B的询问
根据 \((x+1)^2-x^2=2x+1\) 取处理中间过程即可。
此生于此,甚多选择,幸得无悔。
#include <bits/stdc++.h>
#define N 50004
#define int long long
using namespace std;
int n,m,K,a[N],ans[N],len,num,sum[N];
struct T{int l,r,id;}t[N];
bool cmp(T a,T b) {return (a.l / len) != (b.l / len) ? a.l < b.l : a.r < b.r;}
void Input(){
scanf("%lld%lld%lld",&n,&m,&K);len = n / sqrt(m * 2 / 3);
for(int i = 1;i <= n;i ++) scanf("%lld",&a[i]);
for(int i = 1;i <= m;i ++) scanf("%lld%lld",&t[i].l,&t[i].r),t[i].id = i;
sort(t + 1,t + m + 1,cmp);
}
void del(int x) {sum[a[x]] --,num -= sum[a[x]] * 2 + 1;}
void add(int x) {num += sum[a[x]] * 2 + 1,sum[a[x]] ++;}
void work(){
int l = 1,r = 0;
for(int i = 1;i <= m;i ++){
int pl = t[i].l,pr = t[i].r;
while(l < pl) del(l ++);while(l > pl) add(-- l);
while(r < pr) add(++ r);while(r > pr) del(r --);
ans[t[i].id] = num;
}
for(int i = 1;i <= m;i ++) printf("%lld\n",ans[i]);
}
signed main(){Input();work();return 0;}
\(2.\) P1494 [国家集训队] 小 Z 的袜子
根据 \(C_x^2-C_{x-1}^2=x-1\) 处理中间过程即可。
长安之外,风起云涌,潜藏在暗中的,已经显现。
#include <bits/stdc++.h>
#define int long long
#define N 50004
using namespace std;
int n,m,a[N],sum[N],num,len;
struct ANS{int a,b;}ans[N];
struct T{int l,r,id;}t[N];
bool cmp(T a,T b) {return (a.l / len != b.l / len) ? a.l < b.l : a.r < b.r;}
void Input(){
scanf("%lld%lld",&n,&m);len = n / sqrt(m * 2 / 3);
for(int i = 1;i <= n;i ++) scanf("%lld",&a[i]);
for(int i = 1;i <= m;i ++) scanf("%lld%lld",&t[i].l,&t[i].r),t[i].id = i;
sort(t + 1,t + m + 1,cmp);
}
void del(int x) {sum[a[x]] --;num -= sum[a[x]];}
void add(int x) {num += sum[a[x]];sum[a[x]] ++;}
int C(int x) {return x * (x - 1) / 2;}
void work(){
int l = 1,r = 0;
for(int i = 1;i <= m;i ++){
int pl = t[i].l,pr = t[i].r;
if(pl == pr){ans[t[i].id] = (ANS){0,1};continue;}
while(l < pl) del(l ++);while(l > pl) add(-- l);
while(r < pr) add(++ r);while(r > pr) del(r --);
int o = __gcd(num,C(pr - pl + 1));
ans[t[i].id] = (ANS){num / o,C(pr - pl + 1) / o};
}
for(int i = 1;i <= m;i ++) printf("%lld/%lld\n",ans[i].a,ans[i].b);
}
signed main(){Input();work();return 0;}
带修莫队
例题
带修莫队其实就是上面普通莫队的进化版,是增加了一个时间维,表示这次操作的时间。
只不过排序多了一维关键字。以左端点所在块为第一关键字,右端点所在块为第二关键字,时间维第三关键字排序。
以雷霆之力,燃余烬而生!
bool cmp(T a,T b){
if(a.l / len != b.l / len) return a.l < b.l;
if(a.r / len != b.r / len) return a.r < b.r;
return a.pre < b.pre;
}
从 \(l,r\to l',r'\) 的转移同普通莫队一样,我们现在要考虑的是如何实现从 \(t\to t'\) 的转移。
假设一个修改是将第 \(pos\) 个位置的颜色由 \(a\) 修改成 \(b\):
首先,判断 \(pos\) 是否在 \(l'\sim r'\) 这个区间内,不在就不用管;在的话,我们相当于就删去一个颜色 \(a\),加上一个颜色 \(b\),和普通转移时候的增删就没有什么区别了。
下面给出完整代码:可以参考第 \(21\) 行记录时间维的代码实现以及第 \(34\) 行的代码实现(假设我这一次操作将 \(3\) 变成了 \(7\),那下一次还原此操作就是将 \(7\) 还原为 \(3\))。
我们的命运,必须掌握在自己手中。
#include <bits/stdc++.h>
#define N 1000006
using namespace std;
int n,m,a[N],sum[N],num,ans[N],len,c_cnt,t_cnt;
struct C{int pos,val;}c[N];
struct T{int l,r,pre,id;}t[N];
bool cmp(T a,T b){
if(a.l / len != b.l / len) return a.l < b.l;
if(a.r / len != b.r / len) return a.r < b.r;
return a.pre < b.pre;
}
void Input(){
scanf("%d%d",&n,&m);len = pow(n,0.67);
for(int i = 1;i <= n;i ++) scanf("%d",&a[i]);
char opt;int l,r;for(int i = 1;i <= m;i ++){
cin >> opt;scanf("%d%d",&l,&r);
if(opt == 'Q') t[++t_cnt] = (T){l,r,c_cnt,t_cnt};
else c[++c_cnt] = (C){l,r};
}
sort(t + 1,t + t_cnt + 1,cmp);
}
void add(int x) {if(!sum[a[x]]) num ++;sum[a[x]] ++;}
void del(int x) {sum[a[x]] --;if(!sum[a[x]]) num --;}
void JD(int now,int l,int r){
if(c[now].pos >= l and c[now].pos <= r){
del(c[now].pos);
if(!sum[c[now].val]) num ++;sum[c[now].val] ++;
}
swap(a[c[now].pos],c[now].val);
}
void work(){
int l = 1,r = 0,now = 0;
for(int i = 1;i <= t_cnt;i ++){
int pl = t[i].l,pr = t[i].r,pt = t[i].pre;
while(l < pl) del(l ++);while(l > pl) add(-- l);
while(r < pr) add(++ r);while(r > pr) del(r --);
while(now < pt) JD(++ now,pl,pr);while(now > pt) JD(now --,pl,pr);
ans[t[i].id] = num;
}
for(int i = 1;i <= t_cnt;i ++) printf("%d\n",ans[i]);
}
int main(){Input();work();return 0;}