2020年第十一届蓝桥杯大赛软件类省赛C/C++大学A组真题

Preface

受不了了上次做21年把三个组别的都做了,题量有那么一点点大的说

这次只做了A组的题,主要感觉蓝桥杯的题大概看一下难度就差不多得了,水题写多也无宜


门牌制作

直接枚举,答案624

#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int
using namespace std;
int ans=0;
int main()
{
	for (RI i=1;i<=2020;++i)
	{
		int x=i; while (x) ans+=x%10==2,x/=10;
	}
	return printf("%d",ans),0;
}

既约分数

直接枚举,答案2481215

#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int
using namespace std;
int ans=0;
inline int gcd(CI n,CI m)
{
	return m?gcd(m,n%m):n;
}
int main()
{
	for (RI i=1;i<=2020;++i) for (RI j=1;j<=2020;++j)
	if (gcd(i,j)==1) ++ans; return printf("%d",ans),0;
}

蛇形填数

代码都不用写,把原图顺时针旋转\(45\)度,然后就变成一个三角形的数字塔

不难发现对于原来图中\((n,n)\)的位置,再旋转后的图里位置为\((2n-1,n)\),因此我们只要求出在数字塔中第\(39\)行最中间的那个元素即可

简单计算得到答案为\(\frac{(38+1)\times 38}{2}+20=761\)


七段码

枚举每个管的亮灭情况,然后建个图跑一下是不是所有亮管在一个联通块内

注意题目问的是表达的字符的种数,所以都不亮不能算作一种情况,答案为80

#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int
using namespace std;
int c[10],g[10][10],fa[10],ans;
inline int getfa(CI x)
{
	return fa[x]!=x?fa[x]=getfa(fa[x]):x;
}
inline void DFS(CI now=1)
{
	if (now>7)
	{
		bool flag=1; RI i,j; for (i=1;i<=7;++i) fa[i]=i;
		for (i=1,j;i<=7;++i) for (j=1;j<=7;++j)
		if (i!=j&&c[i]&&c[j]&&g[i][j]) fa[getfa(i)]=getfa(j);
		for (i=1;i<=7;++i) for (j=1;j<=7;++j)
		if (i!=j&&c[i]&&c[j]) flag&=getfa(i)==getfa(j);
		ans+=flag; return;
	}
	c[now]=1; DFS(now+1); c[now]=0; DFS(now+1);
}
int main()
{
	g[1][2]=g[1][6]=1; g[2][1]=g[2][7]=g[2][3]=1;
	g[3][2]=g[3][4]=g[3][7]=1; g[4][3]=g[4][5]=1;
	g[5][4]=g[5][6]=g[5][7]=1; g[6][1]=g[6][5]=g[6][7]=1;
	g[7][2]=g[7][3]=g[7][5]=g[7][6]=1;
	return DFS(),printf("%d",ans-1),0;
}

平面分割

有点意思的题,算是狠狠复习了一波平面分割的姿势

首先我们考虑先把圆的划分方案确定在添加直线,那么问题就是\(20\)个圆最多把平面分成多少块

我们假设\(n\)个圆产生的交点数为\(f(n)\),则在加入第\(n+1\)个圆时显然最多产生\(2n\)个交点(新加入的圆和每个圆都相交),区域数目就会增加\(2n\)

然后把式子化一下就是\(f(n)=n^2-n+2\),然后我们考虑在这基础上加入直线的情形

如果不考虑圆仅考虑直线的话就是个经典的结论,\(n\)条直线把平面分成\(g(n)=\frac{n(n+1)}{2}+1\)

然后我们再考虑把两部分拼起来,不难发现当每条直线都和每个圆相交时划分出的区域数最大

具体算的话因为有\(20\)个圆,所以每次多产生\(40\)个交点,\(20\)条直线一共多产生\(800\)个交点

不过细节上如果直接把\(800+f(20)+g(20)\)会有问题,因为此时第一条直线不在默认把平面分成两块了,而是直接在圆划分的区域中产生贡献

因此最后要减去\(g(1)=2\),最后答案是1391


成绩分析

SB题,按题意模拟即可

#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
int n,mi=100,mx,avg,x;
int main()
{
	//freopen("CODE.in","r",stdin); freopen("CODE.out","w",stdout);
	RI i; for (scanf("%d",&n),i=1;i<=n;++i) 
	scanf("%d",&x),mi=min(mi,x),mx=max(mx,x),avg+=x;
	return printf("%d\n%d\n%.2lf",mx,mi,1.0*avg/n),0;
}

回文日期

SB题,按题意模拟即可,注意要处理闰年

#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
const int day[12]={31,28,31,30,31,30,31,31,30,31,30,31};
inline bool judge(CI x)
{
	return x%400==0||(x%4==0&&x%100!=0);
}
int st,y,m,d,ans1,ans2;
int main()
{
	//freopen("CODE.in","r",stdin); freopen("CODE.out","w",stdout);
	for (scanf("%d",&st),y=st/10000,m=st/100%100,d=st%100;;)
	{
		if (++d>day[m-1]+(m==2&&judge(y))) if (++m,d=1,m>12) ++y,m=1;
		int A=m/10,B=m%10,C=d/10,D=d%10;
		if (D*1000+C*100+B*10+A==y)
		{
			if (!ans1) ans1=y*10000+m*100+d;
			if (A==C&&B==D) { ans2=y*10000+m*100+d; break; }
		}
	}
	return printf("%d\n%d",ans1,ans2),0;
}

子串分值

