【题目描述】
输入正整数a,b,c,输出a/b的小数形式,精确到小数点后c位。a,b ≤10^6 ,c≤100。输入包含多组数据,结束标记为a=b=c=0。
【样例输入】
1 6 4
0 0 0
【样例输出】
Case 1: 0.1667
【题目来源】
刘汝佳《算法竞赛入门经典 第2版》习题2-5 分数化小数(decimal)
【解析】
这道题很简单呀!
等等,以前都是题目中明确给出保留几位小数,比如要求保留3位小数,咱们就可以:
printf(“%.3f”, a/b);
本题要保留的小数位数是在程序运行时指定的,取决于用户输入的c,这该怎么办呢?难不成要写成这样:
printf(“%.%df”, c, a/b);
实际测试完全行不通,看来格式说明符是不能嵌套的。这其实很好理解,如果支持嵌套,不光看起来乱,还会导致歧义。比如%%df,按理%%应解释为转义字符,用来表示字符“%”。
一、用系统函数动态输出n位小数
1.C语言版
这里涉及一个新的语法知识点,要实现按变量的值动态输出小数位数,就要把代码改成这样子:
printf(“%.*f”, c, a/b);
*和f一样,也是一个占位符,起占位作用,它表示从参数中取一个整数值来作为浮点数的精度。
学会了这种操作,代码马上呼之欲出:
#include<stdio.h>
int main(){
int a, b, c, kase=0;
while(scanf("%d%d%d", &a, &b, &c)==3 && a*b*c){
double f = (double)a/b;
printf("Case %d: %.*f\n", ++kase, c, f);
}
return 0;
}
输入样例测试也满全符合,是不是大功告成了呢?
2. C++版
除了上面的C语言版,也可以使用纯C++语法动态输出小数位数(语法非常麻烦)。
#include <iostream>
#include <iomanip> // 包含 setprecision 的头文件
using namespace std;
int main() {
int a, b, c, kase=0;
while(scanf("%d%d%d", &a, &b, &c)==3 && a*b*c){
double f = (double)a/b;
cout << "Case " << ++kase << ": " << fixed << setprecision(c) << f << endl;
}
return 0;
}
遗憾的是,咱们费了半天劲儿,上面的面代码却都是错误的。
重点就在于题目要求c≤100,如果输入1 6 100,马上就会发现输出结果不正确。
测试是非常重要的,尤其是边界数据的测试更为重要。
因为double的有效精度只有16位,因而用printf根本无法有效输出100位小数。
怎么办呢?自己动手,丰衣足食。
二、自己动手算小数
咱们要用程序模拟小数除法运算,想想小学时学的运算规则:每次求出的余数后添0,然后继续除。比如28÷16:
算法可以描述为:
①求商的整数部分。
②求商的小数部分:重复将余数乘10继续除。
这就完了吗?别忘了“精确到小数点后c位”的意思?最后一位输出的小数需要四舍五入。
代码如下:
#include<stdio.h>
int main(){
int a, b, c, kase=0;
while(scanf("%d%d%d", &a, &b, &c)==3 && a*b*c){
//输出小数点前的内容
printf("Case %d: %d.", ++kase, a/b);
//输出小数点后的c-1位
a=a%b;
for(int i=1; i<=c-1; i++){
a*=10;
printf("%d", a/b);
a = a%b; //获取第i位运算后的余数
}
//输出第c位小数,四舍五入
a*=10;
printf("%d", a/b + 10*(a%b)/b/5);
printf("\n");
}
return 0;
}
四舍五入部分当然也可以用If语句判断第c+1位小数的值是否大于等于5,这里用了一个巧算,用n/5即可实现当n≥5时,结果为1。
这回终于结束了吧!
No,No,No,客官即将迎来本题的最大深坑。
如果输入下面的数据,就知道上面的代码问题出在哪了。
999 1000 2
9999 1000 2
输出结果:
因为小数部分有一连串的9,进位会导致前面的小数甚至整数全部改变,因而按照上面的代码就会输出错误结果。
如果给咱9999/1000这道算术题,让咱保留两位小数,咱肯定不会算错。但是让咱设计保留2位小数的算法,就非常容易忽略这种情况。前者是看到某种情形,想到对应规则,后者是想到所有情形,想到对应规则,难度不可同日而语。
所以算法设计必须要穷尽所有可能性,遗漏任何一种情形都会导致出错。对于数据来讲,最容易被忽略的就是边界数据。
比如本题,小数位数100位是边界,最后一位小数要四舍五入是边界,9进位会变成0也是边界(9是最大数字)。因此,对于任何类型的数据,一定要养成思考边界、测试边界的习惯。
三、用数组解决999式进位问题
要解决这个问题,就需要用到数组,把每位小数都存到数组中,再根据各位小数值及是否要进位重新修正每位小数的值。
代码如下:
#include<stdio.h>
int main(){
int a, b, c, point, d[105], kase=0;
while(scanf("%d%d%d", &a, &b, &c)==3 && a*b*c){
//求出小数点后的c+1位
int r=a%b;
for(int i=1; i<=c+1; i++){
r*=10;
d[i] = r/b;
r%=b; //获取第i位运算后的余数
}
//根据是否进位修正各位小数
d[0] = 0; //保存小数到整数的进位
if(d[c+1] >= 5){
for(int i=c; i>=0; i--){
if(9 == d[i]){
d[i] = 0;
} else{
d[i] += 1;
break;
}
}
}
//输出小数点前的内容
printf("Case %d: %d.", ++kase, a/b + d[0]);
//输出小数部分
for(int i=1; i<=c; i++){
printf("%d", d[i]);
}
printf("\n");
}
return 0;
}
需要说明的是,本题是第2章习题,此章节还没学到数组,但这道题目前老金只会用数组求解。如有高人知道不用数组的解法,还望指点一二!
最后总结一下,这道题非常容易出错,它有三大神坑:
①用printf函数最多只能输出16位有效小数,无法输出100位,必须要自己模拟除法运算求小数。
②最后一位小数需要根据下一位小数四舍五入。
③最后一位小数如果是9,在四舍五入时会导致前面的小数发生变化,如果有一连串的9,可能会使所有小数甚至整数部分都产生变化。这是本题最大的坑,可谓神之一坑。