莫队分块(带修,不带修,回滚)

基础莫队(不带修)

个人理解

  • 莫队是一种优雅的暴力,是著名的离线算法之一。一般用来解决区间查询问题,利用了区间范围的特点,将查询区间的顺序排序,将时间复杂度降至O(nsqrt(n))。这里的区间查询可查询非常多的东西。基础莫队不支持修改,只支持查询。

算法学习

  • 首先我们引入一到例题
    题目描述:n个数,m个询问,每次询问查询 [ l , r ] [l,r] [l,r] 区间,求 ∑ \sum ci2,ci为每个数出现的次数,n,m<=50000。
    例如:1 3 2 1 1 3,1出现3次,2出现一次,3出现两次,sum=32+11+22=14

  • 暴力算法:O(nm)

  • 一个思维:当我们刚查询完区间 [3,7] ,此时 l = 3,r = 7。假设下一个查询区间为 [2,5],则只需要 l-1,r-2即可,即只需要添加第2个点,删除6,7两个点即可。即我们可以通过 l 和 r 的偏移,而不是每次都暴力从 l 枚举到 r。但是,当区间的左右端点变化很大的时候,l 和 r 的偏移距离过大,达到了O(n)级别,与原来的暴力算法同级。

while(l>p[i].l)move(--l,1);
while(r<p[i].r)move(++r,1);
while(l<p[i].l)move(l++,-1);
while(r>p[i].r)move(r--,-1);
  • 优化这个思维:尝试排序???排序的话,第一反应想到的应该就是直接对 l 排升序,再对 r 排升序吧。但是效果并不是很好,因为当 l 增大的过程中, r 一会儿大一会儿小,即 r 的偏转距离又一次达到了O(n)级别。

  • 莫队!!!:国家集训队的队长莫队想到了一个好方法。它将n个数分成了block(sqrt(n))块,第 i 个数通俗的被放进了第 i / block块里。他 L 为标准分块,排了个序。

int cmp(node x, node y) {//莫队核心 
    if(x.l/block==y.l/block)return x.r<y.r;
    else return x.l<y.l;
}
  • 即:
    如果在同一块内,则按 r 递增排序;
    如果不在同一块内,则按 l 递增排序。
    为什么要这样排序,这样排序有什么好处???
    考虑最坏情况
    对于左端点在一个块中时的若干个查询,右端点从左往右偏移,时间复杂度O(n);左端点在块内随机偏移,时间复杂度O(sqrt(n)sqrt(n))。左端点一共可以在sqrt(n)个块内,总时间复杂度为O(nsqrt(n))

  • 常数优化
    我们尝试对这个莫队进行优化。每次询问的左端点在新的块的时候,其询问的右端点会从最左端开始,从左往右进行了一边O(n)。从最右端偏移回最左端,这显然是多余的,我们考虑一次去一次回,时间复杂度会有一定的优化。

int cmp(node x, node y) {//莫队核心 
    if(x.l/block==y.l/block)return x.l/block&1?x.r>y.r:x.r<y.r;
    else return x.l<y.l;
}

莫队模板

struct node {
	int l,r,pos;
} p[N];
int ans[N],nowAns,block,a[N],n,m;

int cmp(node x, node y) {//莫队核心 
    if(x.l/block==y.l/block)return x.l/block&1?x.r>y.r:x.r<y.r;
    else return x.l<y.l;
}
void move(int pos,int sign) {
    //update nowAns
}
void solve() {
	block=sqrt(n);
	sort(p+1,p+m,cmp);
	int l=1,r=0;
	for(int i=1; i<=m; i++) {
		while(l>p[i].l)move(--l,1);
		while(r<p[i].r)move(++r,1);
		while(l<p[i].l)move(l++,-1);
		while(r>p[i].r)move(r--,-1);
		ans[p[i].pos]=nowAns;
	}
	for(int i=1;i<=m;i++)cout<<ans[i]<<endl;
}
int main() {
	cin>>n>>m;
	for(int i=1;i<=n;i++)cin>>a[i];
	for(int i=1;i<=m;i++){
		cin>>p[i].l>>p[i].r;
		p[i].pos=i;
	} 
	solve();
	return 0;
}

