美团点评CodeM编程大赛|资格赛非官方题解
这是个人题解,不保证和出题人想法一致。官方题解的话,不妨去听2017.06.17晚上红q曾耀辉老师的讲解吧。(逃
Prob A:根据题意实现即可。注意读题,要求连续的子序列。所以只要把A串在B串上,一个个位置比较过去,取最小值作为答案即可。
#include <stdio.h> #include <limits.h> #include <algorithm> using namespace std; int a[1005],b[1005]; int n,m; int f[1005]; void get(int* arr,int& x){ scanf("%d",&x); for(int i=1;i<=x;i++){ scanf("%d",&arr[i]); } } int getans(int* a,int* b,int n){ int ret=0; for(int i=0;i<n;i++)ret+=(a[i]-b[i])*(a[i]-b[i]); return ret; } int main(){ get(a,n);get(b,m); int ans=INT_MAX; for(int i=1;i+n-1<=m;i++){ ans=min(ans,getans(a+1,b+i,n)); } printf("%d\n",ans); return 0; }
Prob B:不需要排序,只需要读入第一个数,然后统计有多少个数>=第一个数,就知道排在倒数第几了。
至于能坚持几轮,最后一名肯定第1轮都过不去,倒数第二名打倒数第一名能挺过去一轮,
倒数第3名的话也只能挺过去一轮——比他弱的2人,一个被他打了,另一个肯定被别人打了,然后这个倒数第三名在下一轮直接出局了。
倒数第4名的话,能做到2轮,以此类推。
显然,n-1个和他一样的/比他弱的+他自己(共n个人),下一轮最多剩下floor(n/2)人。
那就尽量/2,除完之后结果>=1的次数,就是答案。
(或者说,这个排位的二进制表示的最高位,在哪一位。)
#include <stdio.h> int main(){ int n,f=-11111111,cnt=0,tmp; for(scanf("%d",&n);n--;){ scanf("%d",&tmp); if(f==-11111111)f=tmp; if(tmp<=f)++cnt; } int ans=0; while(cnt>1){ cnt/=2; ++ans; } printf("%d\n",ans); }
Prob C:把问号放到平衡树/set里,记录并维护一下?(问号)出现的下标。
开几个数组,记录手上有几张第x种优惠券,上次买入变成1张是什么时候,上次卖出变成0张是什么时候。
买入的时候,如果之前手里有1张了,那么找出买入上1张后,到现在,中间的最早的?,把这个?改成卖出操作,这样这个买入就合理了。
卖出的时候,之前已经没了的情况处理类似。
(现在看看代码,感觉实现有bug:如果真实记录里,就只有1~10w这10万种优惠券、只有I和O两种操作,不允许空操作的话,那:
I 1 I 2 I 3 ... I 100000 ? O 1 O 2 O 3 ... O 100000
我的程序会觉得没问题,但实际上有:买入10万张以后,这个?不可以再买入,只能随便卖出1张。
但是随便卖出谁,都会导致后面10万条不同的卖出操作中,有谁实际上卖不出去了。
那个问号,要不可以代表任何操作,要不可以是I 100001,不然我的程序是错的……
UPD:根据红q(quailty,http://codeforces.com/profile/quailty , http://osu.ppy.sh/u/6423914
)的意思,是允许有I 100001,那就没问题了
)
#include <stdio.h> #include <algorithm> #include <set> using namespace std; int cnt[100005]; int lasto[100005]; int lasti[100005]; int maxid,err; set<int> left; char str[2]; int id,m; int main(){ while(~scanf("%d",&m)){ fill(cnt,cnt+maxid+1,0); fill(lasto,lasto+maxid+1,0); fill(lasti,lasti+maxid+1,0); maxid=0; err=-1; left.clear(); for(int i=1;i<=m;i++){ scanf("%s",str); if(str[0]=='?'){ left.insert(i); }else{ scanf("%d",&id); maxid=max(maxid,id); if(str[0]=='I'){ cnt[id]++; if(cnt[id]>=2){ auto it=left.lower_bound(lasti[id]); if(it!=left.end()){ left.erase(it); --cnt[id]; }else{ if(err==-1)err=i; } } lasti[id]=i; }else{ cnt[id]--; if(cnt[id]<0){ auto it=left.lower_bound(lasto[id]); if(it!=left.end()){ left.erase(it); ++cnt[id]; }else{ if(err==-1)err=i; } } lasto[id]=i; } } } printf("%d\n",err); } return 0; }Prob D:有没有解比较,简单:构建反向图(本来是3能到6的,我加一条6到3的边),从n出发,广度优先搜索(bfs)一遍,能到1就有解。
同时记录下哪些点被经过了,这在原图中,就是能到n的点。
有人问了,那什么时候会是无穷长的解?
看下图:
注意我们要字典序最小,而不是长度最小
那么在3这个位置上,我们要不停地选a,而不能选b,否则,字典序上来讲:
aaba......在aaaa.......之后
这里的处理也比较简单:
1、从1出发,每个点贪心选择:如果走a,这个点能到n,那就走a,否则走b。
2、但如果走回到之前走过的点,那说明答案是Infinity。
#include <stdio.h> #include <algorithm> #include <queue> #include <vector> using namespace std; vector<int> G[100005]; int a[100005],b[100005]; int n; bool vis[100005]; bool vis2[100005]; char str[100005]; void rev_bfs(int p){ queue<int> q; vis[p]=1; q.push(p); while(!q.empty()){ int x=q.front();q.pop(); for(int i=0;i<G[x].size();i++){ if(!vis[G[x][i]]){ vis[G[x][i]]=1; q.push(G[x][i]); } } } } void input(int* a){ for(int i=1;i<=n;i++){ scanf("%d",&a[i]); int tmp=i+a[i]; if(tmp>=1&&tmp<=n) G[i+a[i]].push_back(i); } } int main(){ scanf("%d",&n); input(a); input(b); rev_bfs(n); if(!vis[1]){ puts("No solution!"); return 0; } int p=0; vis2[1]=1; bool infflag=0; for(int x=1;x!=n&&!infflag;){ int nxt=x+a[x]; if(nxt>=1&&nxt<=n&&vis[nxt]){ if(!vis2[nxt]){ vis2[nxt]=1; str[p++]='a'; }else{ infflag=1; } x=nxt; }else{ nxt=x+b[x]; if(nxt>=1&&nxt<=n&&vis[nxt]){ if(!vis2[nxt]){ vis2[nxt]=1; str[p++]='b'; }else{ infflag=1; } }else{ puts("No solution!"); return 0; } x=nxt; } } puts(infflag?"Infinity!":str); return 0; }
Prob E:
这个问题,经典起手式:求[a,b]的问题,拆成求[1,b]-[1,a-1]的问题,而处理[1,x]的问题一般容易搞定不少。
OK,[1,x]怎么处理?
首先要意识到:在[1,x]中,有约数y的数有x/y个。
那么,假设x为常数,对函数f(y)=x/y,在y=1,2,3...n上的点用曲线近似,是反比例函数在第一象限的图像。
(图就靠大家自己画了~)
特点:前半段很陡峭,差个1,函数值都差远了;后半段很平坦,n/2个数的函数值都为1……
根据这两段的不同情况,我们采取不同的处理策略:
对前半段(比如,1到10万),我们直接算有多少个数包含这个约数x,直接加到答案上。
对后半段(10万以上),我们计算有多少个约数,只有1个、2个....个数的约数中包含他,这些数的取值范围是多少。
然后我们统计出这些约数中,有多少是以1、2、3...9开头的数,乘上相应的约数个数,加到答案中。
这样搞定了~基本是一个sqrt(n)*log(n)的时间复杂度。
#include <stdio.h> #include <string.h> #include <stdlib.h> typedef long long ll; ll a[10],b[10],out[10]; int highest[100001]; void cnt(int l,int r){ memset(out,0,sizeof(out)); char tmp[15]; while(l<=r){ sprintf(tmp,"%d",r); int t2=tmp[0]-'0'; for(int i=1;tmp[i];i++)t2=t2*10; if(t2<l)t2=l; out[tmp[0]-'0']+=r-t2+1; r=t2-1; } } void get(int x,ll* a){ if(x==0)return; for(int i=1;i<=100000;i++){ a[highest[i]]+=x/i; } if(x<=100000)return; int last=x; for(int i=1;;i++){ int p2=x/(i+1); ++p2; if(p2<=100000)p2=100001; cnt(p2,last); for(int j=1;j<=9;j++)a[j]+=out[j]*i; last=x/(i+1); if(last<=100000)break; } } int main(){ for(int i=1;i<=9;i++){ for(int j=1;j<=10000;j*=10){ for(int k=0;k<j;k++){ highest[i*j+k]=i; } } } highest[100000]=1; int l,r; scanf("%d%d",&l,&r); get(r,b);get(l-1,a); for(int i=1;i<=9;i++)printf("%lld\n",b[i]-a[i]); }
Prob F:
实现题。开个数组表示棋盘。
3类miss情况,放了子不能再放的miss 1很简单,不赘述。
miss 2,放下去以后自己没气的话,先考虑把对手的、没气的提掉,再检查自己有没有气,没气的话记得还原棋盘。
miss 3,出现重复情况的处理的话,骚年,hash都说得很熟练的话,不妨把这个棋盘也hash起来,用hash来高效查重啊?
反正这题就慢慢写吧~我写+调整了78分钟,红q写+调整了89分钟
(这题有点卡常数,大家要注意自己写出来的代码的效率,我TLE了3发,调了好几下才过……)
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <algorithm> #include <queue> #include <vector> #include <unordered_set> using namespace std; typedef unsigned int ull; const int di[4][2]={{-1,0},{1,0},{0,-1},{0,1}}; struct Point{ int x,y; Point(int a=0,int b=0):x(a),y(b){} Point go(int i)const { return Point(x+di[i][0],y+di[i][1]); } bool check()const { return x>=1&&x<=19&&y>=1&&y<=19; } }; struct Board{ int a[21][21]; unordered_set<ull> state; void init(){ memset(a,0,sizeof(a)); state.clear(); } void output(){ char tmp[20]="\0"; for(int i=1;i<=19;i++){ for(int j=1;j<=19;j++){ if(a[i][j]==0) tmp[j-1]='.'; else if(a[i][j]==1) tmp[j-1]='B'; else if(a[i][j]==2) tmp[j-1]='W'; } puts(tmp); } } ull hash(){ ull ans=0; for(int i=1;i<=19;i++){ for(int j=1;j<=19;j++){ ans*=3; ans+=a[i][j]; ans%=(int)(1e9+7); } } return ans; } int& ref(const Point& p){ return a[p.x][p.y]; } }board,backup; bool vis[21][21],vis2[21][21]; bool chi(const Point& p,bool vis[21][21]){ bool alive=false; int chess=board.ref(p); queue<Point> q; q.push(p); vis[p.x][p.y]=1; while(!q.empty()){ Point x=q.front();q.pop(); for(int i=0;i<4;i++){ Point y=x.go(i); if(!y.check())continue; if(board.ref(y)==0)alive=true; if(board.ref(y)==chess&&!vis[y.x][y.y]){ vis[y.x][y.y]=1; q.push(y); } } } return alive; } int put(int type,const Point& p){ int& place=board.ref(p); if(place!=0) return 1; backup=board; place=type; memset(vis,0,sizeof(vis)); memset(vis2,0,sizeof(vis2)); for(int direct=0;direct<4;direct++){ Point x=p.go(direct); if(!x.check())continue; int i=x.x,j=x.y; if(!vis[i][j] && board.a[i][j]==3-type){ if(!chi(Point(i,j),vis)){ chi(Point(i,j),vis2); } } } for(int i=1;i<=19;i++){ for(int j=1;j<=19;j++){ if(vis2[i][j]){ board.a[i][j]=0; } } } if(!chi(p,vis)){ board=backup; return 2; } ull hash_val=board.hash(); unordered_set<ull>::iterator it=board.state.find(hash_val); if(it!=board.state.end()){ board=backup; return 3; } board.state.insert(hash_val); return 0; } template <class T> inline void scan(T &ret) { char c; ret=0; while((c=getchar())<'0'||c>'9'); while(c>='0'&&c<='9') ret=ret*10+(c-'0'),c=getchar(); } template <class T> inline void scan_c(T &c) { while((c=getchar()),!isalnum(c)); } int main(){ int T,n; for(scan(T);T--;){ board.init(); for(scan(n);n--;){ char tmp[2]; Point x; scan_c(tmp[0]); scan(x.x);scan(x.y); int ret=put(tmp[0]=='B'?1:2,x); if(ret){ printf("miss %d\n",ret); } } board.output(); } return 0; }
UPD:这份代码在比赛的时候是通过了的。平时练习环境下,要想通过,需要做一些常数优化,比如:
(以下,按缩短的运行时间从多到少排序)
0、提掉没气的对手的子的时候,一个bfs/dfs过去,直接提了,不要像上面的代码一样堆起来,最后清。
1、手写一个数组模拟的循环队列。
STL的queue,底层用了deque,块状数组,非常非常慢!
手写个数组模拟的循环队列,快好多了!
(这个,自己写吧。)
2、在hash()函数内,把ans%=(int)(1e9+7)删掉。
这里写这句,是考虑到,很容易构造hash冲突的例子。http://codeforces.com/blog/entry/4898
但是后来了解到,要hash冲突,串长要挺长的……
3、miss 2类型的情况下,你不需要把整个棋盘还原回去,只要把这个棋子提起来即可。
当然,这个代码还有很多可以做常数优化的空间(应该还能优化出很多吧?),这就靠大家去慢慢折腾了。
0、1、2、3 这4点结合使用,时间基本稳定在1000ms左右。
UPD2:红q曾耀辉的代码真快,150ms。