浅谈分块和莫队基础知识
好久没有水一水数据结构了
LOJ 数列分块入门 2
这道题题意很简单,就是要写一个支持区间加法和区间查询小于 \(c^2\) 个数的数据结构。
考虑分块来做,对于区间加法可以直接用 \(lazy\) 标记维护块内和;对于查询,可以考虑对每个块中的按大小排好序后的数列二分找小于 \(c^2\) 的个数,单次修改查询时间复杂度均为 \(O(\sqrt{n}*log_2\sqrt n+\sqrt{n})\)。
#include<bits/stdc++.h>
#define int long long
using namespace std;
inline int read(){
int ans=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-') f=-f;ch=getchar();}
while(isdigit(ch)){ans=(ans<<3)+(ans<<1)+ch-48;ch=getchar();}
return ans*f;
}
const int N=5e4+5;
int T,n,a[N],lazy[N],block,belong[N],L[N],R[N],f[N];
void sortt(int i){
int tem[block+5],cnt=0;
for(int j=L[i];j<=R[i];j++) tem[++cnt]=a[j];
sort(tem+1,tem+1+cnt);
cnt=0;
for(int j=L[i];j<=R[i];j++) f[j]=tem[++cnt];
}
void add(int l,int r,int c){
if(belong[l]==belong[r]){
for(int i=l;i<=r;i++) a[i]+=c;
sortt(belong[l]);
}
else{
for(int i=belong[l]+1;i<=belong[r]-1;i++) lazy[i]+=c;
for(int i=l;i<=R[belong[l]];i++) a[i]+=c;
for(int i=L[belong[r]];i<=r;i++) a[i]+=c;
sortt(belong[l]);
sortt(belong[r]);
}
}
int query(int l,int r,int c){
if(belong[l]==belong[r]){
int res=0;
for(int i=l;i<=r;i++) if(a[i]+lazy[belong[i]]<c) res++;
return res;
}
else{
int res=0;
for(int i=l;i<=R[belong[l]];i++) if(a[i]+lazy[belong[i]]<c) res++;
for(int i=L[belong[r]];i<=r;i++) if(a[i]+lazy[belong[i]]<c) res++;
for(int i=belong[l]+1;i<=belong[r]-1;i++){
int ll=1,rr=R[i]-L[i]+1,mid,ans=0;
while(ll<=rr){
mid=(ll+rr)>>1;
if(f[L[i]+mid-1]+lazy[i]<c) ans=mid,ll=mid+1;
else rr=mid-1;
}
res+=ans;
}
return res;
}
}
signed main(){
T=n=read(),block=sqrt(n);
for(int i=1;i<=n;i++) a[i]=read(),belong[i]=(i-1)/block+1;
for(int i=1;i<=belong[n];i++) L[i]=R[i-1]+1,R[i]=min(n,L[i]+block-1),sortt(i);
while(T--){
int opt=read(),x=read(),y=read(),z=read();
if(!opt) add(x,y,z);
else printf("%lld\n",query(x,y,z*z));
}
return 0;
}
LOJ 数列分块入门 5
这道题是区间开根号下取整(下文开根号均指开根号下取整),不难发现对于 \(2^{32}\) 只需要开 \(6\) 次就为 \(1\) 了,所以在 \(int\) 类型里的非负整数开了 \(6\) 及以上次根号,结果肯定为 \(1\),所以我们只用处理开根号次数小于 \(6\) 的数。
先分块,考虑用一个 \(sum\) 统计区间和,然后再对于每一次开根号操作,都是暴力修改,因为最多总共修改 \(5n\) 次,所以时间可以过。查询的时候就可以直接用查询区间和的方法写。
#include<bits/stdc++.h>
using namespace std;
inline int read(){
int ans=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-') f=-f;ch=getchar();}
while(isdigit(ch)){ans=(ans<<3)+(ans<<1)+ch-48;ch=getchar();}
return ans*f;
}
const int N=5e4+5;
int n,T,a[N],sum[N],lazy[N],belong[N],block,L[N],R[N];
void dosqrt(int l,int r){
if(belong[l]==belong[r]){
for(int i=l;i<=r;i++){
sum[belong[i]]-=a[i];
a[i]=sqrt(a[i]);
sum[belong[i]]+=a[i];
}
}
else{
for(int i=belong[l]+1;i<=belong[r]-1;i++){
if(sum[i]==R[i]-L[i]+1) continue;
for(int j=L[i];j<=R[i];j++){
sum[i]-=a[j];
a[j]=sqrt(a[j]);
sum[i]+=a[j];
}
}
for(int i=l;i<=R[belong[l]];i++){
sum[belong[i]]-=a[i];
a[i]=sqrt(a[i]);
sum[belong[i]]+=a[i];
}
for(int i=L[belong[r]];i<=r;i++){
sum[belong[i]]-=a[i];
a[i]=sqrt(a[i]);
sum[belong[i]]+=a[i];
}
}
}
int query(int l,int r){
if(belong[l]==belong[r]){
int res=0;
for(int i=l;i<=r;i++) res+=a[i];
return res;
}
else{
int res=0;
for(int i=l;i<=R[belong[l]];i++) res+=a[i];
for(int i=L[belong[r]];i<=r;i++) res+=a[i];
for(int i=belong[l]+1;i<=belong[r]-1;i++) res+=sum[i];
return res;
}
}
int main(){
T=n=read();
block=sqrt(n);
for(int i=1;i<=n;i++){
a[i]=read();
belong[i]=(i-1)/block+1;
sum[belong[i]]+=a[i];
}
for(int i=1;i<=belong[n];i++) L[i]=R[i-1]+1,R[i]=min(L[i]+block-1,n);
while(T--){
int opt=read(),l=read(),r=read(),c=read();
if(opt) printf("%d\n",query(l,r));
else dosqrt(l,r);
}
return 0;
}
Luogu P3901 数列找不同
这道题可以用莫队来做,所谓莫队算法都要满足一个最基本的性质,就是知道一个区间,可以 \(O(1)\) 的求出他增加或减少一个元素的区间,这道题就可以使用莫队算法。
考虑双指针来做,用 \(L\) 表示左端点,用 \(R\) 表示右端点,开个桶 \(vis\) 来表示当前 \([L,R]\) 中,\(i\) 这个数出现了几次,初始值 \(L=1,R=0\)。很显然,当区间扩大时,如果有一个 \(vis\) 从 \(0\) 变成了 \(1\),就是有一个数不同了;当区间减小时,如果有一个 \(vis\) 从 \(1\) 变成 \(0\) 了,就是有一个不同没了。
单独这样对与每一个每一个询问的 \(l,r\) 都做一遍这样的操作无疑是 \(n^2\) 的,但是如果我们转为离线做法呢?我们可以调整查询区间的顺序,让 \(L,R\) 不用从头再来一边重复的了,所以我们就有了 \(O(n+mlog_2m)\) 的算法
#include<bits/stdc++.h>
using namespace std;
inline int read(){
int ans=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-') f=-f;ch=getchar();}
while(isdigit(ch)){ans=(ans<<3)+(ans<<1)+ch-48;ch=getchar();}
return ans*f;
}
const int N=1e5+5;
int n,m,a[N],vis[N],block,ans,t[N];
struct lzz{
int l,r,id;
}q[N];
bool cmp(lzz x,lzz y){
if(x.l/block==y.l/block) return x.r<y.r;
return x.l/block<y.l/block;
}
void del(int x){
vis[a[x]]--;
if(vis[a[x]]==0) ans--;
}
void add(int x){
vis[a[x]]++;
if(vis[a[x]]==1) ans++;
}
int main(){
n=read(),m=read();block=sqrt(n);
for(int i=1;i<=n;i++) a[i]=read();
for(int i=1;i<=m;i++) q[i].l=read(),q[i].r=read(),q[i].id=i;
sort(q+1,q+1+m,cmp);
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--);
if(ans==q[i].r-q[i].l+1) t[q[i].id]=1;
}
for(int i=1;i<=m;i++){
if(t[i]) printf("Yes\n");
else printf("No\n");
}
return 0;
}
Luogu P1494 [国家集训队]小Z的袜子
真正的莫队入门经典题,和上面的思路其实是差不多的。
设区间里有 \(a_1,a_2,a3,...,a_n\) 双不同颜色的袜子,区间内的贡献计算是
然后可以发现,当区间扩大时,分子就减去之前和加入袜子相同颜色的组合数,加上添加这双袜子后这种颜色袜子的组合数;当区间减小时,就是减去了。
#include<bits/stdc++.h>
#define int long long
using namespace std;
inline int read(){
int ans=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-') f=-f;ch=getchar();}
while(isdigit(ch)){ans=(ans<<3)+(ans<<1)+ch-48;ch=getchar();}
return ans*f;
}
const int N=5e4+5;
int n,m,a[N],vis[N],block,ans,t1[N],t2[N];
struct lzz{
int l,r,id;
}q[N];
bool cmp(lzz x,lzz y){
if(x.l/block==y.l/block) return x.r<y.r;
return x.l/block<y.l/block;
}
int gcd(int x,int y){
return !y ? x : gcd(y,x%y);
}
int C(int x){
return x*(x-1)/2;
}
void del(int x){
vis[a[x]]--;
ans=ans-C(vis[a[x]]+1)+C(vis[a[x]]);
}
void add(int x){
vis[a[x]]++;
ans=ans-C(vis[a[x]]-1)+C(vis[a[x]]);
}
signed main(){
n=read(),m=read();block=sqrt(n);
for(int i=1;i<=n;i++) a[i]=read();
for(int i=1;i<=m;i++) q[i].l=read(),q[i].r=read(),q[i].id=i;
sort(q+1,q+1+m,cmp);
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--);
t1[q[i].id]=ans;
t2[q[i].id]=C(q[i].r-q[i].l+1);
}
for(int i=1;i<=m;i++){
if(t1[i]==0) printf("0/1\n");
else{
int d=gcd(t1[i],t2[i]);
printf("%lld/%lld\n",t1[i]/d,t2[i]/d);
}
}
return 0;
}