例题1:三道简单模板题

例题1.1
例题1.2
例题1.3

例题2:区间众数出现次数

例题链接

  • 本以为很难,因为我们不知道该怎么删除,需要用什么高级的回滚莫队之类的,后来发现是自己蠢了。
  • 问题分析: c n t [ i ] cnt[i] cnt[i] 作为桶,记录 i i i 出现的次数;用 c n t c n t [ i ] cntcnt[i] cntcnt[i] 作为桶,记录出现次数出现的次数。对于删除操作, c n t c n t [ c n t [ v ] ] = 0 , a n s = c n t [ v ] cntcnt[cnt[v]]=0,ans=cnt[v] cntcnt[cnt[v]]=0,ans=cnt[v] 则代表答案出现 c n t [ v ] cnt[v] cnt[v] 次的次数已经没了,出现最多次数变为 c n t [ v ] − 1 cnt[v]-1 cnt[v]1 次了

Code

void move(int pos,int sign) {
    //update nowAns
    int v=a[pos];
    if(sign==1){
       cntcnt[cnt[v]]--;
       if(cnt[v]==nowAns)nowAns++;
       cnt[v]++;
       cntcnt[cnt[v]]++;
	}
	else{
		cntcnt[cnt[v]]--;
		if(cntcnt[cnt[v]]==0&&nosAns==cnt[v])nowAns--;
		cnt[v]--;
		cntcnt[cnt[v]]++;
	}
}

例题3:莫队求有多少子区间异或和等于 k 。

例题链接

  • 题目描述: n 个数 a i a_i ai ,m 个询问,询问区间 [ l i , r i ] [l_i,r_i] [li,ri] 中,有多少个子区间的异或和等于 k 。 1 < = n < = 1 e 5 , 0 < = a i , k < = 1 e 6 1<=n<=1e5,0<=a_i,k<=1e6 1<=n<=1e5,0<=ai,k<=1e6
  • 问题分析: 考虑对 a i a_i ai 求前缀异或和数组 p r e i pre_i prei,那么问题就变成了区间 [ l − 1 , r ] [l-1,r] [l1,r] 中有多少个区间满足 p r e x pre_x prex xor p r e y = k pre_y=k prey=k p r e x = p r e y pre_x=pre_y prex=prey xor k k k ,这样就变成了莫队模板题了。
  • 注意:区间变成了 [ 0 , n ] [0,n] [0,n] ,为了避免 k = 0 k=0 k=0 产生的影响, l = 0 , r = 0 , c n t [ 0 ] = 1 l=0,r=0,cnt[0]=1 l=0,r=0,cnt[0]=1
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+100,M=3e6+100;
 
struct node {
	int l,r,pos;
} p[N];
long long ans[N],nowAns,block,a[N],n,m,k,pre[N],cnt[M];

int cmp(node x, node y) {//莫队核心
	if(x.l/block==y.l/block)return x.l/block&1?x.r>y.r:x.r<y.r;
	else return x.l<y.l;
}
void move(int pos,int sign) {
    if(sign==1){
    	nowAns+=cnt[pre[pos]^k];
		cnt[pre[pos]]++; 
	}else{
		cnt[pre[pos]]--;
		nowAns-=cnt[pre[pos]^k];
	}
	//update nowAns
}
void solve() {
	block=sqrt(n);
	sort(p+1,p+m,cmp);
	int l=0,r=0;
	cnt[0]=1;
	for(int i=1; i<=m; i++) {
		while(l>p[i].l)move(--l,1);
		while(r<p[i].r)move(++r,1);
		while(l<p[i].l)move(l++,-1);
		while(r>p[i].r)move(r--,-1);
		ans[p[i].pos]=nowAns;
	}
	for(int i=1; i<=m; i++)cout<<ans[i]<<endl;
}
int main() {
	cin>>n>>m>>k;
	for(int i=1; i<=n; i++)cin>>a[i],pre[i]=pre[i-1]^a[i];
	for(int i=1; i<=m; i++) {
		cin>>p[i].l>>p[i].r;
		p[i].l--;
		p[i].pos=i;
	}
	solve();
	return 0;
}

