简单分块和不带修改的莫队

  分块,一个大体优化,局部暴力的东西,就是把N个序列放在M个容积为sqrt(n)的容器里进行修改和查询。这种方法虽然没有树状数组,线段树的时间复杂度优秀,但是他的扩展性和长度都比较好,时间复杂度是O(n*log(n)),同时也是学习莫队的一个基础。下面我们来看一些题目。

  1,给出一个长为n的数列,以及n个操作,操作涉及区间加法,单点查值。

这是一道模板的线段树(或树状数组),但是也是一到很好的分块。

以此题为例,如果我们把每m个元素分为一块,共有n/m块,每次区间加的操作会涉及O(n/m)个整块,以及区间两侧两个不完整的块中至多2m个元素。

我们给每个块设置一个加法标记(就是记录这个块中元素一起加了多少),每次操作对每个整块直接O(1)标记,而不完整的块由于元素比较少,暴力修改元素的值。

每次询问时返回元素的值加上其所在块的加法标记。

这样每次操作的复杂度是O(n/m)+O(m),根据均值不等式,当m取sqrt(n)时总复杂度最低,为了方便,我们都默认下文的分块大小为sqrt(n)。

代码如下:

#include<bits/stdc++.h>
using namespace std;
int read()
{
	int num=0;bool flag=1;
	char c=getchar();
	for(;c<'0'||c>'9';c=getchar())
	  if(c=='-')flag=0;
	for(;c>='0'&&c<='9';c=getchar())
	  num=(num<<1)+(num<<3)+c-'0';
	return flag?num:-num;
}
const int N=50010;
int n,a[N],v[N],lazy[N],gh;
void add(int x,int y,int sum)
{
	for(int i=x;i<=min(y,v[x]*gh);i++)//枚举x所在的分块被覆盖的部分
	  a[i]+=sum;//暴力
	for(int i=(v[y]-1)*gh+1;i<=y&&v[x]!=v[y];i++)//枚举y所在的分块被覆盖的部分(还有和x不在同一分块的条件)
	  a[i]+=sum;//暴力
	for(int i=v[x]+1;i<=v[y]-1;i++)//把他们中间的部分枚举一遍
	  lazy[i]+=sum;//加入lazy里
}
int main()
{
	n=read();gh=sqrt(n);
	for(int i=1;i<=n;i++)
	  a[i]=read(),v[i]=(i-1)/gh+1;//把i的分块下标放在v[i]这个数组里
	for(int i=1;i<=n;i++)
	{
		int opt=read(),l=read(),r=read(),c=read();
		if(opt==0)add(l,r,c);
		  else printf("%d\n",lazy[v[r]]+a[r]);//输出值+这一个区间的值
	}
	return 0;
}

2,给出一个长为n的数列,以及n个操作,操作涉及区间加法,询问区间内小于某个值x的元素个数。

这道题你用线段树就有点难了对不对,现在我们来想想分块。

在做分块的时候就是想着要爆力,这样你就能有意想不到的效果。

首先,假设个我们这么一个区间,怎么查询?这时我们可以进行分块,把每一个分块排一下序,中间完整的部分用lower_bound()找一下减头座标即可,至于旁边的,不用怕,就是暴力,顶多也就2sqrt(n)个要暴力而已,对吧?

对于这么一个区间,我们还是分成三个部分,v[x]+v[y]是没问题的,还是用lazy做一遍,在查询的是后像这种区间就可以把中间的要查的数减去lazy,在用lower_bound找一下坐标减去头坐标即可,但是就是有一个问题就是如何把旁边两个部分处理好,此时不要怕,就是暴力,我们把这些东西加完时,再重新排一下序,这样即可(我们可以用vector做一遍)

代码如下:

