分块与莫队

我们在做线段树等问题时,会通过将区间分成2部分,再将每一部分二分,成了一个较为严谨的树状结构。

而分块在复杂度或是结构上很不严谨,它的本质上只是将数据分成了一个个块状区域,每一个区域大小都由君定。

但分块与线段树的核心思想都是一样的:将本来整合的总体信息区域化

明显的,当我们综合信息时,我们希望信息越整体越好,这样我们调用就越方便,但是在我们修改信息时我们则更希望信息区域性更强,这样我们的修改影响就越小。

所以我们往往就得在这两者间做一个权衡,避免走向两个方向中任意一个极端,所以就需要将信息区域化。

那么在每次询问时,由于信息的区域性性,我们需要将一个个块访问一遍,将所求信息综合,这样的时间复杂度最高应该是o(分块数+次大块大小+最大块大小)。

而在修改时,为维护区域信息的整体性,我们需要将这个块信息重新修改,时间复杂度最高明显是o(最大块大小)。

至于空间复杂度,因为一个点信息只会隶属于一个块,所以我们并不需要去特意维护,复杂度不会改变多少。

那么怎么样分块能使我们在修改 查询 两者间取得最优的平衡呢?

我们可以将每个块的大小定为根号n,很明显这里是用了基本不等式的思想,也是主流思想。

当然前面说过了,区域大小(即分块大小)是任由君意的,只要能过即可,由于分块思想很简单,在下也就多嘴到此了。

例题:hnoi2010 bzoj2002弹飞绵羊

to[i]表示从i一直弹直到进入下一个块的时候第一个位置在哪(就是它能到得下一块的第一个位置啦在下好啰嗦啊)

step[i]表示从I弹到to[i]所需的步数。

很明显能影响to[i]与step[i]的只可能是块内的点的改变。

#include<iostream>
#include<string.h>
#include<algorithm>
#include<math.h>
#include<stdio.h>
using namespace std; 
const int N=200050;
int n,m;
int k[N];
int to[N],step[N];
int num,fakes,faken,size[N],pre[N];
int lei,loc,v,ans,loc2;
int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
		scanf("%d",&k[i]);
	fakes=(int)sqrt(n);
	faken=n;
	while(faken>0)
	{
		size[++num]=min(fakes,faken);
		faken-=fakes;
	}
	for(int i=1;i<=num;i++)
	{
		for(int j=size[i];j>=1;j--)
		{
			if(j+pre[i]+k[j+pre[i]]>pre[i]+size[i])
			{
				to[j+pre[i]]=j+pre[i]+k[j+pre[i]];
				step[j+pre[i]]=1;
			}
			else
			{
				to[j+pre[i]]=to[j+pre[i]+k[j+pre[i]]];
				step[j+pre[i]]=step[j+pre[i]+k[j+pre[i]]]+1;
			}
		}
		pre[i+1]=pre[i]+size[i];
	}
	scanf("%d",&m);
	while(m--)
	{
		scanf("%d%d",&lei,&loc);
		loc++;
		if(lei==1)
		{
			ans=0;
			while(loc<=n)
			{
				ans+=step[loc];
				loc=to[loc];
			}
			printf("%d\n",ans);
		}
		else
		{
			scanf("%d",&v);
			loc2=0;
			for(loc2=1;loc2<=num;loc2++)
				if(pre[loc2]>=loc)
					break;
			loc2--;
			k[loc]=v;
			for(int i=loc-pre[loc2];i>=1;i--)
			{
				if(i+pre[loc2]+k[i+pre[loc2]]>pre[loc2]+size[loc2])
				{
					to[i+pre[loc2]]=i+pre[loc2]+k[i+pre[loc2]];
					step[i+pre[loc2]]=1;
				}
				else
				{
					to[i+pre[loc2]]=to[i+pre[loc2]+k[i+pre[loc2]]];
					step[i+pre[loc2]]=step[i+pre[loc2]+k[i+pre[loc2]]]+1;
				}
			}
		}
	}
}

分块的更多应用,详见 数列分块入门1-9 by hzwer;

然后就是莫队了。

首先解释下这个高端的名字,这个算法是由之前的国家队队长神犇莫涛先生发明的,orz大佬。