带修莫队

个人理解

  • 对于简单的单点修改,其实就是将查询的区间多了一维时间轴,其他基本没有什么变化

算法学习

  • 首先我们引入一道例题
  • 题目描述: n个数,m个询问,每次询问查询 [ l , r ] [l,r] [l,r] 区间的不重复的数的个数或修改第 k 个点的值 x。n,m<=133333。
  • 如何变成带修莫队呢?我们定义一个时间 t 表示已经修改完了多少次,记录每次查询的是在第几次修改后查询的。莫队偏移的时候,将 t 也跟着偏移就好了。例如:此次查询是在第五次修改之后的,而此时只修改了一次,则 t 偏移,即进行第二次到第五次的修改。相当于多了一维 t ,那么就很简单了。先附上模板再解析。

模板如下

const int N=1e6+10;
struct node {
	int l,r,t,pos;
} q[N];
struct change{
	int pos,v;
}c[N];
int ans[N],nowAns=0,block,n,m,a[N],qnum=0,cnum=0;

int cmp(node x,node y) {
    if (x.l / block != y.l / block)return x.l < y.l;
    else if (x.r / block != y.r / block)return (x.l / block) & 1 ? x.r > y.r : x.r < y.r;
    else return (((x.l / block) & 1) ^ ((x.r / block) & 1)) ? x.t > y.t : x.t < y.t;
}
void move(int pos,int sign) {
	//update nowAns
}
void work(int now,int i) {
	if(c[now].pos>=q[i].l&&c[now].pos<=q[i].r){
		//update nowAns
	}
	swap(c[now].v,a[c[now].pos]);//下次会自动改回来 
}
void solve() {
	block=pow(n,0.66);
	sort(q+1,q+qnum,cmp);
	int l=1,r=0,t=0;
	for(int i=1; i<=qnum; i++) {

		while(l>q[i].l)move(--l,1);
		while(r<q[i].r)move(++r,1);
		while(l<q[i].l)move(l++,-1);
		while(r>q[i].r)move(r--,-1);
		while(t<q[i].t)work(++t,i);
		while(t>q[i].t)work(t--,i);

		ans[q[i].pos]=nowAns;
	}
	for(int i=1; i<=qnum; i++)printf("%d\n",ans[i]);
}
int main() {
	char op;
	int l,r;
	cin>>n>>m;
	for(int i=1; i<=n; i++)scanf("%d",&a[i]);
	
	for(int i=1; i<=m; i++) {
		cin>>op>>l>>r;
		if(op=='Q') {
			qnum++;
			q[qnum].l=l;
			q[qnum].r=r;
			q[qnum].pos=qnum;
			q[qnum].t=cnum;
		} else {
			cnum++;
			c[cnum].pos=l;
			c[cnum].v=r;
		}
	}
	solve();
	return 0;
}

回滚莫队

个人理解

  • 对于某些问题,增加容易删除难,比如求区间最大值。(别跟我说线段树就行,现在是在学莫队,我只是举个不好删除的例子)。或者删除容易添加难(一般都是删除难)。这时我们想出一个办法通过多一些计算来避免删除操作的出现,即只增不删。

算法学习

  • 附上模板题
  • 题目描述:给定 n 个数 a i a_i ai,m个询问 [ l , r ] [l,r] [l,r] ,每次去查询区间中相同的数的最远间隔距离。 1 < = n < = 2 e 5 , 1 < = a i < = 1 e 9 1<=n<=2e5,1<=ai<=1e9 1<=n<=2e5,1<=ai<=1e9
  • 问题分析
  • 数字值域大先离散化。我们用两个桶 m a x l [ i ] maxl[i] maxl[i] m a x r [ i ] maxr[i] maxr[i] 分别记录数字 i 出现的最左距离和最右距离。
  • 每次增加一个点时,只需要更新一下这个数字的最左距离和最右距离再判断差值是否大于nowans即可。
  • 那么如果删除呢?人傻了,我不会删。那干脆不删了。

