高精度運算


本節概要

高精度運算涉及高精度加法高精度減法高精度乘法高精度除低精度高精度除高精度 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;
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM