2021年蓝桥杯第十二届省赛真题

Preface

做一下21年的省赛题目,虽然确实感觉比22年的简单了一些

但由于没有数据结构题所以很多题要动脑子做起来还是挺慢的说

之前还豪言壮语一周两套,结果事情太多属实没时间,苦路西


卡片

直接暴力枚举即可,答案3181

#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
int c[10],t[10];
int main()
{
	RI i,j; for (i=0;i<10;++i) c[i]=2021;
	for (i=1;;++i)
	{
		for (j=0;j<10;++j) t[j]=0; int tmp=i;
		while (tmp) ++t[tmp%10],tmp/=10;
		for (j=0;j<10;++j) if (c[j]<t[j]) return printf("%d",i-1),0; else c[j]-=t[j];
	}
	return 0;
}

直线

直接暴力枚举即可,特判掉所有横着和竖着的直线,再用斜率和截距来判断直线是否重复

懒得写重载两个元素等于的set了,反正是题答直接暴力枚举了,在自己电脑上跑的还挺快

不过要注意一下精度,答案为40257

#include<cstdio>
#include<iostream>
#include<cmath>
#define RI register int
#define CI const int&
using namespace std;
const double EPS=1e-8;
double k[500000],b[500000]; int cnt;
int main()
{
	RI x1,y1,x2,y2,i;
	for (x1=0;x1<=19;++x1) for (y1=0;y1<=20;++y1)
	for (x2=0;x2<=19;++x2) for (y2=0;y2<=20;++y2)
	if (x1!=x2&&y1!=y2)
	{
		double K=1.0*(y2-y1)/(x2-x1),B=-K*x1+y1; bool flag=1;
		for (i=1;i<=cnt&&flag;++i) if (fabs(K-k[i])<EPS&&fabs(B-b[i])<EPS) flag=0;
		if (flag) k[++cnt]=K,b[cnt]=B;
	}
	return printf("%d",cnt+41),0;
}

货物摆放

考虑给\(n\)质因数分解,对于每一个质数\(p_i\),它的次数为\(c_i\)

不难发现在只考虑\(p_i\)的情况下,就是相当于要把\(c_i\)分成\(3\)份,其中每一份可以为\(0\)的方案数

利用隔板法,这部分的贡献为\(C_{c_i+2}^2\),最后答案就是把所有的贡献乘起来即可,最后答案为2430

#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
long long ans=1,n=2021041820210418LL;
int main()
{
	for (RI i=2;1LL*i*i<=n;++i) if (n%i==0)
	{
		int k=0; while (n%i==0) ++k,n/=i;
		ans*=1LL*(k+1)*(k+2)/2LL;
	}
	if (n>1) ans*=3LL;
	return printf("%lld",ans),0;
}

路径

SB题,直接暴力跑最短路即可,本来想偷懒写个Floyd的,但还是不折磨电脑了写个SPFA意思一下,答案是10266837

#include<cstdio>
#include<iostream>
#include<vector>
#include<utility>
#include<queue>
#define RI register int
#define CI const int&
#define mp make_pair
using namespace std;
const int n=2021;
vector < pair <int,int> > v[n+5]; int dis[n+5]; queue <int> q; bool vis[n+5];
inline int gcd(CI n,CI m)
{
	return m?gcd(m,n%m):n;
}
int main()
{
	RI i,j; for (i=1;i<=n;++i) for (dis[i]=1e9,j=i+1;j<=n;++j)
	if (j-i<=21) v[i].push_back(mp(j,i*j/gcd(i,j))),v[j].push_back(mp(i,i*j/gcd(i,j)));
	dis[1]=0; q.push(1); vis[1]=1; while (!q.empty())
	{
		int now=q.front(); q.pop(); vis[now]=1;
		for (auto [to,w]:v[now]) if (dis[to]>dis[now]+w)
		if (dis[to]=dis[now]+w,!vis[to]) vis[to]=1,q.push(to);
	}
	return printf("%d",dis[n]),0;
}

砝码称重

傻逼填空题终于都结束了,然后开始傻逼代码题环节

