[蓝桥杯]R格式(CC++双语版)
如果你看了多篇题解还是不明白,那么相信该文可以让你有所收获,本文同样适用于不会高精度算法的同学,只是理解起来可能比会高精度的同学略难以理解。话不多说,我们开始学习!
题目链接
(这道题不建议用洛谷,洛谷少了一个一直进位直到进到最高位的测试用例)
蓝桥杯2024年第十五届省赛真题-R 格式 - C语言网
题目理解
简单来说,小蓝研究的这种 R 格式表示方法,就是对于一个大于 0 的浮点数(比如 3.14 这样的数),给定一个整数参数 n(比如 n = 2 ),先把这个浮点数乘以 2 的 n 次方(这里就是 3.14 乘以 2 的 2 次方,也就是 3.14×4 = 12.56 ),然后把得到的结果四舍五入到离它最近的整数(12.56 四舍五入后就是 13 ),最后得到的这个整数就是该浮点数用 R 格式表示的结果。
解题思路
最开始的浮点型我们用字符串类型接收,原因我放在了疑难解答处。
我们以题目示例展开讲解:
1.存放数字
下面是浮点数在字符型数组中的存储情况:
2.记录小数点位置并将其转为整型
但是字符型是不便于我们进行运算的,于是我们创建整型数组将其放入并记录小数点位置:
与此同时,我们将字符型转换为整型,放入新建的intd数组中:
3.计算整型数乘以2ⁿ
后面我们要做的就简单了,只需要计算如下公式:
4.计算操作
计算操作为将该数组每一位数乘2,然后处理进位,我们还以示例中n=2举例:
第一次乘2,不需要进位:
第二次乘2:
进位,当某数组中有大于等于10的数字时,我们将该数字减10,并给前一位加1。如果是数组的首位数字,我们应该在最前方添加一位数字‘1’:
但是我们操作时候发现进位不方便操作,并且添加数字时也遇到了困难。所以从一开始,这个数组是逆序存放的,也就是说,此时存储的形式为:
这样一来,不仅方便从后向前记录小数点位置。我们首位数字进位时,只需在末尾添加1就可以了。(至于逆序,大家完全不用纠结,我们只需要在输出的时候逆序输出就可以,学过高精度的同学应该很好理解):
//循环次数为字符串长度 //从后向前遍历,方便记录 for(int i=d.length()-1;i>=0;i--) { if(d[i]!='.') { //move变量用于跟踪数组,直到找到小数点 move++; //将字符型转为整型存入intd数组中 intd.push_back(d[i]-'0'); } else { //用pos记录小数点位置 pos=move; } }
其实很简单,只需要将i的初始值赋值为字符串长度,i小于0时终止循环,我们就从数组末尾开始操作了。
下面是我们计算的代码:
//进行n次,每一位乘2 for(int i=0;i<n;i++) { //每一位乘2并进位 for(int j=0;j<intd.size();j++) { intd[j]*=2; } for(int j=0;j<intd.size();j++) { if(intd[j]>=10) { //数组第一位从0开始,故减1 if(j==intd.size()-1) { intd[j]-=10; //向数组末尾加1,即进位 intd.push_back(1); } else { intd[j]-=10; intd[j+1]++; } } } }
5.处理四舍五入
此时我们用于记录小数点后位数的pos就有用了,因为数组从0开始存储,那么此时数组中的pos-1就是小数点后第一位数。比如数字为 3.14 时,pos=2(小数点后的位数)。因为是逆序,所以整型数组为413,数组[pos-1]即第2位数字‘1’。其为小数点后第一位数字。当其大于等于5时,进位即可。我们接着刚刚的例子分析:
数字大于等于5,我们向前加1,小数点后的数字就不用理会,输出时我们限制条件使其不进行输出即可:
代码如下:
//处理四舍五入 //intd[pos-1]为小数点后一位的数字,大于5进位 if(intd[pos-1]>=5) { //前一位进位加1 intd[pos]++; //intd[pos] 加 1 后可能会产生进位,且进位可能会持续影响后续高位 if(intd[pos]>=10) { //数组第一位从0开始,故减1 if(pos==intd.size()-1) { intd[pos]-=10; //向数组末尾加1,即进位 intd.push_back(1); } else { intd[pos]-=10; intd[pos+1]++; } } }
6.输出结果
我们给for的循环条件限定如下,这样就可以逆序输出小数点以前的数字:
//因为数组中的数字是逆序的,这里输出需要逆序 for(int j=intd.size()-1;j>=pos;j--) { cout<<intd[j]; }
完整代码
1.C++版
#include<bits/stdc++.h>
using namespace std;
int main()
{
int n;
//string省去了最后去除末尾0的步骤
string d;
cin>>n>>d;
//记录小数点后有几位数,即小数点位置
int pos=0,move=0;
//创建数组intd,作为字符串d转换成可运算的int型数组的容器
vector<int> intd;
//循环次数为字符串长度
//从后向前遍历,方便记录
for(int i=d.length()-1;i>=0;i--)
{
if(d[i]!='.')
{
//move变量用于跟踪数组,直到找到小数点
move++;
//将字符型转为整型存入intd数组中
intd.push_back(d[i]-'0');
}
else
{
//用pos记录小数点位置
pos=move;
}
}
//进行n次,每一位乘2
for(int i=0;i<n;i++)
{
//每一位乘2并进位
for(int j=0;j<intd.size();j++)
{
intd[j]*=2;
}
for(int j=0;j<intd.size();j++)
{
if(intd[j]>=10)
{
//数组第一位从0开始,故减1
if(j==intd.size()-1)
{
intd[j]-=10;
//向数组末尾加1,即进位
intd.push_back(1);
}
else
{
intd[j]-=10;
intd[j+1]++;
}
}
}
}
//处理四舍五入
//intd[pos-1]为小数点后一位的数字,大于5进位
if(intd[pos-1]>=5)
{
//前一位进位加1
intd[pos]++;
//intd[pos] 加 1 后可能会产生进位,且进位可能会持续影响后续高位
if(intd[pos]>=10)
{
//数组第一位从0开始,故减1
if(pos==intd.size()-1)
{
intd[pos]-=10;
//向数组末尾加1,即进位
intd.push_back(1);
}
else
{
intd[pos]-=10;
intd[pos+1]++;
}
}
}
//因为数组中的数字是逆序的,这里输出需要逆序
for(int j=intd.size()-1;j>=pos;j--)
{
cout<<intd[j];
}
return 0;
}
2.C语言版
思路是一样的,只是因为数组是静态的,需要进行附加操作。
#include <stdio.h>
#include <string.h>
#include <stdbool.h>
#define MAX_LEN 100000 // 假设足够大的数组长度
int main() {
int n;
char d[MAX_LEN];
scanf("%d %s", &n, d); // 读取输入参数n和浮点数字符串d
int intd[MAX_LEN * 2] = {0}; // 存储各数字位的数组,初始化为0
int len = 0; // 数组当前长度
int pos = 0; // 小数点后的位数
bool has_decimal = false; // 标记是否有小数点
// 逆序处理字符串d,将每个数字存入数组,并记录小数点位置
for (int i = strlen(d) - 1; i >= 0; i--) {
if (d[i] != '.') {
intd[len++] = d[i] - '0'; // 字符转数字并存入数组
} else {
pos = len; // 记录小数点后的位数(当前已处理数字的个数)
has_decimal = true;
}
}
// 乘以2的n次方
for (int i = 0; i < n; i++) {
// 每位数字乘以2
for (int j = 0; j < len; j++) {
intd[j] *= 2;
}
// 处理进位
for (int j = 0; j < len; j++) {
if (intd[j] >= 10) {
int carry = intd[j] / 10;
intd[j] %= 10;
if (j + 1 < len) {
intd[j + 1] += carry; // 向前进位
} else {
intd[len++] = carry; // 扩展数组
}
}
}
}
// 四舍五入处理
if (has_decimal && pos > 0) { // 当存在小数部分时才处理
if (intd[pos - 1] >= 5) {
// 处理进位,需要考虑pos可能等于len的情况
int j = pos;
bool carry = true; // 初始需要进位
while (carry && j < MAX_LEN * 2) {
if (j >= len) {
intd[len++] = 1; // 扩展数组
carry = false;
} else {
intd[j] += 1;
if (intd[j] < 10) {
carry = false;
} else {
intd[j] = 0;
j++; // 继续进位
}
}
}
}
}
// 输出整数部分(逆序输出小数点前的部分)
// 需要处理全零的情况
bool all_zero = true;
for (int j = len - 1; j >= pos; j--) {
if (intd[j] != 0) {
all_zero = false;
break;
}
}
if (all_zero && len > pos) {
printf("0"); // 如果整数部分全零,输出0
} else {
for (int j = len - 1; j >= pos; j--) {
printf("%d", intd[j]);
}
}
printf("\n");
return 0;
}
疑难解答
1.用字符串接收浮点数的原因
精度问题
- 浮点型存储限制:
float
和double
存在精度限制。对于一些小数,无法精确表示其真实值,会产生舍入误差。例如,对于一些无限循环小数或者非常大 / 小的数,浮点型变量只能存储其近似值。- 题目要求精确计算:本题需要将浮点数乘以
2^n
后再进行四舍五入,在这个过程中,如果使用浮点型进行计算,精度误差可能会不断累积,最终导致结果不准确。而使用字符串存储浮点数,可以将其每一位数字单独存储和处理,避免了精度丢失问题,保证计算结果的准确性。
大数值处理
- 范围限制:浮点型有其表示范围的限制,当
n
较大时,浮点数乘以2^n
可能会超出浮点型所能表示的范围,导致溢出错误。- 字符串灵活性:使用字符串可以处理任意长度的数字,不受数据类型范围的限制。程序可以按位处理字符串中的数字,无论数字有多大,都能正确进行乘法和进位操作。
记录小数点
- 乘法和进位处理:本题的核心是将浮点数乘以
2^n
并处理进位,使用字符串可以方便地按位进行乘法和进位操作。可以将字符串中的每一位数字转换为整数,乘以 2 后处理进位,再将结果存储回字符串中。- 四舍五入操作:在进行四舍五入时,通过字符串可以清晰地定位小数点位置和需要处理的数字位,方便进行四舍五入操作。
———(如有问题,欢迎评论区提问)———