1. 程式人生 > >C++高精度運算模板

C++高精度運算模板

       更新:2015-02-09

       友情提醒:後面我開發了另一種版本的高精度類,魯棒性會更好。

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

       更新:2014-05-05    中午

       功能:正整數的加、減、乘、除、取餘、大小關係運算。

       概述:本文分三個模組——模板、介紹、例題。在“模板”貼出高精度模板的完整不含註釋原始碼,在“介紹”分節講解各個功能的原理及要點。在“例題”舉出5道UVA的高精度題作為應用舉例。

       感謝:劉汝佳的《演算法競賽入門經典》,模板的源框架摘自xiaobaibuhei,除法和取餘運算借鑑自誤@解。當然,集大成和功能的擴充套件、程式碼精簡,至少有五成還是自己的功勞大笑

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

模板:建議計算時把較大的數放在左邊對較小的數做運算,比如“999+1”而不是"1+999",因為我的模板針對該型別進行了很大的效率優化。另外模板可能因為更新的緣故,跟後面的解說會有細微出入。

#include <iostream>
#include <string>
#include <cstring>
#include <cstdio>
using namespace std;

const int maxn = 1000;

struct bign{
    int d[maxn], len;

	void clean() { while(len > 1 && !d[len-1]) len--; }

    bign() 			{ memset(d, 0, sizeof(d)); len = 1; }
    bign(int num) 	{ *this = num; } 
	bign(char* num) { *this = num; }
    bign operator = (const char* num){
        memset(d, 0, sizeof(d)); len = strlen(num);
        for(int i = 0; i < len; i++) d[i] = num[len-1-i] - '0';
        clean();
		return *this;
    }
    bign operator = (int num){
        char s[20]; sprintf(s, "%d", num);
        *this = s;
		return *this;
    }

    bign operator + (const bign& b){
        bign c = *this; int i;
        for (i = 0; i < b.len; i++){
        	c.d[i] += b.d[i];
        	if (c.d[i] > 9) c.d[i]%=10, c.d[i+1]++;
		}
		while (c.d[i] > 9) c.d[i++]%=10, c.d[i]++;
		c.len = max(len, b.len);
		if (c.d[i] && c.len <= i) c.len = i+1;
        return c;
    }
    bign operator - (const bign& b){
        bign c = *this; int i;
        for (i = 0; i < b.len; i++){
        	c.d[i] -= b.d[i];
        	if (c.d[i] < 0) c.d[i]+=10, c.d[i+1]--;
		}
		while (c.d[i] < 0) c.d[i++]+=10, c.d[i]--;
		c.clean();
		return c;
    }
    bign operator * (const bign& b)const{
        int i, j; bign c; c.len = len + b.len; 
        for(j = 0; j < b.len; j++) for(i = 0; i < len; i++) 
			c.d[i+j] += d[i] * b.d[j];
        for(i = 0; i < c.len-1; i++)
            c.d[i+1] += c.d[i]/10, c.d[i] %= 10;
        c.clean();
		return c;
    }
    bign operator / (const bign& b){
    	int i, j;
		bign c = *this, a = 0;
    	for (i = len - 1; i >= 0; i--)
    	{
    		a = a*10 + d[i];
    		for (j = 0; j < 10; j++) if (a < b*(j+1)) break;
    		c.d[i] = j;
    		a = a - b*j;
    	}
    	c.clean();
    	return c;
    }
    bign operator % (const bign& b){
    	int i, j;
		bign a = 0;
    	for (i = len - 1; i >= 0; i--)
    	{
    		a = a*10 + d[i];
    		for (j = 0; j < 10; j++) if (a < b*(j+1)) break;
    		a = a - b*j;
    	}
    	return a;
    }
	bign operator += (const bign& b){
        *this = *this + b;
        return *this;
    }

    bool operator <(const bign& b) const{
        if(len != b.len) return len < b.len;
        for(int i = len-1; i >= 0; i--)
            if(d[i] != b.d[i]) return d[i] < b.d[i];
        return false;
    }
    bool operator >(const bign& b) const{return b < *this;}
    bool operator<=(const bign& b) const{return !(b < *this);}
    bool operator>=(const bign& b) const{return !(*this < b);}
    bool operator!=(const bign& b) const{return b < *this || *this < b;}
    bool operator==(const bign& b) const{return !(b < *this) && !(b > *this);}

