优雅的暴力
在 n n n\sqrt{n} nn的时间内离线求解一段区间内不同数字的个数
暴力做法:
用一个桶记录每种颜色出现的数量
随后扫描桶,进行统计
显然会超时
我们对询问进行排序,以便利用前一个询问的信息更新下一个询问
我们建立双指针,每次移动指针加入新数
这便是莫队算法的雏形(是暴力,但不优雅)
很容易发现我们刚才的做法仍然是 O ( n ) O(n) O(n)的,并没有得到优化
通过调整查询的顺序,我们可以把复杂度降到 O ( n ) O(\sqrt{n}) O(n)
我们通过排序,可以使得其中一个指针变得单调
另一个指针怎么解决?我们使用分块
我们进行排序时,应按这种方法排
于是我们每块的内部右端点递增
这样在更新的过程中,在每一块内部右端点移动数量不会超过 n n n(右端点在 1 → n 1\to n 1→n,且递增),移动总数不会超过 n n n\sqrt{n} nn
左端点: 块 内 块 间 { 块 内 : n 块 间 : 2 n 块内块间\begin{cases}块内:\sqrt{n}\\块间:2\sqrt{n}\end{cases} 块内块间{块内:n块间:2n
这样我们的总时间复杂度就是 O ( n n ) O(n\sqrt{n}) O(nn)
之所以说它玄学,是因为这些优化在某些数据中有奇效,但在其他数据中可能会失效
奇偶性优化
在排序时,第二关键字我们遵循以下原则:
{ 奇 数 块 : 块 内 右 端 点 从 小 到 大 排 偶 数 块 : 块 内 右 端 点 从 大 到 小 排 \begin{cases} 奇数块:块内右端点从小到大排\\ 偶数块:块内右端点从大到小排\\ \end{cases} {奇数块:块内右端点从小到大排偶数块:块内右端点从大到小排
玄学解释:右端点可以减小反复横跳的次数,滚到右边后可以顺势滚回左端点,而不是回到起点
分块优化
块长取 n 2 m \sqrt{\frac{n^2}{m}} mn2(具体视题目数据而定)
其他玄学技巧请自行bdfs
AcWing2492. HH的项链
模板题,没什么可说的
/*************************************************************************
> File Name: p1972[SDOI]HH的项链2.cpp
> Author: Typedef
> Mail: [email protected]
> Created Time: 2021/3/8 8:15:05
> Tags: 莫对,分块,玄学
************************************************************************/
#include
#include
#include
#include
using namespace std;
const int N=5e5+10,M=2e5+10,S=1e6+10;
int n,m,len;
int w[N],ans[N];
struct Query{
int id,l,r;
}q[M];
int cnt[S];
int get(int x){
return x/len;
}
bool cmp(const Query& a,const Query& b){
int i=get(a.l),j=get(b.l);
if(i!=j) return i<j;
return i&1?a.r<b.r:a.r>b.r;//玄学拉满
}
void add(int x,int& res){
if(!cnt[x]) res++;
cnt[x]++;
}
void del(int x,int &res){
cnt[x]--;
if(!cnt[x]) res--;
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",&w[i]);
scanf("%d",&m);
len=sqrt((double)n*n/m);//玄学拉满
for(int i=0;i<m;i++){
int l,r;
scanf("%d%d",&l,&r);
q[i]={i,l,r};
}
sort(q,q+m,cmp);
for(int k=0,i=0,j=1,res=0;k<m;k++){
int id=q[k].id,l=q[k].l,r=q[k].r;
while(i<r) add(w[++i],res);
while(i>r) del(w[i--],res);
while(j<l) del(w[j++],res);
while(j>l) add(w[--j],res);
ans[id]=res;
}
for(int i=0;i<m;i++) printf("%d\n",ans[i]);
system("pause");
return 0;
}
玄学拉满: 1575 m s 1575ms 1575ms
不使用奇偶性优化: 1511 m s 1511ms 1511ms
不使用分块优化: 1797 m s 1797ms 1797ms
不使用任何玄学: 1737 m s 1737ms 1737ms
在本题中奇偶性优化效果不佳,但分块优化十分有效
谁说莫队不能带修?
不带修改的莫队是一维的,而带修莫队则是二维的,其中第二维是时间
我们假设有 t t t个修改操作,那么我们的莫队就有 t t t层
其中的第 i i i层表示经过第 i i i次修改操作后的情况
对于在 k k k次修改之后, k + 1 k+1 k+1次修改之前的操作[L,R]
,我们给它一个时间为 k k k的时间戳
这样操作都变成了[L,R,K]
我们的指针同样也变成了三个:i,j,t
每次我们需要将i
移动到L
的位置,j
移动到R
,t
移动到K
的位置上
i
与j
我们可以用 O ( 1 ) O(1) O(1)时间维护,问题在于如何维护t
假设我们在k-1
行,经过修改操作变成k
行
在这个过程中,只会有一个数发生了变化
{ 被 修 改 数 不 在 i , j 区 间 中 : 无 需 更 改 c n t 被 修 改 数 在 i , j 区 间 中 : 进 行 一 次 d e l 操 作 和 a d d 操 作 即 可 \begin{cases} 被修改数不在i,j区间中:无需更改cnt\\ 被修改数在i,j区间中:进行一次del操作和add操作即可\\ \end{cases} {被修改数不在i,j区间中:无需更改cnt被修改数在i,j区间中:进行一次del操作和add操作即可
复杂度 O ( 1 ) O(1) O(1)
若t
是从 0 → k 0\to k 0→k走,并将x
修改为x'
,那么我们在从 k → 0 k\to0 k→0走时需将其恢复
有一个取巧的方式,对于修改操作,我们交换x
和x'
这样向下走时会复原
那么如何排序呢?
同样是让一个指针单调,不能单调的分块
于是我们用三个关键字进行排序
{ 第 一 关 键 字 : l 所 在 块 的 编 号 第 二 关 键 字 : r 所 在 块 的 编 号 第 三 关 键 字 : t ( 时 间 戳 ) \begin{cases} 第一关键字:l所在块的编号\\ 第二关键字:r所在块的编号\\ 第三关键字:t(时间戳)\\ \end{cases} ⎩⎪⎨⎪⎧第一关键字:l所在块的编号第二关键字:r所在块的编号第三关键字:t(时间戳)
很好,那么又进行分块呢?
本题中我们块长取 n t 3 \sqrt[3]{nt} 3nt
最终总的时间复杂度 n 4 t 3 \sqrt[3]{n^4t} 3n4t
t
在极端情况下是m
/*************************************************************************
> File Name: p1903[国家集训队]数颜色.cpp
> Author: Typedef
> Mail: [email protected]
> Created Time: 2021/3/8 10:00:12
> Tags: 莫队,带修莫队
************************************************************************/
#include
#include
#include
#include
using namespace std;
const int N=233333,S=1000010;
int n,m,len,mq,mc;
int w[N],cnt[S],ans[N];
int read(){
int x=0;char r=getchar();
while(!isdigit(r))r=getchar();
while(isdigit(r)){x=x*10+r-'0';r=getchar();}
return x;
}
struct Query{
int id,l,r,t;
}q[N];
struct Modify{
int p,c;
}c[N];
int get(int x){
return x/len;
}
bool cmp(const Query& a,const Query& b){
int al=get(a.l),ar=get(a.r);
int bl=get(b.l),br=get(b.r);
if(al!=bl) return al<bl;
if(ar!=br) return ar<br;
return a.t<b.t;
}
void add(int x,int& res){
if(!cnt[x]) res++;
cnt[x]++;
}
void del(int x,int& res){
cnt[x]--;
if(!cnt[x]) res--;
}
int main(){
n=read(),m=read();
for(int i=1;i<=n;i++) w[i]=read();
for(int i=0;i<m;i++){
char op[2];
int a,b;
cin>>op;
a=read(),b=read();
if(*op=='Q') mq++,q[mq]={mq,a,b,mc};
else c[++mc]={a,b};
}
len=cbrt((double)n*mc)+1;
if(!mc) len=sqrt((double)n)+1;
sort(q+1,q+mq+1,cmp);
for(int i=0,j=1,t=0,k=1,res=0;k<=mq;k++){
int id=q[k].id,l=q[k].l,r=q[k].r,tm=q[k].t;
while(i<r) add(w[++i],res);
while(i>r) del(w[i--],res);
while(j<l) del(w[j++],res);
while(j>l) add(w[--j],res);
while(t<tm){
t++;
if(c[t].p>=j&&c[t].p<=i){
del(w[c[t].p],res);
add(c[t].c,res);
}
swap(w[c[t].p],c[t].c);
}
while(t>tm){
if(c[t].p>=j&&c[t].p<=i){
del(w[c[t].p],res);
add(c[t].c,res);
}
swap(w[c[t].p],c[t].c);
t--;
}
ans[id]=res;
}
for(int i=1;i<=mq;i++) printf("%d\n",ans[i]);
system("pause");
return 0;
}
有些情况中,我们很容易进行插入操作,却很难进行删除操作
比如维护集合内最大值
[JOI2013]历史研究
给我们一个序列,每次询问一个区间
我们定义一个数的重要度,为该数的值与该数在区间中出现次数的乘积
每次求区间重要度的最大值
保证所有的数为正
同样双关键字排序
{ 第 一 关 键 字 : l 所 在 块 的 编 号 第 二 关 键 字 : r 右 端 点 \begin{cases} 第一关键字:l所在块的编号\\ 第二关键字:r右端点\\ \end{cases} {第一关键字:l所在块的编号第二关键字:r右端点
我们考虑块内:
/*************************************************************************
> File Name: AT1219历史研究.cpp
> Author: Typedef
> Mail: [email protected]
> Created Time: 2021/3/9 9:38:59
> Tags:
************************************************************************/
#include
#include
#include
#include
#include
#include
using namespace std;
typedef long long ll;
const int N=1e5+7;
int n,m,len;
int w[N],cnt[N];
ll ans[N];
struct Query{
int id,l,r;
}q[N];
vector<int> nums;
int get(int x){
return x/len;
}
bool cmp(const Query& a,const Query& b){
int i=get(a.l),j=get(b.l);
if(i!=j) return i<j;
return a.r<b.r;
}
void add(int x,ll& res){
cnt[x]++;
res=max(res,(ll)cnt[x]*nums[x]);
}
int main(){
scanf("%d%d",&n,&m);
len=sqrt(n);
for(int i=1;i<=n;i++) scanf("%d",&w[i]),nums.push_back(w[i]);
sort(nums.begin(),nums.end());
nums.erase(unique(nums.begin(),nums.end()),nums.end());
for(int i=1;i<=n;i++)
w[i]=lower_bound(nums.begin(),nums.end(),w[i])-nums.begin();
for(int i=0;i<m;i++){
int l,r;
scanf("%d%d",&l,&r);
q[i]={i,l,r};
}
sort(q,q+m,cmp);
for(int x=0;x<m;){
int y=x;
while(y<m&&get(q[y].l)==get(q[x].l)) y++;
int right=get(q[x].l)*len+len-1;//暴力右边界
while(x<y&&q[x].r<=right){
ll res=0;
int id=q[x].id,l=q[x].l,r=q[x].r;
for(int k=l;k<=r;k++) add(w[k],res);
ans[id]=res;
for(int k=l;k<=r;k++) cnt[w[k]]--;
x++;
}
ll res=0;
int i=right,j=right+1;
while(x<y){
int id=q[x].id,l=q[x].l,r=q[x].r;
while(i<r) add(w[++i],res);
ll backup=res;
while(j>l) add(w[--j],res);
ans[id]=res;
while(j<right+1) cnt[w[j++]]--;
res=backup;
x++;
}
memset(cnt,0,sizeof(cnt));
}
for(int i=0;i<m;i++) printf("%lld\n",ans[i]);
system("pause");
return 0;
}
很好理解,莫队跑树上
SP10707 COT2 - Count on a tree II
树上每一个节点都有一个权值
给定两个点u,v
,询问路径上不同权值的个数
**欧拉序:**类似dfs序,不过在回溯的时候要再记一次
**性质:**点u,v
,我们用first[u],last[u]
分别表示u第一次和最后一次出现的位置
若x,y
满足first[x]
lca(x,y)=x
则欧拉序列中,[first[x],first[y]]
中只出现一次的点lca(x,y)!=x
则对应欧拉序列中[last[x],first[y]]
中只出现一次的点这样我们就把树上路径转化为区间了