CSP 202209题解:如此编码,何以包邮,防疫大数据,吉祥物投票,高维亚空间超频物质变压缩技术
CSP202209全部满分题解:如此编码,何以包邮,防疫大数据,吉祥物投票,高维亚空间超频物质变压缩技术
试题内容请前往CCF官网查看:
CCF-CSP计算机软件能力认证考试
http://118.190.20.162/home.page
CCF 官方题解请点击 这里 。
阅读本题解前,您应当了解下列知识:
这是一份以C++代码编写的CSP 专业组 202209题解。
请注意这 不 是CSP-S/J的中学生竞赛的题解。
现将模拟测试系统中的得分列举如下:
题目 | 得分 | 时间 | 内存 |
---|---|---|---|
如此编码 | 100 | 15ms | 2.898MB |
何以包邮 | 100 | 15ms | 7.796MB |
防疫大数据 | 100 | 500ms | 34.66MB |
吉祥物投票 | 100 | 171ms | 17.42MB |
高维亚空间超频物质变压缩技术 | 100 | 140ms | 6.835MB |
1. 如此编码
题目的提示已经非常充足了,没什么好说的。
#include<bits/stdc++.h> using namespace std; int a[30],c[30],cb[30]; int main() { int n,m;c[0]=1;cb[0]=0; ios::sync_with_stdio(false); cin>>n>>m; for(int i=1;i<=n;i++) { cin>>a[i]; for(int i=1;i<=n;i++){ c[i]=a[i]*c[i-1]; cb[i]=m%c[i]; cout<<(cb[i]-cb[i-1])/c[i-1]<<" "; return 0;
2. 何以包邮?
标准的0-1背包,也没什么好说的。
如果您对“背包问题”了解甚少,您可以阅读崔添翼《背包九讲》。该链接指向原作者在Github上发布的文章,建议下载其中的pdf版本进行阅读。
#include<bits/stdc++.h> using namespace std; int a[31]; int f[31][300001]; //f[i][j]表示考虑前i件物品,容积为j时最多能装的体积 int main() { int n,x,sum=0; ios::sync_with_stdio(false); cin>>n>>x; for(int i=1;i<=n;i++) { cin>>a[i]; sum+=a[i]; x=sum-x; for(int i=1;i<=n;i++) { for(int j=0;j<=x;j++) { if(j-a[i]>=0) f[i][j]=max(f[i-1][j],f[i-1][j-a[i]]+a[i]); f[i][j]=f[i-1][j]; cout<<sum-f[n][x]<<endl; return 0;
3. 防疫大数据
按照题目要求细心的做即可,没什么难度。CSP专业组第3题通常用C++STL操作一下就可以搞定。
#include<bits/stdc++.h> using namespace std; map<int,set<int> > risk_areas; //map[region]={risk_days} void set_risk_area(int day,int area){ auto it=risk_areas.find(area); if(it==risk_areas.end()) { risk_areas[area]=set<int>(); it=risk_areas.find(area); for(int i=0;i<7;i++) it->second.insert(day+i); bool is_risk_area(int day_st,int day_ed,int area){ auto it=risk_areas.find(area); if(it==risk_areas.end()) { return false; }else{ for(int i=day_st;i<=day_ed;i++){ if(it->second.find(i)==it->second.end()) return false; return true; struct mdata{ int d,u,r; void read(){ cin>>d>>u>>r; void print(){ printf("{%d,%d,%d}",d,u,r); bool operator<(const mdata &m)const{ return d<m.d; bool user_is_risk(int today){ if(d<=today-7) return false; return is_risk_area(d,today,r); multiset<mdata> routes_data; void clear_expired_data(int today) { auto it=routes_data.lower_bound({today-6}); routes_data.erase(routes_data.begin(),it); void statistics(int today){ set<int> users; clear_expired_data(today); for(auto itm:routes_data){ //itm.print(); if(itm.user_is_risk(today)){ users.insert(itm.u); cout<<today<<' '; for(auto u:users){ cout<<u<<' '; cout<<endl; int main() { int n,r,m,x; ios::sync_with_stdio(false); cin>>n; for(int i=0;i<n;i++){ cin>>r>>m; for(int j=0;j<r;j++){ cin>>x; set_risk_area(i,x); for(int j=0;j<m;j++){ mdata md;md.read(); routes_data.insert(md); statistics(i); return 0;
4. 吉祥物投票
测试数据1~9: 首先测试数据1-4的做法是显然的,此处不再赘述。我们关注测试数据5-7。鉴于 int len(){return r-l+1;} seg(){} seg(pair<int,pair<int,int> > p):l(p.first),r(p.second.first),x(p.second.second){} const int MAXM=1e5+10; map<int,pair<int,int> > st; int cnt[MAXM]; //操作1:请注意这不是最优代码,实际上会导致相邻两段的x相同而不合并他们的问题 void modify(int l,int r,int x){ auto it=st.upper_bound(l);--it; seg last(*it); while(it!=st.end()){ st.erase(it);cnt[last.x]-=(last.r-last.l+1); if(last.l<l){ st[last.l]={l-1,last.x}; cnt[last.x]+=(l-last.l); if(last.r>r){ st[r+1]={last.r,last.x}; cnt[last.x]+=(last.r-r); break; it=st.upper_bound(l);seg=node(*it); st[l]={r,x};cnt[x]+=(r-l+1); //操作2 void alter(int x,int w){ for(auto it=st.begin();it!=st.end();++it){ if(it->second.second==x) it->second.second=w; cnt[w]+=cnt[x];cnt[x]=0; //操作3 void exchange(int x,int y){ for(auto it=st.begin();it!=st.end();++it){ if(it->second.second==x) it->second.second=y; else if(it->second.second==y) it->second.second=x; swap(cnt[x],cnt[y]); int main() { ios::sync_with_stdio(false); int n,m,q,op,l,r,x,w,y; cin>>n>>m>>q;st[1]={n,0};cnt[0]=n; while(q--){ cin>>op; if(op==1) { cin>>l>>r>>x; modify(l,r,x); }else if(op==2){ cin>>x>>w; alter(x,w); }else if(op==3){ cin>>x>>y; exchange(x,y); }else if(op==4){ cin>>w; cout<<cnt[w]<<endl; }else{//op==5 int maxv=cnt[1],wk=1; for(int i=2;i<=m;++i){ if(cnt[i]>maxv) maxv=cnt[i],wk=i; if(cnt[0]==n) wk=0; cout<<wk<<endl; return 0;
尽管上述代码中并未特别考虑数据8~9,但由于 vector<node*> ptr; void __build(node* rt,int l,int r){ rt->l=l;rt->r=r;rt->mx=0;rt->mxpos=l; if(l==r) { ptr[l]=rt; rt->lc=rt->rc=NULL; return; int m=l+r>>1; rt->lc=new node(rt);rt->rc=new node(rt); __build(rt->lc,l,m); __build(rt->rc,m+1,r); void __upd(node *p,int val){ p->mx=val;int i=p->l; while(p->fa!=NULL){ node* ac=(p==p->fa->lc)?p->fa->rc:p->fa->lc; if(ac==NULL || p->mx>ac->mx) p->fa->mx=p->mx,p->fa->mxpos=p->mxpos; else if(p->mx==ac->mx) p->fa->mx=p->mx,p->fa->mxpos=min(p->mxpos,ac->mxpos); else p->fa->mx=ac->mx,p->fa->mxpos=ac->mxpos; p=p->fa; public: void init(int n){ v.resize(n+1,0); ptr.resize(n+1,NULL); rt=new node(NULL); __build(rt,1,n); void update(int i){ if(i>0) __upd(ptr[i],v[i]); int& operator[](int i) {return v[i];} int maxpos(){ if(rt->mx>0) return rt->mxpos; return 0; }cnt; struct seg{ int l,r,x; int len(){return r-l+1;} seg(){} seg(pair<int,pair<int,int> > p):l(p.first),r(p.second.first),x(p.second.second){} map<int,pair<int,int> > st; void modify(int l,int r,int x){ auto it=st.upper_bound(l);--it; seg last(*it); while(it!=st.end()){ st.erase(it);cnt[last.x]-=(last.r-last.l+1); if(last.l<l){ st[last.l]={l-1,last.x}; cnt[last.x]+=(l-last.l); cnt.update(last.x); if(last.r>r){ st[r+1]={last.r,last.x}; cnt[last.x]+=(last.r-r); cnt.update(last.x); break; it=st.upper_bound(l);last=seg(*it); st[l]={r,x};cnt[x]+=(r-l+1);cnt.update(x); void alter(int x,int w){ for(auto it=st.begin();it!=st.end();++it){ if(it->second.second==x) it->second.second=w; cnt[w]+=cnt[x];cnt[x]=0; cnt.update(x);cnt.update(w); void exchange(int x,int y){ for(auto it=st.begin();it!=st.end();++it){ if(it->second.second==x) it->second.second=y; else if(it->second.second==y) it->second.second=x; swap(cnt[x],cnt[y]); cnt.update(x);cnt.update(y); int main() { ios::sync_with_stdio(false); int q,op,l,r,x,w,y; cin>>n>>m>>q; cnt.init(m);cnt[0]=n; st[1]={n,0}; while(q--){ cin>>op; if(op==1) { cin>>l>>r>>x; modify(l,r,x); }else if(op==2){ cin>>x>>w; alter(x,w); }else if(op==3){ cin>>x>>y; exchange(x,y); }else if(op==4){ cin>>w; cout<<cnt[w]<<endl; }else{//op==5 cout<<cnt.maxpos()<<endl; return 0;
完整解答: 在上一解决方案的基础上,我们容易发现性能瓶颈在于操作2和操作3,它们每次都会修改所有投票段。我们不妨定义作品的“编号”和“虚拟编号”的概念。开始时,所有的编号和虚拟编号都相同。我们的操作1~5中输入的都是“虚拟编号”,因此输入时将“虚拟编号”先变回“编号”。当操作2发生时,我们记虚拟编号 int n; vector<int> fa,mp,rmp; //fa:每个点属于的集合; mp:虚拟id(1~n)到 真实id(1~n+q)的映射; rmp:真实id(1~n+q)到虚拟id的映射 int find(int x){ return x!=fa[x]?(fa[x]=find(fa[x])):x; public: void init(int n){ this->n=n;fa.resize(n+1);mp.resize(n+1);rmp.resize(n+1); for(int i=0;i<=n;++i)fa[i]=i,mp[i]=i,rmp[i]=i; int operator[](int x){return mp[x];}//将虚拟编号映射为实际编号 int map_back(int x){return rmp[find(x)];}//将实际编号映射为虚拟编号 void exchange(int u,int v){ swap(rmp[mp[u]],rmp[mp[v]]);//交换虚拟id swap(mp[u],mp[v]);//交换真实id void merge(int v,int u){ fa[find(mp[v])]=find(mp[u]);//合并v的真实id到u的真实id rmp[mp[v]]=-1; fa.push_back(++n);//为v创建新的真实id rmp.push_back(v);mp[v]=n; }uf; //线段树节点 struct node{ int l,r,mx,mxpos; node *lc,*rc,*fa; node(node* f):fa(f){} //线段树 class seg_tree{ node *rt; vector<int> v; vector<node*> ptr; void __build(node* rt,int l,int r){ rt->l=l;rt->r=r;rt->mx=0;rt->mxpos=l; if(l==r) { ptr[l]=rt; rt->lc=rt->rc=NULL; return; int m=l+r>>1; rt->lc=new node(rt);rt->rc=new node(rt); __build(rt->lc,l,m); __build(rt->rc,m+1,r); void __upd(node *p,int val){ p->mx=val;int i=p->l; while(p->fa!=NULL){ node* ac=(p==p->fa->lc)?p->fa->rc:p->fa->lc; if(ac==NULL || p->mx>ac->mx) p->fa->mx=p->mx,p->fa->mxpos=p->mxpos; else if(p->mx==ac->mx) p->fa->mx=p->mx,p->fa->mxpos=min(p->mxpos,ac->mxpos); else p->fa->mx=ac->mx,p->fa->mxpos=ac->mxpos; p=p->fa; public: void init(int n){ v.resize(n+1,0); ptr.resize(n+1,NULL); rt=new node(NULL); __build(rt,1,n); void update(int i){ if(i>0) __upd(ptr[i],v[i]); int& operator[](int i) {return v[i];} int maxpos(){ if(rt->mx>0) return rt->mxpos; return 0; }cnt; struct seg{ int l,r,x; int len(){return r-l+1;} seg(){} seg(pair<int,pair<int,int> > p):l(p.first),r(p.second.first),x(p.second.second){} map<int,pair<int,int> > st; void modify(int l,int r,int tx){ int x=uf[tx];//获取tx的真实id auto it=st.upper_bound(l);--it; seg last(*it); while(it!=st.end()){ int mb_lastx=uf.map_back(last.x); st.erase(it);cnt[mb_lastx]-=(last.r-last.l+1); if(last.l<l){ st[last.l]={l-1,last.x}; cnt[mb_lastx]+=(l-last.l); cnt.update(mb_lastx); if(last.r>r){ st[r+1]={last.r,last.x}; cnt[mb_lastx]+=(last.r-r); cnt.update(mb_lastx); break; it=st.upper_bound(l);last=seg(*it); st[l]={r,x};cnt[tx]+=(r-l+1);cnt.update(tx); void alter(int x,int w){ uf.merge(x,w); cnt[w]+=cnt[x];cnt[x]=0; cnt.update(x);cnt.update(w); void exchange(int x,int y){ uf.exchange(x,y); swap(cnt[x],cnt[y]); cnt.update(x);cnt.update(y); int main() { ios::sync_with_stdio(false); int q,op,l,r,x,w,y; cin>>n>>m>>q; cnt.init(m);cnt[0]=n; st[1]={n,0};uf.init(m); while(q--){ cin>>op; if(op==1) { cin>>l>>r>>x; modify(l,r,x); }else if(op==2){ cin>>x>>w; alter(x,w); }else if(op==3){ cin>>x>>y; exchange(x,y); }else if(op==4){ cin>>w; cout<<cnt[w]<<endl; }else{//op==5 cout<<cnt.maxpos()<<endl; return 0;
5. 高维亚空间超频物质变压缩技术
子任务1: 暴力枚举分段点,然后对每一段逐个判断。期望得分:10分,代码略。时间复杂度: int n,L,x; cin>>n>>L; for(int i=1;i<=n;++i) {cin>>x;s[i]=s[i-1]+x;} for(int i=1;i<=n;++i) {cin>>m[i];f[i]=LONG_LONG_MAX;} f[0]=0; for(int i=1;i<=n;++i) for(int j=0;j<i;++j)//枚举最后一段的开头,上一段的结尾即j-1 if(m[i]>m[j]) f[i]=min(f[i],f[j]+sqr(s[i]-s[j]-L)); //更优时更新 cout<<f[n]<<endl; return 0;
子任务3: 由于保证 ki的点。
注意到每次有新点加入凸壳时,其横坐标是递增的,而每次查询时所用的斜率也是递增的。这种双递增的特性使得我们可以使用单调队列来实现这个凸壳的维护。
#include<bits/stdc++.h> using namespace std; typedef long long ll; typedef long double ld; const int MAXN=1e5+10; int que[MAXN],head,tail; ll s[MAXN];//前缀和 int m[MAXN];//神秘学质量 ll f[MAXN],L;//f[i]表示最后一段以第i个物品结尾时的总成本 inline ll sqr(ll x){return x*x;} inline ll X(int j){return s[j];} inline ll K(int i){return 2ll*s[i];} inline ll Y(int j){return f[j]+s[j]*(s[j]+2*L);} inline ld slope(int i,int j) {return (Y(i)-Y(j))/(ld)(X(i)-X(j));} int main(){ ios::sync_with_stdio(false); int n,x; cin>>n>>L; for(int i=1;i<=n;++i) {cin>>x;s[i]=s[i-1]+x;} for(int i=1;i<=n;++i) {cin>>m[i];} f[0]=0; que[head=tail=1]=0; for(int i=1;i<=n;++i) { while(head<tail && slope(que[head],que[head+1])<=K(i)) ++head; int j=que[head]; cout<<j<<" "<<endl; f[i]=f[j]+sqr(s[i]-s[j]-L); while(head<tail && slope(que[tail],i)<=slope(que[tail-1],que[tail])) --tail; que[++tail]=i; cout<<f[n]<<endl; return 0;
子任务4: 在子任务3的基础上,我们不再有 O(nlog2n)。
所谓CDQ分治,是2008年中国信息学竞赛国家队选手陈丹琪(Chen Danqi)在集训期间归纳的一种针对具有二维偏序特征序列的一种分治算法。详情请见这里,论文原稿请点这里。
对于本题,其动态规划方程如下: bool cmp_id(gold a,gold b){return a.id<b.id;} //按质量排序 bool cmp_mass(gold a,gold b){return a.mass==b.mass?(a.id<b.id):(a.mass<b.mass);} ll s[MAXN];//前缀和 ll f[MAXN];//f[i]表示最后一段以第i个物品结尾时的总成本 ll L; inline ll sqr(ll x){return x*x;} inline void update(int i,int j) {//用编号为j的黄金去更新编号为i的黄金 f[i]=min(f[i],f[j]+sqr(s[i]-s[j]-L)); inline ll X(int j){return s[j];} inline ll K(int i){return 2ll*s[i];} inline ll Y(int j){return f[j]+s[j]*(s[j]+2*L);} inline ld slope(int i,int j) {return (Y(i)-Y(j))/(ld)(X(i)-X(j));} void solve( int l,//要考虑的神秘学质量下限 int r,//要考虑的神秘学质量上限 gold* lb,//神秘学质量下限l在数组a中的开始地址 gold* rb //神秘学质量上限r在数组a中的结束地址+1 if(l>=r) return; //l==r时,该点已经计算好了,不用再算了。 if(rb<=lb) return; int m=l+r>>1; //用神秘学质量<=m的点去更新神秘学质量>m的点 //首先需要找到a数组中 m与 m+1的分界线的下标 auto mb=upper_bound(lb,rb,(gold){MAXN,m},cmp_mass); //mb是第一个神秘学质量>m的下标 solve(l,m,lb,mb); //先求解神秘学质量在l~m的位置 //仿照子任务3,用单调队列维护lb~mb-1的点构成的下凸壳 //由于目前按照神秘学质量排序,我们需要重新变回原来的输入顺序 int cntl=mb-lb,cntr=rb-mb;//<=m的个数,>m的个数 //拷贝并变回输入顺序 vector<gold> tmpl(lb,mb),tmpr(mb,rb); sort(tmpl.begin(),tmpl.end(),cmp_id); sort(tmpr.begin(),tmpr.end(),cmp_id); //用斜率优化的方法更新 que[head=tail=1]=0; auto itl=tmpl.begin(); for(auto itr=tmpr.begin();itr!=tmpr.end();++itr) { int i=itr->id; //开始更新f[i] //编号小于待更新者进入单调队列 for(;itl!=tmpl.end() && itl->id<i;++itl){ int j=itl->id; while(head<tail && slope(que[tail],j)<slope(que[tail-1],que[tail])) --tail; que[++tail]=j; //按照K(i)单调的原则,弹出队首小于K(i)的元素 while(head<tail && slope(que[head],que[head+1])<K(i)) ++head; //留下的队首用于更新f[i] update(i,que[head]); //最后求解神秘学质量在m+1~r的位置 solve(m+1,r,mb,rb); int main(){ ios::sync_with_stdio(false); int n,x; cin>>n>>L; for(int i=1;i<=n;++i) {cin>>x;s[i]=s[i-1]+x;} for(int i=1;i<=n;++i) {cin>>a[i].mass;a[i].id=i;f[i]=LONG_LONG_MAX;} f[0]=0; //初值f[0]=0,其他f[]均为无穷大 sort(a+1,a+n+1,cmp_mass); //将点按照神秘学质量进行排序 solve(0,n,a+1,a+n+1); //以神秘学质量为偏序进行分治 cout<<f[n]<<endl; return 0;
作者本人在本次CSP测试中取得的成绩如下:
今日(2022年9月18日)参加CSP专业级认证,不甚理想,考前目标400,结果仅330(100+100+100+30+0),仔细想来,考试过程中出现诸多问题,在此一一列举,与诸位共勉。
过几天这篇帖子也许会改成一份题解。
做题的心路历程
T1:送分题。对照题目的提示,哐哐哐就出来了。但是由于我码字过慢,30min才写完交掉。 100分
T2:基础题。标准的0-1背包,简单DP一下就OK。但是由于我基础不牢,对自己的DP技术毫无自信,先去写了T3才来写T2,用时30min。 100分
T3:阅读理解题。由于题目太长太绕,不得不先花30min读题,边读边写代码,总体思想就是用STL一通乱搞,搞了4~5把过了。用时60min。100分
T4:不会做。感觉像线段树,但是发现怎么都搞不定。就这样折腾了60min。究其原因,是因为我既想维护每个人的选项,又想维护每个作品的得票数。而且这个操作1不会只影响某个作品的得票数,因为它实质上从别人那里抢票过来。所以感觉很难办。加之 l−r≤10和m=1$的情况,来不及了。 感觉比往年的T4难一些。30分
T5:不会做。盯着题目看了20分钟,没有灵感,花了10min写10分暴力。一遍没搞定,慌了,赶紧跑去写T4暴力,最后T5爆0。
另外,考场电脑慢了5min,到了17:26想交一把,发现考试已结束。晕。说不定还能碰10分呢。
前三题通常不难,建议按顺序做,60min内搞定。具体分配为10min+15min+35min。
剩下的时间集中攻关4,5题,而且一旦放弃某一题,就不要再看了。鄙人的经验是:反复横跳时哪一个都搞不定。个人感觉,以我自己的水平,上来对着T5写个暴力交掉就放弃T5,然后攻关T4尽可能多拿分比较合适。