分块&莫队算法

ps: 2020年南昌理工ACM暑期集训时期所写

分块&莫队算法

1.分块算法

优美的暴力:分块

1.分块:顾名思义,就是把待处理的整个大区间分成若干块。
2.口诀是:块外暴力,块内查表。
3.那么这个块的大小应该怎么分呢?应该是sqrt(n)大小。证明就不说了自己查资料。
4.自我感觉,分块很巧妙的把各种复杂度都向sqrt(N)靠近发现很多的题,都是时间复杂度n根号n,空间复杂度也是n根号n,而且不管是什么操作,基本上都是根号n。既没有n^2,也不存在O(1),感觉,巧妙地把复杂度平均了一下。
5.分块虽然是暴力,但是是一种非常有水平的暴力。关键是状态的设计,怎样达到块外暴力,块内查表。大概的感觉是,都要围绕块来进行设计。而且,通常要有两个以上函数状态有机配合。
6.对于很多的题目,我们都可以找到n^2 的暴力算法。但是,当n在10000到200000之间的时候, n^2 基本稳稳卡掉。发现,这样的题目,经常还与区间有关系的时候,可以考虑分块做法

下面展示的代码只是分块的区间添加,单点查询,区间最值做法,但已经很具代表模板了,而且注释很详尽。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1<<20;
int a[N],k,len,n;//a[N]记录数列,k指块的数目,len指块的长度 
int L[N],R[N],F[N],add[N],Max[N];
// L[N]记录每个块起始元素的下标,R[N]记录每个块末尾元素下标
// F[N]记录每一个元素所属哪一个块,add[N]加法标记 
// Max[N]记录每个区间的最大值 
inline void Build(){	//建块 
	memset(a,0,sizeof(a));
	memset(add,0,sizeof(add));
	memset(Max,0,sizeof(Max));
	for(int i = 1; i <= n; i++)
		scanf("%d",&a[i]);
	len = sqrt(n);k = n/len;	//计算块的长度和数目
	if(n%k) k++;	//特判最后一个不完整块
	for(int i = 1; i <= k; i++)
		R[i] = i*len,L[i]=R[i-1]+1;	//计算每一个块的起始末尾元素下标 
	R[k] = n; 
	for(int i = 1; i <= k; i++)
		for(int j = L[i]; j <= R[i]; j++){
			F[j] = i;	//计算每一个元素所属哪一个块 
			Max[i] = max(Max[i],a[j]);		//计算该区间最大值 
		}	  
}
inline int Ask(int x){return a[x]+add[F[x]];} //单点询问
inline void Add(int x,int y,int z){	//区间加法 
	if(F[x]==F[y]){		//如果区间被包含于一个整块 
		for(int i = x; i <= y; i++) a[i]+=z;
		return;
	}
	//如果区间跨过整个块
	for(int i = x; i <= R[F[x]]; i++)a[i]+=z;
	for(int i = L[F[y]]; i <= y; i++)a[i]+=z;
	for(int i = F[x]+1; i < F[y]; i++)add[i]+=z; 
}
void update(int x,int v){	//单点更新 只是用于区间求最值 区间添加和区间求最值是不能一起用的 
	int t = F[x];
	a[x] = v;
	for(int i = L[t]; i <= R[t]; i++)Max[t] = max(Max[t],a[i]); 
}
inline int Quarymax(int x,int y){	//求区间最大值 
	int ans = -1;
	if(F[x]==F[y]){		//如果区间被包含于一个整块
		for(int i = x; i <= y; i++)ans = max(ans,a[i]);
		return ans;
	} 
	//如果区间跨过整个块
	for(int i = x; i <= R[F[x]]; i++)ans = max(ans,a[i]);
	for(int i = L[F[y]]; i <= y; i++)ans = max(ans,a[i]);
	for(int i = F[x]+1; i < F[y]; i++)ans = max(ans,Max[i]);
	return ans;
}  
int main(){
	cin >> n;
	Build();	//以下用来测试
	int aa,bb;
	int t = 10;
	while(t--){
		cin >> aa >> bb;
		cout << Quarymax(aa,bb) << endl;
		cin >> aa >> bb;
		update(aa,bb);
	}
	return 0;
}
 

2.莫队算法