    string str() const{
        char s[maxn]={};
        for(int i = 0; i < len; i++) s[len-1-i] = d[i]+'0';
        return s;
    }
};

istream& operator >> (istream& in, bign& x)
{
    string s;
    in >> s;
    x = s.c_str();
    return in;
}

ostream& operator << (ostream& out, const bign& x)
{
    out << x.str();
    return out;
}

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --

介紹-1:基本模板

       說明:註釋出現的“[int]”形式,代表這是int型別的變數名或者一個int值。其它[bign]、[char*]同理。

#include <iostream> // 要用cin、cout 
#include <string>   // 要用string類 
#include <cstring>  // 要用strlen 
#include <cstdio>   // 要用sprintf 
using namespace std;

const int maxn = 2000;	// 大整數的最高位數限制 

struct bign{
    int d[maxn], len;

	// 去掉大數的前導0 
	void clean() { while(len > 1 && !d[len-1]) len--; }

	// 初始化:預設初始化為值0 
    bign() { memset(d, 0, sizeof(d)); len = 1; }
    
    // 初始化:可以用“bign [bign] = [int];”或“bign [bign]([int]);”
    bign(int num) { *this = num; }
    
    // 初始化:可以用“bign [bign] = [char*];”或“bign [bign](char*);”
    bign(char* num) { *this = num; }
    
    // 賦值:可以用“[bign] = [char*];” 
    bign operator = (const char* num){
        len = strlen(num);
        for(int i = 0; i < len; i++) d[i] = num[len-1-i] - '0';
        clean();
        return *this;
    }
    
    // 賦值:可以用“[bign] = [int];” 
    bign operator = (int num){
        char s[maxn];
        sprintf(s, "%d", num);
        *this = s;
        return *this;
    }

	// 將int陣列儲存的值轉換為高精度的字串形式 
    string str() const{
        string res;
        for(int i = 0; i < len; i++) res = char(d[i]+'0') + res;
        return res;
    }
};

// 可以用“cin >> [bign];”的方式輸入 
istream& operator >> (istream& in, bign& x)
{
    string s;
    in >> s;
    x = s.c_str();
    return in;
}

// 可以用“cout << [bign];”的方式輸出 
ostream& operator << (ostream& out, const bign& x)
{
    out << x.str();
    return out;
}
  1. 不論做哪類高精度運算最好都完整抄錄該部分程式碼,可以讓bign型別用起來跟int一樣方便。
  2. 四個標頭檔案包含了模板中需要用到的資料型別和函式。
  3. 常量maxn代表會出現的最大整數位數,這個值定太小會出錯(見下文的“例題-2”,有個乘法中的陷阱),但也不要太大浪費過多記憶體。
  4. clean在後續的減法、乘法、除法裡都要用到。

       花絮:1、讀者有沒發現CSDN的程式碼高亮有個bug——#include右邊寫的註釋沒有變綠色。2、4月底又看了部落格,突然明白“bign”是什麼意思了,原來是“big-int”啊!難怪我一直找不到bign這個單詞。

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

介紹-2:擴充套件bign的運算功能

       該部分程式碼包括上面所有過載的運算子:+、-、*、/、%、+=。

  1. 注意第加法運算裡呼叫了max 函式,UVA 的 OJ 可以直接使用,其它OJ 未必。
  2. 模板的減法要注意不能用小數減大數,要算絕對值,可以用(a<b?b-a:a-b)。
  3. 雖然過載的運算子兩邊資料型別都是bign,但因為自動強制轉換,所以用“[bign]*[int]”也不會錯。(這在應用中帶來了相當大的便利)
  4. 這裡的除法與C語言中兩個整數相除的效果相同,會舍尾取整。且除法裡其實包含取餘運算了,最後的a就是。
  5. 末尾寫了個過載“+=”的程式碼,主要是方便bign型別的使用。讀者可以根據自己喜好,①把“*=”等的過載程式碼寫上方便使用,②或者在使用bign型別中不要用這類運算子。

       最後說一下效率問題,因為我的程式碼可以進行大數對大數的運算,如大數除大數、大數對大數取餘,所以在大數除int、大數對int取餘時,效率不及專門功能的函式,這裡犧牲效率增加通用性。

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