嗯,所以也别太在意那个 莫 字,我以前每次看到莫队这个词的时候,觉得带“莫”字的都是高端算法,像是莫比乌斯啊之类的,感觉 莫 简直象征着一种神秘。

队 我开始以为是一种类似于队列的数据结构的表示,但在写下边博客时才明白 队 是指队长。。。。///orz;

其实,莫队算法并不难,但它确实很神,而莫队的题往往都难在了转移位置时改变信息上。

这里讲的莫队不包含树上莫队,也不包含可修改莫队。

所以在下所要说的莫队,是只兹瓷一堆询问。

在刚开始时接触这种多询问时,很多人都想通过一种排序的方法来优化解决询问,比如有了[l,r]的询问,而此时我们能很快的解决[l,r+1],那我们要是一直将r一直往后延伸,更大的询问不也就解决了吗?

是的,这样我们确实得到一个不错的做法,这样的做法确实是一种优化,复杂度上界会变成o(n*n),第一个n是左端点,第二个是往右依次统计。

我们这里明显的优化在于,我们使许多被多个区间访问的公共区间只访问了一遍。

但是啊,当n又变大了。。。这个做法便显得束手无策;

所以我们可以考虑让左端点也动起来,那会不会更快呢?

好像是可以,可是左端点来回动的话,第一个没办法确定排序顺序了,第二个要是左端点右端点都来回走,那实际上我们保住的公共区间大小也就小的可怜了。

所以就要用到莫队算法了(对就是在写到这里时明白了“队”的含义,去补上面那行)。

我们将左端点以根号n分块,在排序时以左端点所在块编号为第一关键字,以右端点为第二关键字,小值优先排序。

与传统的 左端点为第一关键字 右端点为第二关键字 相比,我们给予了左端点移动的空间(大小为根号n),这样右端点的移动就变少了(因为右端点移动时“服务”的对象从只针对一个左端点变成了针对一个块内的左端点)。

来统计一下这个算法的复杂度:

在左端点在相同块的条件下,我们最多移动右端点n次,一共有根号n个块,所以右端点移动的总次数变成了o(n*根号n)

对于那些与上一个询问左端点不在相同块的询问,左端点最多移动n次,因为我们是按照左端点位置排序的,所以这样的询问最多有根号n个,总次数为o(n*根号n)

而左端点在同一个块内一次最多移动根号n次,所以对于那些与上一个询问左端点所在块相同的询问,移动范围最大也就根号n,这样的询问最多有m个,那么总次数为o(m*根号n)(注意m一般与n大小相近,若m大到爆炸,那我们便要预处理一下每个左端点到块末)

所以综上,复杂度大概整体是o(n*根号n)的。

明显这就是一个“平衡”把握的很好的例子。

例题:

bzoj2038

#include<iostream>
#include<stdio.h>
#include<algorithm>
#include<math.h>
#include<string.h>
using namespace std;
const int N=50050;
int n,m;
int col[N],num;
struct que
{
	int l,r,lx;
	int id;
}q[N];
int cmp(que a,que b)
{
	if(a.l==b.l)
		return a.r<b.r;
	return a.l<b.l;
}
int cmp1(que a,que b)
{
	if(a.lx==b.lx)
		return a.r<b.r;
	return a.lx<b.lx;
}
int faken,fakes,pre[N],size[N];
int sum[N];
long long anss[N],ansx[N];
long long gcd(long long a,long long b)
{
	if(b==0)
		return a;
	return gcd(b,a%b);
}
int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)
		scanf("%d",&col[i]);
	for(int i=1;i<=m;i++)
		scanf("%d%d",&q[i].l,&q[i].r),q[i].id=i;
	sort(q+1,q+m+1,cmp);
	fakes=(int)sqrt(n);
	faken=n;
	while(faken>0)
	{
		size[++num]=min(fakes,faken);
		faken-=fakes;
		pre[num]=pre[num-1]+size[num];
	}
	int loc2=0;
	for(int i=1;i<=m;i++)
	{
		while(pre[loc2]<q[i].l)
			loc2++;
		q[i].lx=loc2;
	}
	sort(q+1,q+m+1,cmp1);
	int l=0,r=0;
	long long su=0;
	for(int i=1;i<=m;i++)
	{
		while(q[i].r>r)
		{
			r++;
			su+=(long long)sum[col[r]];
			sum[col[r]]++;
		}
		while(q[i].r<r)
		{
			sum[col[r]]--;
			su-=(long long)sum[col[r]];
			r--;
		}
		while(q[i].l<l)
		{
			l--;
			su+=(long long)sum[col[l]];
			sum[col[l]]++;
		}//4540罪(wrong)恶(answer)之源
		while(q[i].l>l)
		{
			if(l!=0)
				sum[col[l]]--;
			su-=(long long)sum[col[l]];
			l++;
		}
		anss[q[i].id]=su;
		ansx[q[i].id]=(long long)(r-l)*(long long)(r-l+1)/2LL;
	}
	for(int i=1;i<=m;i++)
	{
		if(anss[i]==0)
			printf("0/1\n");
		else
		{
			long long d=gcd(anss[i],ansx[i]);
			printf("%lld/%lld\n",anss[i]/d,ansx[i]/d);
		}
	}
}