莫队算法是一种可以解决大部分区间离线问题的离线算法
其主要思想是分块
所以呢,时间复杂度O(n n \sqrt{n} n ),也不失为一个解决问题的好办法。
莫队算法主要是基于分块算法,所以学习莫队算法之前要现了解分块算法。
光有分块还是不够的还需要进行对访问区间进行从小到大排序。
莫队算法其他部分很好写,最主要就是Add()(注:贡献加法函数)和Sub()(注:贡献减法函数)两个函数难写,因为这两个函数会根据不同的题目不同而写法不同。

建议结合题目和代码来深刻理解莫队这个算法

1. 下面给出洛谷的一道经典的利用莫队算法的例题 题目链接
在这里插入图片描述
题意: 题目很好理解,就是求给定区间中元素出现次数平方和。
贴上代码:

#include <bits/stdc++.h>
using namespace std;
#define JS ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
typedef long long ll;
const int maxn = 5e4+10;
int a[maxn],pos[maxn],cnt[maxn];
ll ans[maxn],res;
struct Q{
	int l,r,k;
}q[maxn];
bool cmp(Q x,Q y){return pos[x.l]==pos[y.l]?x.r<y.r:pos[x.l]<pos[y.l];}
void Add(int n){cnt[a[n]]++;res+=cnt[a[n]]*cnt[a[n]]-(cnt[a[n]]-1)*(cnt[a[n]]-1);}//用更新后的平方减去之前的平方
void Sub(int n){cnt[a[n]]--;res-=(cnt[a[n]]+1)*(cnt[a[n]]+1)-cnt[a[n]]*cnt[a[n]];}//用更新前的平方减去之后平方
int main(){
	JS
	int n,m,k;
	cin >> n >> m >> k;
	int le = sqrt(n);		//分块
	if(n%le)le++;
	for(int i = 1; i <= n; i++)cin >> a[i],pos[i]=i/le;
	for(int i = 0; i < m; i++){
		cin >> q[i].l >> q[i].r;
		q[i].k = i;			//记录这个询问的次序
	}
	sort(q,q+m,cmp);		//排序
	int l = 1,r = 0;
	for(int i = 0; i < m; i++){
		while(q[i].l < l) Add(--l);	//当前答案res的l大于询问的l
		while(q[i].r > r) Add(++r);	//当前答案res的r小于询问的r
		while(q[i].l > l) Sub(l++);	//当前答案res的l小于询问的l
		while(q[i].r < r) Sub(r--);	//当前答案res的r大于询问的r
		ans[q[i].k] = res;			//记录答案
	}
	for(int i = 0; i < m; i++)cout << ans[i] << endl;
	return 0;
} 

2. 有能力可以试着做一做牛课上一道有关莫队算法 题目链接
代码也贴上一下

#include<bits/stdc++.h>
using namespace std;
typedef long long int ll;
const ll N=50010;
const ll M=100010;
const ll S=1000;
ll n,m,a[N],cnt[M],nr,nl,now,ans[M],pos[N];
set<ll> s;
struct Q{
    ll l,r,k,id;
}q[N];
bool cmp(Q x,Q y){return pos[x.l]==pos[y.l]?x.r<y.r:pos[x.l]<pos[y.l];}
ll gcd(ll a,ll b){return b?gcd(b,a%b):a;}
//这两个函数Add()和Sub()这次的作用就是缩短计数的次数
void Add(ll x,ll y){++cnt[x];}
void Sub(ll x,ll y){--cnt[x];}
void move_to(ll tl,ll tr,ll tt){
    while(nr<tr)Add(a[++nr],tt);
    while(nl>tl)Add(a[--nl],tt);
    while(nr>tr)Sub(a[nr--],tt);
    while(nl<tl)Sub(a[nl++],tt);
    for(set<ll>::iterator it=s.begin();it!=s.end();it++){	//再次优化,用set去重
        if(cnt[*it]!=0&&gcd(cnt[*it],tt)==1){
            now++;
        }
    }//记录结果
}//区间维护
int main(){
    cin>>n>>m;
    int le = sqrt(n);
    if(n%le)le++;
    for(ll i=1;i<=n;i++){
        cin>>a[i];
        pos[i]=i/le;
        s.insert(a[i]);
    }
    for(ll i=1;i<=m;i++){
        scanf("%lld%lld%lld",&q[i].l,&q[i].r,&q[i].id);
        q[i].k = i; 
    }
    sort(q+1,q+m+1,cmp);
    for(ll i=1;i<=m;i++){
        move_to(q[i].l,q[i].r,q[i].id);
        ans[q[i].k]=now;
        now=0;
    }
    for(ll i=1;i<m;i++){
        cout<<ans[i]<<endl;
    }
    cout<<ans[m];//输出结果
    return 0;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值