介紹-3:擴充套件bign的比較功能

        只要定義了“<”符號,即可用它定義其他所有比較運算子。實際題目中根據需要抄錄小於和其它需要的運算子,不必全部寫入(雖然到了高階運算,如取餘,就環環相扣,很難刪除某一部分了),在“介紹-2”中的運算子也是一樣,這樣在ACM比賽中能加快解題速度。

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

       題意是求一系列大數的累加和。

       我就不復制模板浪費版面了,下面的所有例題也是一樣。解題程式碼:

int main()
{
	bign s = 0, t;
	while (cin >> t)
	{
		if (t.len == 1 && !t.d[0]) break;
		s = s + t;
	}
	cout << s << endl;
	return 0;
}
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

       大數乘法。

       注意,雖然單個大數的範圍是 250 位以內,但相乘後卻會超過250, maxn 至少要定義兩個250,即500。筆者曾悲劇的RE了4次才發覺。

int main()
{
	bign a, b;
	while (cin >> a >> b) cout << a*b << endl;
	return 0;
}

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

       大數加法和乘法,判斷數值是否超過int。

       注意格式要求:每組答案輸出前,要輸出原輸入表示式(保留前導0),如果 cin 流直接到 bign,則前導 0 會被過濾掉。故要用兩個 string 型別的 a,b 作中介。

int main()
{
	bign x, y;
	string a, t, b;
	const bign L = 0x7fffffff;
	
	while (cin >> a >> t >> b)
	{
		cout << a << ' ' + t + ' ' << b << endl;
        x = a.c_str(); y = b.c_str();
		if (x > L) puts("first number too big");
		if (y > L) puts("second number too big");
		if (t == "+" && x+y > L) puts("result too big");
		if (t == "*" && x*y > L) puts("result too big");
	}
	return 0;
}
       這題也可以不用模板解,見:Norcy。注:浮點數方法雖然精簡,但通用性不及高精度。實際使用要看題目給出的數值範圍。最後,說一個未嚴格驗證,另我驚訝的發現:高精度的運算效率比浮點數高,前者執行 0.012m,後者 0.019m。

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

       小數的指數高精度運算。

       如果為小數高精度也寫一個模板,在比賽中會浪費太多時間,而且我暫時也找不到好的實現思路和模板。一般可以仔細觀察這類題目,利用整數高精度來完成特殊的小數高精度運算。
       例如這題可以理解為整數位數為 5 的連乘積,最後統一做一個放縮,去掉小數尾部的 0,具體實現方法見程式碼。

int main()
{
	int n;
	char str[10];
	
	while (scanf("%s%d", &str, &n) != EOF)
	{
		int w = 10000, val = 0, i = -1, j, k;
		while (i++, w) // 要用特殊的方法錄入數值val,同時記錄小數位置k
		{
			if (str[i] != '.') val += w*(str[i]-'0'), w /= 10;
			else k = 5 - i;
		}
		
		bign a = val, p = 1;  // 初始化
		for (i = 0; i < n; i++) p = p * a; // 直接連乘,指數不大沒必要用逐次平方法
		
		for (i = p.len - 1; i > n*k; i--) putchar(p.d[i] + '0'); // 輸出個位以上的值
		if (p.len > n*k) putchar(p.d[n*k] + '0'); // 整數部分不為0,要輸出個位的值
		putchar('.'); // 輸出小數點
		for (i = -1; !p.d[i+1]; i++); // 記錄小數部分最末一個非0值的位置為i
		for (j = n*k-1; j > i; j--) // 輸出小數
		{
			if (j > p.len-1) putchar('0'); // 小數部分的前導0
			else putchar(p.d[j] + '0');
		}
		putchar('\n');
	}
	return 0;
}
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

       大數除法與取餘。

int main()
{
	bign a, b;
	string t;
	
	while (cin >> a >> t >> b)
	{
		if (t == "/") cout << a/b << endl;
		else cout << a%b << endl;
	}
	return 0;
}
       不用模板的解法,見誤@解。其實,讀者從這些不用模板的程式碼可以看出,其演算法本質與使用模板都是一樣的,只要掌握高精度的運算方法,萬變不離其宗。現場賽中,如果熟悉高精度演算法原理,不用模板直接求解當然速度最快。但網路賽或平時練題,還是模板一貼,解起來輕鬆加愉快。二者各有優勢。