您的位置:首页 > 其它

【高精度】 利用分段存储的方法储存大数与运算

2017-12-31 20:41 281 查看
引:有时候我们遇到一种图论题,就是要你将算出来的路径中每条边的权值之积或和求出来,虽然每条边的权值都比较小,但算到最后结果却很大,不得不用高精度方法存储数据的时候,你怎么处理?

传统的高精度是用char[]数组来存储,这个对于上述问题,运算起来并不是很方便,这里介绍一种基于分段进行数据存储的大数处理方法给大家,其运算方便程度、空间和时间复杂度对比传统的高精度算法都有了一定提高。

=====================================================================

先介绍存储机制,分段存储的,这里先随便给个数,比如:5432156454

我们把这个数分成3段,每4位数作为一个段:



我们可以用三个变量来存储这三段数

int first, seconed, thrid;



输出时只需按thrid, seconed, first的顺序把变量逐个输出,就可以得到原来的大数了。

实际上我们可以把first, seconed, third合并成一个数组BigNumber[3] = {6454, 3215, 54},那么分成3段的大数实际上是以数量级的形式被划分成了3段,每个数量级之间的进制为10000,其中BigNumber[0]为该大数的第1数量级,这就是大数的基本储存原理,其核心思想就是设置一个极大的进制,然后按进制分段存到数组里。



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

一个大数的存储结构为:

#define SCALE ScaleNum  //数量级之间的进制,一般设成10的幂次方数

struct BigNumber{
int num[MAX]; //最高能存储MAX个数量级位数的数
int oom; //该数的最高段存储位置,可以看成是该数的数据规模

};

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

我们分析一下存储结构,可以发现能存的最大的数的位数为MAX* lg ScaleNum,而传统的大数存储的位数位MAX。

既然使用了这样的存储结构,那么我们怎么把一个数输出?

其实很简单,把BigNumber.num[]数组里面的数从下标oom到0依次输出即可,需要注意的地方就是除了最高段(BigNumber.num[oom])可以不管外,其他段的数其位数要是不足以填满该段,则需要在前面补0,下面是个例子:

有个数5600000400210,我们依旧以4位为一段将其分段,其数量级之间的进制ScaleNum = 10000



存到BigNumber里,其BigNumber.num[4] = {214, 40, 6000, 5}; BigNumber.oom = 4。
因为BigNumber.num[]是int数组,每个段如果前面有0的话是会被舍去的,这时候直接输出的话就只有5 6000 40 214,而不是5 6000 0040 0210。
我们看看要正确地输出一个BigNumber类型的数,其函数该怎么写
#include <iostream>
#define SCALE 10000
struct BigNumber{
int num[100];
int oom;
};

int Bits(int x) //计算一个数有几位,时间复杂度为O(lg ScaleNum)
{
if(x == 0) return 1;

int count = 1;
for(int i=10; i<=SCALE ;i*=10){
if(x/i == 0) return count;
count++;
}
}

void Print(BigNumber BigNum, int PrintBits = 4) //输出大数BigNum,因为4位为1段,故每段应该打印出PrintBits = 4位数字
{
cout<<BigNum.num[BigNum.oom];//最高位可以直接输出

int zero_fill; //计算在前面需要补几个0的变量
for(int i=BigNum.oom-1; i>=0 ;i--){ //大循环,每次循环输出一个段
zero_fill = PrintBits - Bits(BigNum.num[i]);//补0数量 = 应该打印的位数 - 当前段的数的位数

for(int count=0; count<zero_fill ;count++)//补0小循环
cout<<0;

cout<<BigNum.num[i];
}
cout<<endl;
}
设大数的位数为N,Print()函数最外层时间复杂度为O(ooe),而求位数Bits()函数跟补0小循环的函数的时间复杂度都是O(lg ScaleNum)故整个输出函数实际的时间复杂度为O(ooe*lg
ScaleNum) 其中ooe = N/lg ScaleNum 故该输出算法时间复杂度为O(N)。
=====================================================================
介绍完了大数的存储机制跟输出方法,大家也应该对这种算法有一定的了解了,那么我们谈谈怎么用这种存储结构进行加法以及乘法运算,引子里提到过,这种储存结构主要是为了方便大数与普通的int数据类型进行运算而创造的,所以接下来讲的加法跟乘法,都是BigNumber + int 或 BigNumber
* int的操作。
-----------------------------
先看加法
有一个int类型的数x,我们先分析一下加法的运算规则,5 6000 0040 0210 + 9999为例



我们依旧采取竖式计算,所以也是从最低段开始,一步一步往高的段进位。



代码很短很简单,直接看就明白了。
#include <iostream>
#define SCALE 10000
BigNumber operator+ (int x)const
{
BigNumber R = *this;

int temp = x;

int pos = 0;
while(temp){
R.num[pos] += temp;
temp = R.num[pos] / SCALE;
R.num[pos++] %= SCALE;
}

/*数量级扩增判断*/
if(pos > R.oom && R.num[pos]) R.oom = pos;

return R;
};