#include<bits/stdc++.h>
using namespace std;
int read()
{
	int num=0;bool flag=1;
	char c=getchar();
	for(;c<'0'||c>'9';c=getchar())
	  if(c=='-')flag=0;
	for(;c>='0'&&c<='9';c=getchar())
	  num=(num<<1)+(num<<3)+c-'0';
	return flag?num:-num;
}
const int N=50010;
int n,a[N],v[N],lazy[N],gh;
vector<int>e[N/100];
int find(int x,int i){return lower_bound(e[i].begin(),e[i].end(),x)-e[i].begin();}
//找到最大的比x小的坐标减去头坐标就是个数
void clean(int x)
{
	e[v[x]].clear();//清空
	for(int i=gh*(v[x]-1)+1;i<=min(gh*v[x],n);i++)//枚举x这一个块
	  e[v[x]].push_back(a[i]);//加入
	sort(e[v[x]].begin(),e[v[x]].end());//拍一遍序
}
void add(int x,int y,int sum)
{
	for(int i=x;i<=min(y,v[x]*gh);i++)
	  a[i]+=sum;//先把这些数加上
	clean(x);//重新排一遍序
	if(v[x]!=v[y])
	{
	  for(int i=(v[y]-1)*gh+1;i<=y;i++)
	    a[i]+=sum;//先把这些数加上
	  clean(y);//重新排一遍序
	}
	for(int i=v[x]+1;i<=v[y]-1;i++)
	  lazy[i]+=sum;//累计一下
}
void query(int x,int y,int sum)
{
	int ans=0;
	for(int i=x;i<=min(y,v[x]*gh);i++)
	  if(a[i]+lazy[v[i]]<sum)ans++;//暴力查找
	for(int i=(v[y]-1)*gh+1;i<=y&&v[x]!=v[y];i++)
	  if(a[i]+lazy[v[i]]<sum)ans++;//暴力查找
	for(int i=v[x]+1;i<=v[y]-1;i++)
	  ans+=find(sum-lazy[i],i);//寻找这一段的个数
	printf("%d\n",ans);
}
int main()
{
	freopen("a.in","r",stdin);
	freopen("a.out","w",stdout);	
	n=read();gh=sqrt(n);
	for(int i=1;i<=n;i++)
	{
	  a[i]=read();
	  v[i]=(i-1)/gh+1;
	  e[v[i]].push_back(a[i]);
    }
    for(int i=1;i<=v[n];i++)
	  sort(e[i].begin(),e[i].end()); //排序一遍
	for(int i=1;i<=n;i++)
	{
		int opt=read(),l=read(),r=read(),c=read();
		if(opt==0)add(l,r,c);
		  else query(l,r,c*c); 
	}
	return 0;
}

3,给出一个长为n的数列,以及n个操作,操作涉及区间加法,询问区间内小于某个值x的前驱(比其小的最大元素)。

n<=100000其实是为了区分暴力和一些常数较大的写法。

接着第二题的解法,其实只要把块内查询的二分稍作修改即可。

不过这题其实想表达:可以在块内维护其它结构使其更具有拓展性,比如放一个 set,这样如果还有插入、删除元素的操作,会更加的方便。

代码如下:

set的写法

#include<bits/stdc++.h>
using namespace std;
int read()
{
	int num=0;bool flag=1;
	char c=getchar();
	for(;c<'0'||c>'9';c=getchar())
	  if(c=='-')flag=0;
	for(;c>='0'&&c<='9';c=getchar())
	  num=(num<<1)+(num<<3)+c-'0';
	return flag?num:-num;
}
const int N=100010;
int n,a[N],v[N],lazy[N],gh;
set<int>e[N/100];
int D(int x,int sum)
{
	e[v[x]].erase(a[x]);//删除这一个元素
	a[x]+=sum;//加一下
	e[v[x]].insert(a[x]);//把这个加上
}
void add(int x,int y,int sum)//和T2基本一样
{
	for(int i=x;i<=min(y,v[x]*gh);i++)
	  D(i,sum);
	if(v[x]!=v[y])
	  for(int i=(v[y]-1)*gh+1;i<=y;i++)
	    D(i,sum);
	for(int i=v[x]+1;i<=v[y]-1;i++)
	  lazy[i]+=sum;
}
void query(int x,int y,int sum)
{
	int maxx=-1;
	for(int i=x;i<=min(y,v[x]*gh);i++)
	  if(a[i]+lazy[v[i]]<sum)maxx=max(maxx,a[i]+lazy[v[i]]);//暴力查找最大值
	for(int i=(v[y]-1)*gh+1;i<=y&&v[x]!=v[y];i++)
	  if(a[i]+lazy[v[i]]<sum)maxx=max(maxx,a[i]+lazy[v[i]]);//暴力查找最大值
	for(int i=v[x]+1;i<=v[y]-1;i++)//区间查找
	{
		set<int>::iterator it=e[i].lower_bound(sum-lazy[i]);
		if(it!=e[i].begin()){it--;maxx=max(maxx,*it+lazy[i]);}
	}
	printf("%d\n",maxx);
}
int main()
{
	n=read();gh=sqrt(n);
	for(int i=1;i<=n;i++)
	{
	  a[i]=read();
	  v[i]=(i-1)/gh+1;
	  e[v[i]].insert(a[i]);//加入set
        }
	for(int i=1;i<=n;i++)
	{
		int opt=read(),l=read(),r=read(),c=read();
		if(opt==0)add(l,r,c);
		  else query(l,r,c); 
	}
	return 0;
}

