草叶间牵手回望的两位少女。
草叶间牵手回望

目录

前言

A.九进制转十进制

B.顺子日期

C.刷题统计

D.修剪灌木

E.X 进制减法

F.统计子矩阵

G.积木画

H.扫雷

I.李白打酒加强版

J.砍竹子


前言

相信很多朋友都报名了2023年的蓝桥杯吧,为了准备今年的蓝桥杯,今天带大家回味一下去年的蓝桥杯,文里的题解都是我当时考场代码或者赛后补题代码,可能会有些不合理的地方,欢迎大家提意见!

下面我们开始吧!!


A.九进制转十进制

题目:

9进制整数

九进制数2022
九进制数2022
转换为10进制等于多少?

输入:

无

输出:

写个程序直接输出结果即可。

问题解析: 

签到题,考验进制的转换,而且还是转换成较容易转换的十进制。而且数值比较小,可以直接用计算器计算得出结果。

九进制2022转为十进制:2×9³+0×9²+2×9+2=1478。
九进制2022 → 十进制1478


代码:

注意本题是填空题!代码要求直接输出答案,不要输出多于内容,如下:

1
2
3
4
5
#include<cstdio>
int main(){
printf("1478");
return 0;
}

B.顺子日期

题目:

小明特别喜欢顺子。顺子指的就是连续的三个数字: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个。


代码:

注意本题是填空题!代码要求直接输出答案,不要输出多于内容,如下:

1
2
3
4
5
#include<cstdio>
int main(){
printf("14");
return 0;
}

C.刷题统计

题目:

小明决定从下周一开始努力刷题准备蓝桥杯竞赛。他计划周一至周五每天做a道题目,周六和周日每天做b道题目。请你帮小明计算,按照计划他将在第几天实现做题数大于等于n题?

输入:

10 20 99

输出:

8

评测用例规模与约定:

对于50%的评测样例,

1≤a、b、n≤10⁶。
1≤a、b、n≤10⁶

对于100%的评测样例,

1≤a、b、n≤10¹⁸。
1≤a、b、n≤10¹⁸

问题解析:

签到题, 基本没有太多坑的一道题,要注意题目中大于等于,并且评测样例要求10的18次方,需要开longlong。


代码:

70%做法(直接暴力):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<cstdio>
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%做法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include<cstdio>
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;
}
}
}

只需要一点很小的优化就可以省去大部分时间,先求整星期,再求具体天数。


D.修剪灌木

 题目:

爱丽丝要完成一项修剪灌木的工作。

有 N 棵灌木整齐的从左到右排成一排。爱丽丝在每天傍晚会修剪一棵灌木,让灌木的高度变为 0 厘米。爱丽丝修剪灌木的顺序是从最左侧的灌木开始,每天向右修剪一棵灌木。当修剪了最右侧的灌木后,她会调转方向,下一天开始向左修剪灌木。直到修剪了最左的灌木后再次调转方向。然后如此循环往复。

灌木每天从早上到傍晩会长高 1 厘米, 而其余时间不会长高。在第一天的早晨, 所有灌木的高度都是 0 厘米。爱丽丝想知道每棵灌木最高长到多高。

输入:

3

输出:

4

2

4

评测用例规模与约定:

对于 30% 的数据,N≤10;

对于 100% 的数据,1< N ≤10000。

问题解析:

签到题, 因为这道题的解法比较好想,在这里就不写直接暴力模拟每一天的代码了,直接说优化解法。

首先我们从最容易想的两个点说起,即左右两个端点,很容易想到最左端的灌木最长的时刻,即爱丽丝从最左到最右,再从最右回到最左的那一刻,同理最右端点也是如此。

以此类推,对于中间的点,我们先判断他对于最左或者最右哪个距离更长,因为每天增长一厘米,我们可以抽象理解为爱丽丝走一个距离即增长一厘米,直接用长距离×2即为所求。


代码:

100%做法:

1
2
3
4
5
6
7
8
9
10
11
#include<cstdio>
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;
}

当然这其实是一个左右对称的过程,只求一半也可以,但求全过程时间也完全不会超。我建议大家在比赛的时候选择更加稳妥的写法。


E.X 进制减法

 题目:

进制规定了数字在数位上逢几进一。

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 按从高位到低位顺序各个数位上的数字在十进制下的表示。

请注意,输入中的所有数字都是十进制的。

1
2
3
4
5
11
3
10 4 0
3
1 2 0

输出:

输出一行一个整数,表示 X 进制数A−B的结果的最小可能值转换为十进制后再模 1000000007 的结果。

94

样例说明:

 当进制为:最低位 2 进制,第二数位 5 进制,第三数位 11 进制时,减法得到的差最小。此时 A 在十进制下是 108,B 在十进制下是 14,差值是 94。

评测用例规模与约定:

对于30%的数据,

N≤10,M下标a、M下标b≤8。
N≤10,M下标a、b≤8

对于100%的数据,

2≤N≤1000,1≤M下标a、M下标b≤100000,A≥B。
N、M与A、B的范围
。

问题解析:

去年考场上看到这个题是蒙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%做法(直接暴力):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include<cstdio>
#include<iostream>
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<i;j++){
a[i]=a[i]*c[j];
b[i]=c[j]*b[i];
}
ans1+=a[i];
ans2+=b[i];
}
printf("%lld",(ans1-ans2)%mod);
return 0;
}

就是对上面思路的直接暴力模拟,这个代码不过多进行解释了,时间复杂度不合理,而且两个数很大,即使开longlong也会爆。

100%做法(累乘,每一步都取mod):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include<cstdio>
#include<iostream>
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取模,就可以防止负数出现。


F.统计子矩阵

 题目:

给定一个 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≤

A下标i,j。
A下标i,j
≤1000,1≤K≤250000000。

问题解析:

题目本身很容易理解。

因为矩阵本身没什么规律,题目本质是要求出所有子矩阵的和进行比较, 运用前缀和的思想。

可套用求子矩阵和,前缀和模板:796. 子矩阵的和 - AcWing题库

代码:

70%做法(子矩阵的和):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include<cstdio>
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%做法(双指针优化):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include<cstdio>
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;
}

用双指针省去时间复杂度。


G.积木画

 题目:

小明最近迷上了积木画,有这么两种类型的积木,分别为 I 型(大小为 2 个单位面积)和 L 型(大小为 3 个单位面积):

两种积木:I型由两个相邻单位方格组成,L型由三个单位方格组成直角形。
I型与L型积木

同时,小明有一块面积大小为 2×N 的画布,画布由 2×N 个 1×1 区域构成。小明需要用以上两种积木将画布拼满,他想知道总共有多少种不同的方式?

积木可以任意旋转,且画布的方向固定。

输入:

输入一个整数 N,表示画布大小。

3

输出:

输出一个整数表示答案。由于答案可能很大,所以输出其对 1000000007 取模后的值。

5

样例说明:

五种情况如下图所示,颜色只是为了标识不同的积木:

2×3画布的五种铺法:三个竖I型一种,两个横I型加一个竖I型两种,两个L型两种。
2×3画布的五种铺法

评测用例规模与约定:

对于所有测试用例,1≤N≤10000000。

问题解析:

状压dp,场上看到这个题目,就想到​​​​​​  291. 蒙德里安的梦想 - AcWing题库

这个知名的状态压缩dp题(如果不知道状压dp建议先去了解),而且积木画题目中的画布为2*N,完全可以对积木出现的方式直接进行枚举。

既然是动态规划,其核心就是假设已知其中一个状态,求另一个状态的值。这种又被称为状态转移。

dp[i][j]题目给出的状态转移的合法条件为:前i-1列已摆好,当前摆第i列,第i列是j的所有方案。

dp问题的核心是假设已知其中一个状态,求另一个状态的值。这种又被称为状态转移。

每当我们需要状态转移时,都要先考虑一个问题:状态转移是否合法?

根据题意,合法的意义即摆积木不能重叠。

先假设第i列状态如下:

积木画状态00:前i−1列已铺满,第i列上下两格均未被覆盖。
积木·状态00

 当i-1列摆放完之后,我们可以看到在这种情况下,第i列没有被伸出来的积木块覆盖。

如果用0表示没有被覆盖,1表示被覆盖,那么我们可以称这种情况为00

那么,在00的基础上,我们可以在i上进行四种不同方式的积木摆放,如下:

