前言:
本人大一新生,正式学习算法的时间大概有半年,前有两次竞赛经验------蓝桥杯和天梯赛。
蓝桥杯时,已学习大部分简单算法,但并未达到熟练掌握的水平,当时对DFS和BFS还未深刻理解,二分与双指针也并未熟练。按照赛后分析,可能是拿了三道大题的分数,略有差异,勉强省三。
天梯赛时,刷题数略有上升,本人对模拟也有所心得,只是阅读题目和实现代码能力较弱,做题速度较慢,只取得了125分的成绩。
CCPC省赛前,由于原队友的离开,与如今两个队友临时组起队伍,只进行过几次模拟训练。两个队友算法学习都比较强力,都是大一同学了。一位蓝桥杯省一,天梯赛144分(截至2023.7.1已经是国三选手了orz),另一位省二,天梯赛168分,而本人喜欢偷懒,除了刷题并未对算法有过深入复习。
正式比赛时,与队友只A出3题,F题滑动窗口大家都没有掌握,痛失银牌。但经此一役,本人对算法学习又有了新的热情,希望能在来年比赛有所突破。以下是本次比赛的做题顺序和心路历程,也是对此次比赛的一次复盘。
题目链接:Dashboard - 2023 CCPC Henan Provincial Collegiate Programming Contest - Codeforces (Unofficial mirror by Menci)
题目:Problem A.小水濑游河南(简单的库函数应用)
思路 :
首先由题意可判断s至少由两个长度大于等于1的字符串组成,那么s长度为1时必然不满足题意,需要进行特判处理。
其次由于a中每种字符最多出现一次,那么a的长度最多即为26,我们最多需要26次判断即可遍历一次样例的所有情况,时间复杂度非常低。
那么问题的关键就在于如何判断b串是否是回文串。我们可以每次从a的末尾的后一位开始,利用substr函数截取b,再利用reverse函数对b进行翻转,即可判断b是否为回文串。
我们可以利用map结构对a中字符数量进行统计,由于每次要从a之后遍历b串,我们预先处理第一个字符,然后从i=0开始,依次枚举从i+1处截断的b串,根据上述思路判重即可。如果遇到a中字符有重复的情况则立刻终止。
代码:
#include<bits/stdc++.h> using namespace std; int t; void slove() { string s; map<char,int> m; cin>>s; int len=s.size(); if(len==1){ cout<<"NaN\n"; return; } m[s[0]]++; for(int i=0;i<min(len,26);i++){ m[s[i+1]]++; string d=s.substr(i+1); string e=d; reverse(e.begin(),e.end()); if(e==d){ cout<<"HE\n"; return ; } if(m[s[i+1]]==2){ cout<<"NaN\n"; return ; } } cout<<"NaN\n"; return ; } int main() { cin>>t; while(t--) slove(); return 0; }
心路历程:
刚开始没有榜单,所以我们都一致看A题,一眼盯真出这是道签到,我们的省一大佬坐在正中间,就请他先敲代码,他对库函数的运用也比较熟练。他敲完就我出样例,另一个队友在看别的题,然后我出的样例他没过,我就上去又改了改,结果出的样例过了,但题给的样例没试,直接WA了一次(心痛),但好在只WA了一次,改完后就拿下了。
题目:Problem H.(贪心/规律/思维)
思路:
依题意,四舍五入的规则为,小数部分满0.5则进1。
先看最大值如何处理。
我们把k的个数看成有几个空位,假如把这k个空位放满0.5,那么我们就能得到k个1。而在此基础上,我们如果想再得到一个1,那么至少需要两个0.5,否则无法进1。
以3为例,3最多可以分出6个0.5,按照上述规则,在k=7时,它最多只能在6个位置上放够0.5,所以这种情况的最大值为6;而在k=3时,它首先可以放3个0.5,可以得到3,之后它还剩下6-3个0.5,那么按照上述解释,它最多还能产生一个1(3/2下取整),最终得到的最大值即为4。
综上所述,我们最大值可在 2 * n 和 k + (2 * n - k )/ 2之间取最小值来得到。
按照类似的处理思路,最小值就可以这样求:将一个数分成多个0.499999(后续忽略不计),在可以分出有限个0.5的基础上,我们可以把每个0.5都移出0.000001,最终可以得到 2 * n个0.499999,以及一个很小的数。
我们可以近似看成分出了 2 * n + 1个0.49(因为都无法满足进1的条件)。如果把这2 * n + 1个数,先放在 k - 1个位置上,那么就还剩下 2 * n + 2 - k 个数,都要放到剩下的最后一个位置上。
这样分的话,前 k - 1个位置我们可以就不再考虑,因为它们必定无法满足进1的条件,我们只需确定剩下的数能产生几个1。因为至少2个0.49才能进1,所以我们将剩下的数除以2下取整即可得到最小值(同时要确保所剩的数不能为负数)。
代码:
#include <bits/stdc++.h>
#define int long long
void slove(){
int n, k;
std::cin >> n >> k;
int res = (2 * n + 2 - k) / 2;
res = std::max(res, 0LL);
std::cout << res << " "<<std::min(2*n,(k+(2*n-k)/2))<<"\n";
}
signed main()
{
int t;
std::cin >> t;
while(t--)
slove();
return 0;
}
心路历程:
比赛时我们其实第二道跟榜做的F题,但当时因为滑动窗口不太会调,我就去先看了看H,他们俩继续优化F(当时其实正解思路已经出来了,但他们觉得不用滑动窗口也能做,换思路了,后面这道题基本跟我就没啥关系了,因为他们的新思路我是真听不懂。。。。。。)。
我当时的思路其实跟现在的差不多,但是最小值那个情况我没有想的很清楚,以至于我还把n直接除以0.5,还开了个double(因为这个后面他们改的时候还WA了一发),反正比赛时心情比较激动,思路没有整理好,导致后面还是喊他俩过来优化了,看他俩后面做的还分了好几种情况,只能说我们之间沟通存在很大问题,浪费了很多时间。
还有就是当初那个最小值求出来可能有负的情况我们也是找了好久才找出来,我第一次写的主要问题应该也就是那个。
题目:Problem C.Toxel与随机数生成器(阅读理解)
思路:
本题思路比较简单,我们通过分析题面可以得到:如果用第二个生成器,那么它在生成length-i 的01串时,前面的部分一定会重复,所以我们只需判断开头长度最短(即1000)的子串,是否在总串中出现多次即可。
代码:
#include <bits/stdc++.h>
using namespace std;
int main()
{
string s;
cin >> s;
string d = s.substr(0, 1000);
if(s.find(d,1001)!=-1)
cout << "No\n";
else
cout << "Yes\n";
return 0;
}
心路历程:
这道题,也是我开的......。但其实比赛现场C题是没多少人开的,我刚看的时候也看不太懂,都准备放弃了。但当时我们前面的一支队伍,我们在看榜的时候发现他们把C题交了,而且比较早,所以我就抱着试试的心态去开了。
当时我们处于一个比较危险的状态,临近比赛结束才只做出两题,F迟迟优化不出来,E题DP我又不是很擅长,本来想让队友先去看的,但他迟迟放不下F,实在没办法我就又看了看C。第一次交的时候理解错了,以为是每个length的字符串都要找一遍,当时因为这个题没法测样例,所以也不知道自己是否做对,连着WA了两次。
这时都准备放弃了,队友继续上机优化他的F。我没办法,只能继续看C,最后突然理解到第二种随机器的前一千个字符串是每次都会重复的,于是我又上机敲了一次,最后交的时候也不知道自己对不对,结果突然看到题目过了,那感受真是难以忘怀,和队友忍不住欢呼了出来。也是靠这题,我们才勉强拿到了铜奖,只能说天时地利人和每个因素都不可或缺。
题目:Problem F.Art for Last(滑动窗口/贪心)
思路:
阅读题目,发现题目让我们从给定序列中任意选择k项,且顺序是可以打乱重排的。
在选出的k项中,任意两项之差的绝对值中,最小值和最大值的乘积最小。
由此,我们可以对原数组进行排序。排序后我们发现,当连续选k个数时,每两个数之间的差值一定是最小的。也就是说这样找两个数的差值一定能让我们最后的答案最小化。
所以我们可以从左到右连续地找k个数,那么区间内两项之差最大即为最左侧和最右侧两项的差。
那么剩下的问题即为,如何找区间内最小值(也就是滑动窗口的思想)。
我们可以给排完序后,所求出的差分区间标上号,当每次窗口移动时,我们将出队的元素删除即可。
之后剩下的就是枚举每个区间最小值和最大值乘积,选出最小的那一个即可。
代码:
#include <bits/stdc++.h>
#define int long long
int n,k;
int x;
typedef std::pair<int, int> PII;
void slove()
{
std::cin >> n >> k;
std::vector<int> a(n + 1);
for (int i = 1; i <= n;i++)
std::cin >> a[i];
std::sort(a.begin() + 1, a.end());
std::vector<PII> b(n + 1);
for (int i = 1; i <= n;i++){
b[i].first = a[i] - a[i - 1];
b[i].second = i;
}
std::priority_queue<PII, std::vector<PII>, std::greater<PII>> q;
int res = 1e18;
q.push({b[2].first, b[2].second});//由于差分数组比原数组少一个,所以我们从第二个开始枚举
for (int i = 2; i <= n;i++){
while(q.top().second<i-k+1&&q.size())
q.pop();
q.push({b[i].first, b[i].second});
if(i>=k) res = std::min(res, (a[i] - a[i - k + 1]) * q.top().first);
}
std::cout << res << "\n";
}
signed main(){
std::ios::sync_with_stdio(false);
std::cin.tie(0);
slove();
return 0;
}
心路历程:
当时我已经把正解思路分析出来了,本来是跟他们说只要想办法整出一个滑动窗口就行。但不知为何我们每个人都执着于用数组模拟,而忘了优先队列这种天然的窗口。
结果就导致他们俩开始换思路,我感觉他们思路有很大问题但又说不上话,就交给他们做了,后面他们再优化我也帮不上什么忙,最终就导致这道题卡了快三个小时也没做出来,痛失银牌,现在想想如果我们三个能一起沟通好,拿个银是没问题的,可惜啊可惜。
总结:
从整个比赛复盘来看,我觉得我们队伍目前最大的问题还是沟通方面,不管是赛前的模拟赛,还是正式比赛,我们互相都没有完整的交流过思路,这导致我们在做题时即没有发挥团队做题的优势,甚至还成为了比赛失利的罪魁祸首。
所以在以后的比赛中,一定要注意以下几点:
1.不要盲目跟榜
虽然说F题现在看来好像也不是很难,但对当时的我们确实是写不出一个好的方案,而且在卡了很久之后没有考虑过及时放弃,去了解别的题目,我觉得这是做题策略的一大重要失误。
2.队友之间应该注意沟通
虽然比赛应该讲究分工,但对于比赛后期,三个人齐力攻破难题才是比赛的关键,每个队友都有必要认真倾听其他队友的思路和想法,多多沟通,多多交流才是一场难忘比赛的重点。
3.保持心态平和
我觉得我们之间没沟通好的原因也在于心态出了问题,因为在我们前面的那个队伍他开题是比较快的。别人比赛的成绩很容易对个人产生影响,而我们应该积极避免此类因素,安心比赛。
最后的最后
希望通过这次比赛,能在接下来的ICPC省赛中有所突破,为第一年的竞赛生涯画下美好的结尾。