第二次CSP模拟:
第一题:HRZ的序列
1.题意简介:HRZ刷b站的时候看到了这样一个序列:一个长度为n的序列,问是否存在这样一个数k,使得序列中一些数加上这个数 ,一些数减去这个数,一些数不变,使得整个序列中所有的数相等(其中对于序列中的每个位置上的数字,至多只能执行一次加运算或减运算或是对该位置不进行任何操作)。
输入格式:输入第一行是一个正整数 t, 表示数据组数。接下来对于每组数据,输入的第一个正整数n表示a序列的长度,随后一行有n个整数,表示a序列 。
要求输出:
输出共包含t行,每组数据输出一行。对于每组数据,如果存在这样的K,输出"YES",否则输出“NO”。
2.做法思路:通过分析题目可以知道,一个序列如果能够达成题目要求,那么这组序列最多只能包含三个数a1,a2,a3,其中两个数加上k或减去k为另外一个数,当这个序列包含的数少于三个数,那么这个序列就一定能够满足要求,无论这串序列有多长,都必须满足上述条件。
根据这个特点,我用set<long long int>类型的集合s来存储序列,它能够自动压缩这个序列,只存一次不同的数,通过判断s.size(),若s.size()==1 || s.size()==2,那么这个序列就是满足要求的序列,若果s.size()==3,那么就需要先将s中的三个数进行排序,然后判断最小数和最大数之和是否为中间数的两倍,若是,则该序列也满足条件,若不是,或者s.size()>3,那么这个序列就不满足条件。
(注意:该题的数据范围很大,不能直接用Int类型,需要用long long int!!!)
3.代码:
#include <iostream> #include <algorithm> #include <cmath> #include <set> #include <stdio.h> using namespace std; long long int a[10010], num[3]; set<long long int> s; int main() { int can,j; int t; cin>>t; for(int i=0;i<t;i++) { int n; can=0, j=0; cin>>n; s.clear(); for(int i=0;i<n;i++) { cin>>a[i]; s.insert(a[i]); } set<long long int> ::iterator it = s.begin(); if(s.size()==3) { while(it!=s.end()) { num[j++] = *it; it++; } sort(num,num+3); if(num[0]+num[2]==2*num[1]) can=1; } if(s.size()==1 || s.size()==2) cout<<"YES"<<endl; else if(can) cout<<"YES"<<endl; else cout<<"NO"<<endl; } return 0; }
第二题:HRZ学英语
1.题意简介:现在给定一个字符串,字符串中包括26个大写字母和特殊字符 ‘?’ ,特殊字符 ‘?’ 可以代表任何一个大写字母。问是否存在一个位置连续的且由26个大写字母组成的子串,在这个子串中每个字母出现且仅出现一次,如果存在,请输出从左侧算起的第一个出现的符合要求的子串,并且要求,如果有多组解同时符合位置最靠左,则输出字典序最小的那个解!如果不存在,就输出-1!
说明:字典序 先按照第一个字母,以 A、B、C……Z 的顺序排列;如果第一个字母一样,那么比较第二个、第三个乃至后面的字母。如果比到最后两个单词不一样长(比如,SIGH 和 SIGHT),那么把短者排在前。
(注意:只输出字典序最小的!)
输入格式:输入一行,一个符合题目描述的字符串。
要求输出:如果存在这样的子串,则输出,不存在则输出-1.
2.做法思路:本题和之前做过的区间移动很类似,用一个队列类型的word来存储一个区间内的字符(共26个),并用int类型的变量unknow记录字符‘?’出现在区间内的次数,用int类型的变量have记录区间内已有的字母。
初始化时向word内存入26个字符,在存储时记录unknow和have的值,输入完毕后,调用judge()函数来判断该子串是否满足条件。
Judge()函数:首先判断have是否为26,是则该子串已经满足条件,按队列顺序输出即可;否则再判断26-have是否等于unknow,等于则说明字符‘?’可以填补为缺少的所有字母,此时按顺序输出word中的字母,遇到‘?’时替换为字典序最靠前的那个字母并输出;不等于则说明该字符串不满足条件,返回主函数,将word区间向后移动一份字符,并更新unknow和have的值,再调用judge()函数,重复执行上述区间移动操作,直到judge()函数中的子串满足条件为止。
3.代码:
#include <iostream> #include <stdio.h> #include <queue> using namespace std; char Words[26]={'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z'}; queue<char> word; long long unknow=0;//记录'?'字符出现的次数 long long have=0;//记录当前已经得到的26个字母的个数 bool can=false; bool judge(int get[100]) { if(have==26) {//该区间满足条件,直接输出 for(int i=0;i<26;i++) { char a=word.front(); word.pop(); cout<<a; } can=true; return true; } else {//不满足条件,用'?'字符判断能否满足 int m=26-have; if(unknow==m) {//'?'字符和缺少的字符数量相等,则按条件输出 int j=65; for(j;j<=90;j++) {//找到第一个缺少的字母 if(get[j]==0) break; } for(int i=0;i<26;i++) { char a=word.front(); word.pop(); if(a=='?') {//'?'字符替换为缺少的字母 int b=j-65; cout<<Words[b]; get[j]=1; for(j;j<=90;j++) {//找到下一个缺少的字母 if(get[j]==0) break; } } else cout<<a; } can=true; return true; } else//区间不满足条件 return false; } } int main() { int get[100]={0}; for(int i=0;i<26;i++) { char a; cin>>a; word.push(a); if(word.back()=='?') unknow++; else { get[word.back()]++; if(get[word.back()]==1) have++; } } if(judge(get)) return 0; else { char b; while(cin>>b) { char a=word.front(); word.pop(); if(a=='?') { unknow--; word.push(b); if(b=='?') unknow++; else { get[b]++; if(get[b]==1) have++; } if(judge(get)) break; } else { get[a]--; if(get[a]>1) { word.push(b); if(b=='?') unknow++; else { get[b]++; if(get[b]==1) have++; } } else { if(get[a]==1) { word.push(b); if(b=='?') unknow++; else { get[b]++; if(get[b]==1) have++; } if(judge(get)) break; } if(get[a]==0) { have--; word.push(b); if(b=='?') unknow++; else { get[b]++; if(get[b]==1) have++; } if(judge(get)) break; } } } } if(!can) cout<<"-1"<<endl; } return 0; }
第三题:咕咕咚的奇妙序列
1.题意简介:112123123412345 …这个序列由连续正整数组成的若干部分构成,其中第一部分包含1至1之间的所有数字,第二部分包含1至2之间的所有数字,第三部分包含1至3之间的所有数字,第i部分总是包含1至i之间的所有数字,11212312341234512345612345671234567812345678912345678910,其中第1项是1,第3项是2,第20项是5,第38项是2,第56项是0。现在想知道第 k(1<=k<=10^18) 项数字是多少。
输入格式:输入由多行组成。
第一行一个整数q表示有q组询问(1<=q<=500)
接下来第i+1行表示第i个输入ki,表示询问第ki项数字。
要求输出:输出包含q行
第i行输出对询问ki的输出结果
2.做法思路:首先观察数据范围,本题的k最大值为10^18,直接查找ki的话无疑会超时,那么就需要思考更加简化的查找方式。这里我选择的是二分的查找思想。
但是只用二分还是无法解决问题,要考虑到每个部分的数字总数,还要计算ki所在的部分,然后在这一部分中再查找ki的具体数字,这样做也会花很长时间。
另外需要注意到,该题中的数字在超出个位数后,是由多个个位数组成的,例如10这个数,在序列中由1和0组成,所以第56项为0,而不是10,这又给我计算每一个部分的数字的总数增加了难度。
在向已经AC该题目的同学“取经”后,我意识到可以不单独计算每一部分的数字总数,而是变成计算数字位数相同的“层级”的数字总数,通过分析给出的序列,可以得出一个计算规律,例如数字位数为2的层级,这一层中的数最大为99,从10到99,每一个部分中的两位数个数为1到90个,这些个数相加后的数字乘以2便是在最大数为99的部分中的层级的数字个数,然后再加上数字位数为3的层级中的两位数的部分的数字个数,就能得到2层级的数字个数总和,通过上述算法可以依次计算出1到9、10到99和100到999及其他层级的数字总数。计算完成后,根据想要求的数ki可以确定该数字所在的层数,然后通过二分来确定ki的具体数值即可。
(反思:同学提出的这个思路十分简便,但是要想在CSP模拟的有限时间内想出来对我来说还有一定难度,如果按照我一开始的思路来写代码,那么大概率会超时,因此需要在平时做题时多思考其他的思路,多思考更加简化的算法,来训练寻找更优解题思路的能力。)
3.代码:
#include <iostream> #include <stdio.h> #include <cmath> using namespace std; long long int sum(long long int x,int choose) { if(choose==0) { long long int ans=0,i=1,j=1; for (j=1;j*10<=x;j*=10) { ans=ans+i*(j*9+1)*(j*9)/2+i*j*9*(x-j*10+1); i++; } long long int a=(x-j+1); long long int b=(a+1)*a/2; return ans+i*b; } if(choose==1) { long long int ans=0, i=1, j=1; for (j=1;j*10<=x;j*=10) { ans+=i*j*9; i++; } long long int a=i*(x-j+1); return ans+a; } } int main() { long long int n,m; long long int level,num,s,r,l; cin>>n; for (int i=0;i<n;i++) { cin>>m; l=0,r=1e9; while(l<=r) { long long int mid=(l+r)/2; if (sum(mid,0)<m) { l=mid+1; level=mid; } else r=mid-1; } m=m-sum(level++,0); l=0,r=level; while(l<=r) { long long int mid=(l+r)/2; if (sum(mid,1)<m) { l=mid+1; num=mid; } else r=mid-1; } m=m-sum(num++,1); int a[30]={0},j=0; while (num!=0) { a[j++]=num%10; num=num/10; } cout<<a[j-m]<<endl; } return 0; }
第二次CSP反思:CSP和之前的作业不同,提交之后不会直接出结果,需要自己在本地确保无误后再提交,由于之前都是靠oj来检测自己的代码是否还有错误,所以在CSP模拟的时候,尤其是第二题,就有很多没有考虑到的地方,自己只是测试了题目中给出的例题就急急忙忙的提交了,导致最后的评测结果惨不忍睹(捂脸),所以今后在自我测试方面需要加强,最起码要将题目中涉及的所有可能都包含在代码里才有可能拿高分。
另外是时间把握的问题,平常的作业,就算对我而言比较难,也能花更多的时间去慢慢解决(熬夜党问题言论),所以平时就没有刻意去关注敲代码的时间。但是CSP模拟是限时测试,要在有限的时间内写出解决算法,还要确保错误率较低,对于我来说现在还比较头疼,所以在之后敲代码的时候要多注意把握时间,必要的时候也可以考虑使用助教说的”骗分“策略。