考虑对于每一种字符分别考虑贡献,对于每一个位置\(i\),我们考虑求出\(s_i\)这个字符上次出现的位置\(pre_i\)(默认为\(0\))和下次出现的位置\(nxt_i\)(默认为\(n+1\)

然后\(i\)这个位置上的字符产生贡献的区间数目就是\((i-pre_i)(nxt_i-i)\),然后就很简单了

#include<cstdio>
#include<iostream>
#include<cstring>
#define RI register int
#define CI const int&
using namespace std;
const int N=100005;
char s[N]; int n,lst[26],pre[N]; long long ans;
int main()
{
	//freopen("CODE.in","r",stdin); freopen("CODE.out","w",stdout);
	RI i; for (scanf("%s",s+1),n=strlen(s+1),i=1;i<=n;++i)
	pre[i]=lst[s[i]-'a'],lst[s[i]-'a']=i;
	for (i=0;i<26;++i) lst[i]=n+1;
	for (i=n;i;--i) ans+=1LL*(i-pre[i])*(lst[s[i]-'a']-i),lst[s[i]-'a']=i;
	return printf("%lld",ans),0;
}

字串排序

还是很有意思的一道题,感觉这种思路的题目好像之前在CF见过类似的

首先我们考虑解决长度为\(n\)的串的最大逆序对个数这一问题,如果\(n\le 26\)时答案显然就是一个\(\frac{n(n-1)}{2}\)

但是当\(n>26\)时就会出现有字符重复的情况,我们稍作分析发现此时只要把每个字符分布地均匀即是最优的

通过这个我们可以得到最后答案的长度\(n\),稍微手算一下大概是\(150\)这个级别的,然后再考虑让字典序最小

这个问题算是经典套路了吧,我们从前往后枚举每一位,然后从小到大枚举这一位上放的数,接下来就是要求出在一段前缀确定的情况下后面能产生的最大逆序对数是多少

根据前面的分析我们知道后面这一段一定是单调不增的,那么我们很容易考虑由此设计DP状态

\(f_{i,j,k}\)表示处理了后面的第\(i\)个位置,其中第\(i\)个位置是字符\(j\),最后一段连续的字符\(j\)的长度为\(k\)时,产生的最大逆序对数目

我们记\(bkt_x\)表示前面确定了的那段前缀中,大于字符\(x\)的数目,则很容易列出转移方程:

\[f_{i+1,j,k+1}=\max(f_{i+1,j,k+1},f_{i,j,k}+i-k+bkt_j)\\ f_{i+1,j-1,1}=\max(f_{i+1,j-1,1},f_{i,j,k}+i+bkt_{j-1}) \]

然后写完交一发,喜提82pts,小TLE两个点,仔细一算复杂度\(150^3\times 26^2\)好像确实跑不太动(\(20\)多亿吧好像)

然后各种尝试优化无果,最后只能去看了眼Solution,这里先放一下DP的代码

#include<cstdio>
#include<iostream>
#include<cstring>
#define RI register int
#define CI const int&
using namespace std;
const int N=150;
int tot,n,ret,bkt[N],f[N][26][N]; char ans[N];
inline int check(CI n)
{
	RI i,j,k; for (memset(f,0,sizeof(f)),i=0;i<26;++i) f[1][i][1]=bkt[i];
	for (i=1;i<n;++i) for (j=0;j<26;++j) for (k=1;k<=i;++k)
	{
		f[i+1][j][k+1]=max(f[i+1][j][k+1],f[i][j][k]+i-k+bkt[j]);
		if (j) f[i+1][j-1][1]=max(f[i+1][j-1][1],f[i][j][k]+i+bkt[j-1]);
	}
	int cur=0; for (j=0;j<26;++j) for (k=1;k<=n;++k)
	cur=max(cur,f[n][j][k]); return cur;
}
int main()
{
	//freopen("CODE.in","r",stdin); freopen("CODE.out","w",stdout);
	RI i,j,k; for (scanf("%d",&tot),i=1;;++i) if (check(i)>=tot) { n=i; break; }
	for (i=1;i<=n;++i) for (j=0;j<26;++j)
	{
		for (ret+=bkt[j],k=0;k<j;++k) ++bkt[k];
		if (check(n-i)+ret>=tot) { ans[i]=j+'a'; break; }
		for (ret-=bkt[j],k=0;k<j;++k) --bkt[k];
	}
	for (i=1;i<=n;++i) putchar(ans[i]); return 0;
}

Solution的意思是说我们在算后面那部分最大的逆序对数的时候,每个位置填什么其实只要贪心地确定下来即可

因为只要满足这一位填上去带来的可能的逆序对数最大,换做其它的方法一定不会更优

这样复杂度就少了一个\(150\),可以轻松跑过

#include<cstdio>
#include<iostream>
#include<cstring>
#define RI register int
#define CI const int&
using namespace std;
const int N=150;
int tot,n,ret,bkt[N],c[26]; char ans[N];
inline int check(CI n,int cur=0)
{
	RI i,j; for (memset(c,0,sizeof(c)),i=1;i<=n;++i)
	{
		int mx=-1,pos=0; for (j=0;j<26;++j)
		if (i-1-c[j]+bkt[j]>mx) mx=i-1-c[j]+bkt[j],pos=j;
		cur+=mx; ++c[pos];
	}
	return cur;
}
int main()
{
	//freopen("CODE.in","r",stdin); freopen("CODE.out","w",stdout);
	RI i,j,k; for (scanf("%d",&tot),i=1;;++i) if (check(i)>=tot) { n=i; break; }
	for (i=1;i<=n;++i) for (j=0;j<26;++j)
	{
		for (ret+=bkt[j],k=0;k<j;++k) ++bkt[k];
		if (check(n-i)+ret>=tot) { ans[i]=j+'a'; break; }
		for (ret-=bkt[j],k=0;k<j;++k) --bkt[k];
	}
	for (i=1;i<=n;++i) putchar(ans[i]); return 0;
}

荒岛探测

题目啰了一大堆,核心其实就一句话——求一个椭圆和一个三角形的公共部分的面积

对于这种曲线图形和多边形求面积的问题,一般是没有绝对精确的算法的,然后这题精度要求也不高,显然可以直接手动用微元法来分割求面积

但是如果直接上的话会有一个问题,在已知\(x\)坐标的情况下我们解不出和椭圆的交点的\(y\)坐标(用牛顿迭代太麻烦)

那么处理方法也很容易,把坐标系旋转再平移,把椭圆变成标准形即可

具体写的时候我取\(\Delta x=10^{-4}\),直接一发入魂

#include<cstdio>
#include<iostream>
#include<cmath>
#define RI register int
#define CI const int
using namespace std;
const double EPS=1e-4;
struct Point
{
	double x,y;
	inline Point(const double& X=0,const double& Y=0)
	{
		x=X; y=Y;
	}
	inline friend Point operator + (const Point& A,const Point& B)
	{
		return Point(A.x+B.x,A.y+B.y);
	}
	inline friend Point operator - (const Point& A,const Point& B)
	{
		return Point(A.x-B.x,A.y-B.y);
	}
	inline friend Point operator * (const Point& A,const double x)
	{
		return Point(A.x*x,A.y*x);
	}
	inline friend Point operator / (const Point& A,const double x)
	{
		return Point(A.x/x,A.y/x);
	}
	inline void rotate(const double& ang)
	{
		double tx=x,ty=y; x=tx*cos(ang)-ty*sin(ang); y=tx*sin(ang)+ty*cos(ang);
	}
}O1,O2,T1,T2,T3; double l,a,b,ans;
int main()
{
	//freopen("CODE.in","r",stdin); freopen("CODE.out","w",stdout);
	scanf("%lf%lf%lf%lf%lf",&O1.x,&O1.y,&O2.x,&O2.y,&l);
	scanf("%lf%lf%lf%lf%lf%lf",&T1.x,&T1.y,&T2.x,&T2.y,&T3.x,&T3.y);
	double ang=-atan2(O1.y-O2.y,O1.x-O2.x);
	O1.rotate(ang); O2.rotate(ang); T1.rotate(ang); T2.rotate(ang); T3.rotate(ang);
	Point O=(O1+O2)/2.0; O1=O1-O; O2=O2-O; T1=T1-O; T2=T2-O; T3=T3-O;
	a=l/2; b=sqrt(a*a-O1.x*O1.x);
	if (T2.x>T3.x) swap(T2,T3); if (T1.x>T2.x) swap(T1,T2); if (T2.x>T3.x) swap(T2,T3);
	double k1=(T1.y-T2.y)/(T1.x-T2.x),b1=T1.y-k1*T1.x;
	double k2=(T2.y-T3.y)/(T2.x-T3.x),b2=T2.y-k2*T2.x;
	double k3=(T1.y-T3.y)/(T1.x-T3.x),b3=T3.y-k3*T3.x;
	for (double x0=-a;x0<=a;x0+=EPS)
	{
		double l1=-b*sqrt(1.0-x0*x0/(a*a)),r1=b*sqrt(1.0-x0*x0/(a*a)),l2,r2=k3*x0+b3;
		if (T1.x<=x0&&x0<=T2.x) l2=k1*x0+b1; else
		if (T2.x<=x0&&x0<=T3.x) l2=k2*x0+b2; else l2=r2=0;
		if (l2>r2) swap(l2,r2); l1=max(l1,l2); r1=min(r1,r2);
		if (l1<=r1) ans+=(r1-l1)*EPS;
	}
	return printf("%.2lf",ans),0;
}

据说这题还有一种蒙特卡洛算法,就是随机取点,然后统计落在不同区域内的次数,最后比一下就得到一个近似的答案,感觉还是很有意思的,不过我懒得写了


Postscript

今天晚上还要打CF,明天早上上日语晚上开组会,这个礼拜也只能堪堪做一套题了

posted @ 2023-03-18 19:36  空気力学の詩  阅读(116)  评论(0编辑  收藏  举报