状态00转移到00:在第i列竖放一个I型积木,第i+1列上下两格仍为空。
状态00 → 00

对应i+1列为00

状态00转移到10:L型积木铺满第i列,并覆盖第i+1列上格,下格仍为空。
状态00 → 10

对应i+1列状态为10 

状态00转移到01:L型积木铺满第i列,并覆盖第i+1列下格,上格仍为空。
状态00 → 01

对应i+1列状态为01 

状态00转移到11:上下各横放一个I型积木,覆盖第i列和第i+1列。
状态00 → 11

对应i+1列状态为11

那么我们可以得到一个状态转移结论:
假如j当前为00,那么对应的下一列的状态可以为:00,01,10,11

同理,假如i当前为10,那么对应的下一列的状态可以为01, 11,如下:

状态10转移到11:L型积木覆盖第i列下格和第i+1列上下两格。
状态10 → 11

状态10转移到01:横放I型积木,覆盖第i列与第i+1列的下格。
状态10 → 01

假如i当前为01,那么对应的下一列的状态可以为10, 11,如下:

状态01转移到10:横放I型积木,覆盖第i列与第i+1列的上格。
状态01 → 10

状态01转移到11:L型积木覆盖第i列上格和第i+1列上下两格。
状态01 → 11

 假如i当前为11,那么下一列的状态只能为00

(因为第i列什么也不能放了,所以第i+1列一定是空的)

此时我们已经判断出了所有的状态转移情况,此时我们建立一个二维数组进行存放

(第一维表示初状态,第二维表示目标状态,1表示可以转化,0表示不能转化)

1
2
3
4
5
6
int g[4][4]={
{1,1,1,1},
{0,0,1,1},
{0,1,0,1},
{1,0,0,0}
};

积木状态转移表:行列顺序均为00、01、10、11,四行依次为1111、0011、0101、1000。
积木·状态转移表

知道了状态转移,我们就可以写出dp了:

还记得我们的dp[i][j]的定义吗,前i-1列已经摆好,有几种可能,使得第i列情况是j

dp[i][j]中存的数字即为这个可能的事件数,容易知道,j一共只有四种可能(00,01,10,11)我们需建立一个dp[N][4]

100%做法(状压dp):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include<cstdio>
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的方案集合中去,实现状态转移。


H.扫雷

 题目:

扫雷题面:排雷火箭及炸雷会引爆各自圆形范围内的炸雷,包含边界,求连锁引爆的炸雷总数。
扫雷·题目

输入:

扫雷输入格式:先输入炸雷数n和排雷火箭数m,再分别输入各自的横坐标、纵坐标和爆炸半径。
扫雷·输入格式

2 1

2 2 4

4 4 2

0 0 5

输出:

输出一个整数表示答案。

2

样例说明:

扫雷样例示意:排雷火箭1覆盖炸雷1,炸雷1再覆盖炸雷2,两颗炸雷均被引爆。
扫雷·连锁引爆示例

评测用例规模与约定:

扫雷数据范围:坐标在0至10⁹之间,爆炸半径在1至10之间;40%用例n、m不超过10³,全部用例不超过5×10⁴。
扫雷·数据范围

问题解析:

看到这题人麻了

首先看到雷爆炸会带动周围其他雷爆炸,很容易想到要用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):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
#include<cstdio>
#include<queue>
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<int> 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<M;i++) h[i]=-1;
for(int i=1;i<=n;i++){ //地雷
scanf("%d%d%d",&a[i].x,&a[i].y,&a[i].r);
x=a[i].x;y=a[i].y;r=a[i].r;
int key=find(x,y);
/*if(h[key]==-1) */h[key]=getvalue(x,y);//如果哈希对应位置为空,则插入。
if(!id[key]||a[id[key]].r<r){//如果id数组没有被记录,或者来了一颗同位置且爆炸半径更大的雷
id[key]=i;
}
}
for(int i=1;i<=m;i++){ //导弹
scanf("%d%d%d",&x,&y,&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)) bfs(id[key]);
//有地雷,没有被访问过,能炸到,则开始进行扫雷。
}
}
}
for(int i=1;i<=n;i++){
int key=find(a[i].x,a[i].y);//找到坐标点对应的哈希下标
if(id[key]&&st[id[key]]) ans++;//如果这一点有地雷,且被访问过,则被扫到了
}
printf("%lld",ans);
return 0;
}

