万进制高精度运算(C++语言)
目前在青少年信息学奥林匹克竞赛中所涉及到的高精度计算包括加(addition)、减(subtract)、乘(multiply)、除(divide)四种基本运算。其中乘法分高精度数乘高精度数和单精度数乘高精度数两种,除法一般指两个单精度数相除,求解最终指定精度的解,找出循环节或输出指定精度位数的小数。(注:高精度数与单精度数均指整数)
主要的解题思想是利用在小学就曾学习过的坚式加减乘除法则,用程序语言实现存在的问题主要有如何存储高精度数的值,如何实现计算等问题。 一. 高精度数字的存储
我们日常书写一个高精度数字,左侧为其高位,右侧为其低位,在计算中往往会因进位(carry )或借位(borrow )导致高位增长或减少,因此我们定义一个整型数组(int bignum[maxlen])从低位向高位实现高精度整数的存储,数组的每个元素存储高精度数中的一位。(如下表所示)
高精度数 3(高位)
…… 7 9 4(低位)
int bignum[i]
n
……
2
1
显然,在C++语言中,int 类型(4个字节/32位计算机)元素存储十进制的一位数字非常浪费空间,并且运算量也非常大,因此常将程序代码优化为万进制,即数组的每个元素存储高精数字的四位。在后面的叙述过程中均以万进制为例介绍。(为什么选择万进制,而不选择更大的进制呢?十万进制中的最大值99999相乘时得到的值是9999800001超过4个字节的存储范围而溢出,从而导致程序计算错误。)
在实际编写程序代码过程中常作如下定义: const int base=10000; const int maxlen=1000+1; int bignum[maxlen];
说明:base 表示进制为万进制,maxlen 表示高精度数的长度,1个元素能存储4个十进制位,1000个元素就存储4000个十进制位,而加1表示下标为0的元素另有它用,常用作存储当前高精度数字的位数。 二. 各种运算的程序实现 (一)加法:
首先回顾一下小学中曾学习的坚式加法,见图一:
bignum1[] 9475 46 1243 bignum2[]
918 1324
341 carry
1 0 0 0 bignum_ans[]
1
393
1370
1584
图一 加法的计算过程
从上面的图中我们可以得知,做加法运算是从低位向高位进行,如果有进位,下一位进行相加时要加上进位,如果最高位已计算完还有进位,就要增加存储结果的位数,保存起进位来。关于进位的处理,往往定义单独变量carry 进行存储,程序实现的过程如图二所示:
图二 加法的实现过程
初始化
进位carry 赋初始值0,结果的位数为两个加数的最大位数。
当前位超过最高位了?
处理当前位和进位
N
Y
还有进位么?
N
结束
处理进位
Y
高精度加法程序代码(bignum1+bignum2→bignum_ans ):
void addition(int *bignum1, int *bignum2, int *bignum_ans){ int carry=0; memset( bignum_ans, 0, sizeof(bignum_ans) ); *bignum_ans=*bignum1>*bignu2?*bignum1:*bignum2; for(int pos=1; pos<=*bignum_ans; pos++){ carry+=bignum1[pos]+bignum2[pos]; bignum_ans[pos]=carry%base; carry/=base; } while(carry){ bignum_ans[++*bignum_ans]=carry%base; carry/=base; } }
说明:函数中的数组是引用传递,传递的是数组的首元素的地址,可用指针来代替,当前指针所指向的为0元素,后面的代码相同。
有的同学可能提出,在加法运算中的进位不管进制是多少,进位只可能出现1,用while 循环没有意义,在这里说明一下,为了跟后面乘法中出现的代码相匹配才采用这种处理方法,实现代码的统一性。 (二)减法:
bignum1[] 132 9475 46 1243 bignum2[]
132 918 1324
341 borrow
0 1 0 0 bignum_ans[]
8556
8722
902
图三 减法的计算过程
图三表示出了减法的计算过程,与加法不同的地方是进位变成了借位,另外就是计算结果的位数可能会比被减数的位数少,因此在处理过程中要更要注意结果到底是多少位的。
其次,日常我们做减法时,如果被减数小于减数,我们是把两数反过来进行相减,在前面添加负号标识。因此,我们在编写减法子函数时是约定bignum1大于bignum2的,调用时首先判断两个高精度数的大小,然后根据两数的大小决定如何调用。减法的实现过程如图四所示:
图四 减法的实现过程
高精度数比较程序代码:
int bignumcmp( int *bignum1, int *bignum2 ){ if (*bignum1-*bignum2) return *bignum1-*bignum2; for (int pos=*bignum1; pos>0; pos--) if ( bignum1[pos]-bignum2[pos] ) return bignum1[pos]-bignum2[pos]; return 0; }
初始化
借位borrow 赋初始值0,结果的位数为被减数的位数。
当前位超过最高位了?
处理当前位和借位
N
Y
结果高位为0?
N
结束
结果位数减1
Y
说明:bignum1>bignum2返回正整数,bignum1==bignum2返回0,bignum1<bignum2返回负整数。 解释:首先进行两数的位数的比较,如果位数相同再从高位向低位比较。
高精度减法程序代码(bignum1-bignum2→bignum_ans ):
void subtract( int *bignum1, int *bignum2, int *bignum_ans ){ int borrow=0; memset( bignum_ans, 0, sizeof(bignum_ans) ); *bignum_ans=*bignum1; for(int pos=1; pos<=*bignum_ans; pos++){ bignum_ans[pos]=bignum1[pos]-borrow-bignum2[pos]; if(bignum_ans[pos]<0){ bignum_ans[pos]+=base; borrow=1; }else{ borrow=0; } } while( !bignum_ans[*bignum_ans] ) --*bignum_ans; }
(三)乘法:
乘法的计算过程正如图五所示的从乘数的最低位起枚举每一位与被乘数相乘,累加到结果当中。高精度乘高精度实际是多次调用单精度数乘高精高数运算。
1 2 3 4 X 4 3 2 1 (1) 1 2 3 4 (2) 2 4 6 8 (3) 3 7 0 2 (4) 4 9 3 6
5
3
3
2
1
1
4
图五 乘法的计算过程
首先看一下单精度数乘高精度数的实现过程,如图六所示:
图六 单精度乘高精度实现过程
初始化
进位carry 赋初始值0,结果的位数被乘数的位数。
当前位超过最高位了?
处理当前位和进位
N
Y
还有进位么?
N
结束
处理进位
Y
单精度乘高精度程序代码(n*bignum→bignum_ans):
void SingleMultiply(int n, int *bignum, int *bignum_ans){
int carry=0;
memset(bignum_ans, 0, sizeof(bignum_ans);
*bignum_ans=*bignum;
for(int pos=1; pos<=*bignum_ans; pos++){
carry+=n*bignum[pos];
bignum_ans[pos]=carry%base;
carry/=base;
}
while(carry){
bignum_ans[++*bignum_ans]=carry%base;
carry/=base;
}
}
高精度数乘高精度数,实质就是在单精度数乘高精度数的基础上枚举各个乘数位与被乘数相乘,累计到结果当中。其中乘数中的第J位与被乘数中的第I位相乘时,结果应该保存到结果的第I+J-1位中,因为如果存在进位的问题结果的位数可能为乘数与被乘数的位数和,也可能为两者位数和减一,这一点也应该单独处理。过程就不再展示了,具体的请阅读下面的程序代码:
高精度乘高精度程序代码(bignum1*bignum2→bignum_ans):
void BignumMultiply( int *bignum1, int *bignum2, int *bignum_ans){
int carry=0, i, j;
memset(bignum_ans, 0, sizeof(bignum_ans) );
for (j=1; j<=*bignum2; j++){
for(i=1; i<=*bignum1; i++){
bignum_ans[i+j-1]+=carry+bignum1[i]*bignum2[j];
carry=bignum_ans[i+j-1]/base;
bignum_ans[i+j-1]%=base;
}
i=j+*bignum1;
while(carry){
bignum_ans[i++]=carry%base;
carry/=base;
}
}
*bignum_ans=*bignum1+*bignum2;
while( !bignum_ans[*bignum_ans] ) --*bignum_ans;
}
(四)除法:
除法在高精度计算中是最为特殊的,在近几年联赛中并没有出现类似的题目,除法类的高精度题目会涉及到精度和循环节问题,在这里首先用表格分析两个例子:
例一:3除以8,结果为0.375
被除数 3 30 60 40
商0 . 3 7 5
余数 3 6 4 0
例二:45除以56,结果为0.803(571428)
被除数45 450 20 200 320 400 80 240 160 480
商0 . 8 0 3 5 7 1 4 2 8
余数45 2 20 32 40 8 24 16 48 32
在例一中展示的为能被除尽的情形,能被除尽的条件是余数为0,而在例二中56并不能除尽45,出
现571428这个循环节,出现循环节的条件是当前的余数曾经出现在前面求得的余数序列中。 直接模拟除法操作进行程序设计的过程如图七所示:
图七 高精度除法实现过程
根据上述处理过程编写代码如下: void divide(int x, int y){ int remainder[maxlen], quotient[maxlen], repeat_pos=-1, pos=0; quotient[0]=x/y; remainder[0]=x%y; while( remainder[pos] ){ for(int i=0; i 说明:maxlen 为指定的精度或最大的小数位数加一,根据程序需要而定义。 从上面的程序可以看出,在求得每一个余数后,都要到前面的余数序列中查找是否已存在,来判定是否出现循环节,这种方法即费时又浪费空间。有没有更好的方法来解决这个问题呢? 我们从数学上的自然数找一下规律。任何一个自然数都可拆分为若干质数的幂的形式 (n=p 1K1*p 2K2…p n Kn ),在所有的质数中,只有2和5才能被除尽,其他的均出现循环节。我们是否可以从 初始化 商存储数组quotient[]清零,余数数组remainder[]清零。 余数为0? 处理当前位 N Y 结束 输出整除值,存储余数 余数序列中出现过么? 处理循环节 Y N 2和5上找出解决方法来呢? 将被除数和除数转化为互质的两个数,从除数中统计出2和5的个数n2和n5,且一个2和一个5都仅产生一位小数,这些小数是肯定不出现在循环节中的。反过来说,在小数点后面,循环节前面小数的位数就是n2和n5中的较大者。如果求解出循环节前的小数以后,余数仍不为0,那就存在着循环节了,循环节的长度为再次得到的这个余数。 请看下面的代码: 小数点后循环节前的位数程序代码: int numBeforeRepeat(int x, int y){ int n2=0, n5=0; while(y%2==0){n2++; y/=2;} while(y%5==0){n5++; y/=5;} while(x%2==0){n2--; x/=2;} while(x%5==0){n5--; x/=2;} if(n2 return n2>0?n2:0; } 高精度除法程序代码: void divide(int x, int y){ int BeforeRepeat=numBeforeRepeat(x, y); cout< x%=y; if(x){ cout<<'.'; for(int i=0; i x*=10; cout< x%=y; } if(x){ cout<<'('; int remainder=x; do{ remainder*=10; cout< remainder%=y; }while(remainder != x); cout<<')'; } } cout< } 利用这种解法一方面省去了余数和商的存储空间,另一方面也无需再到余数序列中查找余数是否出现过,即节省空间,又提高了程序的执行效率。所以遇到高精度除法问题时,可以优选第二种解法。 三. 万进制高精度数的输出问题: 采用万进制来进行高精的加、减、乘三种运算,虽然提高了程序的执行效率,但在输出时却带来了问题,如在加法示例中的结果从高位到低位分别为1,393,1370,1584,如果我们仍按照平常的输出一样直接输出时,结果为139313701584,但我们定义万进制时明确过每一位是占十进制的四位,393这一位应该输出0393而不是393。因此我们在输出时应首先输出最高位(因最高位前面是不补0的),然后再输出其他位,如果不足四位,用0补充。 万进制输出程序代码: void printbignum(int *n){ int *p=*n+n; cout<<*p--; cout.fill('0'); //定义填充字符'0' while(p>n){ cout.width(4); cout<<*p--; } cout< } 至此,有关万进制的相关内容已全部介绍完毕,在本文中的示例代码仍可根据个人的习惯进行精简,在熟练使用以后,在解题的过程中,就不需要再为如何解决高精度问题浪费宝贵的时间啦。相关练习题可参考“编程实战”中的高精度专项练习题。 附一:C++语言输出控制语句: cout.fill(char ch) 设置输出留空位置的填充字符(作用于所有的输出)只调用一次 cout.width(int n) 设置输出域的宽度(只作用于下一次输出)每次都要调用 附二:相关英文单词 carry['k?r?]: 进位borrow['b?r??]: 借位addition[?'di??n]: 加subtra ct[s?b'tr?kt]: 减multiply['m?lt?pla?]: 乘divide[di'vaid]: 除 quotient['kw?u??nt]: 商remainder[r?'me?nd?(r)]: 余数decimal['des?ml]: 小数 附三:编程实战URL https://www.sodocs.net/doc/287706107.html,/bbs/forumdisplay.php?fid=163 关于圆周率的计算 祖冲之在数学方面的突出贡献是关于圆周率的计算,确定了相当精确的圆周率值。中国古代最初采用的圆周率是“周三径一”,也就是说,π=3。这个数值与当时文化发达的其他国家所用的圆周率相同。但这个数值非常粗疏,用它计算会造成很大的误差。随着生产和科学的发展,π=3 就越来越不能满足精确计算的要求。因此,中外数学家都开始探索圆周率的算法和推求比较精确的圆周率值。在中国,据公元一世纪初制造的新莽嘉量斛(亦称律嘉量斛,王莽铜斛,是一种圆柱形标准量器,现存)推算,它所取的圆周率是3.1547 。二世纪初,东汉天文学家张衡在《灵宪》中取用π=≈3.1466,又在球体积计算中取用π≈3.1622。三国时东吴天文学家王蕃在浑仪论说中取用π≈3.1556。以上这些圆周率近似值,比起古率“周三径一”,精确度有所提高,其中π= 10还是世界上最早的记录。但这些数值大多是经验结果,并没有可靠的理论依据。 在这方面最先取得突破性进展的是魏晋之际的数学家刘徽,他在《九章算术注》中创立了“割圆术”,为计算圆周率建立起相当严密的理论和完善的算法。他所得到的圆周率值π=3.14 与π==3.1416,都很精确,在当时世界上是很先进的,至今仍在经常使用。继刘徽之后,祖冲之则将圆周率推算到更加精确的程度。据《隋书·律历志》记载,祖冲之确定了π的不足近似值 3.1415926 和过剩近似值 3.1415927,π的真值在这两个近似值之间,即 3.1415926<π<3.1415927 精确到小数 7 位。这是当时世界上最先进的数学成果,直到约一千年后,才为 15 世纪中亚数学家阿尔·卡西(Al—? kash1)和16世纪法国数学家韦达(F.Vièta,1540—1603)所超过。 关于他得到这两个数值的方法,史无明载,一般认为是基于刘徽割圆术。通过现代计算验证,如果按照割圆术计算,要得到小数 7 位准确的圆周率值,必须求出圆内接正12288 边形的边长和 24576边形的面积,这样,就要对9位数进行上百次加减乘除和开方运算,还要选择适当的有效数字,保证准确的误差范围。对于用算筹计算的古代数学家来说,这绝不是一件轻而易举的事情,只有掌握纯熟的理论和技巧,并具备踏踏实实和一丝不苟的研究精神,才能取得这样的杰出成就。祖冲之的这项记录在中国也保持了一千多年。 中国古代数学家和天文学家还往往用分数表示常量的近似值。为此,祖冲之确定了π的两个分数形式的近似值:约率π=22/7≈3.14 ,密率π=355/113 ≈3.1415929。这两个数值都是π的渐近分数。刘宋天文学家何承天及古希腊阿基米德等都已用到过。密率355/113 是π的分母小于10000的最佳近似分数,则为祖冲之首创。关于密率355/113是如何得到的,今人有“调日法”术,连分数法,解同余式或不定方程,割圆术等种种推测,迄今尚无定论。在欧洲,π= 355/113 是16世纪由德国数学家奥托(V.Otto,1550(?)—1605)和荷兰工程师安托尼兹(A.Anthonisz,1527—1607)分别得到,后通称“安托尼兹率”,但这已是祖冲之以后一千多年的事情了。自从我国古代灿烂的科学文化逐渐得到世界公认以来,一些学者就建议把π= 355 称为“祖率”,以纪念祖冲之的杰出贡献。 关于球的体积公式及其证明: 祖冲之的另一项重要数学成就是关于球的体积公式及其证明。各种几何体的体积计算是古代几何学中的基本内容。《九章算术》商功章已经正确地解决了 运算符重载基础概念练习题 1、下列运算符中, ()运算符在C++中不能重载。 A = B () C :: D delete 2、下列运算符中, ()运算符在C++中不能重载。 A ?: B [] C new D && 3、下列关于C++运算符函数的返回类型的描述中,错误的是()。 A 可以是类类型 B 可以是int类型 C 可以是void类型 D 可以是float类型 4、下列运算符不能用友元函数重载的是()。 A + B = C * D << 5、在重载运算符函数时,下面()运算符必须重载为类成员函数形式。 A + B - C ++ D -> 6、下列关于运算符重载的描述中,正确的是()。 A 运算符重载可以改变运算符的操作数的个数 B 运算符重载可以改变优先级 C 运算符重载可以改变结合性 D 运算符重载不可以改变语法结构 7、友元运算符obj>obj2被C++编译器解释为()。 A operator>(obj1,obj2) B >(obj1,obj2) C obj2.operator>(obj1) D obj1.oprator>(obj2) 8、在表达式x+y*z中,+是作为成员函数重载的运算符,*是作为非成员函数重载的运算符。下列叙述中正确的是()。 A operator+有两个参数,operator*有两个参数 B operator+有两个参数,operator*有一个参数 C operator+有一个参数,operator*有两个参数 D operator+有一个参数,operator*有一个参数 9、重载赋值操作符时,应声明为()函数。 A 友元 B 虚 C 成员 D 多态 10、在一个类中可以对一个操作符进行()重载。 A 1种 B 2种以下 C 3种以下 D 多种 11、在重载一个运算符时,其参数表中没有任何参数,这表明该运算符是()。 高精度计算 由于计算机具有运算速度快,计算精度高的特点,许多过去由人来完成的烦琐、复杂的数学计算,现在都可以由计算机来代替。 计算机计算结果的精度,通常要受到计算机硬件环境的限制。例如,pascal 要计算的数字超过19位,计算机将按浮点形式输出;另一方面,计算机又有数的表示范围的限制,在一般的微型计算机上,实数的表示范围为l0-38 -l038。例如,在计算N!时,当N=21时计算结果就超过了这个范围,无法计算了。这是由计算机的硬件性质决定的,但是,我们可以通过程序设计的方法进行高精度计算(多位数计算)。 学习重点 1、掌握高精度加、减、乘、除法。 3、理解高精度除法运算中被除数、除数、商和余数之间的关系。 4、能编写相应的程序,解决生活中高精度问题。 学习过程 一、高精度计算的基本方法 用free pascal程序进行高精度计算,首先要处理好以下几个基本问题:【数据的输入与保存】 (1)一般采用字符串变量存储数据,然后用length函数测量字符串长度确定其位数。 (2)分离各位数位上的数字 分离各数位上的数通常采用正向存储的方法。以“163848192”为例,见下表:A[9] A[8] A[7] A[6] A[5] A[4] A[3] A[2] A[1] 1 6 3 8 4 8 1 9 2 基本原理是A[1]存放个位上的数字,A[2]存放十位上的数字,……依此类推。即下标小的元素存低位上的数字,下标大的元素存高位上的数字,这叫“下标与位权一致”原则。 【计算结果位数的确定】 (1)高精度加法:和的位数为两个加数中较大数的位数+1。 (2)高精度减法:差的位数为被减数和减数中较大数的位数。 (3)高精度乘法:积的位数为两个相乘的数的位数之和。 (4)高精度除法:商的位数按题目的要求确定。 【计算顺序与结果的输出】 高精度加、减、乘法,都是从低位到高位算起,而除法相反。输出结果都是从高位到低位的顺序,注意:高位上的零不输出(整数部分是零除外)。 高精度加法 【参考程序】 var a,b:array[1..10000] of byte; i,w,la,lb:integer; 常用数学公式大全 1、每份数×份数=总数总数÷每份数=份数总数÷份数=每份数 2、1倍数×倍数=几倍数几倍数÷1倍数=倍数几倍数÷倍数=1倍数 3、速度×时间=路程路程÷速度=时间路程÷时间=速度 4、单价×数量=总价总价÷单价=数量总价÷数量=单价 5、工作效率×工作时间=工作总量工作总量÷工作效率=工作时间工作总量÷工作时间=工作效率 6、加数+加数=和和-一个加数=另一个加数 7、被减数-减数=差被减数-差=减数差+减数=被减数 8、因数×因数=积积÷一个因数=另一个因数 9、被除数÷除数=商被除数÷商=除数商×除数=被除数 小学数学图形计算公式 1、正方形C周长S面积a边长周长=边长×4C=4a面积=边长×边长S=a×a 2、正方体V:体积a:棱长表面积=棱长×棱长×6S表=a×a×6体积=棱长×棱长×棱长V=a×a×a 3、长方形 C周长S面积a边长 周长=(长+宽)×2C=2(a+b) 面积=长×宽S=ab 4、长方体 V:体积s:面积a:长b:宽h:高 (1)表面积(长×宽+长×高+宽×高)×2S=2(ab+ah+bh) (2)体积=长×宽×高V=abh 5三角形s面积a底h高 面积=底×高÷2s=ah÷2 三角形高=面积×2÷底 三角形底=面积×2÷高 6平行四边形 s面积a底h高 面积=底×高s=ah 7梯形 s面积a上底b下底h高 面积=(上底+下底)×高÷2s=(a+b)×h÷2 8圆形 S面积C周长∏d=直径r=半径 (1)周长=直径×∏=2×∏×半径C=∏d=2∏r (2)面积=半径×半径×∏ 9圆柱体 v:体积h:高s;底面积r:底面半径c:底面周长 (1)侧面积=底面周长×高 (2)表面积=侧面积+底面积×2 (3)体积=底面积×高 (4)体积=侧面积÷2×半径 10圆锥体 v:体积h:高s;底面积r:底面半径 体积=底面积×高÷3 总数÷总份数=平均数 运算符重载 一.单项选择题 1.下列运算符中,运算符在C++中不能重载。 A.?: B.+ C. D.<= 解:C++中不能被重载的运算符有:·,一,::,?:。本题答案为A。 2.下列运算符中,运算符在C++中不能重载。 A.&& B.[] C.:: D.new 解:c++中不能被重载的运算符有:·,·+,::,?:。本题答案为c。 3.下列关于运算符重载的描述中,是正确的。 A.运算符重载可以改变操作数的个数 B.运算符重载可以改变优先级 C.运算符重载可以改变结合性 D.运算符重载不可以改变语法结构 解:运算符重载不能改变操作数的个数、运算符的优先级、运算符的结合性和运算程的语法结构。本题答案为D。 4.友元运算符objl>obj2被C++编译器解释为。 A.operator>(objl,obj2) B.>(obj1,obj2) C.obj2.operator:>(obj1) D.objl.operator>(obj2) 解:重载为友元函数的运算符的调用形式如下: operator<运算符>(<参数1>,<参数2>) 等价于:<参数1><运算符><参数2> 本题答案为A。 5.现需要对list类对象使用的逻辑运算符“==”重载,以下函数声明是正确的。 A、list & list::operator==(const list &a); B、list list::operator==(const list &a); C、bool & list::operator==(const list &a); D、bool list::operator==(const list &a); 6. 以下类中分别说明了“+=”和“++”运算符重载函数的原型。如果主函数中有定义: fun m,c,d;,那么,执行语句c=m++; 时,编译器把m++解释为: (33) A) c.operator++(m); B) m=operator++(m); C) m.operator++(m); D) operator++(m); class fun { public: .. .. .. fun operator +=(fun ); friend fun operator ++(fun &,int); }; 答案:D 7. 在第33题中,当执行语句d+=m; 时,C++编译器对语句作如下解释: (34) A. d=operator+=(m); B. m=operator+=(d); C. d.operator+=(m); D. m.operator+=(d); 答案:C 8. 设有以下类定义,其中说明了“+”运算符重载函数的原型。这是一个友元函数,当类关于圆周率的计算
运算符重载基础概念练习题
高精度计算
常用数学公式
运算符重载练习题.
高精度数计算