2019.8.3 NOIP模拟测试12 反思总结【P3938 斐波那契,P3939 数颜色,P3940 分组】

时间:2022-10-30 01:05:06

【题解在下面】

早上5:50,Gekoo同学来到机房并表态:“打暴力,打暴力就对了,打出来我就赢了。”

我:深以为然。

(这是个伏笔)

据说hzoi的人还差两次考试【现在是一次了】就要重新分配机房,不知道我们几个的安排是什么样的,瑟瑟发抖。各种原因作用,心情有些微妙地一遍瞎画一边等着7:10考试开始。

不怎么适合涂鸦的本,不怎么适合涂鸦的笔,不怎么适合涂鸦的心情。考试开始,我摔笔看题。

T1上来感觉东西很多很麻烦,过了一遍题发现大概要耐下心来去仔细推一推性质,于是没细想先跳过。然后看T2,受到上一次考试第一题的荼毒,一眼线段树,仔细想想感觉动态开点线段树能写,自然平衡树也能写,但是前者我不太熟悉后者容易写炸。于是再思索几秒,承接上一次考试第一题被pass掉的分块思路,毅然决然分块骗分。想到这里先去看一眼T3,一眼过去大约是完全懵逼…感觉T3是最难的,也没细想,回来先搞T2。

四十分钟打完分块检查了一遍,然后又五十分钟搞完T1的暴力,一个半小时过去,剩下的时间开始一边摸鱼一边推T3……

考完发现最后结果居然还不错【咦】。这次的题目出得很良心,数据点分得很细。题解更良心,最后一题针对每个测试点各个分数段都写了题解,每道题的特殊性质都解释得很清楚(与某份题解形成鲜明对比)。

习惯性上来讲,每次考试我都算是尽力,虽然最近这几天不知道是因为累以及身体原因还是其它缘故,状态并不好。然而现在周日早上的懒觉时间被ban了…能不能好起来还是会恶化到什么程度,大约听天由命吧。

T1【P3938 斐波那契】:

考试的时候不知道是没有细想还是想难了,选择处理出每只兔子的父亲,暴力跳父亲直到LCA。用扫一遍斐波那契数组f的方式预处理出fa数组,最后得到70分。其实对f数组的利用,以及对f数组前缀和也是斐波那契数的性质的发现,我都已经接近正解了。只差最后临门一脚…就是这最简单的一脚,生生钉在了门外。

好,来说一下正解。其实题解已经说得很详细了。我们列出每个月份新产生的兔子数,1,1,1,2,3,5,8,13...发现是斐波那契数列。然后把他们1,2,3,4,5,6,7按这个数列排序一下,即写成:

1

2

3

4 5

6 7 8

9 10 11 12 13

14 15 16 17 18 19 20 21

这就是每个月新产生的兔子的编号。在旁边列出它们的父亲,发现它在这一行里排第几个,它的父亲就是几。例如14在这一行是第一个,那么它的父亲就是1。性质很显然,因为同一个月出生的兔子的编号是按父亲的编号由小到大编好的,而且每个月所生的兔子是一段连续的序列。

那么我们只要能找出所询问的两个兔子在它们所处的那行中分别是第几个,就能知道对应的父亲。同理对于父亲也能求出它的父亲…就像LCA过程,发现每一次父亲的编号都会缩小至少一半,那么其实跳不了多少次。并且其实斐波那契数没处理多少层【六十层左右】就超过1e12了。

接下来,怎么求兔子在某一行中的位置呢?

我企图计算一个前缀和,然后快速查找位置。然后在计算前缀和的过程中意外发现…设si为到上面表中第i层的前缀和,那么s1=1,s2=2,s3=3,s4=5,s5=8,s6=13……

哦豁。斐波那契。

我发现这一点了,然后我没再往下想,去写暴力了…

其实发现这一点以后就很好办了,既然前缀和也是一个斐波那契,f为斐波那契数列数组。设所询问的兔子编号为x,那么一定有f[i]<x<=f[i-1]。我们找出这个f[i],x-f[i]就是这只兔子在这一行的位置了。

斐波那契数列是单调的,直接二分查找。于是就这么AC了…

稍微聊一下这个前缀和。我们发现兔子数列的前三行都是1,凑出一个3,与第四项的2结合正好是f[4]+f[5],得出f[6]。然后这个前缀和f[6]再与f[5]相加就是f[7]。所以当前这一行的前缀和永远在斐波那契数列上是这一行个数的后两项。

