2021年第十二届蓝桥杯大赛软件类决赛C/C++大学A组真题
Preface
突然想起来蓝桥杯临近,赶紧补一补题
这场前6题好像还是上个月写的来着,好多都记不太清了了,不过都是Easy题也无伤大雅
总体来说这场的难度感觉挺大的,尤其是最后一题已经高于绝大部分的省选题难度了(无所谓我会投降)
而且后面的一些题要么不好想要么不好写,感觉如果当时去考这场的话如果策略不好可能要大败而归了\kel
纯质数
枚举+模拟题不解释,答案是1903
#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
const int N=20210605;
int pri[N+5],cnt,ans; bool vis[N+5];
inline void init(CI n)
{
vis[0]=vis[1]=1; for (RI i=2,j;i<=n;++i)
{
if (!vis[i]) pri[++cnt]=i;
for (j=1;j<=cnt&&i*pri[j]<=n;++j)
if (vis[i*pri[j]]=1,i%pri[j]==0) break;
}
}
int main()
{
init(N); for (RI i=1;i<=N;++i) if (!vis[i])
{
int tmp=i; bool flag=1;
for (;tmp&&flag;tmp/=10) if (vis[tmp%10]) flag=0;
ans+=flag;
}
return printf("%d",ans),0;
}
完全日期
细节更多一些的模拟题,答案977
#include<cstdio>
#include<iostream>
#include<cmath>
#define RI register int
#define CI const int&
using namespace std;
const int mouth[13]={0,31,28,31,30,31,30,31,31,30,31,30,31};
int ans;
int main()
{
auto fix=[&](CI y,CI m)
{
if (m!=2) return false;
return y%400==0||(y%4==0&&y%100!=0);
};
auto calc=[&](int x)
{
int ret=0; while (x) ret+=x%10,x/=10; return ret;
};
for (RI y=2001,m,d;y<=2021;++y)
for (m=1;m<=12;++m) for (d=1;d<=mouth[m]+fix(y,m);++d)
{
int cur=calc(y)+calc(m)+calc(d);
int s=sqrt(cur); if (s*s==cur) ++ans;
}
return printf("%d",ans),0;
}
最小权值
题目把转移式子给你了,直接记忆化搜索一下即可,复杂度是\(O(n^2)\)的,答案2653631372
#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
const int N=2021;
long long f[N+5];
inline long long F(CI x)
{
if (f[x]) return f[x]; if (!x) return 0; long long ret=1e18;
for (RI i=0;i<=x-1;++i) ret=min(ret,1+2LL*F(i)+3LL*F(x-1-i)+1LL*i*i*(x-1-i));
return f[x]=ret;
}
int main()
{
return printf("%lld",F(N)),0;
}
覆盖
爆搜即可,数据范围不大稍等一会就能跑出来,答案12988816
#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
const int N=8;
int ans; bool a[N+5][N+5];
inline void DFS(CI cs=0)
{
if (cs==N*N/2) return (void)(++ans);
RI i,j; for (i=1;i<=N;++i) for (j=1;j<=N;++j) if (!a[i][j]) goto loop;
loop:
if (j+1<=N&&!a[i][j+1]) a[i][j]=a[i][j+1]=1,DFS(cs+1),a[i][j]=a[i][j+1]=0;
if (i+1<=N&&!a[i+1][j]) a[i][j]=a[i+1][j]=1,DFS(cs+1),a[i][j]=a[i+1][j]=0;
}
int main()
{
return DFS(),printf("%d",ans),0;
}
123
没啥好说的,先一元二次方程解出前面整段的有多少个,然后分别计算即可,复杂度大概是\(O(1)\)单组询问
#include<cstdio>
#include<iostream>
#include<cmath>
#define RI register int
#define CI const int&
using namespace std;
const int N=2000000;
int t; long long l,r,pfx[N+5];
int main()
{
//freopen("CODE.in","r",stdin); freopen("CODE.out","w",stdout);
auto calc=[&](const long long& x)
{
int s=(sqrt(1+8LL*x)-1)*0.5;
while (1LL*s*(s+1)/2LL>x) --s;
while (1LL*(s+1)*(s+2)/2LL<=x) ++s;
long long left=x-1LL*s*(s+1)/2LL;
return pfx[s]+left*(left+1)/2LL;
};
for (RI i=1;i<=N;++i) pfx[i]=pfx[i-1]+1LL*i*(i+1)/2LL;
for (scanf("%d",&t);t;--t) scanf("%lld%lld",&l,&r),printf("%lld\n",calc(r)-calc(l-1));
return 0;
}
异或变换
这题一个月之前找规律做的,然后现在暴力的代码找不到了对着最后交的代码看了会好像也看不出啥东西来
大致思路就是通过暴力然后分析最后每一位上的数是原来那些位上数的异或和
然后发现这些位置间大概有某种二进制下拆分开来的规律,我记得当时对着一些小情况看了下就找到了
时间久了都忘完了就体谅下吧,复杂度好像是\(O(n\log t\log n)\)的(逃
#include<cstdio>
#include<iostream>
#include<vector>
#define RI register int
#define CI const int&
using namespace std;
const int N=10005;
int n,a[N]; long long t; char s[N];
int main()
{
//freopen("CODE.in","r",stdin); freopen("CODE.out","w",stdout);
RI i,j; for (scanf("%d%lld%s",&n,&t,s+1),i=1;i<=n;++i) a[i]=s[i]-'0';
for (i=1;i<=n;++i)
{
vector <int> S,T; S.push_back(i);
for (j=16;~j;--j) if ((t>>j)&1LL)
{
T=S; for (int x:S) if (x-(1<<j)>0) T.push_back(x-(1<<j)); S=T;
}
int ret=0; for (int x:S) ret^=a[x]; putchar(ret+'0');
}
return 0;
}
冰山
这题好像一个月前看了眼感觉用FHQ_Treap维护下所有权值和个数的二元组
然后整体加法用打全局标记解决,删去小冰山和分裂大冰山都是经典的按权值split
操作
但是要知道在比赛的时候没有板子的情况下以老年闪总的水平肯定是码不出FHQ_Treap的,所以今天再看了一眼发现好像可以用map
直接水过
思路其实和上面一样,然后删除和分裂全部暴力遍历着做,而且仔细分析下复杂度其实挺对的
因为删除操作显然当删去一个数后它后面就不会再造成复杂度上的影响了,由于里面总元素是\(O(n+m)\)级别的,所以这部分复杂度是对的
然后分裂的过程我们发现每次其实相当于把一堆本来权值不同的元素变成了两个元素,因此分裂我们可以看作删除+插入两个元素,这么看复杂度也是对的
因此本题就做完了,注意下取模的细节,以及一些STL的防RE问题
(好像map
快得一批直接吊打了Luogu上各种平衡树的做法,代码最短跑的还最快)
#include<cstdio>
#include<iostream>
#include<map>
#define int long long
#define RI register int
#define CI const int&
#define fi first
#define se second
using namespace std;
const int mod=998244353;
int n,m,k,x,y,dlt,num,ans; map <int,int> rst;
inline void inc(int& x,CI y)
{
if ((x+=y)>=mod) x-=mod;
}
inline void dec(int& x,CI y)
{
if ((x-=y)<0) x+=mod;
}
signed main()
{
//freopen("CODE.in","r",stdin); freopen("CODE.out","w",stdout);
RI i; for (scanf("%lld%lld%lld",&n,&m,&k),i=1;i<=n;++i)
scanf("%lld",&x),++rst[x],++num,inc(ans,x%mod);
for (i=1;i<=m;++i)
{
scanf("%lld%lld",&x,&y); dlt+=x;
if (x>0)
{
inc(ans,x*num%mod); if (!rst.empty())
{
auto it=--rst.end(); while (it->fi+dlt>k)
{
inc(num,(it->fi+dlt-k)*it->se%mod);
inc(rst[1-dlt],(it->fi+dlt-k)*it->se%mod);
inc(rst[k-dlt],it->se); if (it==rst.begin()) break; else --it;
}
if (it->fi+dlt<=k) ++it; rst.erase(it,rst.end());
}
} else
{
dec(ans,-x*num%mod); auto it=rst.begin();
while (it!=rst.end()&&it->fi+dlt<=0)
inc(ans,-(it->fi+dlt)*it->se%mod),dec(num,it->se),++it;
rst.erase(rst.begin(),it);
}
if (y) inc(rst[y-dlt],1),inc(num,1),inc(ans,y%mod);
printf("%lld\n",ans);
}
return 0;
}
翻转括号序列
好题,蓝桥杯能有这么好的题真是让人肃然起敬(其实这场的代码题都挺好的)
首先考虑没有修改操作怎么做,我们看到合法括号序列就想到把左括号看成\(1\),右括号看成\(-1\)
然后考虑求出前缀和数组\(pfx\)之后,对于一个询问点\(l\),要求的就是最靠右的点\(r\),满足\(pfx_r=pfx_{l-1}\)且\(\forall i\in[l,r],pfx_i\ge pfx_{l-1}\)
乍一看不好直接处理,因此我们分成两部分,先找出\([l,n]\)中最靠左的满足\(pfx_{pos}<pfx_{l-1}\)的位置\(pos\)(若没有则为\(n+1\))
然后再在\([1,pos-1]\)中找到最靠右的满足\(pfx_r\le pfx_{l-1}\)的位置\(r\),最后若\(r\le l\)则无解,否则答案就是\(r\)
以上的过程我们只要维护区间内\(pfx\)的最小值就可以在线段树上二分完成,接下来考虑加入修改操作怎么处理
首先是一个处理区间反转的经典套路,反转\([l,r]\)等价于反转\([1,l-1]\)和\([1,r]\),因此接下来仅考虑如何处理反转\([1,x]\)
不难发现记反转前\(pfx_x=v\),则反转后对于\([x+1,n]\)的\(pfx_i\)相当于全部减去了\(2v\)
然后对于区间\([1,x]\)的\(pfx_i\)的影响相当于区间乘上了个\(-1\),此时我们要维护区间最小值的话就要额外维护下区间最大值,然后乘\(-1\)的时候交换最大最小值并取反即可
然后这题就做完了,注意下各种操作的顺序,总复杂度\(O(n\log n)\)
#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
const int N=1000005;
int n,m,pfx[N],opt,l,r; char s[N];
class Segment_Tree
{
private:
struct segment
{
int mi,mx,add,rev;
}node[N<<2];
#define MI(x) node[x].mi
#define MX(x) node[x].mx
#define A(x) node[x].add
#define R(x) node[x].rev
#define TN CI now=1,CI l=1,CI r=n
#define LS now<<1,l,mid
#define RS now<<1|1,mid+1,r
inline void apply_add(CI now,CI mv)
{
MI(now)+=mv; MX(now)+=mv; A(now)+=mv;
}
inline void apply_rev(CI now)
{
swap(MI(now),MX(now)); MI(now)*=-1; MX(now)*=-1; A(now)*=-1; R(now)^=1;
}
inline void pushup(CI now)
{
MI(now)=min(MI(now<<1),MI(now<<1|1)); MX(now)=max(MX(now<<1),MX(now<<1|1));
}
inline void pushdown(CI now)
{
if (R(now)) apply_rev(now<<1),apply_rev(now<<1|1),R(now)=0;
if (A(now)) apply_add(now<<1,A(now)),apply_add(now<<1|1,A(now)),A(now)=0;
}
public:
inline void build(TN)
{
if (l==r) return (void)(MI(now)=MX(now)=pfx[l]);
int mid=l+r>>1; build(LS); build(RS); pushup(now);
}
inline int query_val(CI pos,TN)
{
if (l==r) return MI(now); int mid=l+r>>1; pushdown(now);
if (pos<=mid) return query_val(pos,LS); else return query_val(pos,RS);
}
inline void modify_add(CI beg,CI end,CI mv,TN)
{
if (beg<=l&&r<=end) return apply_add(now,mv); int mid=l+r>>1; pushdown(now);
if (beg<=mid) modify_add(beg,end,mv,LS); if (end>mid) modify_add(beg,end,mv,RS); pushup(now);
}
inline void modify_rev(CI beg,CI end,TN)
{
if (beg<=l&&r<=end) return apply_rev(now); int mid=l+r>>1; pushdown(now);
if (beg<=mid) modify_rev(beg,end,LS); if (end>mid) modify_rev(beg,end,RS); pushup(now);
}
inline int query_bound(CI pos,CI val,TN)
{
if (l==r) return l; int mid=l+r>>1,ret=n+1; pushdown(now);
if (pos<=mid&&MI(now<<1)<val) ret=query_bound(pos,val,LS);
if (ret!=n+1) return ret;
if (MI(now<<1|1)<val) ret=query_bound(pos,val,RS); return ret;
}
inline int query_forward(CI pos,CI val,TN)
{
if (l==r) return l; int mid=l+r>>1,ret=0; pushdown(now);
if (pos>mid&&MI(now<<1|1)<=val) ret=query_forward(pos,val,RS);
if (ret) return ret;
if (MI(now<<1)<=val) ret=query_forward(pos,val,LS); return ret;
}
#undef Mi
#undef Mx
#undef A
#undef R
#undef TN
#undef LS
#undef RS
}SEG;
inline void reverse(CI pos)
{
if (!pos) return; int pre=SEG.query_val(pos);
SEG.modify_rev(1,pos); if (pos!=n) SEG.modify_add(pos+1,n,-2*pre);
}
int main()
{
//freopen("CODE.in","r",stdin); freopen("CODE.out","w",stdout);
RI i; for (scanf("%d%d%s",&n,&m,s+1),i=1;i<=n;++i)
pfx[i]=pfx[i-1]+(s[i]=='('?1:-1);
for (SEG.build(),i=1;i<=m;++i)
if (scanf("%d%d",&opt,&l),opt==1)
scanf("%d",&r),reverse(l-1),reverse(r); else
{
int pre=l!=1?SEG.query_val(l-1):0;
int lim=SEG.query_bound(l,pre);
r=SEG.query_forward(lim-1,pre);
printf("%d\n",r<=l?0:r);
}
return 0;
}
异或三角
好题,前面本来想了个大概\(O(\log^3 a_i)\)单组数据的找性质做法,感觉预处理优化下可以做到\(O(\log^2 a_i)\),但细节挺多不太好写就没写
大致思路就是首先仅考虑统计\(a>b>c\)的三元组,由于不存在两个数相同,因此最后答案乘上\(6\)即可
然后考虑如果枚举\(a\),考虑对\(a\)二进制分解后从前往后讨论\(b,c\)的每一位的取值,会发现在出现在开头的一段\(0\)的取法是固定的
同时记后面\(0\)的个数为\(c_0\),\(1\)的个数为\(c_1\),则贡献为\((2^{c_0}-1)\times (2^{c_1}-1)\),这个手玩一下还是很好理解的
然后要算答案的话就是枚举最高位,再枚举开头的\(0\)的个数,再枚举后面\(0\)的个数,但是由于要满足最大数小于等于\(n\)所以不太好写的说
那有没有简单好想好写的做法呢,其实这种和进制相关且有上界的问题,数位DP肯定是最方便的
和上面有一点区别,我们考虑统计出\(a>b,a>c\)的合法方案数,最后乘\(3\)就是答案,在数位DP的时候卡上界就仅考虑\(a\)的取法即可
现在考虑剩下的条件怎么满足,二进制异或和为\(0\)则说明每一位的填法其实只有四种,即\((0,0,0),(0,1,1),(1,0,1),(1,1,0)\)
然后对于\(a>b\),我们发现只要满足存在某一位\(k\)使得\(a_k=1,b_k=0\),且在第\(k\)位之前的更高位\(s\),\(a_s=b_s\)
因此我们可以用一个\(0/1\)状态表示是否出现过某一位\(a_k=1,b_k=0\),对于\(a>c\)的限制亦同理
最后就是\(a<b+c\)的限制,不难发现只要存在一位的取法是\((0,1,1)\)最后的答案就一定满足这个限制
但是要注意要加入这个状态时,必须要满足之前某一位出现过\(a_p=1,b_p=0\),\(a_q=1,c_q=0\),即之前的两个限制都满足
因此这题就很简单了,我们设\(f_{i,0/1,0/1,0/1,0/1}\)表示处理了前\(i\)位,\(a>b,a>c,a<b+c\)的限制是否完成,卡上界的状态为\(0/1\)的状态的方案数,转移就显然了
单组数据\(O(2^4\times \log a_i)\),可以轻松通过此题
#include<cstdio>
#include<iostream>
#include<cstring>
#define RI register int
#define CI const int&
using namespace std;
const int N=35;
int t,n,d[N],len; long long f[N][2][2][2][2];
inline long long DP(CI now,CI s1,CI s2,CI s3,CI ub)
{
if (now<0) return s1&&s2&&s3;
if (~f[now][s1][s2][s3][ub]) return f[now][s1][s2][s3][ub];
long long ret=0; for (RI i=0;i<=(ub?d[now]:1);++i)
{
if (i==0)
{
ret+=DP(now-1,s1,s2,s3,ub&&(i==d[now]));
if (s1&&s2) ret+=DP(now-1,s1,s2,s3|1,ub&&(i==d[now]));
} else
{
ret+=DP(now-1,s1|1,s2,s3,ub&&(i==d[now]));
ret+=DP(now-1,s1,s2|1,s3,ub&&(i==d[now]));
}
}
return f[now][s1][s2][s3][ub]=ret;
}
int main()
{
//freopen("CODE.in","r",stdin); freopen("CODE.out","w",stdout);
for (scanf("%d",&t);t;--t)
{
scanf("%d",&n); for (len=-1;n;d[++len]=n%2,n/=2);
memset(f,-1,sizeof(f)); printf("%lld\n",3LL*DP(len,0,0,0,1));
}
return 0;
}
积木
苦思冥想两节大物课,最后通透了写个暴力DP才是我的归宿
设\(f_{i,j}\)表示处理了前\(i\)行,第\(i\)行有\(w+j\)块积木的方案数,转移很显然:
随便用前缀和优化一下就能做到\(O(n^2(R-L+1))\)的复杂度
然后考虑把答案分三段计算,先DP求出\(1\sim x\)行的答案,然后枚举第\(x\)行的取值\(p\)
则第\(y\)行的取值就应该是\(q=z\times (w+p)-w\),对所有可能的\((p,q)\)统计方案数即可
具体做法就是暴力地把DP的第\(x\)行状态仅保留\(f_{x,p}\),其余全部置\(0\),然后从\(x+1\sim y\)跑暴力DP求出此时\(f_{y,q}\)的值即可
注意最后\(y+1\sim n\)的行的取法是任意的,因此还要乘上\((R-L+1)^{n-y}\)
总复杂度看起来挺爆炸的,但由于合法的\((p.q)\)数量很少,因此可以跑过本题35pts的数据(主要再往后数组开不下了)
#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
const int N=505,mod=998244353;
int n,w,L,R,x,y,z,ans,f[N][N*10],g[N][N*10],pfx[N][N*10];
inline int sum(CI x,CI y)
{
return x+y>=mod?x+y-mod:x+y;
}
inline int sub(CI x,CI y)
{
return x-y<0?x-y+mod:x-y;
}
int main()
{
//freopen("CODE.in","r",stdin); freopen("CODE.out","w",stdout);
RI i,j,k; scanf("%d%d%d%d%d%d%d",&n,&w,&L,&R,&x,&y,&z);
for (--x,--y,--n,f[0][0]=i=1;i<=x;++i) for (j=L*i;j<=R*i;++j)
for (k=j-R;k<=j-L;++k) if (k>=L*(i-1)&&k<=R*(i-1)) f[i][j]=sum(f[i][j],f[i-1][k]);
int coef=1; for (i=y+1;i<=n;++i) coef=1LL*coef*(R-L+1)%mod;
for (i=L*x;i<=R*x;++i)
{
long long tar=1LL*z*(w+i)-w; if (tar<L*y||tar>R*y) continue;
for (j=L*x;j<=R*x;++j) g[x][j]=0; g[x][i]=f[x][i];
for (j=L*x;j<=R*x;++j) pfx[x][j]=sum(pfx[x][j-1],g[x][j]);
for (j=x+1;j<=y;++j) for (k=L*j;k<=R*j;++k)
pfx[j][k]=sum(pfx[j][k-1],g[j][k]=sub(pfx[j-1][min(R*(j-1),k-L)],pfx[j-1][min(R*(j-1),k-R-1)]));
ans=sum(ans,1LL*g[y][tar]*coef%mod);
}
return printf("%d",ans),0;
}
然后看了眼题解上来就是什么生成函数云云,而且涉及到很多技巧比如系数平移(还要平移两次),甚至因为最后\(n(R-L)\)的上界来到\(2\times 10^7\)连NTT都败下阵来,要递推求解一个复杂式子的解
具体可以自己看Luogu上的题解,反正我是做不来的说
Postscript
平心而论这场如果现场打后面三题都不一定写的出来,线段树上二分那个断断续续想了一上午,数位DP那个想前面那个做法也花了挺久的
不过就像前面说的这场真是蓝桥杯为数不多的好题了,题面也写的很专业,部分分区分度也很足,给出题人狠狠地点赞