vector的写法

#include<bits/stdc++.h>
using namespace std;
int read()
{
	int num=0;bool flag=1;
	char c=getchar();
	for(;c<'0'||c>'9';c=getchar())
	  if(c=='-')flag=0;
	for(;c>='0'&&c<='9';c=getchar())
	  num=(num<<1)+(num<<3)+c-'0';
	return flag?num:-num;
}
const int N=100010;
int n,a[N],v[N],lazy[N],gh;
vector<int>e[N/10];
int find(int x,int i){return lower_bound(e[i].begin(),e[i].end(),x)-e[i].begin();}
void clean(int x)
{
	e[v[x]].clear();
	for(int i=gh*(v[x]-1)+1;i<=min(gh*v[x],n);i++)
	  e[v[x]].push_back(a[i]);
	sort(e[v[x]].begin(),e[v[x]].end());
}
void add(int x,int y,int sum)
{
	for(int i=x;i<=min(y,v[x]*gh);i++)
	  a[i]+=sum;
	clean(x);
	if(v[x]!=v[y])
	{
	  for(int i=(v[y]-1)*gh+1;i<=y;i++)
	    a[i]+=sum;
	  clean(y);
	}
	for(int i=v[x]+1;i<=v[y]-1;i++)
	  lazy[i]+=sum;
}
void query(int x,int y,int sum)
{
	int ans=-1;
	for(int i=x;i<=min(y,v[x]*gh);i++)
	  if(a[i]+lazy[v[i]]<sum)ans=max(ans,a[i]+lazy[v[i]]);
	for(int i=(v[y]-1)*gh+1;i<=y&&v[x]!=v[y];i++)
	  if(a[i]+lazy[v[i]]<sum)ans=max(ans,a[i]+lazy[v[i]]);
	for(int i=v[x]+1;i<=v[y]-1;i++)
	{
		vector<int>::iterator it=lower_bound(e[i].begin(),e[i].end(),sum-lazy[i]);
		if(it!=e[i].begin()){it--;ans=max(ans,*it+lazy[i]);}
    } 
	printf("%d\n",ans);
}
int main()
{
	freopen("a.in","r",stdin);
	freopen("a.out","w",stdout);	
	n=read();gh=sqrt(n);
	for(int i=1;i<=n;i++)
	{
	  a[i]=read();
	  v[i]=(i-1)/gh+1;
	  e[v[i]].push_back(a[i]);
    }
    for(int i=1;i<=v[n];i++)
	  sort(e[i].begin(),e[i].end()); 
	for(int i=1;i<=n;i++)
	{
		int opt=read(),l=read(),r=read(),c=read();
		if(opt==0)add(l,r,c);
		  else query(l,r,c); 
	}
	return 0;
}

4,给出一个长为n的数列,以及n个操作,操作涉及区间加法,区间求和。

这题的询问变成了区间上的询问,不完整的块还是暴力;而要想快速统计完整块的答案,需要维护每个块的元素和,先要预处理一下。

考虑区间修改操作,不完整的块直接改,顺便更新块的元素和;完整的块类似之前标记的做法,直接根据块的元素和所加的值计算元素和的增量。