竖式计算谁都懂,就不讲下去了,上面图片的两个数怎么通过代码所示的方法加起来,稍微认真点读程序的话我想还是很容易想到的。。。

设大数的位数为N,加数的位数为M(M<= lg ScaleNum),则该算法的时间复杂度为O(oom),而oom = N/ScaleNum,故实际时间复杂度为O(N/lg ScaleNum)

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

乘法计算

---

乘法计算的话这个存储结构就用不着像传统结构那样也要竖式计算了,而是根据乘法分配律,将乘数x与被乘的大数BigNumber的每一个小段相乘,再从最低段开始一步一步向高的一段把数进位过来。

拿5 6000 0040 0210 * 9999做例子



实现的代码如下

BigNumber operator* (int x) const
{
BigNumber R = *this;
int last_carry = 0; //从前一位取得的进位
int now_carry; //当前位取得的进位
/*计算*/
for(int i=0; i<=R.oom ;i++){
now_carry = (R.num[i]*x+last_carry) / SCALE; //获得进位
R.num[i] = (R.num[i]*x+last_carry) % SCALE;

last_carry = now_carry;
}

/*数量级扩增判断*/
if(last_carry > 0){ //若最后的进位不为0,则扩增数量级
R.oom++;
R.num[R.oom] = last_carry;
}

return R;
};


设大数的位数为N,乘数的位数为M,由于乘数不用进行数据类型转换,所以这样算时间复杂度与M无关,该算法时间复杂度为O(oom),而oom = N/ScaleNum所以该算法实际时间复杂度为O(N/ScaleNum),比起传统的O(N*M)要好很多。
=====================================================================
以上便是该存储结构下的大数加法与乘法运算,由于该存储结构能直接跟int类型运算,所以在进行像阶乘或者是路径各权值之积这类需要一步一步叠加比较小的数最终结果为大数的运算时,有着比传统高精度写法写法更简便,时间复杂度更低的优点。与传统高精度开同样多的数组,能存的数据的位数却是传统高精度存储结构的lg ScaleNum倍,所以此写法能表示的数据范围更大,空间也更省。
=====================================================================
下面给一段代码,其功能是计算n的阶乘,输出,然后将其结果加上9999,再输出

#include <iostream>
#include <cstring>
#define SCALE 10000
using namespace std;

struct BigNumber{
int num[100];
int oom;

BigNumber operator+ (int x)const
{
BigNumber R = *this;

int temp = x;

int pos = 0;
while(temp){
R.num[pos] += temp;
temp = R.num[pos] / SCALE;
R.num[pos++] %= SCALE;
}

/*数量级扩增判断*/
if(pos > R.oom && R.num[pos]) R.oom = pos;

return R;
};

BigNumber operator* (int x) const { BigNumber R = *this; int last_carry = 0; //从前一位取得的进位 int now_carry; //当前位取得的进位 /*计算*/ for(int i=0; i<=R.oom ;i++){ now_carry = (R.num[i]*x+last_carry) / SCALE; //获得进位 R.num[i] = (R.num[i]*x+last_carry) % SCALE; last_carry = now_carry; } /*数量级扩增判断*/ if(last_carry > 0){ //若最后的进位不为0,则扩增数量级 R.oom++; R.num[R.oom] = last_carry; } return R; };
};

int Bits(int x) //计算一个数有几位,时间复杂度为O(lgScaleNum)
{
if(x == 0) return 1;

int count = 1;
for(int i=10; i<=SCALE ;i*=10){
if(x/i == 0) return count;
count++;
}
}

void Print(BigNumber BigNum, int PrintBits = 4) //输出大数BigNum,因为4位为1段,故每段应该打印出PrintBits = 4位数字
{
cout<<BigNum.num[BigNum.oom];//最高位可以直接输出

int zero_fill; //计算在前面需要补几个0的变量
for(int i=BigNum.oom-1; i>=0 ;i--){ //大循环,每次循环输出一个段
zero_fill = PrintBits - Bits(BigNum.num[i]);//补0数量 = 应该打印的位数 - 当前段的数的位数

for(int count=0; count<zero_fill ;count++)//补0小循环
cout<<0;

cout<<BigNum.num[i];
}
cout<<endl;
}

int main()
{
BigNumber big_num;

big_num.oom = 0;
memset(big_num.num, 0, sizeof(big_num.num));

/*算n的阶乘*/
int n;
cin>>n;
big_num.num[0] = 1;
for(int i=n; i>
91eb
=1; i--)
big_num = big_num * i;
Print(big_num);
/*之后加上9999*/
big_num = big_num + 9999;
Print(big_num);

return 0;
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  存储 算法 合并 结构