#include<iostream>
#include<cstdio>
using namespace std;
long long m,a[],num,fa[],cnt;
long long x,y;
long long get(long long x){
long long l=,r=num;
long long ans=;
while(l<=r){
int mid=(l+r)/;
if(a[mid]<x){
ans=mid;
l=mid+;
}
else r=mid-;
}
return x-a[ans];
}
int main()
{
scanf("%lld",&m);
a[]=,a[]=,a[]=,num=;
for(int i=;;i++){
a[i]=a[i-]+a[i-];
num++;
if(a[i]>1e12)break;
}
while(m--){
scanf("%lld%lld",&x,&y);
while(x!=y){
if(x>y)x=get(x);
else y=get(y);
}
printf("%lld\n",x);
}
return ;
}

这道题告诉我一件很重要的事情,永远拼命地去想,去思考自己发现的东西怎么用,不要想到某个地步就不自信地悄悄离开了。如果思想懈怠了,差那临门一脚,就真的给自己一脚。

T2【P3939 数颜色】:

这是我第一道动键盘的题。很快确定了分块的思路,其实再想想说不定真的能想到vector里面存每个颜色的位置,然后二分查找边界。

这题的做法真的是八仙过海各显神通…官方给的题解就出现了多种解法。vector,动态开点线段树,平衡树,主席树,还有不完全得分的做法带修莫队,树套树【结果这些数据结构做法都被出题人嘲讽数据结构学傻】。以及玄学分块…理论上是能过的,但是全机房没人写出来。

后来去听AK神仙TKJ所说这三题在洛谷都有原题,于是兴致勃勃去翻了T2的题解区。震惊地发现题解区更可怕,除了上面的做法,还有CDQ分治,淳朴排序枚举,还有优化把分块卡过去的代码…

恕鄙人才疏学浅一个都没写出来.jpg

最后还是写了好写的vector做法。开颜色个数个vector【vector是动态的不用担心空间】,读入序列的时候在对应颜色的vector里存入位置。因为一边读入一遍扔进vector,所以vector里的位置是有序了,满足lower_bound和upper_bound的使用条件。那么对于询问,lower_bound【查大于等于询问元素的第一个】找左边界,upper_bound【查大于询问元素的第一个】正好卡在右边界以外的位置,后者减去前者就是所求的元素数量。

对于修改操作,直接交换x和x+1所在vector里相应位置的信息,然后交换两个元素本身的信息。需要注意的是,如果x和x+1的颜色是一样的,要跳过这个操作。交换不同颜色的信息是不会改变某个颜色vector里位置的相对关系的,而如果交换同种颜色就不满足单调性了。我在这个地方卡75分卡了一小会才不确定地加了这个判断,后来听人一说才明白是怎么回事【感谢HEOI-动动】。

#include<iostream>
#include<cstdio>
#include<vector>
#include<cstring>
#include<algorithm>
using namespace std;
int n,m,opt;
struct node{
int c,pos;
}a[];
vector<int>col[];
int main()
{
scanf("%d%d",&n,&m);
for(int i=,x;i<=n;i++){
scanf("%d",&x);
col[x].push_back(i);
a[i].c=x,a[i].pos=col[x].size()-;
}
while(m--){
scanf("%d",&opt);
if(opt==){
int l,r,cc;
scanf("%d%d%d",&l,&r,&cc);
if(!col[cc].size()){
printf("0\n");
continue;
}
int x1=lower_bound(col[cc].begin(),col[cc].end(),l)-col[cc].begin();
int x2=upper_bound(col[cc].begin(),col[cc].end(),r)-col[cc].begin();
printf("%d\n",x2-x1);
}
else{
int x;
scanf("%d",&x);
if(a[x].c==a[x+].c)continue;
col[a[x].c][a[x].pos]=x+;
col[a[x+].c][a[x+].pos]=x;
swap(a[x],a[x+]);
}
}
return ;
}

没想到做法还是因为对STL不熟悉…于是我之后去查了一些和vector有关的东西【之后再说其他STL_(:з」∠)_】,以及lower_bound和upper_bound。

 c.begin()传回迭代器中的第一个数据地址。
