高精度运算

本节概要

高精度运算涉及高精度加法高精度减法高精度乘法高精度除低精度高精度除高精度 5类。以下的讲解中,不考虑负数的情况;除法运算中,我们规定除数小于被除数;规定高精度数字的位数不超过200。

  • 本节内容
  1. 高精度数字的输入和存储方法
  2. 高精度加法
  3. 高精度减法
  4. 高精度乘法
  5. 高精度除低精度
  6. 高精度除高精度
  7. 总结

1. 高精度数字的输入和存储方法

输入

单个高精度数字可能长达200位,可以通过两种方式读入:1. 单字符读取、判断并存储,直到遇到“\n”(换行符),可以使用scanf("%c", &c),或c=getchar()这种方法麻烦;2. 将数字作为字符串读入,存入字符数组中,这种方法最简单、高效:scanf("%s", temp);

存储

字符到数字:使用字符数组存储容易,但计算比较麻烦,因数字在字符数组中是以字符0~9的形式存在(ASCII码范围:48~57),将所有数位以数字0~9的形式转存在一个int数组中会方便运算。N=='N'-'0'可以实现字符N到数字N的转换。

反序存放:使用竖式做加法运算时,规则是最低位和最低位对齐,在int数组中如果将数字正序存放,对应计算位的下标就不一样,不便于计算,因此我们将数字反序存放在数组中,就可以使得低位和低位一一对应(下标相同),方便运算,如12345+9987:

//array index   1 2 3 4 5 6 7 8 9
                5 4 3 2 1
            +   7 8 9 9
            -------------
                2 3 3 2 2
//jw            1 1 1 1

//array index是数组下标,jw代表进位

存储位数: 上面的例子并没有使用数组的0号单元,这主要是为了方便运算的缘故,例如n位数字,我们从1循环至n即可,这样思路更加清晰。而留下的0号单元刚好可以用来存储数组的长度,长度Array[0]=strlen(temp);

代码示例

void getNum(int num[]){			    //数组初始化和高精度数字构建 
	char temp[500];
	scanf("%s", temp);              //数字作为字符串读入
	memset(num, 0, sizeof(int)*500);//将数组的所有字节置0  
	num[0]=strlen(temp);			//将位数存于num[0]中 
	for(int i=1; i<=num[0]; i++)	//逆序将每一位存储于num中
		num[i]=temp[num[0]-i]-'0';
	return;
}

//使用方法
int main(){
    int num1[500];
    int num2[500];
    getNum(num1);
    getNum(num2);
    ......
    return 0;
}

上面的代码中用到了memset(),该函数位于cstring头文件中,用于内存块的赋值,在例子中起到了初始化数组的作用
参数1是内存块的起始地址, num是数组的名字,即起始地址
参数2是初始化值,范围是0~255,因memset是按字节为单位进行的
参数3是初始化的字节数,sizeof(int)计算出int的字节数,乘以数组大小500

这里若是没有用memset进行内存块初始化会导致运算出错:如果参与计算的数字A和数字B位数不同,高位相加时,小数字的高位空间的值具有不确定性,如31415+12:

//array index       1 2 3 4 5 6 7 8 9 10 ...
                A   5 1 4 1 3
                B   2 1 ? ? ?
                ------------------------
                C   7 2 ? ? ?
//jw                0 0 ? ? ?

可见,数组在使用前全部置0可以避免这个问题,你也可以用循环来解决,当然使用memset更加方便。

2. 高精度加法

思路

按位相加:从两个数A、B的低位开始按位相加,一边加一边进位,C[i]=A[i]+B[i]+jw,进位是上一次相加产生的。

代码示例