这题本质上就是一个背包,我们把每个砝码看成有两个,一个是正值,一个是负值

然后跑0/1背包即可,不过要注意对于重量为负的物品,枚举的顺序和正常的时候是相反的

#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
const int N=105,S=100000;
int n,a[N],f[(S<<1)+5],ans,sum;
int main()
{
	//freopen("CODE.in","r",stdin); freopen("CODE.out","w",stdout);
	RI i,j; for (scanf("%d",&n),i=1;i<=n;++i) scanf("%d",&a[i]);
	for (f[S]=i=1;i<=n;++i)
	{
		for (sum+=a[i],j=sum;j>=a[i]-sum;--j) f[j+S]|=f[j-a[i]+S];
		for (j=-sum;j<=sum-a[i];++j) f[j+S]|=f[j+a[i]+S];
	}
	for (i=1;i<=sum;++i) ans+=f[i+S];
	return printf("%d",ans),0;
}

异或数列

刚开始题目没看清楚WA的死去活来,后来仔细看了一眼题目才搞出来

首先显然从高位到低位考虑每一个二进制位,然后统计一下这一位上为\(1\)的数的个数\(c\)

不难发现如果\(2|c\)则这一位上一定分不出胜负,因为此时两个人不管怎么分这一位的奇偶性都是相同的

否则若\(2\nmid c\),则分情况讨论下:

  • \(c=1\),此时先手胜,直接拿着这唯一一个\(1\)即可
  • \(2|(n-c)\),即这一位上偶数的个数为偶数,则先手胜
  • \(2\nmid (n-c)\),即这一位上偶数的个数为奇数,则后手胜

后面两种情况的原理很简单,由于操作人可以指定将某个数给另一个人,因此操作到最后一个\(1\)的人一定获胜

因为偶数相当于“轮空”,故此时胜利便取决于偶数的个数,然后如果这一位分不出胜负再看下一位即可

#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
const int N=200005;
int t,n,x,c[20];
int main()
{
	//freopen("CODE.in","r",stdin); freopen("CODE.out","w",stdout);
	for (scanf("%d",&t);t;--t)
	{
		RI i,j; for (i=0;i<20;++i) c[i]=0;
		for (scanf("%d",&n),i=1;i<=n;++i)
		for (scanf("%d",&x),j=0;j<20;++j) if (x&(1<<j)) ++c[j];
		int flag=0; for (i=19;~i&&!flag;--i) if (c[i]&1)
		{
			if (c[i]==1) flag=1; else if ((n-c[i])&1) flag=-1; else flag=1;
		}
		printf("%d\n",flag);
	}
	return 0;
}

左孩子右兄弟

首先骂一嘴出题人,这个什么“左孩子右兄弟”妈的听都没听过,默认大家都会是吧,在题目里写一句会死?

其实一句话说来就是一种把多叉树转化为二叉树的方法,在二叉树上,一个点的左儿子是原树上它的孩子,而这个点的右儿子是原树上它的兄弟

回到这道题我们知道定义后就是个傻逼题了,对于原树上的每个点\(x\),我们记\(f_x\)表示它转成二叉树后最大的树高

显然对于每个点\(x\),我们找出它所有儿子\(y\)\(f_y\)最大的那个,让它作为右儿子链的最底端即可

#include<cstdio>
#include<iostream>
#include<vector>
#define RI register int
#define CI const int&
using namespace std;
const int N=100005;
int n,x,f[N]; vector <int> v[N];
inline void DFS(CI now=1)
{
	f[now]=0; for (int to:v[now]) DFS(to),f[now]=max(f[now],f[to]); f[now]+=v[now].size();
}
int main()
{
	//freopen("CODE.in","r",stdin); freopen("CODE.out","w",stdout);
	RI i; for (scanf("%d",&n),i=1;i<=n;++i) v[i].clear();
	for (i=2;i<=n;++i) scanf("%d",&x),v[x].push_back(i);
	return DFS(),printf("%d",f[1]),0;
}

括号序列

有点意思的DP题,话说这场的一些DP题都不错的说

