第二届四川省青少年C++算法设计大赛小学4-5年级组全省总决赛 综合题解
题面以洛谷B4230,B4231,B4232,B4233,B4234,B4235,B4236为准。
话说回来我一个初一的还来切小学生的红题是不是不太厚道
这是一场全省总决赛。题确实出得不错。
但 四五年级 的 小朋友 能AK吗???
最好不要低估现在小学生的实力。我六年级时都没AK市赛,现在好像已有五年级学生做到了。
#1
传送门
签到题。
考察整除及取余运算符的应用。没什么好说的。
#include<iostream>
using namespace std;
int main(){
int n, a;
cin >> n >> a;
cout << n / a * 5 << ' ' << n % a;//写给小学生:C++中整数/整数时会取整
return 0;
}
我绝对不会告诉你这道题我做了3遍才过
#2
传送门
这题让我想起P1425 小鱼的游泳时间,但实际上本题更难,因为本题时间会跨天。
所以得写分支结构。因为是24小时制,且最长睡23小时(真能睡的),所以如果闹钟时间大于睡觉时间就是同一天,否则跨天。
跨天怎么处理?24小时-睡觉时间+闹钟时间。
不够减时(减出来是负数时)没有必要“借一个小时”,难以理解,全部化秒然后转回来。
#include<iostream>
using namespace std;
int h, m, s;
//转分钟
int to_minute(int h, int m, int s) {
return 3600 * h + 60 * m + s;
}
//算分钟
int sleep_time(int h1, int m1, int s1, int h2, int m2, int s2) {
if (h1 >= h2) return to_minute(h1, m1, s1) - to_minute(h2, m2, s2);//同日
else return 60 * 60 * 24 - to_minute(h2, m2, s2) + to_minute(h1, m1, s1);//跨日
//本来想用三目运算符,但那样可读性太差
}
//转时间
void to_clock(int se) {//需要新名字防止被覆盖
h = se / 3600; se %= 3600;//小时
m = se / 60; se %= 60;//分钟&秒
s = se;
//为什么这个函数是void?因为要返回多个值。
}
int main(){
int h1, m1, s1, h2, m2, s2;
cin >> h1 >> m1 >> s1;
cin >> h2 >> m2 >> s2;
to_clock(sleep_time(h1, m1, s1, h2, m2, s2));
cout << h << ' ' << m << ' ' << s;
return 0;
}
作为第二题写得太烦了。
#3
传送门
注意四个点的坐标并没有按一定的顺序排列。本来想把矩形割成两个三角形算,后来发现没有必要。作为平行于坐标轴的矩形,设长为
l
l
l,宽为
w
w
w ,则四个点依次为:(图不给了)
- ( x , y ) (x,y) (x,y)
- ( x , y + l ) (x,y+l) (x,y+l)
- ( x + w , y ) (x+w,y) (x+w,y)
- ( x + w , y + l ) (x+w,y+l) (x+w,y+l)
很好理解对吧?所以我们只要比较前一项,大的减小的就是 w w w;比较后一项,大的减小的就是 l l l。然后你就知道了,乘一乘。
#include<iostream>
#include<algorithm>
using namespace std;
int main(){
int x1, y1, x2, y2, x3, y3, x4, y4, l, w;
cin >> x1 >> y1;
cin >> x2 >> y2;
cin >> x3 >> y3;
cin >> x4 >> y4;
//热知识:只用比较三个,且“可以用函数”
w = max(x1, max(x2, x3)) - min(x1, min(x2, x3));//一种很傻的写法,真的。
l = max(y1, max(y2, y3)) - min(y1, min(y2, y3));
cout << w * l;
return 0;
}
怎么只有五个点?太敷衍了。
#4
传送门
这题让我想起B2046 骑车与走路,本质上一模一样。但本题要注意:
如果时间相同,则优先步行,其次坐公交车,最后骑共享单车。
这一点的实现或许会让人费一点脑子,甚至直接上手强行写了。其实我们想:如果三种都相等,不是直接选步行就好了?如果步行慢但另两种相等,不是直接选公交车就好了?这让我们在不知不觉中完成了这一过程。
另外,我认为计算时乘个
1.0
1.0
1.0 会好一点。这样有效处理小数的情况。
如果小朋友们知道怎么保留小数的话,就没问题了。
#include<iostream>
#include<algorithm>
#include<cstdio>
using namespace std;
int main(){
int n, t, m, a, b, c;
cin >> n >> t >> m;
cin >> a >> b >> c;
double walk_time = 1.0 * n / a;//步行
double bus_time = t + 1.0 * n / b;//公交
double bike_time = (1.0 * m / a) + 1.0 * (n + m) / c;//共享单车
//这一段跟题解其实是一样的
//但下面又是那种很傻的写法,真的。
if (walk_time == min(walk_time, min(bus_time, bike_time))) cout << "MODE I";
else if (bus_time == min(walk_time, min(bus_time, bike_time))) cout << "MODE II";
else cout << "MODE III";
printf("\n%.2lf", min(walk_time, min(bus_time, bike_time)));
//定义一个新变量也不至于像上面一样可读性这么差。
//以下代码才有可读性:
/*
min_t = min(walk_time, min(bus_time, bike_time));
if (walk_time == min_t) cout << "MODE I";
else if (bus_time == min_t) cout << "MODE II";
else cout << "MODE III";
printf("\n%.2lf", min_t);
*/
return 0;
}
#5
传送门
模拟。纯模拟。
简单来说,开数组,读数据,然后打模拟。按照题意增加学习值与专注度,结束以后直接输出。这道题反而非常简单,除了很明显的变量要初始化以外根本没坑,还只有两种分支,可以说是世界上最良心的模拟了。
但还要说一点提到过的:C++中的除法/
。它是取整的,所以取一半值时自动向下取整了,和py的//
一样,这个特性可以完全忽视向下取整。(前提是操作的是整数)
#include<iostream>
using namespace std;
int main(){
int n, m, a[1010], S = 0, tot = 0;//天数,认真阈值,每日学习值,专注度,总学习值
//其实不调用数组也没关系,每天的学习值前后无关联,可以边输入边判断
cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> a[i];
//不写函数了,直接模拟
for (int i = 1; i <= n; i++) {
if (S >= m) {
tot += a[i] * 2;
S /= 2;//无需考虑向下取整
}
else {
tot += a[i] / 2;//同样地,无需考虑
S += a[i];
}
}
cout<<tot;
return 0;
}
#6
传送门
还记得B2083 画矩形吗?当时我坐在电脑前调试了一遍又一遍。
不过本题简单得多。全是正方形,还只有空心没有实心,还不用用户输入字符,简直就是极简版。
一看就知道打循环嵌套。求出实心的行数(
a
−
b
2
\frac{a-b}{2}
2a−b),再求空心的行中实心的列数(因为是正方形所以也是
a
−
b
2
\frac{a-b}{2}
2a−b),然后外面的循环控制行,要输出无空格——有空格——无空格的行,里面的循环输出实——空——实的字符,注意换行,这道题就解决了。
#include<iostream>
using namespace std;
int main() {
int a, b, emp;
cin >> a >> b;
emp = (a - b) / 2;//框中间的宽度
//上边框
for (int i = 1; i <= emp; i++) {//实
for (int j = 1; j <= a; j++) cout << '*';
cout << endl;
}
//中边框(有空余部分)
for (int i = 1; i <= b; i++) {//是正方形所以可以一直用,空
for (int j = 1; j <= emp; j++) cout << '*';//实
for (int j = 1; j <= b; j++) cout << ' ';//空
for (int j = 1; j <= emp; j++) cout << '*';//实
cout << endl;
}
//下边框
for (int i = 1; i <= emp; i++) {//实
for (int j = 1; j <= a; j++) cout << '*';
cout << endl;
}
return 0;
}
居然图形输出也会出,确实考得很全面。
#7
传送门
最后一题。最复杂的题目往往只需要最简单的题面,(再往后看你就会知道代码也是最简单的)看一眼我们就明白了——没错,数论。
第二眼:这什么数论啊,不会有人现场写暴力吧?真应该让他们看看TLE声一片的场景。
阶乘末尾零的个数……那就让我们用小学的思路想想吧。小学奥数里好像教过,那肯定会先想到
10
=
2
×
5
10=2\times5
10=2×5,所以考虑有多少个
2
2
2 和
5
5
5 就好了。
如果是普通的数还得分解质因数之类的,但阶乘不用。因为首先我们要理解:
n
!
=
∏
i
=
1
n
i
n!=\prod_{i=1}^n i
n!=i=1∏ni
然后,一个数的阶乘中
2
2
2 的数量肯定比
5
5
5 的数量多,这个可以列举证明,所以考虑
5
5
5 的数量就好了。
怎么考虑呢?
当然是跳过不为
5
5
5 的倍数的数,计算为
5
5
5 的倍数的数的数量了。
废话,看看数据吧,
n
<
2
×
1
0
9
n<2\times10^9
n<2×109,把枚举
20
20
20 亿次的工作量效率提升
5
5
5 倍,跟不提升有区别吗???
我们想到,可以按区间来找
5
5
5 的个数。以什么为区间呢?如果每五个为一个区间,那可以得到
O
(
1
)
O(1)
O(1) 的复杂度。但问题就在这里:区间中不仅有
5
1
5^1
51,还有
5
2
5^2
52,
5
3
5^3
53 等,这些数的倍数一个含有多个五。
然后我们递推一下:
- 当区间为 5 0 ∼ 5 1 − 1 5^0\sim5^1-1 50∼51−1 时,共 0 0 0 个 5 5 5,为 { } \{\} {}。
- 当区间为 5 1 ∼ 5 2 − 1 5^1\sim5^2-1 51∼52−1 时,共 4 4 4 个 5 5 5,为 { 5 , 10 , 15 , 20 } \{5,10,15,20\} {5,10,15,20}。
- 当区间为 5 2 ∼ 5 3 − 1 5^2\sim5^3-1 52∼53−1 时,共 20 20 20 个 5 5 5,为 { 25 , 30 , 35 , 40 , 45 , 50 , 55 , ⋯ , 120 } \{25,30,35,40,45,50,55,\cdots,120\} {25,30,35,40,45,50,55,⋯,120}。
- 当区间为 5 3 ∼ 5 4 − 1 5^3\sim5^4-1 53∼54−1 时,共 100 100 100 个 5 5 5,为 { 125 , 130 , 135 , 140 , 145 , 150 , 155 ⋯ , 620 } \{125,130,135,140,145,150,155\cdots,620\} {125,130,135,140,145,150,155⋯,620}。
那不就好了吗?
什么,没懂?那……
换句话说,我们发现了一个伟大的定律:只要我们把这个数不断除以五,直到除不了为止,把每次除得的数相加,就是它含有的
5
5
5 的个数,即末尾
0
0
0 的个数!
这一过程类似于进制转换。可以从枚举过程中推出,不过写成幂次方的形式就不怎么看得出来,需要自己算一下。
最后我们模拟该过程即可。代码是真简单。
#include<iostream>
using namespace std;
int main() {
int n, c = 0;//初始化
cin >> n;
while (n > 0) {
n /= 5;//除以5
c += n;//用总结出的定律计算
}
cout << c;
return 0;
}
是不是很强大?
就这样。我们AK了一场省选。不过是小学生省选,而且我们也不是小学生。
不过用来切切水题也是不错的。