讲讲分块算法 题目Lucas的数列

Description

讲讲分块算法 题目Lucas的数列_第1张图片

Input

在这里插入图片描述

Output

讲讲分块算法 题目Lucas的数列_第2张图片

Sample Input

5 5
1 2
2 3
3 4
4 5
5 6
1 2 4
1 3 0
1 5 3
1 5 2
5 5 0

Sample Output

1
empty
6
1
empty

Data Constraint

讲讲分块算法 题目Lucas的数列_第3张图片

solution

先说一下题意,给定一个a数组,每次询问l到r之间复杂度不大于z的方差。

20分做法

直接模拟?嗯没错,但是一不小心还是会wa
为什么呢,由于题目的数据过于**,注意到n方乘t方小于等于1e18,这说明当n十分小的时候(n<=5)t会极大,导致不会爆longlong,但是也会爆double的精度。
那么如何做呢,我们设S表示1到m之间x的总和,F表示1到m之间x^2的总和,然后把方差那一项拆开:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
由于我们只是担心精度,所以我们只需把除法给省掉就行了。
我们又知道:

在这里插入图片描述
所以上述式子中的m与p可以抵消,得:
在这里插入图片描述
所以就是:
在这里插入图片描述
最后可得
在这里插入图片描述
OK,那么答案就上面啦,这样子直接模拟就有二十分啦。
那么我们的问题就变成了,如何快速求得x总和与x方总和,也就是快速知道x是哪些。
那么关于这个,说一下惨痛的做题史:
昨天,也就是8.05早上,一如既往,开始了比赛,T1dp求逆序对优化搞了好久,放弃,拿到60分后开始莽T2,也就是这题。打了二十分暴力后,10:00开始想选择打T2正解还是T3部分分(这时T2有了分块思路),然后还是选择先莽T360分,之后发现还有1个小时。然后直接莽T2的分块。但是分块这个东西是前几天才接触到的,也从来没打过,但是感觉这次很有感觉,选择了搏一搏。(这件事让我想起几周前有一道关于树形结构的题,正解是最小生成树+一些操作,但是我选择莽树剖,(这时树剖就对着板子码过一次)调过好久终于a了,然后树剖也直接彻底掌握了,这次同理,这道题让我真正理解分块,顺便发下那道树形结构题)但是很不熟练,所以没打完就到时间了,然后原来二十分由于覆盖而且c++出了点小问题所以找不到了,最后是120分第六名。
但是即是这样,由于分块搞了许久,所以不想放弃,也在一直尝试,也看了下题解,是离线加线段树(树状数组)(还有操作与区间排序之类的)维护的,并没有谈分块做法,心里确实有点虚,但是还是一直尝试,拖了40分钟才去恰饭。这时也没改出来。
然后下午讲题前也在搞,讲正解时也没听,相当于是背水一战了。然后改题时将T1T3A了以后又开始了。
这时过了样例,然后交上去WA了,发现没有TLE(2s,跑了800ms)之后一直搞搞了好久才发现边边两块只能暴力,不能同中间一样二分(这个地方实际是分块的基本操作,分块就是中间快速求,两边暴力求得,但是当时没想那么多)之后改改交了上去,期间心想不要TLE,不要TLE,结果一出,1600ms,AC!开心。
所以通过这次,也是真的理解分块了。

分块的优势

首先如同上题,大家线段树(树状数组更快)都是300ms,而分块却很慢,因为线段树是nlogn的,但是分块却是nsqrt(n)的。这么一看感觉分块能干的事情线段树都能干啊,而且更好?是的,但是关于这个分块,凭我自己的感受,是真的香。首先它打起来确实挺长的,其次跑的不算快(一般就是n<=50000吧,这题虽然说n很大,但是竟然跑进了,也让我很吃惊)但是呢,它有一个最显著的地方,就是无脑,这个玩意儿比线段树还无脑,如这道题我一开始感觉线段树不可做,然后就想到了分块了,并且这个东西基本上就是套板子(线段树也一样,但是线性结构总比树形结构稳一点,起码出的锅不算多)真的没什么技术含量,所以其有美名曰优雅的暴力,实质上的确。其次,有些题涉及的数据结构比较偏,如前几天遇到个题,正解就是分块,但是有种叫做吉司机线段树(多用来求区间max)也可以做,像这种时候常见的数据结构维护不了时,就可以用分块,并且十分方便。

