分块+莫队+根号分治
分块思想
通过对原数据的适当划分,在划分后的每一个块上预处理并维护一些信息,从而在处理区间操作时,用“大段维护、小段朴素”的方式,取得比纯暴力更优的时间复杂度。
进行区间操作时,将区间分成中间整块和两边散块, 对于整块直接对块操作,对于散块需要暴力操作
一般来说对于长度为n的数列分成sqrt(n)个块(特殊的待补充)
区间加+区间查 : P2801 教主的魔法 - 洛谷
首先,先考虑怎么分块?
先计算:块数,块长,块的编号,块的左端点、右端点
int B;//块数
int tot;//块长
int belong[N];//记录块的编号
int L[N];//左端点
int R[N];//右端点
//下面分块
B=sqrt(n);
tot=(n+B-1)/B;
for(int i=1;i<=n;i++){
belong[i]=(i-1)/B+1;//下标i所在块的编号
}
for(int i=1;i<=tot;i++){
L[i]=(i-1)*B+1;//第i块的左端点
R[i]=min(B*i,n);//第i块的右端点
}
然后,考虑区间加法的问题
想要区间[l,r]加一个数v,首先判断l,r是否在同一块内?
如果是,说明[l,r]是一个散块,比如[1,4]是一个整块,[2,3]则是其中的散块,那么对[l,r]暴力修改就好;
如果l,r不在同一块内,那么将[l,r]分成了两部分,即中间的整块和两边散块,对散块和上边一样暴力,对中间的整块则直接进行懒标记即可
void modify_part(int id,int l,int r,int v){
for(int i=l;i<=r;i++){
a[i]+=v;
}
/*
*/
}
void modify(int l,int r,int v){
if(belong[l]==belong[r]){ //l,r在同一块内
modify_part(belong[l],l,r,v);
return;
}
//如果l,r不在同一块内
modify_part(belong[l],l,R[belong[l]],v);//左边散块
modify_part(belong[r],L[belong[r]],r,v);//右边散块
//下面处理中间的整块
for(int i=belong[l]+1;i<=belong[r]-1;i++){
lazy[i]+=v;//懒标记
}
}
接着,考虑怎么查询?
另外,我们想要查询的是[l,r]中 >=c 的个数,我们对整块是直接进行的懒标记,但是仍然不知道有多少数 >=c ,如果进行枚举的话时间复杂度最坏为O(n),那么分块的作用就没了
所以想要快速的查询,我们可以维护一个有序的数组,对其二分查找
对于整块,我们想要找 a[i]+lazy[i] >=c 的个数,直接二分查找最小的>=c的数的下标然后用块的右端点减掉+1就好了
对于散块,我们已经暴力修改原数组,然后对于一个有序的副本,我们再一次将散块中的修改后的数排序,然后二分查找
完整代码:
int a[N],b[N];//原数组,副本
int tot,B;
int belong[N];
int L[N],R[N];
int n,m;
int lazy[N];
void modify_part(int id,int l,int r,int v){
for(int i=l;i<=r;i++){
a[i]+=v;
}
int ql=L[id];
int qr=R[id];
//下面是对散块的重新排序
for(int i=ql;i<=qr;i++){
b[i]=a[i];
}
sort(b+ql,b+1+qr);
}
void modify(int l,int r,int w){
if(belong[l]==belong[r]){
modify_part(belong[l],l,r,w);
return;
}
modify_part(belong[l],l,R[belong[l]],w);
modify_part(belong[r],L[belong[r]],r,w);
int ql=belong[l]+1;
int qr=belong[r]-1;
for(int i=ql;i<=qr;i++){
lazy[i]+=w;
}
}
int query_part(int id,int l,int r,int c){
int ans=0;
for(int i=l;i<=r;i++){
if(a[i]+lazy[id]>=c){
ans++;
}
}
return ans;
}
int query(int l,int r,int c){
if(belong[l]==belong[r]){//如果[l,r]是散块
return query_part(belong[l],l,r,c);
}
int ansL=query_part(belong[l],l,R[belong[l]],c);//左散块的个数
int ansR=query_part(belong[r],L[belong[r]],r,c);//右散块的个数
int ans=0;//记录中间整块中的个数
int ql=belong[l]+1;
int qr=belong[r]-1;
for(int i=ql;i<=qr;i++){
int pos=lower_bound(b+L[i],b+1+R[i],c-lazy[i])-b;
ans+=R[i]-pos+1;
}
return ansL+ansR+ans;
}
void solve(){
cin>>n>>m;
B=sqrt(n);
tot=(n+B-1)/B;
for(int i=1;i<=n;i++){
cin>>a[i];
b[i]=a[i];//副本
belong[i]=(i-1)/B+1;
}
for(int i=1;i<=tot;i++){
L[i]=(i-1)*B+1;
R[i]=min(i*B,n);
sort(b+L[i],b+1+R[i]);//对每个块进行排序
}
while(m--){
char op;
int l,r,c;
cin>>op>>l>>r>>c;
if(op=='M'){
modify(l,r,c);
}
if(op=='A'){
cout<<query(l,r,c)<<endl;
}
}
}
莫队算法
用来处理离线区间查询问题
打破朴素纯暴力的瓶颈,优化效率
其核心思想是:分块排序
给一个长度为N的数组,M次查询[l,r]中有多少种数字至少出现2次?
例如:n=5,m=4
a:1 2 3 2 1
[l,r]: [1,3],[1,5],[3,4],[2,5]
如果是纯暴力的话,我们每次查询时双指针会重复跳很多次,时间复杂度O(n*m),如果数据很大会TLE
而利用莫队思想,我们先用结构体将所有查询的区间存起来,然后按l,r的块两层排序
先对l所在的块升序排序,然后对l在同一块中的再按r升序排序,这是标准的排序
struct ss{
int l,r,id,cnt;//左端点,右端点,编号(输出顺序),个数
};
vector<ss> q;
for(int i=1;i<=m;i++){
int l,r;
cin>>l>>r;
q.push_back({l,r,i,0});
}
sort(q.begin(),q.end(),[&](const ss &a,const ss &b){
if(belong[a.l]!=belong[b.l]) return belong[a.l]<belong[b.l];
return a.r<b.r;
})
当然还有优化的排序方法,时间复杂度大大降低,就是奇偶性排序优化
对l在奇数号块中的按r的升序排,对l在偶数号块中的按r的降序排
在从奇块跳到偶块的时候,右端点直接跳到最大的r,然后令右端点回跳计数,这样就可以避免大幅回跳
sort(q.begin(),q.end(),[&](const ss &a,const ss &b){
if(belong[a.l]!=belong[b.l]) return belong[a.l]<belong[b.l];
if(belong[a.l]&1) return a.r<b.r;
else return a.r>b.r;
})
一般来说,最好的块数是 n/sqrt(m)+1
[P1494 国家集训队] 小 Z 的袜子 - 洛谷
通过上边我们已经大致知道莫队的板子了,敲一下吧
int n,m;
int B,tot;
int a[N];
int belong[N];
int ans;
int cnt[N];
struct ss{
int l,r,id,fz,fm;
};
void bui(){//分块
for(int i=1;i<=n;i++){
belong[i]=(i-1)/B+1;
}
}
void add(int x){//加点
//扩张的内容
}
void del(int x){//删点
//收缩的内容
}
void solve(){
cin>>n>>m;
B=n/sqrt(m)+1;
tot=(n+B-1)/B;
for(int i=1;i<=n;i++){
cin>>a[i];
}
bui();
vector<ss> q;
for(int i=1;i<=m;i++){
int l,r;
cin>>l>>r;
q.push_back({l,r,i,0,0});
}
sort(q.begin(),q.end(),[&](const ss &a,const ss &b){
if(belong[a.l]!=belong[b.l]){
return belong[a.l]<belong[b.l];
}
if(belong[a.l]&1){
return a.r>b.r;
}else{
return a.r<b.r;
}
});
int L=1,R=0;
for(int i=0;i<m;i++){
/* 前置条件
*/
//这里实现双指针
//一般是先扩张后收缩
while(L>q[i].l){
add(--L);
}
while(R<q[i].r){
add(++R);
}
while(L<q[i].l){
del(L++);
}
while(R>q[i].r){
del(R--);
}
/*得出答案
*/
}
sort(q.begin(),q.end(),[&](const ss &a,const ss &b){
return a.id<b.id;
});
for(int i=0;i<m;i++){
cout<</*答案*/<<endl;
}
}
然后我们在考虑将注释中的内容补充,这一部分我不想写了QAQ
加点的时候就是将原点的内容(如果满足条件已经计算在内的)删掉,然后加入新点的
删点同理
反正莫队算法大多都是可以直接套板子的,主要的就是怎么补充注释内容啦~
根号分治
其实是一种管理学思想:根据问题规模大小,选择不同策略。
对于一个大数据管理,我们取一个阈值,超过这个阈值的用一种方法,小于阈值的用另一种方法,以此来达到最大效率
假设你现在要管理一个送快递平台,需要运送两种包裹:
1. **“大客户”包裹**:一次性要送几百件到同一个小区的包裹。
2. **“散客”包裹**:地址五花八门,遍布全城,每个地址只有一两件。
你手下有两种派送员:
A型员工(专线司机):开着大货车,适合跑专线。让他去送“大客户”的包裹效率极高,但如果让 他去送“散客”包裹,开着大车在小巷里穿梭,油费和时间成本都受不了。
B型员工(外卖小哥):骑着电瓶车,灵活机动。送“散客”包裹非常快,但如果让他去送“大客户” 的几百件包裹,他得来回跑无数趟,效率极低。
显而易见,我们对“大客户”包裹用专线司机,“散客”包裹用外卖小哥
一般取这个阈值为sqrt(n)
对于小于阈值的进行DP预处理,O(1)查询即可
超过阈值的暴力即可
因为很少做到根号分治的题,而且内容主要需要看题意,所以只给个板子:
int n,q;
int B;
int a[N];
void solve(){
cin>>n;
B=sqrt(n);
for(int i=1;i<=n;i++){
cin>>a[i];
}
vector<vector<int>> f(n+1,vector<int>(B+1,0));//dp数组
//下面进行dp
/*
*/
cin>>q;
while(q--){
int p,k;
cin>>p>>k;
int ans=0;
if(k>=B){
/*
*/
}else{
ans=f[][];
}
cout<<ans<<endl;
}
}
Problem - 797E - Codeforces
完整代码:
#include <bits/stdc++.h>
using namespace std;
//-------------------------------------------------------------------------------------------
#define int long long
#define R ios::sync_with_stdio(false),cin.tie(0),cout.tie(0)
#define P pair<int,int>
#define lowbit(x) (x&(-x))
#define dbg1(x) cout<<"# "<<x<<endl
#define dbg2(x,y) cout<<"# "<<x<<" "<<y<<endl
#define endl '\n'
const int mod=998244353;
const int N=1e5+10;
const int INF=0x3f3f3f3f3f3f3f3f;
const int inf=0x3f3f3f3f;
//--------------------------------------------------------------------------------------
int n,q;
int B;
int a[N];
void solve(){
cin>>n;
B=sqrt(n);
for(int i=1;i<=n;i++){
cin>>a[i];
}
vector<vector<int>> f(n+1,vector<int>(B+1,0));
for(int i=1;i<=B;i++){
f[n][i]=1;
}
for(int i=n-1;i>=1;i--){
for(int j=B;j>=1;j--){
if(i+a[i]+j>n) f[i][j]++;
else f[i][j]=f[i+a[i]+j][j]+1;
}
}
cin>>q;
while(q--){
int p,k;
cin>>p>>k;
int ans=0;
if(k>=B){
while(p<=n){
p+=a[p]+k;
ans++;
}
}else{
ans=f[p][k];
}
cout<<ans<<endl;
}
}
signed main(){
R;
// freopen("jia.in","r",stdin);
// freopen("jia.out","w",stdout);
int T=1;
//cin>>T;
for(int i=1;i<=T;i++){
solve();
}
return 0;
}