代码如下:

#include<bits/stdc++.h>
using namespace std;
#define ll long long
ll read()
{
	ll num=0;bool flag=1;
	char c=getchar();
	for(;c<'0'||c>'9';c=getchar())
	  if(c=='-')flag=0;
	for(;c>='0'&&c<='9';c=getchar())
	  num=(num<<1)+(num<<3)+c-'0';
	return flag?num:-num;
}
const ll N=50010;
ll n,a[N],v[N],lazy[N],gh,LAZY[N];
void add(ll x,ll y,ll sum)
{
	for(ll i=x;i<=min(y,v[x]*gh);i++)
	  a[i]+=sum,LAZY[v[x]]+=sum;//记录累加和
	for(ll i=(v[y]-1)*gh+1;i<=y&&v[x]!=v[y];i++)
	  a[i]+=sum,LAZY[v[y]]+=sum;//记录累加和
	for(ll i=v[x]+1;i<=v[y]-1;i++)
	  lazy[i]+=sum,LAZY[i]+=sum*gh;//lazy记录要加的,LAZY记录总的累加和
}
void query(ll x,ll y,ll sum)//总的累加
{
	ll ans=0;
	for(ll i=x;i<=min(y,v[x]*gh);i++)
	  ans=(ans+a[i]+lazy[v[x]])%sum;
	for(ll i=(v[y]-1)*gh+1;i<=y&&v[x]!=v[y];i++)
	  ans=(ans+a[i]+lazy[v[y]])%sum;
	for(ll i=v[x]+1;i<=v[y]-1;i++)
	  ans=(ans+LAZY[i])%sum;
	printf("%lld\n",ans);
}
int main()
{
	n=read();gh=sqrt(n);
	for(ll i=1;i<=n;i++)
	  a[i]=read(),v[i]=(i-1)/gh+1,LAZY[v[i]]+=a[i];//记录总和
	for(ll i=1;i<=n;i++)
	{
		ll opt=read(),l=read(),r=read(),c=read();
		if(opt==0)add(l,r,c);
		  else query(l,r,c+1);
	}
	return 0;
}

5,给出一个长为n的数列,以及n个操作,操作涉及区间开方,区间求和。

稍作思考可以发现,开方操作比较棘手,主要是对于整块开方时,必须要知道每一个元素,才能知道他们开方后的和,也就是说,难以快速对一个块信息进行更新。

看来我们要另辟蹊径。不难发现,这题的修改就只有下取整开方,而一个数经过几次开方之后,它的值就会变成 0 或者 1。

如果每次区间开方只不涉及完整的块,意味着不超过2√n个元素,直接暴力即可。

如果涉及了一些完整的块,这些块经过几次操作以后就会都变成 0 / 1,于是我们采取一种分块优化的暴力做法,只要每个整块暴力开方后,记录一下元素是否都变成了 0 / 1,区间修改时跳过那些全为 0 / 1 的块即可。

这样每个元素至多被开方不超过4次,显然复杂度没有问题。

代码如下:

#include<bits/stdc++.h>
using namespace std;
#define ll long long
ll read()
{
	ll num=0;bool flag=1;
	char c=getchar();
	for(;c<'0'||c>'9';c=getchar())
	  if(c=='-')flag=0;
	for(;c>='0'&&c<='9';c=getchar())
	  num=(num<<1)+(num<<3)+c-'0';
	return flag?num:-num;
}
const ll N=50010;
ll n,a[N],v[N],gh,ans[N];
bool flag[N];
void add(ll x,ll y)
{
	for(ll i=x;i<=min(y,v[x]*gh);i++)
	{
		ans[v[x]]-=a[i];//减去
		a[i]=sqrt(a[i]);//开方
		ans[v[x]]+=a[i];//加上
	}
	if(v[x]!=v[y])
	for(ll i=(v[y]-1)*gh+1;i<=y;i++)
	{
		ans[v[y]]-=a[i];//同上
		a[i]=sqrt(a[i]);
		ans[v[y]]+=a[i];
	}
	for(ll i=v[x]+1;i<=v[y]-1;i++)
	{
		if(flag[i])continue;flag[i]=1;ans[i]=0; 
		for(ll j=(i-1)*gh+1;j<=i*gh;j++)
		{
			a[j]=sqrt(a[j]);//开方
			ans[i]+=a[j];//加上
			if(a[j]>1)flag[i]=0;//如果有一个没有变成0,或1就还要做
	    }
	}
}
void query(ll x,ll y)
{
	ll sum=0;
	for(ll i=x;i<=min(y,v[x]*gh);i++)
	  sum+=a[i];
	if(v[x]!=v[y])
	for(ll i=(v[y]-1)*gh+1;i<=y;i++)
	  sum+=a[i];
	for(ll i=v[x]+1;i<=v[y]-1;i++)
	  sum+=ans[i];
	printf("%lld\n",sum);
}
int main()
{
	n=read();gh=sqrt(n);
	for(ll i=1;i<=n;i++)
	{
	  a[i]=read();
	  v[i]=(i-1)/gh+1;
	  ans[v[i]]+=a[i];
    }
	for(ll i=1;i<=n;i++)
	{
		ll opt=read(),l=read(),r=read(),c=read();
		if(opt==0)add(l,r);
		  else query(l,r); 
	}
	return 0;
}

6,给出一个长为n的数列,以及n个操作,操作涉及区间乘法,区间加法,单点询问。

很显然,如果只有区间乘法,和分块入门 1 的做法没有本质区别,但要思考如何同时维护两种标记。

我们让乘法标记的优先级高于加法(如果反过来的话,新的加法标记无法处理)

若当前的一个块乘以m1后加上a1,这时进行一个乘m2的操作,则原来的标记变成m1*m2,a1*m2

若当前的一个块乘以m1后加上a1,这时进行一个加a2的操作,则原来的标记变成m1,a1+a2

同时每一步做的时候,头尾我们也要累加一下,保证准确性。

代码如下:

#include<bits/stdc++.h>
using namespace std;
#define Mod 10007
int read()
{
	int num=0;bool flag=1;
	char c=getchar();
	for(;c<'0'||c>'9';c=getchar())
	  if(c=='-')flag=0;
	for(;c>='0'&&c<='9';c=getchar())
	  num=(num<<1)+(num<<3)+c-'0';
	return flag?num:-num;
}
const int N=100010;
int n,a[N],v[N],lazy[N],gh,LAZY[N];
void lazytime(int x)
{
	for(int i=(x-1)*gh+1;i<=min(n,x*gh);i++)
	  a[i]=(a[i]*LAZY[x]+lazy[x])%Mod;//暴力枚举一遍
	LAZY[x]=1;lazy[x]=0;//重新初始化
}
void add(int x,int y,int sum,int p)
{
	lazytime(v[x]);//把这个区间的LAZY和lazy加入整一个区间
	for(int i=x;i<=min(y,v[x]*gh);i++)
	  if(!p)a[i]=(a[i]+sum)%Mod;else a[i]=a[i]*sum%Mod;
            //暴力做一遍
	if(v[x]!=v[y])
	{
	  lazytime(v[y]);
	  for(int i=(v[y]-1)*gh+1;i<=y;i++)
	    if(!p)a[i]=(a[i]+sum)%Mod;else a[i]=a[i]*sum%Mod;
         	//暴力做一遍
           }
	for(int i=v[x]+1;i<=v[y]-1;i++)
	  if(!p)lazy[i]=(lazy[i]+sum)%Mod;else       lazy[i]=lazy[i]*sum%Mod,LAZY[i]=LAZY[i]*sum%Mod;//按照所说的公式做一遍
}
int main()
{
	freopen("a.in","r",stdin);
	freopen("a.out","w",stdout);
	n=read();gh=sqrt(n);
	for(int i=1;i<=n;i++)LAZY[i]=1;
	for(int i=1;i<=n;i++)
	  a[i]=read(),v[i]=(i-1)/gh+1;
	for(int i=1;i<=n;i++)
	{
		int opt=read(),l=read(),r=read(),c=read();
		if(opt==0||opt==1)add(l,r,c,opt);
		  else printf("%d\n",(a[r]*LAZY[v[r]]%Mod+lazy[v[r]])%Mod);//乘上再加上
	}
	return 0;
}