bzoj3289

#include<iostream>
#include<string.h>
#include<algorithm>
#include<stdio.h>
#include<math.h>
using namespace std;
const int N=50050;
int n,m;
long long la[N];
long long la2[N];
long long ans[N];
int faken,fakes,size[N],num,pre[N];
int discret(long long x)
{
	return lower_bound(la2+1,la2+n+1,x)-la2;
}
int c[N];
int lowbit(int x)
{
	return x&(-x);
}
void plu(int x,int v)
{
	while(x<=n)
	{
		c[x]+=v;
		x+=lowbit(x);
	}
}
int sum(int x)
{
	int res=0;
	while(x>0)
	{
		res+=c[x];
		x-=lowbit(x);
	}
	return res;
}
struct query
{
	int l,r;
	int id;
	int lx;
}q[N];
int cmp(query a,query b)
{
	if(a.lx==b.lx)	return a.r<b.r;
	return a.lx<b.lx;
}
int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
		scanf("%lld",&la[i]),la2[i]=la[i];
	sort(la2+1,la2+n+1);
	for(int i=1;i<=n;i++)
		la[i]=discret(la[i]);
	int fakes=(int)sqrt(n);
	faken=n; 
	while(faken>0)
		size[++num]=min(faken,fakes),faken-=fakes,pre[num]=pre[num-1]+size[num];
	scanf("%d",&m);
	for(int i=1;i<=m;i++)
	{
		scanf("%d%d",&q[i].l,&q[i].r);
		q[i].id=i;
		q[i].lx=lower_bound(pre+1,pre+num+1,q[i].l)-pre;
	}
	sort(q+1,q+m+1,cmp);
	int l=0,r=0;
	long long su=0;
	for(int i=1;i<=m;i++)
	{
		while(r<q[i].r)
		{
			r++;
			su+=(long long)sum(n)-(long long)sum(la[r]);
			plu(la[r],1);
		}
		while(r>q[i].r)
		{
			plu(la[r],-1);
			su-=(long long)sum(n)-(long long)sum(la[r]);
			r--;
		}
		while(l>q[i].l)
		{
			l--;
			su+=(long long)sum(la[l]-1);
			plu(la[l],1);
		}//小朋友们别学这样写,这样写别的题可能会挂,这个应该顶替楼上的位置
		while(l<q[i].l)
		{
			if(l!=0)
				plu(la[l],-1);
			su-=(long long)sum(la[l]-1);
			l++;
		}
		ans[q[i].id]=su;
	}
	for(int i=1;i<=m;i++)
		printf("%lld\n",ans[i]);
} 

bzoj4540

