前几天刚打完国赛,离考试周也还有几个星期,一堆实验报告看着就头疼,于是乎就来写写题解顺便复盘一下。
先来说说个人对今年cb省赛题目的感觉吧,整体而言比去年简单不少,动态规划、树、图什么的都没考,然后AC是签到题,其他感觉差不多吧。
既然你都来看题解了,想必高低也是想拿个省一吧,那我先说一些比赛小撬门吧。
首先是万能头文件#include <bits/stdc++.h>,比赛时直接写这个,这里面几乎包含了所有常用的函数。
然后是 ios::sync_with_stdio(false),cin.tie(0),cout.tie(0); 如果你是用cin和cout的话,建议加上这一句,这是用来关闭同步流的,因为开启同步流的话cin和cout相比于scanf和printf会慢很多,关闭了之后就跟scanf和printf差不多了。好像你用了这个的话就最好不要用scanf和printf了,会混乱什么的。
其次,蓝桥杯官方指定的编译器是dev c++,如果你是使用c++11的话,要在工具—编译选项那里加上-std=c++11,然后比赛时提交代码时要注意选择c++11。
最后,也是最重要的一点,一定要return 0,一定要return 0,一定要return 0!
如果你想刷题的话,建议去力扣,洛谷,牛客等网站刷,然后考前可以去蓝桥杯官网刷,或者去C语言网,那里有往年的蓝桥杯题集。
其实你可能平时在网上看到说蓝桥杯挺水的,(好吧其实确实挺水的),但是他的题并不水,大部分题在力扣里都是中等题甚至困难题,水的只是人而已,因为他不像ACM比赛一样要有资格才能参加,而且还是oi赛制,所以不会可以打暴力,所谓“暴力挂着机,打表出省一”就是这个道理,不然怎么说是暴力杯呢,我感觉只要学一些基本的算法,打打暴力都能省一了。
好啦,说了这么多,接下来就步入正题吧~
目录
A.握手问题
【问题描述】
小蓝组织了一场算法交流会议,总共有 50 人参加了本次会议。在会议上,大家进行了握手交流。按照惯例他们每个人都要与除自己以外的其他所有人进行一次握手(且仅有一次)。但有 7 个人,这 7 人彼此之间没有进行握手(但这 7 人与除这 7 人以外的所有人进行了握手)。请问这些人之间一共进行了多少次握手?
注意 A 和 B 握手的同时也意味着 B 和 A 握手了,所以算作是一次握手。
【答案提交】
这是一道结果填空的题,你只需要算出结果后提交即可。本题的结果为一个整数,在提交答案时只填写这个整数,填写多余的内容将无法得分。
思路:纯水题,感觉就是高中的简单数学题。第一个人和其他49个人握手,第二个人和除了第一个人以外的48个人握手,以此类推,答案就是49一直加到7,加到7是因为有7个人之间没有握手。可以用一层for循环解决,也可以直接用数列求和算。答案是1204。
#include <bits/stdc++.h>
using namespace std;
int main()
{
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
int ans=0;
for(int i=49;i>6;i--)
ans+=i;
cout<<ans;
return 0;
}
B.小球反弹
【问题描述】
有一长方形,长为 343720单位长度,宽为 233333 单位长度。在其内部左上角顶点有一小球(无视其体积),其初速度如图所示且保持运动速率不变,分解到长宽两个方向上的速率之比为 𝑑𝑥:𝑑𝑦=15:17。小球碰到长方形的边框时会发生反弹,每次反弹的入射角与反射角相等,因此小球会改变方向且保持速率不变(如果小球刚好射向角落,则按入射方向原路返回)。从小球出发到其第一次回到左上角顶点这段时间里,小球运动的路程为多少单位长度?答案四舍五入保留两位小数。
【答案提交】
这是一道结果填空的题,你只需要算出结果后提交即可。本题的结果为一个整数,在提交答案时只填写这个整数,填写多余的内容将无法得分。
思路:怎么说呢,感觉就是纯考数学。当时看了一会没什么思路就直接跳了,随便填了个20240413.15。
正解是,当他最后返回左上角的时候,走过的水平路程和垂直路程一定是343720和233333的偶倍数,而且他们的比是15 : 17,所以直接两层循环找符合条件的就行(我也不知道怎么推出来的)。答案是1100325199.77。
#include <bits/stdc++.h>
using namespace std;
int main()
{
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
for(long long i=2;i<10000;i+=2)
{
for(long long j=2;j<10000;j+=2)
{
if((long long)17*i*343720==(long long)15*j*233333)
printf("%llf\n",sqrt((343720*i*343720*i)+(233333*j*233333*j)));
}
}
return 0;
}
C.好数
题目描述
一个整数如果按从低位到高位的顺序,奇数位(个位、百位、万位 · · · )上 的数字是奇数,偶数位(十位、千位、十万位 · · · )上的数字是偶数,我们就称之为“好数”。 给定一个正整数 N,请计算从 1 到 N 一共有多少个好数。
输入格式
一个整数 N。
输出格式
一个整数代表答案。
样例
输入数据 1
24
输出数据 1
7
解释 #1
对于第一个样例,24 以内的好数有 1、3、5、7、9、21、23,一共 7 个。
输入数据 2
2024
输出数据 2
150
数据范围
- 对于 10% 的评测用例,1≤𝑁≤100。
- 对于 100% 的评测用例,1≤𝑁≤1e7。
思路:水题,直接遍历1到N的每一个数,判断是不是好数就行。
#include <bits/stdc++.h>
using namespace std;
int main()
{
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
int ans=0;
int n;
cin>>n;
for(int i=1;i<=n;i++)
{
int m=i,k=1;//用k计数当前是奇数位还是偶数位
int flag=1; //标记当前i是否符合条件
while(m)
{
int t=m%10;//当前位的数字
if(k%2&&t%2==0) //判断奇数位上的数字
flag=0;
if(k%2==0&&t%2) //判断偶数位上的数字
flag=0;
m/=10;
k++;
}
if(flag) //若i符合则答案加一
ans++;
}
cout<<ans;
return 0;
}
D.R格式
题目描述
小蓝最近在研究一种浮点数的表示方法:R 格式。对于一个大于 0 的浮点数 d,可以用 R 格式的整数来表示。给定一个转换参数 n,将浮点数转换为 R 格式整数的做法是:
- 将浮点数乘以 2^n;
- 四舍五入到最接近的整数。
输入格式
一行输入一个整数 n 和一个浮点数 d,分别表示转换参数,和待转换的浮点数。
输出格式
输出一行表示答案:d 用 R 格式表示出来的值。
样例
输入数据
2 3.14
输出数据
13
解释
3.14×2^2=12.56,四舍五入后为 13。
数据范围
- 对于 50% 的评测用例:1≤𝑛≤10,1≤ 将 d 视为字符串时的长度 ≤15。
- 对于 100% 的评测用例:1≤𝑛≤1000,1≤ 将 d 视为字符串时的长度 ≤1024;保证 d 是小数,即包含小数点。
思路:先来说说暴力流解法,直接用long double来存浮点数,然后循环n次乘以2,再加0.5四舍五入为整数,不过只能过一半的数据点左右。
#include <bits/stdc++.h>
using namespace std;
int main()
{
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
long double d;
int n;
cin>>n>>d;
while(n--)
d*=2;
long long ans=d+0.5;
cout<<ans;
return 0;
}
接下来说说正解,其实这题就是类似于高精度加法,我记得力扣上有类似的题,思路就是用一个字符串来存浮点数,然后将他倒置,从最低位开始乘2,同时注意进位和小数点,最后把小数点后的那位判断一下是否需要四舍五入即可,还是挺多细节要注意的。
比如输入2 3.14
我们将3.14倒置,变成41.3
然后从头开始乘以2,第一次变成82.6
第二次8*2=16,16,所以当前位变成6,有进位1;然后2*2=4,加上进位变成5;小数点跳过,然后6*2=12,当前位变成2,遍历完后仍然有进位,所以直接在最后面加个1,变成65.21
然后四舍五入,5大于4,需要进位,将2+1变成3
最后从尾遍历到小数点,答案就是13。
#include <bits/stdc++.h>
using namespace std;
int main()
{
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
string s;
int n;
cin>>n>>s;
reverse(s.begin(),s.end());//c++的倒置函数,这里将字符串反转会更好处理
while(n--) //循环n次
{
int k=0; //记录进位
for(int i=0;i<s.size();i++)
{
if(s[i]=='.') continue; //跳过小数点
int t=s[i]-'0'; //当前数字
t=t*2+k; //乘以2同时加上进位
s[i]=t%10+'0'; //更新当前位
k=t/10; //更新进位
}
if(k) //若最后仍然有进位,则在最后一位加上1
s+="1";
}
int i,k=0;
for(i=0;i<s.size();i++)
{
if(s[i+1]=='.') //此时是小数点后的第一位
{
k=(s[i]>'4')?1:0; //判断是否需要四舍五入
break; //跳过小数点
}
}
i+=2;//到小数点前的第一位
while(i<s.size())
{
int t=s[i]-'0';
t+=k;
s[i]=t%10+'0';
k=t/10;
i++;
}
if(k)
s+="1";
string ans=""; //记录小数点前的整数部分,即答案
for(int i=s.size()-1;s[i]!='.';i--)
ans+=s[i];
cout<<ans;
return 0;
}
E.宝石组合
题目描述
在一个神秘的森林里,住着一个小精灵名叫小蓝。有一天,他偶然发现了 一个隐藏在树洞里的宝藏,里面装满了闪烁着美丽光芒的宝石。这些宝石都有 着不同的颜色和形状,但最引人注目的是它们各自独特的 “闪亮度” 属性。每颗 宝石都有一个与生俱来的特殊能力,可以发出不同强度的闪光。小蓝共找到了 N 枚宝石,第 i 枚宝石的“闪亮度”属性值为 Hi,小蓝将会从这 N 枚宝石中选出三枚进行组合,组合之后的精美程度 S 可以用以下公式来衡量:
其中 LCM 表示的是最小公倍数函数。小蓝想要使得三枚宝石组合后的精美程度 S 尽可能的高,请你帮他找出精美程度最高的方案。如果存在多个方案 S 值相同,优先选择按照 H 值升序排列后字典序最小的方案。
输入格式
第一行包含一个整数 N 表示宝石个数。
第二行包含 N 个整数表示 N 个宝石的 “闪亮度”。
输出格式
输出一行包含三个整数表示满足条件的三枚宝石的 “闪亮度”。
样例
输入数据
5
1 2 3 4 9
输出数据
1 2 3
数据范围
- 对于 30% 的评测用例:3≤𝑁≤100,1≤𝐻𝑖≤1000。
- 对于 60% 的评测用例:3≤𝑁≤2000。
- 对于 100% 的评测用例:3≤𝑁≤1e5,1≤𝐻𝑖≤1e5。
思路:感觉这题也是考数学,应该是有什么规律,或者式子可以化简什么的,当时研究了二十分钟没研究出来直接暴力了,三层循环,感觉只能过百分之四十的数据点。
两个数的最小公倍数等于他们的乘积除以最大公约数,最大公约数可以用辗转相除法,也可以直接用c++库里自带的__gcd()函数,不知道是在哪个头文件里,直接用万能头文件就好了。
F.数字接龙
题目描述
小蓝最近迷上了一款名为《数字接龙》的迷宫游戏,游戏在一个大小为N × N 的格子棋盘上展开,其中每一个格子处都有着一个 0 . . . K − 1 之间的整数。游戏规则如下:
1. 从左上角 (0, 0) 处出发,目标是到达右下角 (N − 1, N − 1) 处的格子,每一步可以选择沿着水平/垂直/对角线方向移动到下一个格子。
2. 对于路径经过的棋盘格子,按照经过的格子顺序,上面的数字组成的序列要满足:0, 1, 2, . . . , K − 1, 0, 1, 2, . . . , K − 1, 0, 1, 2 . . . 。
3. 途中需要对棋盘上的每个格子恰好都经过一次(仅一次)。
4. 路径中不可以出现交叉的线路。例如之前有从 (0, 0) 移动到 (1, 1),那么再从 (1, 0) 移动到 (0, 1) 线路就会交叉。
为了方便表示,我们对可以行进的所有八个方向进行了数字编号,如下图2 所示;因此行进路径可以用一个包含 0 . . . 7 之间的数字字符串表示,如下图 1是一个迷宫示例,它所对应的答案就是:41255214。
现在请你帮小蓝规划出一条行进路径并将其输出。如果有多条路径,输出字典序最小的那一个;如果不存在任何一条路径,则输出 −1。
输入格式
第一行包含两个整数 N、K。
接下来输入 N 行,每行 N 个整数表示棋盘格子上的数字。
输出格式
输出一行表示答案。如果存在答案输出路径,否则输出 −1。
样例
输入数据
3 3
0 2 0
1 1 1
2 0 2
输出数据
41255214
解释
行进路径如图 1 所示。
数据范围
- 对于 80% 的评测用例:1≤𝑁≤5。
- 对于 100% 的评测用例:1≤𝑁≤10,1≤𝐾≤10。
思路:这题其实就是dfs走迷宫,只是多了一些约束条件。首先我们对dfs深搜设置几个变量,横坐标 x 和纵坐标 y ,也就是二维数组的 i 和 j ,还有一个字符串s用于记录到这一步所走过的路程。接下来我们来对条件一一分析。
第一个规则,从(0,0)到(n-1,n-1),每次走可以选择八个方向,那就设置两个数组,一个用于x的变化,一个用于y的变化,然后判断是否有越界即可。
第二个规则,经过的格子顺序满足0,1,2,……,k-1一直循环,那每次递归就加多一个变量t,用于记录下一次走的格子要满足的数字,比如这一次要走的格子数字是t,那下一次要走的格子数字就是(t+1)%k。
第三个规则,要对每个格子都恰好经过一次,那就设置多一个二维数组m,用于标记某一点是否走过,同时判断s的长度是否等于n*n-1,等于的时候才能将最终答案ans更新。
第四个规则,不可出现交叉的路线,我们仔细想想,其实只有当我们斜着走的时候才有可能出现交叉路线,比如我们从左下角走到右上角时,判断左上角和右下角是否已经走过,若都走过则直接跳过。
以下是具体代码实现。
#include <bits/stdc++.h>
using namespace std;
string ans; //最终答案,用全局变量会比较方便
int n,k;
int a[15][15],m[15][15];
int nx[8]={-1,-1,0,1,1,1,0,-1}; //八个方向中对应x的变化
int ny[8]={0,1,1,1,0,-1,-1,-1}; //八个方向中对应y的变化
void dfs(int x,int y,string s,int t)
{
if(x==n-1&&y==n-1) //判断已经到终点
{
if((ans==""||s<ans)&&s.size()==n*n-1) //判断是否全部点都走过
ans=s;
return ; //直接返回
}
for(int i=0;i<8;i++) //八个方向依次遍历
{
int tx=x+nx[i],ty=y+ny[i]; //下一步的坐标
if(tx<n&&tx>=0&&ty<n&&ty>=0&&a[tx][ty]==t&&m[tx][ty]==0) //判断是否越界与是否走过
{
if(i%2&&m[x][ty]&&m[tx][y]) continue; //判断交叉
m[tx][ty]=1; //标记走过
dfs(tx,ty,s+to_string(i),(t+1)%k); //to_string用于将数字转换为字符串
m[tx][ty]=0; //撤销标记
}
}
}
int main()
{
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>k;
for(int i=0;i<n;i++)
{
for(int j=0;j<n;j++)
cin>>a[i][j];
}
m[0][0]=1; //标记起点
dfs(0,0,"",1); //从起点开始深搜
if(ans=="") //没有路径可到终点
cout<<-1;
else
cout<<ans;
return 0;
}
上面是我比赛的时候写的,但是后来比赛完同学问我才发现这样子是错的,不能AC,能过90%左右。
例如上面这种情况,左下角和右上角都走过,但是并没有交叉。
因此我们直接用一个四维数组标记路径,比如从(3,1)走到(2,2)就标记mark[3][1][2][2]和mark[2][2][3][1],当要从(3,2)走到(2,1)时,再检查mark[3][1][2][2]是否标记过即可。
此外还有一个错误,就是从起点开始深搜的时候,t也要进行%k操作,因为当k=1时,就是一直走0的格子。这一步还是挺细节的。
同时我们可以进行剪枝优化,因为我们遍历八个方向是从0到7依次遍历的,所以第一个到达终点且满足所有点都走过的一定是字典序最小的答案,其余的通通return。
#include <bits/stdc++.h>
using namespace std;
string ans;
int n,k;
int a[15][15],m[15][15];
int mark[15][15][15][15];
int nx[8]={-1,-1,0,1,1,1,0,-1};
int ny[8]={0,1,1,1,0,-1,-1,-1};
void dfs(int x,int y,string s,int t)
{
if(ans.size()) //剪枝
return ;
if(x==n-1&&y==n-1)
{
if(s.size()==n*n-1)
ans=s;
return ;
}
for(int i=0;i<8;i++)
{
int tx=x+nx[i],ty=y+ny[i];
if(tx<n&&tx>=0&&ty<n&&ty>=0&&a[tx][ty]==t&&m[tx][ty]==0)
{
if(i%2&&mark[tx][y][x][ty]) continue; //奇数时即为斜着走
m[tx][ty]=1;
mark[tx][ty][x][y]=mark[x][y][tx][ty]=1; //标记
dfs(tx,ty,s+to_string(i),(t+1)%k);
mark[tx][ty][x][y]=mark[x][y][tx][ty]=0; //撤销标记
m[tx][ty]=0;
}
}
}
int main()
{
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>k;
for(int i=0;i<n;i++)
{
for(int j=0;j<n;j++)
cin>>a[i][j];
}
m[0][0]=1;
dfs(0,0,"",1%k); //第一步也需要处理%k
if(ans=="")
cout<<-1;
else
cout<<ans;
return 0;
}
G.爬山
题目描述
小明这天在参加公司团建,团建项目是爬山。在 x 轴上从左到右一共有 n 座山,第 i 座山的高度为 hi。他们需要从左到右依次爬过所有的山,需要花费 的体力值为 。
然而小明偷偷学了魔法,可以降低一些山的高度。他掌握两种魔法,第一 种魔法可以将高度为 H 的山的高度变为 ⌊⌋,可以使用 P 次;第二种魔法可以将高度为 H 的山的高度变为 ⌊H/2⌋,可以使用 Q 次。并且对于每座山可以按任意顺序多次释放这两种魔法。
小明想合理规划在哪些山使用魔法,使得爬山花费的体力值最少。请问最优情况下需要花费的体力值是多少?
输入格式
输入共两行。 第一行为三个整数 n,P,Q。
第二行为 n 个整数 h1,h2,...,hn。
输出格式
输出共一行,一个整数代表答案。
样例
输入数据
4 1 1
4 5 6 49
输出数据
18
解释
将第四座山变为 =7,然后再将第四座山变为 ⌊7/2⌋=3。 体力值为 4+5+6+3=18。
数据范围
- 对于 20% 的评测用例,保证 n≤8,P=0。
- 对于 100% 的评测用例,保证 n≤100000,0≤P≤n,0≤Q≤n,0≤hi≤100000。
思路:怎么说呢,当时看完题目后第一感觉就是贪心,就是每次取最大的山,而且优先开根号然后再除以二,因为开根号比除以二所减少的更多。每次取最大值可以用大根堆,也就是优先队列,c++的priority_queue,插入的时间复杂度是log(n)。
#include <bits/stdc++.h>
using namespace std;
int main()
{
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
int ans=0;
int n,P,Q,h;
cin>>n>>P>>Q;
priority_queue<int> q; //默认大根堆
for(int i=0;i<n;i++)
{
cin>>h;
q.push(h);
}
while(q.size()&&P) //先处理根号
{
int t=q.top();
q.pop();
q.push(sqrt(t));
P--;
}
while(q.size()&&Q) //除以二
{
int t=q.top();
q.pop();
q.push(t/2);
Q--;
}
while(q.size()) //剩下的加一起
{
ans+=q.top();
q.pop();
}
cout<<ans;
return 0;
}
是不是看起来很简单而且挺有道理的样子,当时比赛我感觉好像不太对劲,应该没这么简单,但是想了挺久找不出反例,就没管他了。
结果这是错的,会被hack,大概能过80%,反例是:
2 1 1
49 48
根号49+48/2=7+24=31,49/2+根号48=24+6=30
(听说出题人的题解也是被这个数据给hack了,大概率是错题……)
H. 拔河
题目描述
小明是学校里的一名老师,他带的班级共有 n 名同学,第 i 名同学力量值为 ai。在闲暇之余,小明决定在班级里组织一场拔河比赛。
为了保证比赛的双方实力尽可能相近,需要在这 n 名同学中挑选出两个队伍,队伍内的同学编号连续:{al1, al1+1, ..., ar1−1, ar1} 和 {al2, al2+1, ..., ar2−1, ar2},其中 l1 ≤ r1 < l2 ≤ r2。
两个队伍的人数不必相同,但是需要让队伍内的同学们的力量值之和尽可能相近。请计算出力量值之和差距最小的挑选队伍的方式。
输入格式
输入共两行。
第一行为一个正整数 n。
第二行为 n 个正整数 ai。
输出格式
输出共一行,一个非负整数,表示两个队伍力量值之和的最小差距。
样例
输入数据
5
10 9 8 12 14
输出数据
1
解释
其中一种最优选择方式:队伍 1:{a1, a2, a3},队伍 2:{a4, a5},力量值和分别为 10 + 9 + 8 = 27,12 + 14 = 26,差距为 |27 − 26| = 1。
数据范围
- 对于 20% 的评测用例,保证 n≤50。
- 对于 100% 的评测用例,保证 n≤1e3,ai≤1e9。
思路:先用一个数组记录前缀和,也就是a[i]代表从0到i的力量值之和,这样l到r之间的力量值就等于a[r]-a[l],能大大减少时间。
首先是喜闻乐见的暴力流做法,直接四层for循环,遍历所有情况l1,r1,l2,r2,简单轻松拿下小数据,估计能拿下40%,注意题目没说所有人都要上场。
当时我比赛的时候用的是三层循环+二分,就是只遍历r1,l2,r2,至于l1可以二分查找0到r1的下标,因为力量都是非负整数,所以肯定是递增数列,估计能过70%或者80%左右吧。
正解是两层循环+二分,要用到c++的set,就是首先用两层循环遍历所有区间,将所有区间的值保存到set里,然后再用两层循环遍历l1和r1,然后再在set里二分查找最接近当前区间的值(因为set内部会自动排序数据,所以是递增的,可以二分),同时实时更新set里的数据即可。
感谢大家能看到这里QAQ,希望大家都能赛出好成绩~~~