首先关于最少添加括号数量,这是个经典问题了,我们用老方法把(看作\(1\))看作\(-1\),然后找出前缀和数组中最小的数\(L\)

\(-L\)即为最少要添加的左括号数量,然后我们再直接算出此时需要补上多少个右括号(记为\(R\))即可

我们只考虑添加左括号的算法,而补填右括号的话我们显然可以找出一个分界点\(p\),满足\(pfx_p=L\)\(p\)最小

那么此时所有左括号只能填在\([1,p]\)中,然后我们只要在\([p+1,n]\)中填上\(R\)个右括号即可

不难发现此时如果我们把\([p+1,n]\)的原序列reverse一遍,然后把括号都变成另一种,不难发现此时后面的问题和前面一样了,因此算出两部分的答案然后乘起来即可

那么现在问题就是添加左括号的方案数了,考虑DP,设\(f_{i,j}\)表示做到前\(i\)个括号,当前前缀和为\(j\)的方案数

考虑我们怎么避免计算重复,由于右括号的个数不会增加,因此我们以右括号为分界点来统计方案

\(i\)为一个右括号,我们枚举在它前面放多少个左括号,则\(f_{i,j}=f_{i-1,0}+f_{i-1,1}+\cdots+f_{i-1,j+1}\)

不难发现前面那一部分就是\(f_{i,j-1}\),于是\(f_{i,j}=f_{i,j-1}+f_{i-1,j+1}\)

总复杂度\(O(n^2)\)

#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
#define RI register int
#define CI const int&
using namespace std;
const int N=5005,mod=1e9+7;
int n,a[N],pfx[N],add_l,add_r,f[N][N]; char s[N];
inline int sum(CI x,CI y)
{
	return x+y>=mod?x+y-mod:x+y;
}
inline int calc(CI l,CI r)
{
	RI i,j; for (memset(f,0,sizeof(f)),f[l-1][0]=1,i=l;i<=r;++i)
	if (a[i]==1)
	{
		for (j=1;j<=n;++j) f[i][j]=f[i-1][j-1];
	} else
	{
		for (f[i][0]=sum(f[i-1][1],f[i-1][0]),j=1;j<=n;++j)
		f[i][j]=sum(f[i-1][j+1],f[i][j-1]);
	}
	return f[r][0];
}
int main()
{
	//freopen("CODE.in","r",stdin); freopen("CODE.out","w",stdout);
	RI i,j; for (scanf("%s",s+1),n=strlen(s+1),i=1;i<=n;++i)
	a[i]=s[i]=='('?1:-1,pfx[i]=pfx[i-1]+a[i],add_l=min(add_l,pfx[i]);
	add_l*=-1; add_r=pfx[n]+add_l;
	if (!add_r) printf("%d",calc(1,n)); else
	if (!add_l)
	{
		for (reverse(a+1,a+n+1),i=1;i<=n;++i) a[i]*=-1;
		printf("%d",calc(1,n));
	} else
	{
		int pos; for (i=1;i<=n;++i) if (pfx[i]==-add_l) { pos=i; break; }
		for (reverse(a+pos+1,a+n+1),i=pos+1;i<=n;++i) a[i]*=-1;
		printf("%d",1LL*calc(1,pos)*calc(pos+1,n)%mod);
	}
	return 0;
}

时间显示

因为我没按照上面的顺序来做,所以会有傻逼题反而出现在后面,这题就直接模拟即可

#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
long long t;
int main()
{
	//freopen("CODE.in","r",stdin); freopen("CODE.out","w",stdout);
	scanf("%lld",&t); t%=86400000; t/=1000;
	int h,m,s; s=t%60; t/=60; m=t%60; t/=60; h=t;
	if (h<10) putchar('0'); printf("%d:",h);
	if (m<10) putchar('0'); printf("%d:",m);
	if (s<10) putchar('0'); printf("%d",s);
	return 0;
}

杨辉三角形

妈的\(N=1\)的情况我前面特判输了一个\(0\),然后一直WA一个点还以为是爆long long

直接暴力做显然不显示,我们考虑先固定下组合数\(C_n^m\)中的\(m\),由于要求第一次出现,我们默认\(2m\le n\)

