高精度整数算法总结,尤其是乘法,面向小白版

啊,今天算是来“还债”的吧哈哈哈,上周Oj作了一个简单的类的乘法重载,里面虽然没有涉及高精度,但是却对高精度乘法的思想提出了很高的要求,上个学期,老师初略地讲过大整数的加减乘除,但当时去因为各种各样的原因没有很好地掌握乘除,以至于,在五天前我对高精度乘法还只停留在要么是加法实现,要么是分两个数据域一个高位一个低位上面,但实际上,有更好的算法对乘法进行运算,而我发现网上又没有很多这方面的内容(其实感觉还挺重要的哈哈)所以今天就打算来献丑一下,讲讲我对这个知识的理解。

接下来分为三个模块

目录

第一:高低位结构体传统实现法:

​编辑

加法:

第二:压缩四位法

加法:

第三:高阶乘法思想(除法也相似)



第一:高低位结构体传统实现法:

先从一个形象的例子说起:P1005 [NOIP2007 提高组] 矩阵取数游戏

PS:高精度其实远远不仅限于oi,它作为一种重要的数据结构,在编程应用里面也是很重要的。

高精度整数算法总结,尤其是乘法,面向小白版_第1张图片

高精度整数算法总结,尤其是乘法,面向小白版_第2张图片

 这道题其实主要是一个简单的动态规划,那我们具体的主要=题不是它,我们需要注意的是,根据数据的范围,发现最大数为80x1000x(2^81-1),显然超了Long int但是不会超过128位。这就需要我们对高精度做一个简单的处理。

key:

1.用a.hig储存a的高位,a.low储存a的低位。高位显然不会溢出(因为只有2^81-1),所以将低位的1e18空出,防止两数相加时溢出.

2.输出时要格外注意:当高位为零时直接输出低位;当高位不为零时直接输出高位,低位位数不足18位要补零.

struct int128//结构体更自然 
{
    long long hig;
    long long low;
};//定义int128,最简单的雏形就有了

高精度加法,就是把低位和高位分开,低位高位分别相加,然后处理一下进位

int128 operator + (int128 a,int128 b)//重载运算符
{
    int128 k;
    k.low=0,k.hig=0;
    k.low=a.low+b.low;
    k.hig=k.low/p+a.hig+b.hig;//防止溢出,两个数加没问题,主要是怕后面很多加起来 
    k.low%=p;
    return k;
}

高精度乘法也是,这里因为是最简单的结构,int128里面都是longlong的low high,不涉及数组,字符串等,我们也只要简单地把高位低位分开乘就行了,并且考虑进位

int128 operator * (int128 a,int b)
{
    int128 k;
    k.low=0,k.hig=0;
    k.low=a.low*b;
    k.hig+=k.low/p+b*a.hig;//与上同理,乘法就是加法 
    k.low%=p;
    return k;
}

顺带提一下,使用模的思想在更复杂的时候会利于你的思路的整理

const long long p=1e18;//作mod用,10的18次方

大家可以通过这个十分简单的例子发现一个重要的思想,这也是传统的高精度算法核心:把一个超出long long或者由于需求,不想用太大的Longlong的时候,我们就把一个大数分为高位和低位,通过小学计算的思路,对高位低位分别进行操作,同时考虑模和进位的问题。

这就是最基本的思路,尤其使用于加减法,我们知道乘法和除法可以用一些更高级的算法来优化,但是加减法却是永远离不开这个思路。

当然出于题目的完整性,稍微提一下这里的dp思想,并给一个完整代码

#include
struct int128//结构体更自然 
{
    long long hig;
    long long low;
};//定义int128
int n,m;
const long long p=1e18;//作mod用,10的18次方 

int128 ans,f[85][85][85],a[85][85];