于是回滚莫队出现了

  • 排序与最普通的排序一致(没有优化过的排序),即对于左端点在同一块内的若干个询问,右端点只会往右偏移,即右端点的偏移只会导致增加操作,而不会造成删除操作。(我们先不考虑左端点换块时,右端点从右往左的偏移)
  • 但是左端点在该块内是随机偏移的,有时删除有时增加。如何避免呢?对于左端点在同一块的若干个询问,每次都让左端点从所在块的右边界的下一位置暴力往左增加点,由于每次的偏移量只有sqrt(n)的距离,算起来对总的时间复杂度不影响,而右端点正常往右偏移。
  • 具体实现???
  • 每当左端点换块时,将查询区间清零,l=左端点所在新块的右边界的下一位置,r=左端点所在新块的右边界。
  • 对于右端点在该块内的询问,暴力计算。
  • 对于右端点不在该块内的询问,r正常向右偏移;当l需要向左偏移时,有一个临时l和一个临时ans出现,往左偏移增加点并计算答案,计算完成之后,回滚:即将刚刚留下的痕迹全部删除掉,l回到原来位置,答案变为原来的答案。

模板如下

#include<bits/stdc++.h>
using namespace std;
const int N=2e5+100;
struct query {
	int l, r, pos;//查询的左右边界以及编号
}q[N];

int n, m;//数字个数以及查询个数
int block, blocknum, blo[N];//block代表块的大小,blocknum块的数量,blo[i]记录下标i是属于第几块
int nowans, ans[N];//nowans用于求解过程,ans[]存储最后的答案

bool cmp(query a, query b) {//查询排序
	if (blo[a.l] == blo[b.l])return a.r < b.r;
	else return a.l < b.l;
}
void init() {//分块
	block = sqrt(n);
	for (int i = 1;i <= n;i++)blo[i] = (i - 1) / block + 1;
	blocknum = blo[n];
}
int force(int l, int r) {//暴力求解
}
void addr(int x) {//右边界右移,更新nowans
}
void addl(int x) {//左边界左移,更新nowans
}
void roll(int x) {//回滚,也就是删除痕迹
}
void clear() {
	//清零
}
void solve() {
    init();//初始化
    sort(q + 1, q + m + 1, cmp);//给查询排序
    
	//这层循环在块之间移动
	for (int id = 1, i = 1;id <= blocknum;id++) {//id为块的编号,一块一块解决;i为查询编号
		int R = min(id * block, n);//R为当前块的右边界
		int l = R + 1, r = R;//初始化l、r
		
		//这层循环在属于同一块的查询中移动
		for (;blo[q[i].l] == id;i++) {
			if (blo[q[i].r] == id) {//若第i查询的右边界也属于id块,直接暴力求出答案
				ans[q[i].pos] = force(q[i].l, q[i].r);
				continue;
			}
			while (r < q[i].r)addr(++r);//右边界向右移动
			int tempans = nowans;//tempans用于记录右边界移动后的答案,由于同块的查询右边界是递增的,所以tempans可用于下一次同块的循环
			while (l > q[i].l)addl(--l);//左边界向左移动
			ans[q[i].pos] = nowans;//ans[q[i].pos]用于记录最终的第q[i].pos个答案
			while (l <= R)roll(l++);//每次将左边界移动回R+1的位置,保证下次l也是向左移动,即回滚操作
			nowans = tempans;//将nowans的值置为tempans
		}
		clear();//把所有东西清的干干净净,包括ans
	}
	for (int i = 1;i <= m;i++)cout << ans[i] << endl;
}

int main() {
	cin>>n>>m;
	for (int i = 1;i <= n;i++)cin >> a[i];
	for (int i = 1;i <= m;i++) {
		cin >> q[i].l >> q[i].r;
		q[i].pos = i;
	}
	solve();
}

例题1:查询区间 数*数的出现次数 的最大值

例题链接

  • 问题分析: 拿个桶计数即可,可以使用 map ,也可考虑离散化。增加容易,删除难,显然考虑回滚莫队,然后就变成了模板题
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值