考虑到\(C_{2m}^m\)\(m=20\)时就超过\(10^9\)了,这说明若我们在\([2,20]\)中枚举\(m\)都不能找到对应的\(n\)使得\(C_n^m=N\),则它一定只能在\(C_N^1\)中出现了

那么现在我们只要找到是否存在\(n\)使得\(C_n^m=N\),不难发现当\(m\)固定后\(C_n^m\)关于\(n\)是单调的,可以二分来找

但是在计算\(C_n^m\)的时候为了防止爆long long要一定的技巧,具体实现看代码吧

#include<cstdio>
#include<iostream>
#include<map>
#define RI register int
#define CI const int&
using namespace std;
const long long INF=1e18;
long long n,ans=INF,A[25],B[25];
inline int gcd(CI n,CI m)
{
	return m?gcd(m,n%m):n;
}
inline long long calc(CI x,CI y)
{
	RI i,j,t; for (i=1;i<=y;++i) A[i]=x-i+1,B[i]=i;
	for (j=1;j<=y;++j) for (i=1;i<=y&&B[j]>1;++i)
	t=gcd(A[i],B[j]),A[i]/=t,B[j]/=t;
	long long ret=1; for (i=1;i<=y;++i)
	if ((ret*=A[i])>n) return ret; return ret;
}
inline long long solve(CI k)
{
	int l=0,r=n,mid;
	while (l<=r)
	{
		long long tmp=calc(mid=l+r>>1,k);
		if (tmp==n) return 1LL*mid*(mid+1)/2LL+k+1;
		if (tmp<n) l=mid+1; else r=mid-1;
	}
	return INF;
}
int main()
{
	//freopen("CODE.in","r",stdin); freopen("CODE.out","w",stdout);
	if (scanf("%lld",&n),n==1) return puts("1"),0;
	for (RI i=1;i<=20;++i)	ans=min(ans,solve(i));
	return printf("%lld",ans),0;
}

最少砝码

手玩找下规律就会发现当砝码为\(1,3,9,\cdots,3^n\)时可以达到最优表示

具体证明我也没太细想,手玩一下发现是这样,大概是因为引入了减法之后,最优的构造方法从二进制分解变成了三进制分解

#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
long long n,pw;
int main()
{
	//freopen("CODE.in","r",stdin); freopen("CODE.out","w",stdout);
	pw=1; scanf("%lld",&n); for (RI i=1;;++i)
	if (pw*=3LL,(pw-1)/2>=n) { printf("%d",i); break; }
	return 0;
}

双向排序

刚开始yy了一个设计区间翻转的做法,但感觉蓝桥杯的题肯定不可能要写Splay这类高级数据结构,因此再找一波性质才搞出来

首先我们发现对于一段连续的\(0\)操作,等价于只执行其中\(q_i\)最大的那个,同理对于一段连续的\(1\)操作,等价于只执行其中\(q_i\)最小的那个

因此我们先对操作序列这般处理之后(开头的\(1\)操作也可以去掉),因此现在就要处理\(0/1\)交替的操作序列

我们再考虑若两个连着的\(0\)操作(即中间只隔了一个\(1\)),它们的\(q_i\)分别为\(q'_1,q'_2\)

不难发现如果\(q'_1<q'_2\),则此时前一个\(0\)操作和中间的这个\(1\)操作也可以扔掉,对于\(1\)操作也是同理

因此我们用单调栈来维护整个操作序列,然后我们发现对于\(0\)操作,它的\(q_i\)是单调递减的,因此右边的数会被慢慢确定下来,对于\(1\)操作,其对于左边的确定也是同理

然后最后中间剩下来的这一段只要看最后一个操作时什么类型的即可,具体实现看代码

