分块&莫队

分块

这是一种思想,不是一种数据结构。学校题单里的题大都是用这种思想做的。

分块就是将一个序列分成多个不相交的区间,称为块。

理想块长是 \(\sqrt{n}\) ,至于为什么,它是由时间复杂度 \(O(s+\frac{n}{s})(s\)为块长\()\) 通过均值不等式算出来的。。。

定义

\(pos[i]\):表示 \(i\) 所属的块。

\(st[i]\):表示 \(pos[i]\) 的起始位置。

\(ed[i]\):表示 \(pos[i]\) 的终止位置

\(sum[i]\):表示第 \(i\) 块的总和。

点击查看代码

void build()
{
	len=sqrt(n);
	t=n/len;
	if(n%len) t++;
	for(int i=1;i<=t;i++)
	{
		st[i]=(i-1)*len+1;
		ed[i]=i*len;
	}
	ed[t]=n;
	for(int i=1;i<=n;i++)
	{
		pos[i]=(i-1)/len+1;
		sum[pos[i]]+=a[i];
	}
}

单点修改

直接修改即可,注意将 \(sum\) 数组也要更新。

区间修改

\(add[i]\):表示第 \(i\) 块加的值。

  • 在同一块:直接暴力修改。
  • 不在同一块:零散的块暴力修改,整块修改 \(add\) 数组的值。
点击查看代码
void update(int l,int r,int k)
{
	int sid=pos[l], eid=pos[r];
	if(sid==eid)//在同一块 
	{
		for(int i=l;i<=r;i++)
		{
			a[i]+=k;
		}
		sum[sid]+=(r-l+1)*k;
	}
	else
	{
		for(int i=sid+1;i<eid;i++)
		{
			add[i]+=k;
		}
		for(int i=sid;i<=ed[sid];i++)
		{
			a[i]+=k;
		}
		sum[sid]+=k*(ed[sid]-l+1);
		for(int i=st[eid];i<=r;i++)
		{
			a[i]+=k;
		}
		sum[eid]+=k*(r-st[eid]+1); 
	}
}

区间求和

点击查看代码
int query(int l,int r)
{
	int ans=0;
	int sid=pos[l], eid=pos[r];
	if(sid==eid)//在同一块 
	{
		for(int i=l;i<=r;i++)
		{
			ans+=a[i];
		}
	}
	else
	{
		for(int i=l;i<=ed[sid];i++)
		{
			ans+=a[i];
		}
		for(int i=st[eid];i<=r;i++)
		{
			ans+=a[i];
		}
		for(int i=sid+1;i<eid;i++)
		{
			ans+=sum[i];
		}
	}
	return ans;
}

区间求大于/小于/大于等于/小于等于

将每个块排序,使用二分/ \(lower\_bound\) 查询即可。

注:\(lower\_bound\) 返回的是这个序列里第一个大于等于查找的数

点击查看代码
//区间修改,区间查询大于等于c的个数
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e6+5;
int n, q, a[maxn]; 
int st[maxn], ed[maxn], pos[maxn];
int add[maxn], len;
vector<int> v[maxn];
void reset(int x)
{
	v[x].clear();
	for(int i=st[x];i<=ed[x];i++)
	{
		v[x].push_back(a[i]);
	}
	sort(v[x].begin(), v[x].end());
}
void build()
{
	len=sqrt(n);
	int t=n/len;
	if(n%len) t++;
	for(int i=1;i<=t;i++)
	{
		st[i]=(i-1)*len+1;
		ed[i]=i*len;
	}
	ed[t]=n;
	for(int i=1;i<=n;i++)
	{
		pos[i]=(i-1)/len+1;
		v[pos[i]].push_back(a[i]);
	}
	for(int i=1;i<=t;i++)
	{
		sort(v[i].begin(),v[i].end());
	}
}
void update(int l,int r,int v)
{
	int sid=pos[l], eid=pos[r];
	if(sid==eid)
	{
		for(int i=l;i<=r;i++)
		{
			a[i]+=v;
		}
		reset(sid);
	}
	else
	{
		for(int i=l;i<=ed[sid];i++)
		{
			a[i]+=v;
		}
		reset(sid);
		for(int i=sid+1;i<=eid-1;i++)
		{
			add[i]+=v;
		}
		for(int i=st[eid];i<=r;i++)
		{
			a[i]+=v;
		}
		reset(eid);
	}
}
int ask(int l,int r,int c)
{
	int sid=pos[l], eid=pos[r];
	int ans=0;
	if(sid==eid)
	{
		for(int i=l;i<=r;i++)
		{
			if(a[i]+add[sid]>=c) ans++;	
		}
		return ans;
	}
	else 
	{
		for(int i=l;i<=ed[sid];i++)
		{
			if(a[i]+add[sid]>=c) ans++;
		}
		for(int i=sid+1;i<=eid-1;i++)
		{
			int x=lower_bound(v[i].begin(), v[i].end(), c-add[i])-v[i].begin();
			ans+=(len-x);	
		}
		for(int i=st[eid];i<=r;i++)
		{
			if(a[i]+add[eid]>=c) ans++;
		}	
		return ans;	
	}
}
int main()
{
	cin>>n>>q;
	for(int i=1;i<=n;i++)
	{
		cin>>a[i];
	}
	build();
	while(q--)
	{
		char opt;
		int l, r, c;
		cin>>opt>>l>>r>>c;
		if(opt=='M')
			update(l, r, c);
		else 
			cout<<ask(l, r, c)<<endl;
	}
	return 0;	
} 

根据情况修改ask的写法。