然后说一下,可能你感觉分块十分复杂,但是实际上呢,它就是个套模板的东西,我原来也觉得这东西没必要,但是去接触一下后,看看模板后感觉十分简单上手。当在考试中,若不会正解,一般跑个分块分数也可以拿个60分甚至更高(如这道题80分可以打主席树,但是显然脱出noip考纲而且显然更难打)

好了现在以这道题为例讲一下(等会代码可能很丑,但是会标注的很详细)
首先我们知道这道题就是在l到r中找到时间复杂度不大于z的数对吧,前面也提到过,分块就是说旁边暴力,中间快速求(分块分块,顾名思义就是把数列分成几块几块,然后来求),那么中间如何快速求呢?
我们先提出一个很简单的问题,给个序列,每次询问序列中<=z的数的和,序列长度<=1000000,询问次数<=500000
那么有一个很显然的思路,将序列排序,记录前缀和,然后每次询问直接二分就好了。
ok,如同上面的思想,这道题而言,首先,对于一个询问,显然的,是由几个块组成的,然后左边右边的块不一定完整,但是中间的每一块都是完整的。所以如同上面,对于中间的每一块,我们可以直接二分。那么左右怎么求呢?这就涉及到我们如何分块了,一般来讲,我们的每一块都是取sqrt(n)最优,这样时间复杂度就保证在nsqrt(n)了,当然根据题目,不同的分块方式可能使时间复杂度更优。那么左右两块直接暴力枚举求就可以了(当然你想的话可以套个线段树维护……emm好吧当我没说
然后上面的方法算下复杂度,m次询问,每次询问l到r,最多找sqrt(n)次块,两边暴力O(sqrt(n))求,中间O(log(sqrt(n)))来求,所以最坏时间复杂度是O(m sqrt(n)log(sqrt(n)))
对于这题,没有修改只有询问,当然分块也可以处理修改的,处理方法类似,只要记住两边一定暴力,中间快速算即可。
然后再扯一嘴,若是l与r同属一个块的话,就直接暴力就行了。
好了该讲的也讲了,还有什么不懂的代码里见。

#include
#include
#include
#include
#define N 500007
using namespace std;
int n,q,num,w,l[N],r[N],home[N];
//n为数组长度,q为询问,num为分块的块数,w为块的长度,l,r是每个块的左右端点,home是每个点属于的块
long long fang[N],st[N];//fang表示x方的前缀和,st表示x的前缀和
struct node{
     
	int w,pos;//w表示复杂度,pos表示算答案的每个i的值
}a[N],b[N];
bool cmp(node a,node b){
     return a.w<b.w;}//二分是二分复杂度的,所以按复杂度排序
void build(){
     //建块
	w=sqrt(n),num=n/w;//w为sqrt(n),num为个数
	if(n%w!=0) num++;//若整除不了的话,多余的地方也是个块,num要加1
	for(int i=1;i<num;i++){
     //注意i没有到num,num这一块我们要特殊处理
		l[i]=(i-1)*w+1;//左端点为上个块的右端点+1
		r[i]=i*w;
		sort(a+l[i],a+r[i]+1,cmp);//对于每个块,我们要有个数组排好序,为下面二分做准备
	}
	l[num]=(num-1)*w+1;//num这一块特殊判断
	r[num]=n;//num这一块右端点为n
	sort(a+l[num],a+r[num]+1,cmp);//记得排序
	for(int i=1;i<=n;i++){
     
	//对于每个点记录一下它的块,还有每个块的前缀和
		if(i%w==0) home[i]=i/w;//若整除的话就是这个块
		else home[i]=i/w+1;//没整除的话就是这个块+1
		fang[i]=(long long)a[i].pos*a[i].pos;//x方的前缀和
		st[i]=(long long)a[i].pos;//x的前缀和
		if(home[i]==home[i-1])
			fang[i]+=(long long)fang[i-1],st[i]+=(long long)st[i-1];
			//当i与i-1是一个块的时候,那么我们i的前缀和就要加上i-1的,否则若不是一个快的话,就不能加。
	}
}//上面的处理,关于分块基本上一模一样的,然后处理的值根据题目来变化
long long pd(int x,int y,long long z){
     //求答案了
	if(home[x]==home[y]){
     //这说明左端点与右端点是同一块,那么直接暴力
		int m=0;long long S=0,F=0;
		//m为<=z的值的个数,S为x的和,F为x方的和
		for(int i=x;i<=y;i++)
			if(b[i].w<=z) m++,S+=(long long)b[i].pos,F+=(long long)b[i].pos*b[i].pos;
			//显而易见的暴力,当复杂度小的时候,更新m,S,F
		if(m==0) return -1;//m==0说明没有值,返回-1
		long long ans=F*(long long)m-(long long)2*(S*S)+S*S;
		return ans;
		//上面就是套我们之前的公式啦
	}//下面是指x与y不是同一块的时候
	int m=0;long long S=0,F=0;//与上面同理
	for(int i=x;i<=r[home[x]];i++)//x这一块暴力
		if(b[i].w<=z) m++,S+=(long long)b[i].pos,F+=(long long)b[i].pos*b[i].pos;
	for(int i=l[home[y]];i<=y;i++)//y这一块暴力
		if(b[i].w<=z) m++,S+=(long long)b[i].pos,F+=(long long)b[i].pos*b[i].pos;
	for(int i=home[x]+1;i<home[y];i++){
     
	//x的块的下一个到y的块的上一个,二分快速求
		if(a[l[i]].w>z) continue;
		//如果排好序后的第一个数的复杂度都已经大于z,显然没有求的必要了
		int ll=l[i],rr=r[i],lw=l[i];//ll与rr表示左右端点,lw表示左边的界限(也就是这个块的左端点)
		while(ll<rr){
     
			int mid=(ll+rr+1)>>1;
			if(a[mid].w>z) rr=mid-1;
			else ll=mid;
		}
		m+=ll-lw+1;//ll就是这个块中复杂度小于等于z的最大的值,减去边界+1就是这个区间贡献的个数了
		S+=(long long)st[ll],F+=(long long)fang[ll];
		//S与F相继加一下这个区间的贡献
	}
	if(m==0) return -1;
	long long ans=F*(long long)m-(long long)2*(S*S)+S*S;
	return ans;
	//上面同理统计答案
}
int main(){
     
	freopen("sequence.in","r",stdin);
	freopen("sequence.out","w",stdout);
	scanf("%d%d",&n,&q);
	for(int i=1;i<=n;i++)
		scanf("%d%d",&a[i].w,&a[i].pos),b[i].w=a[i].w,b[i].pos=a[i].pos;
		//我们要有个数组是用来排序后二分,还有个数组是不排序用来暴力的
	build();//这里是建块的操作,实际上基本上每题都是一样的板子
	while(q--){
     
		int x,y;long long z;
		scanf("%d%d%lld",&x,&y,&z);
		long long res=pd(x,y,z);//询问
		if(res!=-1) printf("%lld\n",res);//若是-1说明空集
		else printf("empty\n");
	}
}

emm,现在看起来是不是非常easy呢?但是就个人感受来说,一种算法,只有自己不看标认认真真坐个一天,把一道题做对了,那么自己就会真的掌握这个算法。
如果大家有什么建议,或者需要讨论的,十分欢迎!!!

3q

你可能感兴趣的:(算法,分块,数据结构,算法)