代码中bfs的部分没有做详细解释,可以做一些经典的bfs题学习一下!


I.李白打酒加强版

 题目:

李白打酒题面:初始有2斗酒,遇店酒量翻倍,遇花喝1斗;共遇店N次、花M次,最后遇花且酒恰好喝完,求顺序数量。无酒时遇店合法、遇花不合法。
李白打酒·题目

输入:

李白打酒输入格式:第一行输入两个整数N和M。
李白打酒·输入格式

5 10

输出:

输出一个整数表示答案。由于答案可能很大,输出模 1000000007 的结果。

14

样例说明:

如果我们用 0 代表遇到花,1 代表遇到店,14 种顺序如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
010101101000000
010110010010000
011000110010000
100010110010000
011001000110000
100011000110000
100100010110000
010110100000100
011001001000100
100011001000100
100100011000100
011010000010100
100100100010100
101000001010100

评测用例规模与约定 :

李白打酒数据范围:40%用例满足1≤N、M≤10,全部用例满足1≤N、M≤100。
李白打酒·数据范围

问题解析:

经典李白打酒,很明显的一道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):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include<cstdio>
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;k<N;k++){
if(i>0&&k+1<N) 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;
}

个人认为比积木画简单


J.砍竹子

 题目:

砍竹子题面:每次对连续且等高的一段竹子使用魔法,将高度H变为对“⌊H/2⌋+1”的平方根向下取整,求全部高度变为1的最少次数。
砍竹子·题目

输入:

砍竹子输入格式:第一行输入竹子棵数n,第二行输入n个正整数,表示各棵竹子的高度。
砍竹子·输入格式

6

2 1 4 2 6 7

输出:

一个整数表示答案。

5

样例说明:

其中一种方案:

1
2
3
4
5
6
  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步完成

评测用例规模与约定:

砍竹子数据范围:20%数据满足n≤1000、高度≤10⁶;全部数据满足n≤2×10⁵、高度≤10¹⁸。
砍竹子·数据范围

问题解析:

首先我们需要关注的是,只能对连续一段高度相同的竹子使用魔法

将这一段高度变为

高度变化公式:先对H/2向下取整,加1后开平方,再向下取整。
砍竹子·高度变化公式

我们不禁思考,这样的变化最多能进行几次,经过计算,1e18在经过6次变化后就会变为1,也就是说所有的数最多经历6次变化。

这道题主要考查我们的思维,我们把每个数字,在经历变化,最终变成1的过程,抽象为层,比如说1e18,他有6层,每层都有一个数字,逐渐减小,最终为1。

假如我们把所有数字单独隔离开,独自层层减少,可以得到一个总数,但这显然不是我们的答案,因为当一个数字的邻居和他相同时,可以一起减小,省去一次魔法。

那如果我们能找到所有数字及他们每一层的数字,整体进行比较,如果发现有相邻的情况,就让总魔法使用数减一,是不是就可以解出来了呢。

100%做法(思维):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include<cstdio>
#include<cmath>
#include<iostream>
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;i<n;i++){
long long x;
long long stk[M];//因为最后都为1,所以需要从小往大存,利用栈进行翻转
int top=0;
scanf("%lld",&x);
while(x>1) {
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<m;j++){ //遍历每一层
for(int i=1;i<n;i++){ //每一层的相邻数字,如果有相等则减1
if(f[i][j]&&f[i][j]==f[i-1][j]) ans--;
}
}
printf("%lld\n",ans);
return 0;
}

 看到还有线段树,优先队列等做法,可以去学习一下(

最后,祝大家在2023年蓝桥杯中能取得自己满意的成绩!!!

校订记录(2026-10-09):本页从原文迁入后,在“李白打酒加强版”的遇花状态转移中补充 k+1<N 边界判断,避免读取数组末尾之外的元素。该示例通过未定义行为检查,输入 5 10 仍输出 14;其余解法与原文日期保留,未重新核验整套题的评测结果。参考:C++ 标准工作草案的数组下标与指针运算范围。