7,

给出一个长为n的数列,以及n个操作,操作涉及区间询问等于一个数c的元素,并将这个区间的所有元素改为c

区间修改没有什么难度,这题难在区间查询比较奇怪,因为权值种类比较多,似乎没有什么好的维护方法。

模拟一些数据可以发现,询问后一整段都会被修改,几次询问后数列可能只剩下几段不同的区间了。

我们思考这样一个暴力,还是分块,维护每个分块是否只有一种权值,区间操作的时候,对于同权值的一个块就O(1)统计答案,否则暴力统计答案,并修改标记,不完整的块也暴力。

 

这样看似最差情况每次都会耗费O(n)的时间,但其实可以这样分析:

假设初始序列都是同一个值,那么查询是O(√n),如果这时进行一个区间操作,它最多破坏首尾2个块的标记,所以只能使后面的询问至多多2个块的暴力时间,所以均摊每次操作复杂度还是O(√n)。

换句话说,要想让一个操作耗费O(n)的时间,要先花费√n个操作对数列进行修改。

初始序列不同值,经过类似分析后,就可以放心的暴力啦。

代码如下:

#include<bits/stdc++.h>
using namespace std;
int read()
{
	int num=0;bool flag=1;
	char c=getchar();
	for(;c<'0'||c>'9';c=getchar())
	  if(c=='-')flag=0;
	for(;c>='0'&&c<='9';c=getchar())
	  num=(num<<1)+(num<<3)+c-'0';
	return flag?num:-num;
}
const int N=100010;
int n,a[N],v[N],lazy[N],gh;
void lazytime(int x)
{
	if(lazy[x]==-1)return ;
	for(int i=(x-1)*gh+1;i<=x*gh;i++)
	  a[i]=lazy[x];
	lazy[x]=-1;
}
void add(int x,int y,int sum)
{
	int ans=0;
	lazytime(v[x]);
	for(int i=x;i<=min(y,v[x]*gh);i++)
	  if(a[i]!=sum)a[i]=sum;
	      else ans++;//暴力转换和查找
	if(v[x]!=v[y])
	{
	  lazytime(v[y]);
	  for(int i=(v[y]-1)*gh+1;i<=y;i++)
	    if(a[i]!=sum)a[i]=sum;
	      else ans++;//暴力转换和查找
	}
	for(int i=v[x]+1;i<=v[y]-1;i++)
	  if(lazy[i]!=-1)//如果已经赋值过了
	  	if(lazy[i]!=sum)lazy[i]=sum;//如果值不是,就改成是的
	  	  else ans+=gh;//否则全加上
	  else//如果没赋值
	  {
	  	for(int j=(i-1)*gh+1;j<=i*gh;j++)
	  	  if(a[j]!=sum)a[j]=sum;
	  	  else ans++;//暴力修改和搜索
	  	lazy[i]=sum;//改成这个值
	  }
	  printf("%d\n",ans);
}
int main()
{
	freopen("a.in","r",stdin);
	freopen("a.out","w",stdout);
	n=read();gh=sqrt(n);
	memset(lazy,-1,sizeof(lazy));
	for(int i=1;i<=n;i++)
	  a[i]=read(),v[i]=(i-1)/gh+1;
	for(int i=1;i<=n;i++)
	{
		int l=read(),r=read(),c=read();
		add(l,r,c);
	}
	return 0;
}

莫队:

是一个建立在分块的基础上的方法(只用了分块进行排序),然后进行玄学的操作,下面是他的思路:

假设有一个离线询问 ,我们已经通过某种方法得到了这个询问的答案(例如暴力),那么利用转移的思想,我们尝试把询问 (l,r)的 答案推广到 (l+1,r),(l-1,r) ,(l,r+1) ,(l,r-1) 这四个问题的答案,当然,一般来说我们要求这个转移的耗时是极少的(一般都是O(1)的)在这,我们的详见

https://www.cnblogs.com/RabbitHu/p/MoDuiTutorial.html

代码详见HLOJ,同时还可以看黄学长(hzwer)的博客

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值