目录
前言
A.九进制转十进制
B.顺子日期
C.刷题统计
D.修剪灌木
E.X 进制减法
F.统计子矩阵
G.积木画
H.扫雷
I.李白打酒加强版
J.砍竹子
相信很多朋友都报名了2023年的蓝桥杯吧,为了准备今年的蓝桥杯,今天带大家回味一下去年的蓝桥杯,文里的题解都是我当时考场代码或者赛后补题代码,可能会有些不合理的地方,欢迎大家提意见!
下面我们开始吧!!
题目:
9进制整数转换为10进制等于多少?
输入:
无
输出:
写个程序直接输出结果即可。
问题解析:
签到题,考验进制的转换,而且还是转换成较容易转换的十进制。而且数值比较小,可以直接用计算器计算得出结果。
代码:
注意本题是填空题!代码要求直接输出答案,不要输出多于内容,如下:
#include
int main(){
printf("1478");
return 0;
}
题目:
小明特别喜欢顺子。顺子指的就是连续的三个数字:123,456等。顺子日期指的就是在日期的
yyyymmdd
表示法中,存在任意连续的三位数是一个顺子的日期。例如 20220123 就是一个顺子日期,因为它出现了一个顺子:123。而 20221023则不是一个顺子日期,它一个顺子也没有。小明想知道在整个2022年份中,一共有多少个顺子日期。
输入:
无
输出:
写个程序直接输出结果即可。
问题解析:
签到题,但这个题在去年刚参加完的时候有异议,那就是012是不是顺子,我当时比赛的想法是012也是顺子的。题目本身不难,是蓝桥杯喜闻乐见的日历题,可以写代码也可以对着日历看!
因为前四位2022是确定的,而月份没有第34月,所以如果是顺子日期,则必然是后四位中有顺子,只有在第1,10,11,12月的日期可能满足这个条件,所以我们思考这几个月即可。可以对照日历也可以找出顺子日期。
分别为20220120,20220121,20220122,20220123,20220124,20220125,20220126,20220127,20220128,20220129,20221012,20221123,20221230,20221231共14个。
代码:
注意本题是填空题!代码要求直接输出答案,不要输出多于内容,如下:
#include
int main(){
printf("14");
return 0;
}
题目:
小明决定从下周一开始努力刷题准备蓝桥杯竞赛。他计划周一至周五每天做a道题目,周六和周日每天做b道题目。请你帮小明计算,按照计划他将在第几天实现做题数大于等于n题?
输入:
10 20 99
输出:
8
评测用例规模与约定:
对于50%的评测样例,
对于100%的评测样例,
问题解析:
签到题, 基本没有太多坑的一道题,要注意题目中大于等于,并且评测样例要求10的18次方,需要开longlong。
代码:
70%做法(直接暴力):
#include
long long a,b,n,ans,day;
int main(){
scanf("%lld%lld%lld",&a,&b,&n);
while(1){
for(int i=1;i<=7;i++){
if(i==6||i==7) ans+=b;
else ans+=a;
day++;
if(ans>=n){
printf("%lld",day);
return 0;
}
}
}
}
不断循环每一天,根据星期几来加对应的题目,但数据过大时循环的天数可能很多,很容易就会超过规定时间 。
100%做法:
#include
long long a,b,n,ans;
int main(){
scanf("%lld%lld%lld",&a,&b,&n);
long long week=n/(5*a+2*b);//先求出需要的整星期数。
long long day=week*7;
ans+=week*(5*a+2*b);
if(ans>=n) {//如果正好够,直接输出
printf("%lld",day);
return 0;
}
for(int i=1;i<=7;i++){ //如果不够,再加一个循环。
if(i==6||i==7) ans+=b;
else ans+=a;
day++;
if(ans>=n) {
printf("%lld",day);
return 0;
}
}
}
只需要一点很小的优化就可以省去大部分时间,先求整星期,再求具体天数。
题目:
爱丽丝要完成一项修剪灌木的工作。
有 N 棵灌木整齐的从左到右排成一排。爱丽丝在每天傍晚会修剪一棵灌木,让灌木的高度变为 0 厘米。爱丽丝修剪灌木的顺序是从最左侧的灌木开始,每天向右修剪一棵灌木。当修剪了最右侧的灌木后,她会调转方向,下一天开始向左修剪灌木。直到修剪了最左的灌木后再次调转方向。然后如此循环往复。
灌木每天从早上到傍晩会长高 1 厘米, 而其余时间不会长高。在第一天的早晨, 所有灌木的高度都是 0 厘米。爱丽丝想知道每棵灌木最高长到多高。
输入:
3
输出:
4
2
4
评测用例规模与约定:
对于 30% 的数据,N≤10;
对于 100% 的数据,1< N ≤10000。
问题解析:
签到题, 因为这道题的解法比较好想,在这里就不写直接暴力模拟每一天的代码了,直接说优化解法。
首先我们从最容易想的两个点说起,即左右两个端点,很容易想到最左端的灌木最长的时刻,即爱丽丝从最左到最右,再从最右回到最左的那一刻,同理最右端点也是如此。
以此类推,对于中间的点,我们先判断他对于最左或者最右哪个距离更长,因为每天增长一厘米,我们可以抽象理解为爱丽丝走一个距离即增长一厘米,直接用长距离×2即为所求。
代码:
100%做法:
#include
int N,a[10010];
int main(){
scanf("%d",&N);
for(int i=1;i<=N;i++){
if(i-1>=N-i) a[i]=2*(i-1);
else a[i]=2*(N-i);
}
for(int i=1;i<=N;i++) printf("%d\n",a[i]);
return 0;
}
当然这其实是一个左右对称的过程,只求一半也可以,但求全过程时间也完全不会超。我建议大家在比赛的时候选择更加稳妥的写法。
题目:
进制规定了数字在数位上逢几进一。
X 进制是一种很神奇的进制,因为其每一数位的进制并不固定!例如说某种 X 进制数,最低数位为二进制,第二数位为十进制,第三数位为八进制,则X进制数321转换为十进制数为 65。
现在有两个 X 进制表示的整数 A 和 B,但是其具体每一数位的进制还不确定,只知道 A 和B 是同一进制规则,且每一数位最高为 N进制,最低为二进制。请你算出 A−B的结果最小可能是多少。
请注意,你需要保证 A 和 B 在 X 进制下都是合法的,即每一数位上的数字要小于其进制。
输入:
第一行一个正整数 N,含义如题面所述。
第二行一个正整数 Ma,表示 X 进制数 A 的位数。
第三行 Ma个用空格分开的整数,表示 X 进制数 A 按从高位到低位顺序各个数位上的数字在十进制下的表示。
第四行一个正整数 Mb,表示 X 进制数 B的位数。
第五行 Mb 个用空格分开的整数,表示 X 进制数 B 按从高位到低位顺序各个数位上的数字在十进制下的表示。
请注意,输入中的所有数字都是十进制的。
11 3 10 4 0 3 1 2 0
输出:
输出一行一个整数,表示 X 进制数A−B的结果的最小可能值转换为十进制后再模 1000000007 的结果。
94
样例说明:
当进制为:最低位 2 进制,第二数位 5 进制,第三数位 11 进制时,减法得到的差最小。此时 A 在十进制下是 108,B 在十进制下是 14,差值是 94。
评测用例规模与约定:
对于30%的数据,
对于100%的数据,。
问题解析:
去年考场上看到这个题是蒙b的,题是在太长,来回看了好几遍题才看懂题意。现在我们来一点一点分析。
首先我们可以知道的是,题里面给了我们一个定义:X进制数,他的每一位数进制都是不一样的,并且给我们举了一个例子,X进制数321(从低位到高位分别为2,10,8进制)转化为十进制数为65。我们先来解释一下这个转换,
3*(10*2)+2*2+1=65
很容易发现,将X进制数转化为十进制数,就是将每一位的数乘以所有低于他数位的数制。
例如3在第三位,转化为十进制数就要将3乘以第一和第二位的数制,即2,10。将每一位数都如此计算,最后相加即为10进制数。
下面用到了贪心思想:欲使A-B最小,只需使得各位数字取得合法范围内的最小进制即可,具体做法就是对A和B中相同数位的数字取maxn,该位的合法最小进制即为max(maxn + 1, 2),因为最小进制不能小于2;而对于X进制的数来说,合法的最大数字是X-1,例如8进制中最大数字是7,二进制中最大数字是1。
样例分析:
最低位 0,0 二进制。
第二位 4,2 五进制。
第三位 10 ,1 十一进制。
结果为:(10*5*2+4*2)-(1*5*2+2*2)=94。
思路正确,下面我们用代码实现即可。
代码:
30%做法(直接暴力):
#include
#include
using namespace std;
const int mod=1000000007;
int a[100010],b[100010],c[100010];
int main(){
int N,n,m;
scanf("%d",&N);
scanf("%d",&n);
for(int i=n-1;i>=0;i--) scanf("%d",&a[i]);
scanf("%d",&m);
for(int i=m-1;i>=0;i--) scanf("%d",&b[i]);
long long ans1=0,ans2=0;
for(int i=max(n,m)-1;i>=0;i--){
c[i]=max(max(a[i],b[i])+1,2);
}
for(int i=max(n,m)-1;i>=0;i--){
for(int j=0;j
就是对上面思路的直接暴力模拟,这个代码不过多进行解释了,时间复杂度不合理,而且两个数很大,即使开longlong也会爆。
100%做法(累乘,每一步都取mod):
#include
#include
using namespace std;
const int mod=1000000007;
int a[100010],b[100010],c[100010];
long long w[100010];
int main(){
int N,n,m;
scanf("%d",&N);
scanf("%d",&n);
for(int i=n-1;i>=0;i--) scanf("%d",&a[i]);
scanf("%d",&m);
for(int i=m-1;i>=0;i--) scanf("%d",&b[i]);
long long ans1=0,ans2=0;
for(int i=max(n,m)-1;i>=0;i--) c[i]=max(max(a[i],b[i])+1,2);
w[0]=1;
for(int i=1;i<=max(n,m);i++) w[i]=w[i-1]*c[i-1]%mod;
for(int i=n;i>=0;i--) ans1=(ans1+a[i]*w[i])%mod;
for(int i=m;i>=0;i--) ans2=(ans2+b[i]*w[i])%mod;
printf("%lld",(ans1-ans2+mod)%mod); //防溢出
return 0;
}
通过累乘求出每个数位的权来减少时间复杂度。
通过开longlong和步步取模来防止溢出,注意最后结果可能为负,但根据题意,负数显然没有意义,所以先加mod再对mod取模,就可以防止负数出现。
题目:
给定一个 N×M 的矩阵 A,请你统计有多少个子矩阵(最小 1×1,最大 N×M) 满足子矩阵中所有数的和不超过给定的整数 K?输入:
第一行包含三个整数 N, M和 K。
之后 N 行每行包含 M 个整数,代表矩阵 A。
3 4 10
1 2 3 4
5 6 7 8
9 10 11 12
输出:
一个整数代表答案。
19
样例说明:
满足条件的子矩阵一共有 19,包含:
大小为 1×1 的有 10 个。
大小为 1×2 的有 3 个。
大小为 1×3 的有 2 个。
大小为 1×4 的有 1 个。
大小为 2×1 的有 3 个。
评测用例规模与约定:
对于 30% 的数据,N,M≤20;
对于 70% 的数据,N,M≤100;
对于 100% 的数据,1≤N,M≤500,0≤≤1000,1≤K≤250000000。
问题解析:
题目本身很容易理解。
因为矩阵本身没什么规律,题目本质是要求出所有子矩阵的和进行比较, 运用前缀和的思想。
可套用求子矩阵和,前缀和模板:796. 子矩阵的和 - AcWing题库
代码:
70%做法(子矩阵的和):
#include
const int N=510;
long long n,m,q;
long long s[N][N];
long long ans;
int main(){
scanf("%lld%lld%lld",&n,&m,&q);
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
scanf("%lld",&s[i][j]);
}
}
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
s[i][j]+=s[i-1][j]+s[i][j-1]-s[i-1][j-1];
}
}
for(int x1=1;x1<=n;x1++){
for(int y1=1;y1<=m;y1++){
for(int x2=x1;x2<=n;x2++){
for(int y2=y1;y2<=m;y2++){
if(s[x2][y2]-s[x1-1][y2]-s[x2][y1-1]+s[x1-1][y1-1]<=q) ans++;
}
}
}
}
printf("%lld",ans);
return 0;
}
后面数据过大,四重循环显然会超时。
100%做法(双指针优化):
#include
const int N=510;
long long n,m,q;
long long s[N][N];
long long ans;
int main(){
scanf("%lld%lld%lld",&n,&m,&q);
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
scanf("%lld",&s[i][j]);
}
}
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
s[i][j]+=s[i-1][j]+s[i][j-1]-s[i-1][j-1];
}
}
for(int l=1;l<=m;l++){ //枚举子矩阵左边界
for(int r=l;r<=m;r++){ //枚举子矩阵右边界
for(int i=1,j=1;i<=n;i++){ //i为下边界,j为上边界。
while(j<=i&&s[i][r]-s[j-1][r]-s[i][l-1]+s[j-1][l-1]>q) j++;//判断下一个
if(j<=i) ans+=(i-j+1); //表明i到j之间的行都满足条件。
}
}
}
printf("%lld",ans);
return 0;
}
用双指针省去时间复杂度。
题目:
小明最近迷上了积木画,有这么两种类型的积木,分别为 I 型(大小为 2 个单位面积)和 L 型(大小为 3 个单位面积):
同时,小明有一块面积大小为 2×N 的画布,画布由 2×N 个 1×1 区域构成。小明需要用以上两种积木将画布拼满,他想知道总共有多少种不同的方式?
积木可以任意旋转,且画布的方向固定。
输入:
输入一个整数 N,表示画布大小。
3
输出:
输出一个整数表示答案。由于答案可能很大,所以输出其对 1000000007 取模后的值。
5
样例说明:
五种情况如下图所示,颜色只是为了标识不同的积木:
评测用例规模与约定:
对于所有测试用例,1≤N≤10000000。
问题解析:
状压dp,场上看到这个题目,就想到 291. 蒙德里安的梦想 - AcWing题库
这个知名的状态压缩dp题(如果不知道状压dp建议先去了解),而且积木画题目中的画布为2*N,完全可以对积木出现的方式直接进行枚举。
既然是动态规划,其核心就是假设已知其中一个状态,求另一个状态的值。这种又被称为状态转移。
dp[i][j]题目给出的状态转移的合法条件为:前i-1列已摆好,当前摆第i列,第i列是j的所有方案。
dp问题的核心是假设已知其中一个状态,求另一个状态的值。这种又被称为状态转移。
每当我们需要状态转移时,都要先考虑一个问题:状态转移是否合法?
根据题意,合法的意义即摆积木不能重叠。
先假设第i列状态如下:
当i-1列摆放完之后,我们可以看到在这种情况下,第i列没有被伸出来的积木块覆盖。
如果用0表示没有被覆盖,1表示被覆盖,那么我们可以称这种情况为00
那么,在00的基础上,我们可以在i上进行四种不同方式的积木摆放,如下:
对应i+1列为00
对应i+1列状态为10
对应i+1列状态为01
对应i+1列状态为11
那么我们可以得到一个状态转移结论:
假如j当前为00,那么对应的下一列的状态可以为:00,01,10,11
同理,假如i当前为10,那么对应的下一列的状态可以为01, 11,如下:
假如i当前为01,那么对应的下一列的状态可以为10, 11,如下:
假如i当前为11,那么下一列的状态只能为00
(因为第i列什么也不能放了,所以第i+1列一定是空的)
此时我们已经判断出了所有的状态转移情况,此时我们建立一个二维数组进行存放
(第一维表示初状态,第二维表示目标状态,1表示可以转化,0表示不能转化)
int g[4][4]={
{1,1,1,1},
{0,0,1,1},
{0,1,0,1},
{1,0,0,0}
};
知道了状态转移,我们就可以写出dp了:
还记得我们的dp[i][j]的定义吗,前i-1列已经摆好,有几种可能,使得第i列情况是j
dp[i][j]中存的数字即为这个可能的事件数,容易知道,j一共只有四种可能(00,01,10,11)我们需建立一个dp[N][4]
100%做法(状压dp):
#include
const int N=1e7+10,mod=1000000007;
int g[4][4]={
{1,1,1,1},
{0,0,1,1},
{0,1,0,1},
{1,0,0,0}
};
int dp[N][4];
int main(){
int n;
scanf("%d",&n);
dp[1][0]=1; //初始为“00”状态,且这种状态有唯一一种,所以dp[1][0]=1
for(int i=1;i<=n;i++){
for(int j=0;j<4;j++){
for(int k=0;k<4;k++){
dp[i+1][k]=(dp[i+1][k]+dp[i][j]*g[j][k])%mod;
//g[j][k]判断能不能从j状态转移到k状态,如果能,就把第i列状态为j的方案数加到第i+1列状态为k的方案集合中去
}
}
}
printf("%d",dp[n+1][0]);
//由题目可知,最后画布会被填满,也就是说前面全部填满,且在第n+1列状态为00的方案总量
return 0;
}
循环j,k来判断 dp[i+1][k]=(dp[i+1][k]+dp[i][j]*g[j][k])%mod;
如果能从j转移到k,就把第i列状态为j的方案数加到第i+1列状态为k的方案集合中去,实现状态转移。
题目:
输入:
2 1
2 2 4
4 4 2
0 0 5
输出:
输出一个整数表示答案。
2
样例说明:
评测用例规模与约定:
问题解析:
看到这题人麻了
首先看到雷爆炸会带动周围其他雷爆炸,很容易想到要用bfs算法来解决
但x,y的数据范围过大(10^9)用数组肯定是会爆的,只能用哈希。
一说到哈希,很容易想到利用c++自带的map和unordered_map来进行储存,但很遗憾,map的速度太慢,没有办法过掉全部测试点,只能用手写哈希的方式来储存。如果对哈希不了解,可以看一下840. 模拟散列表 - AcWing题库
下面来针对这个题目讲解一下具体的哈希方法:
首先,因为x,y的数据范围一样大,我们可以把x,y映射到一个唯一的哈希值,
即为:x*(1e9+1)+y, 这是一个(1e9+1)进制数,可以把这个哈希值近似看成一个数字 :xy
注意事项:首先哈希表的长度至少为2n,在这里我们开的越大越好,这样可以避免哈希冲突。
哈希表的key和value,哈希表的value即我们刚刚通关计算得到的数字xy,在获取哈希值之后,我们还要将他储存在哈希表的某一个位置,这个位置即为key,我们将刚刚得到的哈希值对哈希表取模即可得到key。如果发生了冲突,就需要从这一点往后遍历,直到找到第一个空闲的位置
100%做法(哈希,bfs):
#include
#include
using namespace std;
const int N=50010,X=1e9+1;
const int M=1000005;//哈希表的长度
int n,m;
long long ans=0;
long long h[M];//哈希表
bool st[N];//判断这一点是否被访问过
int id[M];//哈希表中key值所对应的地雷下标
struct node{
int x,y,r;
}a[N];//地雷
long long getvalue(int x,int y){ //得到每个坐标的哈希值
return (long long)x*X+y;
}
int find(int x,int y){ //找到该坐标对应的哈希表的key
long long v=getvalue(x,y);
int key=(v%M+M)%M; //映射到哈希表内部
while(h[key]!=-1&&h[key]!=v){ //如果为空,直接弹出,如果发生了冲突,就往后遍历,直到不冲突
key++;
if(key==M) key=0; //如果到了最后,就从头继续遍历
}
return key;
}
bool check(int x1,int y1,int r, int x,int y){//判断以x1,y1为圆心,半径为r的圆中是否包含点x,y
int l=(x1-x)*(x1-x)+(y1-y)*(y1-y);
if(l<=r*r) return true;
else return false;
}
void bfs(int res){ //标准的bfs模板,利用数列
queue que;
que.push(res);
st[res]=1;
while(!que.empty()){
int t=que.front();
que.pop();
int x=a[t].x,y=a[t].y,r=a[t].r;
for(int i=x-r;i<=x+r;i++){ //遍历区域内所有点
for(int j=y-r;j<=y+r;j++){
int key=find(i,j);//找到该点对应的哈希下标
if(id[key]&&!st[id[key]]&&check(x,y,r,i,j)){//如果说这一点存在,没有被访问过,且可以被扫到
int res1=id[key];
st[res1]=1;//扫雷
que.push(res1);
}
}
}
}
}
int main(){
scanf("%d%d",&n,&m);
int x,y,r;
for(int i=0;i
代码中bfs的部分没有做详细解释,可以做一些经典的bfs题学习一下!
题目:
输入:
5 10
输出:
输出一个整数表示答案。由于答案可能很大,输出模 1000000007 的结果。
14
样例说明:
如果我们用 0 代表遇到花,1 代表遇到店,14 种顺序如下:
010101101000000 010110010010000 011000110010000 100010110010000 011001000110000 100011000110000 100100010110000 010110100000100 011001001000100 100011001000100 100100011000100 011010000010100 100100100010100 101000001010100
评测用例规模与约定 :
问题解析:
经典李白打酒,很明显的一道dp题。
还记得dp的两大要点吗?状态和转移
状态:题目中有三个状态需要我们注意,分别是店,花,酒,在考虑整体状态的时候需要全部考虑进去,所以我们设dp[i][j][k],表示经过了i个花,j个店,手里还有k个酒的的方案总数。
转移:有两种情况,第一种是上一个经过了花,第二种是上一个经过了店,显然状态转移方程就是:
dp[i][j][k]=dp[i-1][j][k+1]+dp[i][j-1][k/2]
100%做法(dp):
#include
const int N=110;
const int mod=1000000007;
long long dp[N][N][N];
int main(){
int n,m;
scanf("%d%d",&m,&n);
dp[0][0][2]=1;//初始化,李白一开始手里有两斗酒
for(int i=0;i<=n;i++){
for(int j=0;j<=m;j++){
for(int k=0;k0) dp[i][j][k]=(dp[i][j][k]+dp[i-1][j][k+1])%mod; //上一个是花的方案数
if(j>0&&k%2==0) dp[i][j][k]=(dp[i][j][k]+dp[i][j-1][k/2])%mod; // 上一个是店的方案数
//特别注意判断k%2==0,假如k是奇数,k/2=1,而1*2=2,实际上不能从k=1的情况直接转移到k=3
}
}
}
printf("%lld",dp[n-1][m][1]);
//注意这里不能输出dp[n][m][0],因为dp[n][m][0]不能区分李白最后路过的是花还是店
//往前推一下,如果最后一步是花,那转移前的方案就是dp[n-1][m][1]
return 0;
}
个人认为比积木画简单
题目:
输入:
6
2 1 4 2 6 7
输出:
一个整数表示答案。
5
样例说明:
其中一种方案:
2 1 4 2 6 7 → 2 1 4 2 6 2 → 2 1 4 2 2 2 → 2 1 1 2 2 2 → 1 1 1 2 2 2 → 1 1 1 1 1 1
需要5步完成
评测用例规模与约定:
问题解析:
首先我们需要关注的是,只能对连续一段高度相同的竹子使用魔法
我们不禁思考,这样的变化最多能进行几次,经过计算,1e18在经过6次变化后就会变为1,也就是说所有的数最多经历6次变化。
这道题主要考查我们的思维,我们把每个数字,在经历变化,最终变成1的过程,抽象为层,比如说1e18,他有6层,每层都有一个数字,逐渐减小,最终为1。
假如我们把所有数字单独隔离开,独自层层减少,可以得到一个总数,但这显然不是我们的答案,因为当一个数字的邻居和他相同时,可以一起减小,省去一次魔法。
那如果我们能找到所有数字及他们每一层的数字,整体进行比较,如果发现有相邻的情况,就让总魔法使用数减一,是不是就可以解出来了呢。
100%做法(思维):
#include
#include
#include
const int N=200010,M=10;
int n,m;
long long f[N][M];//f[i][j]:记录第i个数在第j层的数是多少
int main(){
long long ans=0;
scanf("%d",&n);
for(int i=0;i1) {
stk[++top]=x;
x=sqrt(x/2+1);
}
ans+=top;//先求出所有数字单个操作次数的累加
m=std::max(m,top); //最大层数
for(int j=0,k=top;k;j++,k--){//注意栈先进后出,所以k从top开始遍历
f[i][j]=stk[k];
}
}
for(int j=0;j
看到还有线段树,优先队列等做法,可以去学习一下(
最后,祝大家在2023年蓝桥杯中能取得自己满意的成绩!!!