void jiafa(int A[], int B[], int C[]){        //C=A+B
	int i, jw;                              //i从1循环至max(A[0], B[0])
	for(i=1, jw=0; i<=A[0] || i<=B[0]; i++){
		C[i]=A[i]+B[i]+jw;					//按位相加 
		jw=C[i]/10;							//求进位 
		C[i]%=10;                           //与10求余
	}
	C[i]=jw;								//存放最后一个进位
	C[0]=C[i] ? i : i-1;					//确定C的位数
	return;
}
//使用方法
int main(){
    int num1[500];
    int num2[500];
    int result[500];
    getNum(num1);
    getNum(num2);
    memset(result, 0, sizeof(result));      //在当前函数中才可以如此计算大小
    jiafa(num1, num2, result);
    printNum(result);                       //后面提供函数代码
    return 0;
}

3. 高精度减法

思路

大小判断:相减之前要判断两个数A、B的大小关系,如果A>B,则计算A-B,否则计算B-A,需要编程实现大小判断。

按位相减:从两个数A、B的低位开始按位相减,如果L[i]<R[i],就需要向高位借位,因为我们已经确定了左数字L大于右数字R,一定能借到。 if(L[i]<R[i]){ C[i]=L[i]+10-R[i]; L[i+1]--;}

确定有效位:按位减完后,高位有可能变为0,如1000-990后等于10,这时就要重新计算确定结果的有效位数C[0]是多少。可以从位置max(L[0], R[0]开始倒推,直至遇到非0值停止,这时的位置就是有效位数。

代码示例

bool compare(int L[], int R[]){	            //实现两个高精度数字的大小比较 
	if(L[0]>R[0])		return true;        //位数高的大
	if(L[0]<R[0])		return false;       //位数低的小
	int i;
	for(i=L[0]; i>=1; i--){                 //位数相同逐位比较
		if(L[i]>R[i])	return true;        //数字大的大
		if(L[i]<R[i])	return false;       //数字小的小
	}
	return true;                            //能执行到这里说明相等,返回true
}

void jianfa(int L[], int R[], int C[]){     //C=L-R
	for(int i=1; i<=L[0]; i++)
	    C[i]=L[i];                          //把L的所有数位复制到C
	C[0]=L[0];                              //C的位数暂等于L的位数
	int i;
	for(i=1; i<=R[0]; i++){                 //右值有多少位,就减多少次
		if(C[i]<R[i]){
			C[i+1]--;						//向高位借1 
			C[i]+=10;						//当前位+10 
		}
		C[i]-=B[i];							//按位减 
	}
	if(i<C[0])	return;                     //计算有效位数:L[0]-R[0]>=2
	else{									//计算有效位数:L[0]-R[0]==1 or 0 
		while(C[i]==0 && i>=1)	i--;
		A[0]=(i==0) ? 1 : i;                //例如10000-9999
	}
	return;
}
//使用方法
int main(){
    int num1[500];
    int num2[500];
    int result[500];
    getNum(num1);
    getNum(num2);
    memset(result, 0, sizeof(result));      
    if(compare(num1, num2))     jianfa(num1, num2, result);
    else                        jianfa(num2, num1, result);
    printNum(result);
    return 0;
}

4. 高精度乘法

思路

类似加法,可以用竖式求乘法,同样也有进位,对每一位进行乘法运算时,必须进行错位相加:如856×25,见如下规律:

//array index       1  2  3  4  5  6  7  8  9  ...
                A   6  5  8
                B   5  2
                ----------------------------------
                    0  8  2  4
                       2  1  7  1
                ----------------------------------
                    0  0  4  1  2
                    
//array index       1  2  3  4  5  6  7  8  9  ...
                A   a1 a2 a3 a4
                B   b1 b2
                ----------------------------------
                C   c1 c2 c3 c4
                       c2 c3 c4 c5
//本例中未写出进位

综上分析,乘法要进行B[0]轮,每轮要进行A[0]次乘法和进位运算,这就找到了循环的规律。结合错位相加,可知C[i+j-1]=C[i+j-1]+A[i]*B[i]+jw,进位jw=c[i+j-1]/10

有效位数:计算完成后,需要确定结果的有效位数,使用和减法类似的方法从可能的最大位数往后推,直到遇到非0的数位,当前位置就是有效位数。对于M位的数字A和N位的数字B向乘,最大位数为M+N

代码示例

void chengfa(int A[], int B[], int C[]){        //C=A*B
	int i,j;
	for(i=1; i<=B[0]; i++){                     //B[0]轮乘法
		int jw;									//进位 
		for(j=1, jw=0; j<=A[0]; j++){           //每轮按位乘A[0]次
			C[i+j-1]=C[i+j-1]+A[j]*B[i]+jw;     //错位相加
			jw=C[i+j-1]/10;                     //求进位
			C[i+j-1]%=10;                       //进位后,与10求余
		}
		C[i+A[0]]=jw;                           //存储最后一个进位
	}
	int len=A[0]+B[0];						    //最大可能位数 
	while(C[len]==0 && len>1)	len--;          //确定有效位数
	C[0]=len;
	return;
}
//使用方法
int main(){
    ...      
    chengfa(num1, num2, result);
    printNum(result);
    return 0;
}

5. 高精度除低精度

思路

高精度初低精度采用竖式思想,由于除数是低精度,最后的商可能是高精度,余数一定为低精度。

商有效位数:M位的数字A除以N位的数字B,商的位数最大为M-N+1,因为B是低精度数字,计算长度N比较麻烦,简单起见,也可以认为商的最大位数为M, 从最大数位往后推,直到遇到非0的数位,当前位置就是有效位数。

以下是一个模拟计算过程的例子,113056/23:

//
被除数数位      当前位数字  过程被除数      过程除结果
i=6             1           1/23            商:0
i=5             1           11/23           商:0
i=4             3           113/23          商:4,余:21
i=3             0           210/23          商:9,余:3
i=2             5           35/23           商:1,余:12
i=1             6           126/23          商:5,余:11
//计算结果      商:4915, 余数:11

代码示例

void chuDi(int A[], int B, int C[], int &yushu){    //A/B=C 余数:yushu
	int i, t;                                       //t为过程被除数
	for(i=A[0], t=0; i>=1; i--){                    //从被除数高位循环到低位
		t=t*10+A[i];                                //更新t
		C[i]=t/B;                                   //计算C[i]
		t%=B;                                       //更新t
	}
	yushu=t;                                        //计算完后t就是余数
	i=A[0];                                         //计算C有效位数
	while(C[i]==0 && i>1) i--;
	C[0]=i;
	return;
}
//使用方法
int main(){
    int num1[500];
    int num2;
    int shang[500];
    int yushu;
    getNum(num1);
    scanf("%d", num2);
    chuDi(num1, num2, shang, yushu);
    printNum(shang);
    count<<" "<<yushu;
    return 0;
}

6. 高精度除高精度

思路

高精度除高精度是这几种运算中最难的一种。仍然可以模仿竖式的方式进行,过程有点儿麻烦。另一种办法是利用减法:被除数A-除数B,看看能减多少个,直到A<B,这时剪去的个数就是商,剩下的A就是余数。如果利用前面编写好的高精度减法jianfa()compare()的话,实现起来岂不是很容易?可惜不是这样,假设被除数A的位数M与除数B位数N之差M-N很大,这个范围有可能从0~200,按照最大的情况200考虑,这意味着商值也是高精度数字,而你要减10^200次才能减完,这是一个天文数字。

所以,明白否?解决这个问题也很简单,我们不一个一个减,而是按照最大可能的数量级减,例如:12345/45:

//商的最大位数i=M-N+1,即4,设计一个 临时减数,减数后面补齐i-1个0,再进行减法
i=4     12345 < 45000   可以减0个   shang[4]=0      减后A:12345
i=3     12345 < 4500    可以减2个   shang[3]=2      减后A:3345
i=2     3345  < 450     可以减7个   shang[2]=7      减后A:195
i=1     195   < 45      可以减4个   shang[1]=4      减后A:15

//因shang[4]=0,故商的有效位数shang[0]--,为3
//结果  商为274,余数15

看明白了吗,这样的计算过程,仅仅减了2+7+4=13次,而非274次,可见一个一个减是绝对不靠谱的。下面提供了代码,注意:减法函数jianfa()被修改了,使得直接把结果存入A中,而不需要存在另一个数组C中,这是为了简化后面运算的缘故。

代码示例

void jianfa(int A[], int B[]){              //修改了的减法,使得直接在A数组上减
	int i;                                  //不再需要存在数组C,简化后面的运算
	for(i=1; i<=B[0]; i++){
		if(A[i]<B[i]){
			A[i+1]--;					    //向高位借1 
			A[i]+=10;					    //当前位+10 
		}
		A[i]-=B[i];						    //按位减 
	}
	if(i<A[0])	return;                     //计算有效位数
	else{								 
		while(A[i]==0 && i>=1)	i--;
		A[0]=(i==0)? 1 : i;
	}
	return;
}
                                            //shang=A/B, yushu为余数
bool chuGao(int A[], int B[], int shang[], int yushu[]){
	memset(shang, 0, sizeof(int)*300);      //初始化shang
	memset(yushu, 0, sizeof(int)*300);      //初始化yushu
	shang[0]=A[0]-B[0]+1;					//商的最大数位 
	for(int i=shang[0]; i>=1; i--){         //从高位到低位开始计算商
		int jianshu[300];                   //构造要减的减数
		memset(jianshu, 0, sizeof(jianshu));
		                                    //这个函数下面有解释
		memcpy(jianshu+i, B+1, sizeof(int)*B[0]);
		jianshu[0]=B[0]+i-1;                //确定减数的位数
		while(compare(A, jianshu)){         //通过循环减
			jian(A, jianshu);                     
			shang[i]++;                     //减去一个商的对应为+1
		}
	}
	if(shang[shang[0]]==0)	shang[0]--;     //判断商的最高位是否有效
	memcpy(yushu, A, sizeof(int)*300);      //A就是余数,把它完全复制给yushu
}

//使用方法
int main(){
	int num1[300];							//数组A存储第1个数字信息 
	int num2[300];							//数组B存储第2个数字信息
	int shang[300];
	int yushu[300];
	init(num1);
	init(num2);
	chuGao(num1, num2, shang, yushu);
	print(shang);
	printf(" ");
	print(yushu);
	return 0;
}

上面的代码中用到了memcpy(),该函数位于cstring头文件中,用于内存块之间的复制
参数1是destination,即复制的目的地,为一地址
参数2是source,即复制数据的来源,为一地址
参数3是要复制的字节数,sizeof(int)计算出int的字节数,乘以相应的元素个数N

例程中的memcpy(jianshu+i, B+1, sizeof(int)*B[0])是从B数组的1号位置开始(跳过B[0]),复制B[0]个数位,目的地为jianshu数组的第jianshu+1+i-1个位置,+1是为了跳过jianshu[0]+i-1是把这几个位置空成0,以构造最大数量级的减数。晕了么有?别怕,其实可以把代码写的长一些,但是更简单一些的,这里是偷懒了;-)

7. 总结

用途

高精度运算在信息学奥赛计算中常常出现,因此必须掌握。例如计算大型斐波那契数列、计算高阶次幂、计算N!等,如果不使用高精度进行计算,内存溢出是必然结果。

最后提供print()函数,十分简单,你应该可以自己写出来:-)

代码示例

void print(int X[]){                //输出高精度数字
	for(int i=X[0]; i>=1; i--)	printf("%d", X[i]);
	return;
}
posted @ 2017-04-21 19:05  LFYZOI题解  阅读(3034)  评论(1编辑  收藏  举报