c.clear()移除容器中所有数据。
c.empty()判断容器是否为空。
c.end() 指向迭代器中末端元素的下一个,指向一个不存在元素。
c.erase(pos)删除pos位置的数据,传回下一个数据的位置。
c.erase(beg,end)删除[beg,end)区间的数据,传回下一个数据的位置。
c.front()传回第一个数据。
c.pop_back()删除最后一个数据。
c.push_back()在尾部加入一个数据。
c.size()返回容器中实际数据的个数。
函数lower_bound()在first和last的前闭后开区间进行二分查找,返回大于或等于val的第一个元素位置。如果所有元素都小于val,则返回last的位置.
如果所有元素都小于val,则返回last的位置,可能会数组越界。 函数upper_bound()返回的是在前闭后开区间中查找的关键字的上界,返回大于val的第一个元素位置
同样,如果所有元素都不满足查找条件也返回last的位置,也可能越界。 lower_bound是大于等于,upper_bound是大于,如果查不到值返回的东西都可能越界。

T3【P3940 分组】:

考试的时候勤勤恳恳【一边摸鱼一边】骗分。首先是对于直接输出1的那一个点不加赘述,然后考虑k=1并且答案唯一的点好像扫一下就能得到答案。对于k=1或k=2,并且ai<=2的情况好像记录一下2的个数也能骗到分。

于是码了一下,最后骗到24分。

考完试对于正解仍然是一筹莫展…去翻了所有能找到的题解和博客,然后去理解了一下同场考试其他大佬的思路。

正解分成k=1和k=2来分别考虑。显然对于这两种情况,所用的做法是不一样的。

对于k=1,分组所要满足的条件是任意一组中元素之间没有互相冲突,即相加为平方数的。那么每一次分组,我们可以考虑当前元素和上一个分组点之后的所有元素是否产生冲突,如果冲突就考虑进行分组。那么显然扫一遍之前的元素就能判断是否冲突。

但是n有1e5+的级别…最坏的可能性这一扫就要爆炸。那么有什么快速判断当前元素是否合法的方式吗?发现如果我们只用考虑当前元素是否合法,那么前面的元素完全可以只记录值而不记录位置。判断两个数相加是否为一个平方数,可以循环每个数,当然也可以只记录值而循环平方数。

ai的上限其实是一个提示。131072*2=262144=512²。我们只需要枚举1-512,判断枚举到的数的平方减去当前元素,所得到的值是否出现过,就可以判断是否合法。

但是上面这几句都建立在我们只用考虑当前元素是否合法这一前提下。实际上,还有一个重要的限制——字典序。如果从前往后枚举,我们需要记录与当前元素冲突的值的位置,因为把这个位置作为分组点显然比把当前元素的前一位作为分组点要优。举个栗子,对于序列1,2,3,在1和2之间分组显然比在2和3之间分组要优,但是这两种分组都合法。

那么我们能不能让判断产生了冲突的位置成为答案呢——把序列倒过来枚举就可以了。

官方题解也给出了说法,倒过来枚举的冲突点,也就是正序中可能的最靠前的分界点位置,一定更优。因为分界点如果靠后,不仅对于这个分组操作来说不优,并且对于上一个组,它的段长变大,段内产生冲突的可能性变大,分组变多的可能性也随之上升。

于是最后k=1的做法就是,倒过来枚举序列并判断当前元素是否与之前产生冲突,记录冲突点。注意一个一个清空记录存在过的值的vis数组,如果直接memset会增加不必要的复杂度。

对于k=2的情况,思路就要更为复杂一点。首先继承k=1的思路中总体上的倒叙枚举以及枚举平方数的思路。

很显然的是,对于每个分组中的元素,如果根据冲突关系把它们黑白染色,那么只要分组中的元素能组成一张二分图,这个分组就是合法的。根据这一点,枚举序列的时候每次暴力判断二分图其实就是一种高分做法,并且似乎是可以通过全部数据的…

而正常去考虑,每次都跑一次二分图的复杂度显然过高。我们依旧需要能快速判断当前元素是否合法的方法。发现对于交错复杂的敌对关系,即冲突,我们似乎在哪里见到过。

P1525关押罪犯。这道题里我们用并查集的扩展域或者带权并查集维护了交织的敌对关系,并最后判断哪一组不得不产生冲突。这与这道题现在考虑到的这一部分很相似。

其实没有做过这道题也能考虑到并查集,毕竟我们要维护一堆冲突关系,并且判断什么时候不能再把冲突的两个数字分别放在两组中,而是无论怎么放都会产生冲突。

想到这里,尝试用并查集来处理这道题。和关押罪犯一样开一个扩展域,并查集中1-maxx【最大数值】为1-maxx本身的集合,而maxx+1-maxx*2是1-maxx的敌人集合。扫到一个元素,并判断之前有冲突的数值出现的时候,就查看两者并查集维护的信息。如果两个数值本身不在一个集合里,那么它们就能被分别放在两组中。然后维护敌对关系,让两者的敌人域分别和对方的本域合并在一起。

