【蓝桥杯】 历届试题 小数第n位(除法法则、循环节)—— 酱懵静

历届试题 小数第n位

问题描述
我们知道,整数做除法时,有时得到有限小数,有时得到无限循环小数。
如果我们把有限小数的末尾加上无限多个0,它们就有了统一的形式。

本题的任务是:在上面的约定下,求整数除法小数点后的第n位开始的3位数。

输入格式
一行三个整数:a b n,用空格分开。a是被除数,b是除数,n是所求的小数点后位置(0<a,b,n<1000000000)
输出格式
一行3位数字,表示:a除以b,小数后第n位开始的3位数字。

样例输入
1 8 1
样例输出
125
样例输入
1 8 3
样例输出
500
样例输入
282866 999000 6
样例输出
914



——分割线之初试牛刀——



分析:
看到这道题的第一反应是:直接取值!于是唰唰唰写出如下代码:

#include<iostream>
using namespace std;

string cut(double num,int n)	//把除法后的结果转换为目标格式输出 
{
	num=num-int(num);	 		//仅保留小数部分 
	while(n--) num*=10;	 		//定位到第n位数,将其放到个位 
	string str;
	for(int i=0;i<3;i++)		//循环去取出这个个位上的数 
	{
		int temp=int(num)%10;	//取个位 
		str+='0'+temp;
		num*=10;				//把第n位的后一位放到个位 
	}
	return str;
}

int main()
{
	int a,b,n;
	cin>>a>>b>>n;
	double ans=double(a)/b;
	cout<<cut(ans,n)<<endl;
	return 0;
}

上述代码的逻辑意义很清晰:输入,处理函数,输出。但是这代码只得了20%的分,为什么?原因在于double型对小数位数的限制以及double型的数据范围限制。因为在double类型范围下,小数的位数最多只有9位,这将导致在n>9的情形下,在经过while循环之后那个num里面留下的小数部分早就不是正确值了(这是一个很低级的错误);况且对于num而言,其范围也不可能超过101000000000。所以当遇到无限小数且n>9时,在超过9的之后的那些小数都将丢失,进而导致了之后得到的数据也出错。


——分割线之初入江湖——


因此我们不能用一个变量来保存除法结果,然后再对这个结果进行处理,而是应该以除法法则为原理,对这个除法的过程进行模拟。就那1/8来举例。
我们的除法过程是这样的:
首先1/8商0余1,那么我们记下这第一个余数为1,然后我们把1×10变成10;
再让10/8,此时商1余2,然后我们把2×10;
再让20/8,此时商2余4,然后我们把4×10;
再让40/8,此时商5余0(此时已经除尽了,但是之后还要继续执行,直到n-1次),然后我们把0×10;
再让0/8,此时商0余0,然后我们把0×10;
……

根据这样的模拟过程,我写出了以下代码:

#include<iostream>
#include<string>
using namespace std;
int main()
{
	int a,b,n;
	cin>>a>>b>>n;
	a%=b;					//先对其取余,保证最后的形式是一个真分数
	while(--n) a=a*10%b;	//然后模拟前面n-1位的除法过程
	string str="";
	for(int i=0;i<3;i++)	//然后开始输出接下来的3位
	{
		a=a*10;
		str+='0'+a/b;
		a%=b;
	}
	cout<<str<<endl;
	return 0;
}

这次采用了模拟的思想去模拟除法的过程,相较于之前的那个算法,优点在于将处理的数据放在了余数上面,而不是那个除数结果上面,这样的好处就是避免了在n过大时数据的丢失。
具体的做法是:相除(保留余数),然后余数变除数,重复这个过程。这样去逐渐靠近第n-1个位置上的那个值,当到了那个位置之后再连续三次取出即可。

但是同样地,这代码也没有满分(80分代码),为什么?
其实问题很明显,那就是会超时!
当n取到10 0000 0000时,这里面的运算次数达到了2×10 0000 0000 +2×3次,这样显然是会超时的,因此必须对这算法进行优化。


——分割线之艰难磨砺——


首先要知道一件很重要的事(题目中也有暗示):当两个整数相除(即分数),一定是有理数。
何谓有理数?即最终相除的结果要么能除尽;要么是一个循环小数,但这个小数一定是无限循环小数。

