万进制高精度运算(C++语言)
目前在青少年信息学奥林匹克竞赛中所涉及到的高精度计算包括加(addition)、减(subtract)、乘(multiply)、除(divide)四种基本运算。其中乘法分高精度数乘高精度数和单精度数乘高精度数两种,除法一般指两个单精度数相除,求解最终指定精度的解,找出循环节或输出指定精度位数的小数。(注:高精度数与单精度数均指整数)
主要的解题思想是利用在小学就曾学习过的竖式加减乘除法则,用程序语言实现存在的问题主要有如何存储高精度数的值,如何实现计算等问题。
一. 高精度数字的存储
我们日常书写一个高精度数字,左侧为其高位,右侧为其低位,在计算中往往会因进位(carry)或借位(borrow)导致高位增长或减少,因此我们定义一个整型数组(int bignum[maxlen])从低位向高位实现高精度整数的存储,数组的每个元素存储高精度数中的一位。(如下表所示)
高精度数3(高位)……7 9 4(低位)
int bignum[i] n …… 2 1 0
显然,在C++语言中,int类型(4个字节/32位计算机)元素存储十进制的一位数字非常浪费空间,并且运算量也非常大,因此常将程序代码优化为万进制,即数组的每个元素存储高精数字的四位。在后面的叙述过程中均以万进制为例介绍。(为什么选择万进制,而不选择更大的进制呢?十万进制中的最大值99999相乘时得到的值是01超过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/=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[] 0 8556 8722 902
图三减法的计算过程
图三表示出了减法的计算过程,与加法不同的地方是进位变成了借位,另外就是计算结果的位数可能会比被减数的位数少,因此在处理过程中要更要注意结果到底是多少位的。
其次,日常我们做减法时,如果被减数小于减数,我们是把两数反过来进行相减,在前面添加负号标识。因此,我们在编写减法子函数时是约定bignum1大于bignum2的,调用时首先判断两个高精度数的大小,然后根据两数的大小决定如何调用。减法的实现过程如图四所示:
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
图五乘法的计算过程
首先看一下单精度数乘高精度数的实现过程,如图六所示:
图六
乘高精度实
现过程
(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这个循环节,出现循环节的条件是当前的余数曾经出现在前面求得的余数序列中。
直接模拟除法操作进行程序设计的过程如图七所示:
quotient[0]=x/y;
remainder[0]=x%y;
while( remainder[pos] ){
for(int i=0; i if(remainder[i]==remainder[pos]){repeat_pos=i; break; } if(repeat_pos>-1) break; pos++; if(pos==maxlen) {pos--; break; }//是否已到求解的精度 remainder[pos]=remainder[pos-1]*10%y; quotient[pos]=remainder[pos-1]*10/y; } cout< if(remainder[0]){ cout<<'.'; int i=1; if(repeat_pos>-1){ for(i=1; i<=repeat_pos; i++) cout< cout<<'('; } while(i<=pos) cout< if(repeat_pos>-1) cout<<')'; } cout< } 说明:maxlen为指定的精度或最大的小数位数加一,根据程序需要而定义。 从上面的程序可以看出,在求得每一个余数后,都要到前面的余数序列中查找是否已存在,来判定是否出现循环节,这种方法即费时又浪费空间。有没有更好的方法来解决这个问题呢? 我们从数学上的自然数找一下规律。任何一个自然数都可拆分为若干质数的幂的形式 (n=p1K1*p2K2…p n Kn),在所有的质数中,只有2和5才能被除尽,其他的均出现循环节。我们是否可以从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,如果我们仍按照平常的输出一样直接输出时,结果为1584,但我们定义万进制时明确过每一位是占十进制的四位,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]: 加subtract[s?b'tr?kt]: 减multiply['m?lt?pla?]: 乘divide[di'vaid]: 除 quotient['kw?u??nt]: 商remainder[r?'me?nd?(r)]: 余数decimal['des?ml]: 小数 附三:编程实战URL /forumdisplay.php?fid=163 c语言位运算符 C语言既具有高级语言的特点,又具有低级语言的功能。 所谓位运算是指进行二进制位的运算。 C语言提供的位运算: 运算符含义 & 按位与 | 按位或 ∧按位异或 ∽取反 << 左移 >> 右移 说明: 1。位运算符中除∽以外,均为二目(元)运算符,即要求两侧各有一个运算了量。 2、运算量只能是整形或字符型的数据,不能为实型数据。 “按位与”运算符(&) 规定如下: 0&0=0 0&1=0 1&0=0 1&1=1 例:3&5=? 先把3和5以补码表示,再进行按位与运算。 3的补码:00000011 5的补码:00000101 -------------------------------------------------------------------------------- &: 00000001 3&5=1 “按位或”运算符(|) 规定如下: 0|0=0 0|1=1 1|0=1 1|1=1 例:060|017=? 将八进制数60与八进制数17进行按位或运算。 060 00110000 017 00001111 -------------------------------------------------------------------------------- |: 00111111 060|017=077 “异或”运算符(∧),也称XOR运算符 规定如下: 0∧0=0 0∧1=1 1∧0=1 1∧1=0 例:57∧42=? 将十进制数57与十进制数42进行按位异或运算。 57 00111001 42 00101010 -------------------------------------------------------------------------------- ∧: 00010011 57∧42=19 “取反”运算符(∽) 规定如下: ∽0=1 ∽1=0 例:∽025=? 对八进制数25(即二进制0000000000010101)按位求反。 高精度计算 由于计算机具有运算速度快,计算精度高的特点,许多过去由人来完成的烦琐、复杂的数学计算,现在都可以由计算机来代替。 计算机计算结果的精度,通常要受到计算机硬件环境的限制。例如,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; C语言的几种位操作运算 在汇编语言中有直接对位进行操作的指令,如置位、复位、位取反、测试某一位等,这对于硬件操作十分方便,在C语言中尽管也提供了一些位操作手段,如按位与、按位或、按位取反等,但它们是对一个字节进行操作,如要对具体的一位操作,仍旧不方便,以下给出了一些函数,可以模仿汇编语言的一些位操作功能。 #define uchar unsigned char /*测试变量某一位是否为‘1’,是返回真,否返回假,num为待测试的数,bit为位数,其值从0到7,下同*/ uchar bittest(uchar num,uchar bit) { if(num>>bit&0x01==1) return 1; else return 0; } uchar bitclr(uchar num,uchar bit) /*清除某一位*/ { uchar bit_value[]={1,2,4,8,16,32,64,128}; return num&~bit_value[bit]; } uchar bitset(uchar num,uchar bit) /*设置某一位*/ { uchar bit_value[]={1,2,4,8,16,32,64,128}; return num|bit_value[bit]; } uchar bitcpl(uchar num,uchar bit) /*取反某一位*/ { uchar bit_value[]={1,2,4,8,16,32,64,128}; if(num>>bit&0x01==1) return num&~bit_value[bit]; else return num|bit_value[bit];c语言位运算符简介举例
高精度计算
C语言的几种位操作运算
高精度数计算