#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
const int N=100005;
struct Data
{
	int tp,x;
	inline Data(CI Tp=0,CI X=0)
	{
		tp=Tp; x=X;
	}
}stk[N]; int n,m,x,y,a[N],top;
int main()
{
	//freopen("CODE.in","r",stdin); freopen("CODE.out","w",stdout);
	RI i,l,r,k; for (scanf("%d%d",&n,&m),i=1;i<=m;++i)
	if (scanf("%d%d",&x,&y),!x)
	{
		while (top&&!stk[top].tp) y=max(y,stk[top--].x);
		while (top>1&&stk[top-1].x<=y) top-=2; stk[++top]=Data(0,y);
		
	} else if (top)
	{
		while (top&&stk[top].tp) y=min(y,stk[top--].x);
		while (top>1&&stk[top-1].x>=y) top-=2; stk[++top]=Data(1,y);
	}
	for (l=i=1,r=k=n;i<=top&&l<=r;++i)
	if (stk[i].tp)
	{
		while (l<=r&&l<stk[i].x) a[l++]=k--;
	} else
	{
		while (l<=r&&r>stk[i].x) a[r--]=k--;
	}
	if (top&1)
	{
		while (l<=r) a[l++]=k--;
	} else
	{
		while (l<=r) a[r--]=k--;
	}
	for (i=1;i<=n;++i) printf("%d ",a[i]);
	return 0;
}

分果果

之前没想过可以这么处理这种极差最小化的问题,算是学到了一个trick

我们考虑手动枚举一个最大值\(maxv\)(不一定要恰好达到),那么此时的取法就变成了要找出小于\(maxv\)的最大值

一个粗暴的想法是每次二分最优的右端点,但复杂度有点高不是很跑的动(主要\(n\)太小二分了基本相当于没有优化)

那么我们考虑DP,设\(f_{i,j,k}\)表示做到了第\(i\)个区间,其结尾位置为\(j\),第\(i-1\)个区间的结尾位置为\(k\)

转移的话我们再枚举一下第\(i-1\)个区间的选法\(f_{i-1,k,p}\),那么我们可以在\([p,j]\)中任意选择我们的左端点,即

\[f_{i,j,k}=\max(f_{i,j,k},\min(f_{i-1,k,p},pfx_j-pfx_t))\ \ (t\in[p,j]) \]

但直接这么做复杂度还是太高了,我们发现只要稍微修改下状态定义,把第三维变成\(i-1\)个区间的结尾位置小于等于\(k\),此时方程就变成了

\[f_{i,j,k}=f_{i,j,k-1}\\ f_{i,j,k}=\max(f_{i,j,k},\min(f_{i-1,k,p},pfx_j-pfx_p)) \]

于是这题就做完了,总复杂度\(O(n^4m|w|)\),但由于\(w_i\)的随机性因此每次\(p\)往前一般枚举到\(\log\)的步数即可,因此还是可以卡过的

#include<cstdio>
#include<iostream>
#include<cstring>
#define RI register int
#define CI const int&
using namespace std;
const int N=105,M=55;
int n,m,x,mx,avg,ans=1e9,pfx[N],f[M][N][N];
int main()
{
	//freopen("CODE.in","r",stdin); freopen("CODE.out","w",stdout);
	RI v,i,j,k,p; for (scanf("%d%d",&n,&m),i=1;i<=n;++i)
	scanf("%d",&x),mx=max(mx,x),avg+=x,pfx[i]=pfx[i-1]+x;
	if (m==1) return puts("1"),0;
	for (avg=avg*2/(m-1),mx=max(mx,avg/2),v=mx;v<=avg;++v)
	{
		for (memset(f,0,sizeof(f)),f[0][0][0]=v,i=1;i<=m;++i)
		for (j=1;j<=n;++j) for (k=0;k<=j;++k)
		for (k&&(f[i][j][k]=f[i][j][k-1]),p=k;~p;--p)
		if (pfx[j]-pfx[p]<=v)
		f[i][j][k]=max(f[i][j][k],min(f[i-1][k][p],pfx[j]-pfx[p])); else break;
		for (k=0;k<=n;++k) ans=min(ans,v-f[m][n][k]);
	}
	return printf("%d",ans),0;
}

Postscript

这场题目好多啊,写博客都写苦了呜呜呜

posted @ 2023-03-15 17:57  空気力学の詩  阅读(76)  评论(0编辑  收藏  举报