下面介绍一个概念:循环节
循环节:无限小数的小数点后,从某一位起向右进行到某一位止的一节数字循环出现,首尾衔接,称这种小数为循环小数,这一节数字称为循环节。
如3.43535……是无限循环小数,可以简写为3.435(35循环),则它的循环节是35。

回到本题,题目中给定的两个数由于是整数,那么其结果要么是一个能整除的,要么是一个无限循环小数。能整除的属于无限循环小数的一个特殊情况,故我们仅讨论无限循环小数的情况,这里又分为两种:
①纯循环小数:小数部分从第一位开始的循环小数,如0.333…(3循环)、0.1234512345…(12345循环)
②混循环小数:循环节不是从小数部分第一位开始的,如0.166666……(6循环)、0.1423533333(3循环)

先来讨论简单的——纯循环小数
那么这时的代码很简单——直接把每次取到的余数都和最开始输入的a%b进行对比,一旦出现了某个值和这个余数相等,那么根据除法法则就可以知道接下来将会无限循环这个过程,换句话说就是由起始位置到当前位置的之间的这一段即为循环节。
比如就拿123/999举例,也就是a=123,b=999
123/999

第一次,让a×10=1230,然后再对999取余后得到231(商1余231),此时231!=123(a%b),继续
第二次,让a×10=2310,然后再对999取余后得到321(商2余312),此时312!=123(a%b),继续
第三次,让a×10=3120,然后再对999取余后得到123(商3余123),此时123==123(a%b),说明接下来再继续执行这个除法过程时,将会不断循环上面三步,那么也就是说,循环节就是123了
下面给出这一思路的完整代码:

#include<iostream>
#include<string>
#include<vector>
using namespace std;
int main()
{
	cin>>a>>b>>n;
	int sa=a%b;
	for(int i=1;i<=n;i++)
	{
		sa=sa%b*10;
		if(sa%b == a%b){	//只要余数出现了重复,说明已经找到了循环节
			n=n%i;			//这时候,就算你的n取值再大,我只要将其对这个循环节的长度进行取
			i=0;}			//余,就能准确定位到取值为n时其对应的那个商为多少(即绝对位置)
	}						//但是需要注意的是,这里的i要取0
	for(int i=0;i<3;i++)
	{
		cout<<sa/b;
		sa=sa%b*10;
	}
	cout<<endl;
	return 0;
}

接下来结合着代码简单地说下这个执行过程(假设a=123,b=999,n=5)
变量sa声明:由于之后在循环中每次都要用当前取余操作的余数和a%b进行判等操作,所以这里需要用另一个变量sa来保存这个余数,所以首先是int sa=a%b;这一步还有一个目的是为了让a>b的数据能够先化简为真分数。
接下来进入循环:
第一次,让sa=sa%b×10=1230,然后再对999取余后得到231(商1余231),此时231!=123(a%b),继续
第二次,让sa=sa%b×10=2310,然后再对999取余后得到321(商2余312),此时312!=123(a%b),继续
第三次,让sa=sa%b×10=3120,然后再对999取余后得到123(商3余123),此时123==123(a%b),说明已经找到了循环节,此时直接让n=n%i=5%3=2(即,n=5的那个位置上的商其实和n=2的那个位置上的商是一致的,之后也是,因此接下来你需要做的事是直接从第2的那个绝对位置开始求商并输出就行了)。
于是,在满足了上面的的if语句时,你可以直接令i=1,n=2然后i<=2,i++,即能定位到那个位置。
注意:这里有个坑!当你在if语句里面判等成功时,不能令i=1(尽管我们确实要从1开始),而是应该令i=0,因为在这个循环的当前循环结束时,会执行一次i++,因此你应该让i=0。

这就是仅仅考虑a/b的结果为纯循环小数的代码
不过奇怪的是,交上去居然直接AC了!!!看来蓝桥杯的测试数据是真的挺水的,说它是暴力杯还真没错。
为了证明我的猜想,我试了下用282866 999000 1000000000来进行测试(结果为混循环小数),发现这个代码在测试这组数据的时候是真的会超时(大概需要15s的样子),而同样地用1 3 1000000000却不会,所以上述代码在本题中能拿到满分,纯粹是因为测试数据中给出的n非常大的那(几)组数据,其a/b得到的是纯循环小数,否则是不会通过的。