会发现用分块维护一个序列要考虑三个要素:

  1. 不完整的块怎么处理?
  2. 完整的块怎么处理?
  3. 要预处理什么信息?

求值函数怎么写?

  1. 端点在同一块:一般都是暴力处理。
  2. 端点不在同一块:分为角块和整块处理。

例题:


莫队

莫队是一种离线算法。且基于分块思想(一定不要忘了,否则会T的很惨)。一般不带修改,或有简单修改。

普通莫队

求区间数值种类数,暴力移动 \(l,r\) 点,暴力修改即可。根据题目调整 add 和 del 函数。

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=2e5+5;
int n, m, qq, a[maxn], p[maxn], ans[maxn], t[maxn], res;
struct node{
	int l, r, id;
}q[maxn];
int pos[maxn];
void build()
{
	int len=sqrt(n);
	for(int i=1;i<=n;i++)
	{
		pos[i]=(i-1)/len+1;
	}
}
bool cmp(node a,node b)
{
	if(pos[a.l]!=pos[b.l]) return pos[a.l]<pos[b.l];
	return a.r<b.r;
}
void add(int val)
{
	if(++t[val]==1) res++;
}
void del(int val)
{
	if(--t[val]==0) res--;
}
signed main()
{
	cin>>n>>qq;
	for(int i=1;i<=n;i++)
	{
		cin>>a[i];
	}
	build();
	for(int i=1;i<=qq;i++)
	{
		int l, r;
		cin>>l>>r;
		q[i].id=i;
		q[i].l=l;
		q[i].r=r;
	}
	sort(q+1, q+qq+1, cmp);
	int l=0, r=0;
	for(int i=1;i<=qq;i++)
	{
		while(l>q[i].l) add(a[--l]);
		while(r<q[i].r) add(a[++r]);
		while(l<q[i].l) del(a[l++]);
		while(r>q[i].r) del(a[r--]);
		ans[q[i].id]=res;
	}
	for(int i=1;i<=qq;i++)
	{
		cout<<ans[i]<<endl;
	}
	return 0;
}

带修莫队

名字听着挺高级,但实际上就是在普通莫队上加入一些简单的修改。

带修莫队相比普通莫队多维护了一个时间戳,记录这是第几次修改。它的维护方式和 \(l,r\) 一样,都是将多的改回来,将少的补回去。需要注意的是 \(change\) 函数:

void change(int now,int i)
{
	if(c[now].pos>=q[i].l&&c[now].pos<=q[i].r)//在区间内 
	{
		if((++tmp[c[now].val])==1) res++;//将这个值改变,就多了一个新值,判断这个新值是否出现过 
		if((--tmp[a[c[now].pos]])==0) res--;////将这个值改变,就多了一个原来的值,判断这个原来的值是否还有
	}
	swap(c[now].val, a[c[now].pos]);//交换,便于后期使用(这里要仔细想想) 
}

P1903 [国家集训队] 数颜色 / 维护队列

注意块长是 \(n^{\frac{2}{3}}\)

点击查看代码
//带修莫队 
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e6+5;
struct node{
	int l, r, time, id;
}q[maxn];
struct NODE{
	int pos, val;
}c[maxn];
int a[maxn], ans[maxn], tmp[maxn];
int n, m, k, res;
int st[maxn], ed[maxn], pos[maxn];
int len, qnum, tim;//qnum为询问的次数,tim为修改的次数 
void build()
{
	len=int(pow(n, 0.66));
	for(int i=1;i<=n;i++)
	{
		pos[i]=(i-1)/len+1;
	}
} 
void del(int v)
{
	if((--tmp[v])==0) res--; 
}
void add(int v)
{
	if((++tmp[v])==1) res++;
}
void change(int now,int i)
{
	if(c[now].pos>=q[i].l&&c[now].pos<=q[i].r)//在区间内 
	{
		if((++tmp[c[now].val])==1) res++;
		if((--tmp[a[c[now].pos]])==0) res--;
	}
	swap(c[now].val, a[c[now].pos]);//交换,便于后期使用 
}
bool cmp(node x,node y)
{
	if(pos[x.l]!=pos[y.l])
	{
		return pos[x.l]<pos[y.l];
	}
	if(pos[x.r]!=pos[y.r])
	{
		return pos[x.r]<pos[y.r];
	}
	if(x.time!=y.time)
	{
		return x.time<y.time;
	}
}
int main()
{
	cin>>n>>m;
	for(int i=1;i<=n;i++)
	{
		cin>>a[i];
	}
	build();
	for(int i=1;i<=m;i++)
	{
		char opt;
		int x, y;
		cin>>opt>>x>>y;
		if(opt=='Q')
		{
			q[++qnum].l=x;
			q[qnum].r=y;
			q[qnum].time=tim;
			q[qnum].id=qnum;//注意这里是qnum		
		} 
		else
		{
			c[++tim].pos=x;
			c[tim].val=y; 
		}		
	}
	sort(q+1, q+qnum+1, cmp);
	int l=1, r=0, now=0;
	for(int i=1;i<=qnum;i++)
	{
		while(l>q[i].l) add(a[--l]);
		while(r<q[i].r) add(a[++r]);
		while(l<q[i].l) del(a[l++]);
		while(r>q[i].r) del(a[r--]);
		while(now>q[i].time) change(now--, i);
		while(now<q[i].time) change(++now, i);
		ans[q[i].id]=res;	
	}
	for(int i=1;i<=qnum;i++)
	{
		cout<<ans[i]<<endl;
	}
	return 0;
}

温馨提示:莫队无输出多半是板写错了,务必仔细检查。

posted @ 2024-08-19 16:22  zhouyiran2011  阅读(7)  评论(0编辑  收藏  举报