int128 max(int128 a,int128 b)
{
	if(a.hig>b.hig) return a;
	if(a.higb.low) return a;
	if(a.low

PS:

f[l][l+len][i] 表示左端点为l,右端点为l+len,第i行取数所能获得的最大值(状态)
f[l][l+len][i]=max(2*f[l+1][l+len][i]+2*a[i][l],2*f[l][l+len-1][i]+2*a[i][l+len]);(状态转移方程)

当然其实这道题还有一个两点是区间DP,有上下两行都要同时dp,然后状态方程使用了一个更为抽象的,对于每一行来说,f[1][m][]就是这一行所加数的最大值。

而这只是一个引入,下面让我们从数据结构和编程的角度去看高精度的事情

加法:

第一步:对数字进行处理,而我们往往需要对其反向reverse读入(当然不反向的也会在乘法里给)

struct bign{
    int d[1000];
    int len;
    //下面定义构造函数,用来初始化! 
    bign(){
        memset(d,0,sizeof(d));
        len=0;
    }
}; 
//一般来说,大整数一般是使用字符串输入的,下面将字符串储存的大整数
//存放在结构体中
bign change(char str[]){
    bign a;
    a.len=strlen(str);
    for(int i=0;i         a.d[i]=str[a.len-i-1]-'0';//这里把大整数的地位切换为高位 
    }
    return a; 

这样就完成一个转化,这里采取int数组是为了方便基础的计算,如果使用字符串操作也可行,但是要注意一个是字符中ascii码和十进制转化的问题,然后还有输入的判断等,and可能还需要自己构建出一个字符串加减乘除(此内容以后有空我也会写)

第二步,既然读好了,那么大整数也已经出现了,我们可以先给一个比较函数,这对于后续加减以及debug都是很有帮助的

int compare(bign a,bign b){
    if(a.len>b.len)return 1;//a大于b
    else if(a.len     else{
        for(int i=a.len-1;i>=0;i++){
            if(a.d[i]>b.d[i])return 1;
            else if(a.d[i]         }
        return 0;//两个数相等 
    } 
} //这些代码还是我去年写的哈哈哈那时候还不喜欢用bool,希望大家谅解

第三步,就按照上面的核心思想,很自然地给出加法和减法

bign add(bign a,bign b){
    bign c;
    int carry=0;//这里的carry表示进位
    for(int i=0;i         int temp=a.d[i]+b.d[i]+carry;
        c.d[c.len++]=temp%10;
        carry=temp/10;
    } 
    if(carry!=0){//如果最后一位的进位不为0,直接付给结果的最高位
       c.d[c.len++] =carry;
    }
    return c;

同样根据小学计算规则,加法可能出现在第一位进位,需要单独判断。

对于减法,其实就是加法的附庸,两个正数相减,实质上就是一个正数加上一个负数

当然也要处理好借位到第一位,第一位变0的情况

bign sub(bign a,bign b){
    bign c;
    for(int i=0;i         if(a.d[i]             if(a.d[i+1]>0){
            
            a.d[i+1]--;
            a.d[i]+=10; 
        }
        }
        c.d[c.len++]=a.d[i]-b.d[i];
    }
    while(c.len-1>=1&&c.d[c.len-1]==0){
        c.len--;
    }//去除高位为0的!同时保留一个最低位 
    return c;

第四步,关注一下乘除法,其实也依旧是竖式的方法一个个乘过去(不会有人想ab用b个a加起来实现吧,不会吧不会吧)

bign multi(bign a,int b){
    bign c;
    int carry=0;
    for(int i=0;i         int temp=a.d[i]*b+carry;
        c.d[c.len++]=temp%10;
        carry=temp/10;
    }
    while(carry!=0)
    {
        c.d[c.len++]=carry%10;
        carry/=10;
    }
    return c;

除法也一样

//高精度与低精度的除法
bign divide(bign a,int b,int &r){//r为余数,这里表示为引用
    bign c;
    c.len=a.len;
    for(int i=a.len-1;i>=0;i--){
        r=r*10+a.d[i];
        if(r         else{
            c.d[i]=r/b;
            r=r%b;
        }
    } 
    while(c.len-1>=1&&c.d[c.len-1]==0){
        c.len--;
    }
    return c;

但是!通过上面两个代码,大家有没有发现,诶,这都是一个大整数和一个正常的整数计算呢,我想要大整数和大整数计算行不行呢?

答案当然是可以的,"只是一般来说这几个例子其实已经可以应付大部分情况了,而两个大整数的乘除也可以通过加减法实现......"这就是我去年的想法。课实际上,当我们仔细去思考的时候,通过加减实现,是怎么个实现呢?我曾一度以为就是一位一位乘然后一个一个加,或者一位一位除,但是这样子,如果144444444444乘15555555555,你算算要加多少次,显然这个时候会超时,因此我把高阶的乘法放在了第三模块,小伙伴们可以去看看。不过首先,这上面的几个基础方法要掌握。

第二:压缩四位法

首先我问一个问题,十六进制和二进制的转化是怎么样的?

没错,四位二进制表示一位十六进制,于是计算机中的表示总是A9FD CFFF这种形式,那对于一个很大的整数,我们是不是也可以思考这种方法呢?

从而我们很自然地得出:用int类型的数组,每个元素内存放四位数字。

顺序:内部都是正序,但是总体是逆序的

比如12345786328

6328 4578 123

所以先处理读入

//PS:这个我写的代码已经找不到,出于省时,我这里复制了高精度4位压缩法原理与实现_挽颜__-CSDN博客他的代码。

int StrToNum(const char str[], int num[])//从字符串的最后一位倒序向,int数组正序方向赋值(int数组是倒序的大数)
{
    int len=strlen(str),i,j=0;
    for(i=len-1;i>2;i-=4)//倒序每4个数字存放一个单位内
    {
        if(i-3>=0)
            num[j++]=(str[i-3]-'0')*1000+(str[i-2]-'0')*100+(str[i-1]-'0')*10+(str[i]-'0');
    }
    if(i==0)//如果剩余1个数字
        num[j++]=str[0]-'0';
    else if(i==1)//剩余2个数字
        num[j++]=(str[0]-'0')*10+(str[1]-'0');
    else if(i==2)//剩余3个数字
        num[j++]=(str[0]-'0')*100+(str[1]-'0')*10+(str[2]-'0');
        while(j>0&&num[j-1]==0)    //j返回num的长度
            j--;
    return j;
}

加法:

int Add(int num1[],int num2[],int num3[],int len1,int len2,int& num3_begin)//加法  num1,num2是原始数据的数组,num3用来存放结果
{
    int i=0,j=len1>len2?len1:len2;     

  //len1,len2分别代表数组num1,num2的长度,num3_begin返回结果中数值开始的下标。
    if(len1==0&&len2==0)     

//原始数据是0的情况讨论。  0:num1、num2都是0;   1:num1是0    2:num2是0
        return 0;
    else if(len1==0)
        return 1;
    else if(len2==0)
        return 2;
    else
    {
        while(i         {
            num3[i]+=num1[i]+num2[i];//相加
            if(num3[i]>=10000)        //若数值大于进制进行进位操作
                num3[i+1]+=num3[i]/10000,num3[i]%=10000;
            i++;
        }
        if(num3[i]==0)//返回结果中数值开始的下标
            num3_begin=i-1;
        else
            num3_begin=i;
    }
    return -1;

这里给大家讲解一下,核心的思路同第一部分的传统法,便是分位然后分别进行计算,同时处理好借位和进位的问题,无非一个只分了高低位,而这里是分了许多位,每一位存了四个数字,同时需要关注最后可能存的不是四位数。

乘法:

int Multiply(int num1[], int num2[], int num3[], int len1, int len2,int &num3_begin)//乘法    存储在num1,num2中的数是从下标0开始倒序存放的。
{
    int i=0,j=0,k=0;
    if(len1==0||len2==0)		//num1、num2有一方是0
        return 0;
    else
        {
            while(i=10000)
						num3[k+1]+=num3[k]/10000,num3[k]%=10000;
					 k++,j++;
                }
                i++;
            }
        }
        if(num3[k]!=0)		//返回数中最高位不为0的下标
            num3_begin=k;
        else
            num3_begin=k-1;
    return 1;
}

和传统法一样,不过我个人认为他的代码里面,对于四位数的乘法可以多用一个模来处理可能会更好。

第三:高阶乘法思想(除法也相似)

这也算是写这篇博客的动因了吧,在上文中我已经提到了,如果两个大整数的结构体之类的东西相互做乘除法,如果还是用那么朴素的加变乘就会出现一些问题,所以需要我们对计算作出一些处理。

先给出一个类的定义

高精度整数算法总结,尤其是乘法,面向小白版_第3张图片

我们只关注最后两个加法和乘法的重载,以及private元素的定义是int数组

还是一样,加法想必大家现在已经能轻松地给出了,无非需要讨论几个特殊点,已经两个加数的长度什么的。为利于理解,我改了一下,三种情况用if分开了

Number Number::  operator +(const Number& number)
{
		int b[61];
		int blen = 0;
		int op;
		int opp;
		int cur = 0;
		op = len;
		opp = number.len;
		if (opp == 0 ) {
			return *this;
		}
		if (op == 0) {
			return number;
		}
		int i = op - 1;
		int j = opp - 1;//显然op>=opp,所以循环结束的话也只能要么ij都是0,要么i>=0,j=-1
		if(op==opp){
			while(i>=0&&j>=0)
			{
				b[blen] = a[i] + number.a[j] + cur;
				cur = 0;
				if (b[blen] > 9) {
					cur++;
					b[blen] = b[blen] - 10;
				}
				blen++;
				i--; j--;
			}
			if (cur > 0) {
				b[blen] = 1;
				blen++;
			}

		}
		if (op < opp)
		{
			while(i>=0&&j>=0)
			{
				b[blen] = a[i] + number.a[j] + cur;
				cur = 0;
				if (b[blen] > 9) {
					cur++;
					b[blen] = b[blen] - 10;
				}
				blen++;
				i--; j--;
			}//i先到-1
			for (; j >= 0; j--)
			{
				b[blen] = number.a[j] + cur;
				cur = 0;
				if (b[blen] > 9) {
					cur++;
					b[blen] = b[blen] - 10;
				}
				blen++;
			}
			if (cur > 0) {
				b[blen] = 1;
				blen++;
			}
		}
		if (op > opp)
		{
			while(i>=0&&j>=0)
			{
				b[blen] = a[i] + number.a[j] + cur;
				cur = 0;
				if (b[blen] > 9) {
					cur++;
					b[blen] = b[blen] - 10;
				}
				blen++;
				i--; j--;
			}//j先到-1
			for (; i >= 0; i--)
			{
				b[blen] = a[i] + cur;
				cur = 0;
				if (b[blen] > 9) {
					cur++;
					b[blen] = b[blen] - 10;
				}
				blen++;
			}
			if (cur > 0) {
				b[blen] = 1;
				blen++;
			}
		}
		char s[66];
		int pi = 0;
		for (int p = blen - 1; p >= 0, pi < blen; p--, pi++)
		{
			s[pi] = b[p] + '0';
		}
		Number res(s, blen);
		return res;
	}

 然后关键点来了,乘法!

先给大家看一段超时的代码

...找不到了...好吧给大家讲一下:我们假设14乘以12,那么我们根据竖式,14在上,12在下,于是竖式下面第一行计算2*14=28,第二行计算1*14=14,又因为是第二行,于是乎乘10^(i-1),i代表第几行,所以结果是140+28=168.那么我们无非就是用加法模拟一下这个过程,14个2加起来.......

所以这个是比较慢的

那快的代码是什么呢?是不用竖式吗?不,依旧是用竖式,但是在进位和分行算的问题上进行了优化。

先贴出来

高精度整数算法总结,尤其是乘法,面向小白版_第4张图片

 高精度整数算法总结,尤其是乘法,面向小白版_第5张图片

 其实很多人看了这个代码就应该明白了吧,但我知道这个的时候还是呆了一下,使用这里还要根据宿舍同学的解释哈哈,当然,今天是我解释给你们听

首先最外圈的循环,就是循环一个数的长度,随便哪一个,反正谁做乘数谁做被乘数一样的。

cur就代表位数,high代表行数,给张图:

但是我们要注意在算法中,是把各个行数,每一列上的进位等等都是同时做处理的

 res.a[cur] = res.a[cur] + (a[j]*number.a[i]) % 10;这一句就是说,res.a[i]第i列上的最终结果

res.a[cur + 1] = res.a[cur + 1] + (a[j] * number.a[i]) /10;是第i+1列最终结果,这里同时处理了进位

if (res.a[cur] > 9)这个意思是,比如我这一列有0 1 2 3 四行,0行上是9,1行上是8,,2行是1,3行是0 ,8+9=17显然不能在一位中表示出来,所以也要进位,最后这一列的结果是7+1+0=8

所以接下来的就很明显了,多了一个restt只是为了处理多一位的问题罢了

好啦,这就是今天的内容啦,天呐学校又感觉要封校了......周末出游计划又泡汤了.....

有错误的地方欢迎大家指正。

                                                                                                                                    21计科陈昊飏

打个小广告,今年高考即将来临,南大也即将迎来百廿周年,欢迎报考南京大学!

你可能感兴趣的:(算法学习,c++,算法)