——分割线之炉火纯青——


本着严谨的态度,我们还是得想想怎么实现一个算法能同时兼顾这两种情况
于是下面给出本题的真正解决方案:
首先要知道从最基本的原理出发,无论是哪一种情况,我们都能利用除法法则,去模拟这个过程,然后利用循环节来将n过于大的情形进行一个优化,使得循环总次数降低,进而在短时间内找到正解。因此我们的解决思路也应该从这里出发。
就以样例2来举例(因为样例2的结果为混循环小数,而纯循环小数在模拟这个除法过程时,相较混循环小数而言是一个特殊情形)(输入为282866 999000 8),下面来模拟这个除法过程:
282866/999000

第一次,商为0,余数为282866,由于此时余数小于除数,让余数×10=2828660;
第二次,商为2,余数为830660,由于此时余数小于除数,让余数×10=8306600;
第三次,商为8,余数为314600,由于此时余数小于除数,让余数×10=3146000;
第四次,商为3,余数为149000,由于此时余数小于除数,让余数×10=1490000;
第五次,商为1,余数为491000,由于此时余数小于除数,让余数×10=4910000;
第六次,商为4,余数为914000,由于此时余数小于除数,让余数×10=9140000;
第七次,商为9,余数为149000,注:此时的余数已经在上面商3时出现了,那么也就是说接下来将会不断地遇到这几个余数:491000,914000,149000
换言之,循环节也就出现了!
注意,在这之前需要将前面的那些余数保存在一个向量v中,那么现在这里面的内容如下:
{282866、830660、314600、149000、491000、914000},标红部分为循环节的余数部分

接下来要做的事是什么?很简单,找到待求的除数在指定位数为n时,其在当前这个循环节中的绝对位置。比如本题中n=8(根据上面的截图可以知道第8位的商应该为4),那么实际上它在向量v的余数表中,其对应的绝对余数位(绝对余数位:比如0.123123123……中,循环节长度为3,那么第8位的绝对余数位为2,第1000位的绝对余数位为1)应该为(n-pos)%(v.size-pos)=(8-3)%(6-3)=5%3=2,其中pos指明了当第一次出现某个重复余数时,该余数在向量表中的索引(如本题中,第一次找到循环节出现的重复余数149000时,对应在向量表v中其索引为3)。
当找到了这个绝对位置后(比如为3),我们的目的很简单,那就是设置一个循环将前面的那2次给跳过,然后直接进入另一个循环,从头开始输出每一次的商即可。

下面给出这一思路的完整代码:

#include<iostream>
#include<string>
#include<vector>
using namespace std;

int a,b,n;
vector<int> v;

bool isContained(int num)					//用于检测当前传入的num是否包含于向量v中
{
	for(int i=0;i<v.size();i++)
		if(v[i]==num) return true;
	return false;
}
int AbsoluteLength(int num)					//用于返回接下来还需要循环的绝对次数
{
	int pos;
	for(int i=0;i<v.size();i++)
		if(v[i]==num){
			pos=i;
			break;
		}
	return (n-pos)%(v.size()-pos);
}

int main()
{
	cin>>a>>b>>n;
	a%=b;
	for(int i=1;i<n;i++)
	{
		v.push_back(a);
		a=a*10%b;
		if(isContained(a)){
			n=AbsoluteLength(a);
			i=0;							//注意:之后由于在for循环结束处会执行一次i++,因此这里需要让i=0
			v.clear();						//此操作的目的是为了使前面定义的vector这个向量失效
		}
	}
	string str="";
	for(int i=0;i<3;i++)
	{
		a=a*10;
		str+='0'+a/b;
		a=a%b;
	}
	cout<<str<<endl;
	return 0;
}

发布了30 篇原创文章 · 获赞 67 · 访问量 3032

猜你喜欢

转载自blog.csdn.net/the_ZED/article/details/102721241