但是还有需要特殊考虑的情况!我们并查集维护的是每一个数值的信息,而序列中如果出现了同样的数值,不可能再把一个并查集分裂出去也没法维护信息。仔细想想,两个相等的数值只要不会相加产生一个平方数,那么它们就可以分在同一组中,毫无影响。于是对于有相同元素出现的时候,判断它的二倍是不是平方数,如果是的话,再考虑能不能分在同一组。两个这样的相同元素能分在同一组的条件是,它们出现且仅出现两次,没有其它敌对元素。如果有其它敌对元素,那么这三个元素一定不能安定地分成两组。于是对相同元素进行特判处理。

最后清空等操作和k=1时是差不多的。

#include<iostream>
#include<cstdio>
using namespace std;
int n,k,maxx;
int a[],vis[],flag,lst,tag[],fa[];
int ans[],sum;
int get(int x){
if(fa[x]==x)return x;
else return fa[x]=get(fa[x]);
}
void solve1(){
for(int i=n;i>=;i--){
flag=;
for(int j=;j<=;j++){
if(j*j<=a[i])continue;
if(j*j-a[i]>maxx)break;
if(vis[j*j-a[i]]){
flag=;
break;
}
}
if(flag){
ans[++sum]=i;
for(int j=i+;j<=lst;j++){
vis[a[j]]=;
}
lst=i;
}
vis[a[i]]=;
}
}
void solve2(){
for(int i=;i<=maxx;i++)fa[i]=i,fa[i+maxx]=i+maxx;
for(int i=;i*i<=maxx*;i++)tag[i*i]=;
for(int i=n;i>=;i--){
flag=;
if(vis[a[i]]){
if(tag[a[i]*]){//同样的数字出现了两次,并且它的二倍是平方数
for(int j=;j<=;j++){
if(vis[a[i]]==||fa[a[i]+maxx]!=a[i]+maxx){flag=;break;}
//这里可以用判断敌人域的代表元素是不是它本身的方法来得知有没有敌对元素,是因为我每次并查集合并都是让敌人域合向本域
if(j*j<a[i])continue;
if(j*j-a[i]>maxx)break;
if(vis[j*j-a[i]]&&j*j-a[i]!=a[i]){flag=;break;}
}
if(flag){
ans[++sum]=i;
for(int j=i;j<=lst;j++){
fa[a[j]]=a[j],fa[a[j]+maxx]=a[j]+maxx;
vis[a[j]]=;
}
lst=i;
}
}
}
else{
for(int j=;j<=;j++){
if(j*j<a[i])continue;
if(j*j-a[i]>maxx)break;
if(vis[j*j-a[i]]){
if(tag[*(j*j-a[i])]&&vis[j*j-a[i]]==){
flag=;
break;
}
int x1=get(a[i]),x2=get(a[i]+maxx),y1=get(j*j-a[i]),y2=get(j*j-a[i]+maxx);
if(x1==y1){
flag=;
break;
}
else{
fa[y2]=x1;
fa[x2]=y1;
}
}
}
if(flag){
ans[++sum]=i;
for(int j=i;j<=lst;j++){
fa[a[j]]=a[j],fa[a[j]+maxx]=a[j]+maxx;
vis[a[j]]=;
}
lst=i;
}
}
vis[a[i]]++;
}
}
int main()
{
scanf("%d%d",&n,&k);
lst=n;
for(int i=;i<=n;i++)scanf("%d",&a[i]),maxx=max(maxx,a[i]);
if(k==)solve1();
else solve2();
printf("%d\n",sum+);
for(int i=sum;i>=;i--){
printf("%d ",ans[i]);
}
return ;
}

要注意的判断细节有点多,在各种细节上WA了好几次,包括因为清空不正确,自信地认为拿到了前四十分,其实只拿到一半…

思路不是很好想,这题一上来给的东西很多,对于我这种思路混乱的容易搞成一团乱麻。应该条理地观察总结性质,逐一思考获取最后的思路。

这次考试的难度感觉挺接近noip【然后我考炸了】,题目和题解也很良心,整个考试和改题过程都感觉挺好。希望能多做这样的模拟题吧XD

伏笔回收,我们的Gekoo同学立下了打暴力的豪言壮志【何】,然后T3敲了五行代码输出1果断跑路。