#include<iostream>
#include<string.h>
#include<algorithm>
#include<math.h>
#include<stdio.h>
#include<stack>
#define lson rt*2
#define rson rt*2+1
#define mid (l+r)/2
using namespace std;
const int N=100050;
long long a[N];
int s[2*N],t=0;
long long suml[N],sumr[N];
int n,m;
int minx[N][32];
int er[31]={1,2,4,8,16,32,64,128,256,512,1024,2048,4096,8192,16384,32768,65536,131072,262144,524288,1048576,2097152,4194304,8388608,16777216,33554432,67108864,134217728,268435456,536870912,1073741824};
int len[N];
void build()
{
	for(int i=1;i<=n;i++)
		minx[i][0]=i;
	for(int j=1;j<=18;j++)
	{
		if(er[j]>n)
			break;
		for(int i=1;i<=n-er[j]+1;i++)
		{
			int a1=minx[i][j-1],a2=minx[i+er[j-1]][j-1];
			if(a[a1]<=a[a2]||a2==0)
				minx[i][j]=a1;
			else
				minx[i][j]=a2;
		}
	}
	int xx=1,nu=0;
	for(int i=1;i<=n;i++)
	{
		if(i>=2*xx)
			nu++,xx*=2;
		len[i]=nu;
	}
}
int query1(int l,int r)
{
	int a1=minx[l][len[r-l+1]],a2=minx[r-er[len[r-l+1]]+1][len[r-l+1]];
//	cout<<l<<"-"<<r<<":    "<<l<<" "<<len[r-l+1]<<" "<<r-er[len[r-l+1]]+1<<" "<<len[r-l+1]<<endl;
	if(a[a1]<=a[a2])
		return a1;
	else
		return a2;
}
struct query
{
	int l,r,lx,id;
}q[N];
int cmp(query a,query b)
{
	if(a.lx==b.lx)	return a.r<b.r;
	return a.lx<b.lx; 
}
long long ans=0;
int faken,fakes,num;
int size[N],pre[N];
void gengl(int l,int r,int lei)
{
	int xx=query1(l,r);
	long long suan=(long long)(r-xx+1)*a[xx];
	suan+=sumr[l]-sumr[xx];
	ans+=suan*lei;
}
void gengr(int l,int r,int lei)
{
	int xx=query1(l,r);
	long long suan=(long long)(xx-l+1)*a[xx];
	suan+=suml[r]-suml[xx];
	ans+=suan*lei;
}

int l1[N],r1[N];

long long finans[N];
int cmp2(query a,query b)
{
	if(a.l==b.l)
		return a.r<b.r;
	return a.l<b.l;
}
int main()
{
	scanf("%d",&n);
	scanf("%d",&m);
	for(int i=1;i<=n;i++)
		scanf("%lld",&a[i]);
	build();
	for(int i=1;i<=n;i++)
	{
		while(t>0&&a[s[t]]>a[i])
			r1[s[t]]=i,t--;
		l1[i]=s[t];
		s[++t]=i;
	}
	while(t>0)	r1[s[t--]]=n+1;
	for(int i=1;i<=n;i++)
		suml[i]=suml[l1[i]]+(long long)(i-l1[i])*a[i];
	for(int i=n;i>=1;i--)
		sumr[i]=sumr[r1[i]]+(long long)(r1[i]-i)*a[i];
//
	faken=n;
	fakes=sqrt(n);
	while(faken>0)
		size[++num]=min(faken,fakes),faken-=fakes,pre[num]=pre[num-1]+size[num];
	for(int i=1;i<=m;i++)
	{
		scanf("%d%d",&q[i].l,&q[i].r);
		q[i].id=i;
	}
	sort(q+1,q+m+1,cmp2);
	int loc2=1;
	for(int i=1;i<=m;i++)
	{
		while(pre[loc2]<q[i].l)
			loc2++;
		q[i].lx=loc2;
	}
	sort(q+1,q+m+1,cmp);
	//
	int l=1,r=1;
	ans=a[1];
	for(int i=1;i<=m;i++)
	{
		while(r<q[i].r) r++,gengr(l,r,1);
		while(l>q[i].l)	l--,gengl(l,r,1);
		while(q[i].r<r) gengr(l,r,-1),r--;
		while(l<q[i].l)	gengl(l,r,-1),l++;
		finans[q[i].id]=ans;
	}
	for(int i=1;i<=m;i++)
		printf("%lld\n",finans[i]);
} 

为了做这道题还复习了一下st表,发现自己rmq基本全忘了。。。


(ac的感动。。。因为来之不易。。。前面t是因为用了线段树,后面一直wa其实是因为在移动l,r时顺序不对,应该先扩展该扩展的再缩小该减缩的,不然中间会出现不合法情况,有的题目会使答案错到天上去。。)



  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值