写在前面:
最近闲来没事参加了美团的CodeM大赛,赛程最初是一个资格赛环节,只要能够答对任意一题即可以进入初赛环节,本着锻炼自己的目的就报名参加了,没有想过拿到什么名次,大学这么多年,总是仰望各种大神去参加计算机竞赛,这一次自己也去体验一下。
大赛主页:美团点评变成大赛
进入正题:
概要: 总共6个题,三个小时的时间安排。晋级要求只需要答出其中一道题即可。
第一题 音乐研究
题目描述:
美团外卖的品牌代言人袋鼠先生最近正在进行音乐研究。他有两段音频,每段音频是一个表示音高的序列。现在袋鼠先生想要在第二段音频中找出与第一段音频最相近的部分。
具体地说,就是在第二段音频中找到一个长度和第一段音频相等且是连续的子序列,使得它们的 difference 最小。两段等长音频的 difference 定义为:
difference = SUM(a[i] - b[i])2 (1 ≤ i ≤ n),其中SUM()表示求和
其中 n 表示序列长度,a[i], b[i]分别表示两段音频的音高。现在袋鼠先生想要知道,difference的最小值是多少?数据保证第一段音频的长度小于等于第二段音频的长度。
属于开胃菜的第一题,由于目的是尽快完成力能所及的题目,所以选择了一个最直观的方法来做,滑动窗口,时间复杂度为O(n*n)。
#include <bits/stdc++.h>
using namespace std;
//没仔细继续考虑,先把所以题刷掉再说
long long getResult(vector<int>& a, int n, vector<int>& b, int m){
long long min_val = 0x3f3f3f3f3f3f3f3f;
for(int i=0;i<=m-n;i++){
long long tmp = 0;
for(int j=0;j<n;j++){
tmp += pow(a[j] - b[i+j], 2);
}
min_val = min(min_val, tmp);
}
return min_val;
}
//开胃菜,滑动窗口解题,根据给定的数据范围,时间复杂度为O(n*n)还是能接受的
int main(){
int n = 0, m = 0;
cin >> n;
vector<int> a(n, 0);
for(int i=0;i<n;i++) cin >> a[i];
cin >> m;
vector<int> b(m, 0);
for(int i=0;i<m;i++) cin >> b[i];
cout << getResult(a, n, b, m) << endl;
return 0;
}
第二题 竞标赛
题目描述:
组委会正在为美团点评CodeM大赛的决赛设计新赛制。比赛有 n 个人参加(其中 n 为2的幂),每个参赛者根据资格赛和预赛、复赛的成绩,会有不同的积分。比赛采取锦标赛赛制,分轮次进行,设某一轮有 m 个人参加,那么参赛者会被分为 m/2 组,每组恰好 2 人,m/2 组的人分别厮杀。我们假定积分高的人肯定获胜,若积分一样,则随机产生获胜者。获胜者获得参加下一轮的资格,输的人被淘汰。重复这个过程,直至决出冠军。
现在请问,参赛者小美最多可以活到第几轮(初始为第0轮)?
同样也是一道开胃菜的题目,这道题理解题目以后要找到规律,小美能够最多撑到多少轮,取决于一开始她的分数排在总人数的多少位,最后取个对数值即可。可以画画图就能明白其中的道理;
这道题最开始的时候脑袋有点晕,在找到规律以后很傻的我用了一个很复杂的方法去找到小美的段位,先排序之后在用二分查找法来做,后面反应过来以后羞愧地无地自容;比较好的方法就是遍历一遍,知道多少个人分数比小美低即可,时间复杂度为O(n)。
#include <bits/stdc++.h>
using namespace std;
//二分查找的实现
int binarySearch(vector<int>& points, int l, int r, int target){
while(l <= r){
int mid = l + (r-l)/2;
if(points[mid] == target) return mid;
else if(points[mid] > target) r = mid - 1;
else l = mid + 1;
}
return -1;
}
//求解子函数
int getResult(vector<int>& points, int n){
int person = points[0];
sort(points.begin(), points.end());
int index = binarySearch(points, 0, n-1, person);
return log2(index+1);
}
//更快的做法,时间复杂度降至O(n)
int niceSolve(vector<int>& points, int n){
int index = points[0];
int count = 0;
for(int i=1;i<n;i++) count += points[i]<index?1:0;
return log2(count+1);
}
//找到参赛者最多能参加多少轮比赛
//每个参赛者的分数都不一样,所以可以先排序,然后用二分法找到小明的段位位置
//最终参赛轮数经过测试为log2(段位)
int main(){
int n = 0;
cin >> n;
vector<int> points(n, 0);
for(int i=0;i<n;i++) cin >> points[i];
cout << niceSolve(points, n) << endl;
//cout << getResult(points, n) << endl;
return 0;
}
第三题 优惠券
题目描述:
美团点评上有很多餐馆优惠券,用户可以在美团点评App上购买。每张优惠券有一个唯一的正整数编号。当用户在相应餐馆就餐时,可以在餐馆使用优惠券进行消费。优惠券的购买和使用按照时间顺序逐行记录在日志文件中,运营人员会定期抽查日志文件看业务是否正确。业务正确的定义为:一个优惠券必须先被购买,然后才能被使用。
某次抽查时,发现有硬盘故障,历史日志中有部分行损坏,这些行的存在是已知的,但是行的内容读不出来。假设损坏的行可以是任意的优惠券的购买或者使用。
现在问这次抽查中业务是否正确。若有错,输出最早出现错误的那一行,即求出最大s,使得记录1到s-1满足要求;若没有错误,输出-1。
同样是个很简单的题目,可以考虑维护一个二维数组来表示每一种类优惠券的状态,二维数组中的每一个一维数组,第一位表示当前优惠券的张数,然后第二位表示上一次操作过优惠券(可以是使用,也可以是购入)的时刻;
判断优惠券使用异常即是优惠券的数目大于1或者小于0,当这种情况发生时,由于目的是使得整个一切正常的时刻最大,可以考虑利用优惠券上一次被操作的时刻到当前时刻,最早出现的未知操作做文章,来抵消异常情况的发生。
#include <bits/stdc++.h>
using namespace std;
int main(){
int m = 0; cin >> m;
//开辟一块空间用来保存每一种类优惠券的状态
//第一位表示优惠券的数目,第二位表示优惠券的上次被用掉的时刻
vector<vector<int>> coupons(500005, vector<int>(2, 0));
set<int> marks;
for(int i=1;i<=m;i++){
char sign = ' '; cin >> sign;
if(sign != '?'){
int x = 0; cin >> x;
//记录优惠券的操作
int opt = sign=='I'?1:-1;
coupons[x][0] += opt;
if(coupons[x][0]>1 || coupons[x][0]<0){
//在marks里面,如果有发现离上一次用掉最早的不确定操作
//利用该不确定操作来让所有流程变得合理
if(marks.lower_bound(coupons[x][1]) == marks.end()){
cout << i << endl; return 0;
}
marks.erase(marks.lower_bound(coupons[x][1]));
coupons[x][0] = min(max(coupons[x][0], 0), 1);
}
//更新该种类优惠券的上一次被用掉的时刻
coupons[x][1] = i;
}
else marks.insert(i);
}
cout << -1 << endl;
return 0;
}
第四题 送外卖
题目描述:
n 个小区排成一列,编号为从 0 到 n-1 。一开始,美团外卖员在第0号小区,目标为位于第 n-1 个小区的配送站。
给定两个整数数列 a[0]~a[n-1] 和 b[0]~b[n-1] ,在每个小区 i 里你有两种选择:
1) 选择a:向前 a[i] 个小区。
2) 选择b:向前 b[i] 个小区。
把每步的选择写成一个关于字符 ‘a’ 和 ‘b’ 的字符串。求到达小区n-1的方案中,字典序最小的字符串。如果做出某个选择时,你跳出了这n个小区的范围,则这个选择不合法。
• 当没有合法的选择序列时,输出 “No solution!”。
• 当字典序最小的字符串无限长时,输出 “Infinity!”。
• 否则,输出这个选择字符串。
字典序定义如下:串s和串t,如果串 s 字典序比串 t 小,则
• 存在整数 i ≥ -1,使得∀j,0 ≤ j ≤ i,满足s[j] = t[j] 且 s[i+1] < t[i+1]。
• 其中,空字符 < ‘a’ < ‘b’。
题目难度开始增加了,这一题很明显能看得出要使用dfs来做,但是其中有几个地方不是很明确,首先找到了从0到n-1的路径,需要输出字典序最小的那一条,同时,如果没有解的情况,需要输出No solution! 到目前都没有问题,关键是题目说,可能会出现解无穷的情况,需要输出Infinity!第一次看到这个解无穷的情况,脑袋里面全是问号,这就是一个找路径的问题,怎么还涉及到有无穷路径的做法。
后面经过答疑和进一步了解,由于要输出的是字典序最小的解,当有一个解为aab的时候,假如在第二个a有一个选择是选择路径b到达终点,但是这个时候他的路径a是回到最初的第一位也就是位置0,那如果选择了路径a,下一次可以到达终点的选择是aaaab,这个解的字典序比aab小,同样的aaaaaab的解又比上一次aaaab小,所以照这个道理,最终为了最小字典序,这个解是无穷的(谁送外卖会这么傲娇)。吐槽归吐槽,解题还是必须的,下面是最终的解法代码。
#include <bits/stdc++.h>
using namespace std;
vector<int> res;
//联合多个保存状态的数组来做,
//从l点开始,当l和r相等的时候输出最终的路径
//用一个数组来维护哪些点走过,如果按照字典序最小的条件优先选择路径,如果走到一个
//已经访问过的路径,即表示为了字典序最小的话有无尽解
void dfs(vector<int>& visit, int l, int r, const vector<vector<int>>& go, const vector<int>& ok){
if(l == r){
for(int i=0;i<(int)res.size();i++) printf("%c", res[i]+'a');
cout << endl;
exit(0);
}
visit[l] = 1;
for(int i=0;i<(int)go[l].size();i++){
int tmp = go[l][i];
if(tmp<0 || ok[tmp]!=1) continue;
if(visit[tmp] == 1){
cout << "Infinity!" << endl;
exit(0);
}
res.push_back(i);
dfs(visit, tmp, r, go, ok);
}
}
//反向dfs,用一个数组表示,i相对应的值为1表示i点可以通过路径到n-1点
void reverseDFS(vector<int>& ok, int w, const vector<vector<int>>& reverse_go){
ok[w] = 1;
for(int i=0;i<(int)reverse_go[w].size();i++){
int tmp = reverse_go[w][i];
if(ok[tmp] != 1) reverseDFS(ok, tmp, reverse_go);
}
}
int main(){
int n = 0; cin >> n;
//用两个数组来维护送外面的状态
//第一个数组表示i点可以通过两种方式去到的位置
//第二个数组表示i点可以由哪些点走过来
vector<vector<int>> go(n, vector<int>(2, 0));
vector<vector<int>> reverse_go(n);
//分别按规则保存每个点的状态
for(int i=0;i<2;i++){
for(int j=0;j<n;j++){
int tmp = 0; cin >> tmp;
tmp += j;
if(!(tmp<0 || tmp>=n)){
go[j][i] = tmp;
reverse_go[tmp].push_back(j);
}
else go[j][i] = -1;
}
}
vector<int> ok(n, 0);
//先进行一次反向的dfs,找出哪些点可以走到最后一个点
reverseDFS(ok, n-1, reverse_go);
if(ok[0] != 1){
cout << "No solution!" << endl;
return 0;
}
vector<int> visit(n, 0);
dfs(visit, 0, n-1, go, ok);
return 0;
}
第五题 数码
题目描述:给定两个整数 l 和 r ,对于所有满足1 ≤ l ≤ x ≤ r ≤ 10^9 的 x ,把 x 的所有约数全部写下来。对于每个写下来的数,只保留最高位的那个数码。求1~9每个数码出现的次数。
题目描述虽然很简短,但是这道题我认为是最难的,因为取值范围十分广,所以如果算法优化得不是很好,很容易就会超时。同时针对题意来说也是不容易的一个题;具体的解题思路如下:
代码如下:
#include <bits/stdc++.h>
using namespace std;
//计算1到end范围内每个数为约数,总共可以有多少个倍数可取
long long getDivisor(long long val, long long end){
if(end == 0 || val == 0) return 0;
//将end固定为两者的最小值
end = min(val, end);
long long t = 0, res = 0;
for(t=1;t<=end && t*t<=val;t++) res += val/t;
for(long long i=val/t;i>=val/end;i--) res += (min(end, val/i) - val/(i+1))*i;
return res;
}
//对于取值范围,计算取值范围两端到1有多少个数
long long getVal(long long val, long long left, long long right){
return getDivisor(val, right) - getDivisor(val, left-1);
}
//计算对于区间左右取值,对于在取值范围每个取值(约数)有多少个数
long long calcu(long long l, long long r, long long left, long long right){
return getVal(r, left, right) - getVal(l-1, left, right);
}
int main(){
int l = 0, r = 0;
cin >> l >> r;
vector<long long> res(10, 0);
//根据本题取约数的范围,大概可以固定范围在10的10次方内
long long tmp = 1;
for(int i=0;i<10;i++){
//每一次迭代求一下每个j开头的约数的数量
for(int j=1;j<=9;j++){
//固定每一次的约数取值范围
long long left = j*tmp, right = (j+1)*tmp-1;
res[j] += calcu(l, r, left, right);
}
tmp *= 10;
}
for(int i=1;i<=9;i++){
cout << res[i] << endl;
}
return 0;
}
总结:
通过这一次比赛,很容易就意识到了自己的不足,始终竞赛选手挑战的题目还是太高端,而且这只是一个很普通的资格赛环节,自己提升的空间依然还很多,如果能够在巩固好自己的专业技能的同时增强自己编程竞赛这方面的能力是再好不过的。