【学习笔记】高精度算法

前言

之前做题,有时候会听到老师说:“这道题数据太大,要高精度。”

有些题的数据范围可以毒瘤到连开挂神器__int128都救不了你,这个时候我们就会选择:

让电脑跟我们人一样计算!毕竟人只要时间够,不管多大的数都是可以算出来的。

这就是高精度算法。


高精度的那些板子计算

说句闲话:高精度算法需要使用到\(string\)类型的字符串,这里不提,请自行 BDFS。

1.高精度加法

要做高精度,首先需要了解人是怎么算的。

小学一年级内容出现了!(雾,大雾,伦敦雾

\[\ \ \ \ \ \ \ \ \ \ 3\ 2\ 8 \]

\[+\ \ \ \ \ \ \ 1\ 3\ 7 \]

\[\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_ \]

\[\ \ \ \ \ \ \ \ \ \ 4\ 6\ 5 \]

先看以上算式:

第一步:从低位往高位算,\(8+7=15\)。末位为5,往前进一位。

ans 0 1 5

第二步:算十位,\(2+3+1=6\)(1为进位),十位为6,不进位。

ans 0 6 5

第三步:算最高位,\(3+1=4\),最高位为4。

ans 4 6 5

ans就是最终的答案了。

但是有个问题:上面这个例子两个加数的例子是一样的,那要是不一样呢?程序不就弄错了吗?

解决方法很简单:把两个加数倒着存就行了,这样因为数组初始化的原因,后面的数都会变成0,即会在前面自动补0。最后算出来的答案也是倒着的,再进行处理。

具体过程如下:(没错我又在水博客了,巨佬们直接跳过吧【滑稽】)

1.倒序存储

a 8 2 3
b 7 3 1
c 0 0 0

2.算第一位,进位直接存在下一位中

a 8 2 3
b 7 3 1
c 5 1 0

3.继续算,记得加上数组里本来就有的值(即进位)

a 8 2 3
b 7 3 1
c 5 6 0

4.一直循环,直到最后算完出结果

a 8 2 3
b 7 3 1
c 5 6 4

5.把答案倒过来输出

5 6 4 --> 465

这样就是一个不完整的高精度加法的过程。

为什么不完整呢?因为程序可能会在计算位数的时候出错(程序会把和的位数计算为加数中位数较大的那一个数的位数+1,但是不一定会进位),所以在结束计算之后,需要判断和的第一位是否为0,如果是就需要去除。

这就完整了!

最后给出一份有注释的高精度加法板子代码:

#include<bits/stdc++.h>
using namespace std;
int a[5005],b[5005],c[5005];//定义全局,即初始化为0 
string add(string a1,string b1){
	//前置工作 
	string c1;
	int lena=a1.size();
	int lenb=b1.size(); 
	//倒序存储并转换为整数类型 
	for(int i=0;i<lena;i++){
		a[lena-i]=a1[i]-'0';
	}
	for(int i=0;i<lenb;i++){
		b[lenb-i]=b1[i]-'0';
	}
	//做加法
	int lenc=1;
	//做加法的次数为两个高精度数中较大的位数 
	while(lenc<=lena||lenc<=lenb){
		c[lenc]+=a[lenc]+b[lenc]; //加法 
		c[lenc+1]=c[lenc]/10;     //进位 
		c[lenc]%=10;              //取个位 
		lenc++; 
	} 
	//去除前导0
	if(!c[lenc])
		lenc--;
	//把c存到c1里面
	while(lenc>=1)
		c1+=c[lenc--]+'0';
	return c1;
}
int main(){
	string a1,b1;
	cin>>a1;
	cin>>b1;
	cout<<add(a1,b1);
	return 0;
}

2.高精度减法

思路和高精度加法差不多,就是把进位换成退位,每次退位就让当前位数上的数加上10,然后让前面那个位数的答案-1。要注意前导0可能不止一个,需要连续去除。

#include<bits/stdc++.h>
using namespace std;
int a[5005],b[5005],c[5005];
string sub(string a1,string b1){
	//前置工作
	if(a1==b1)
		return "0";
	string c1;
	int lena=a1.size();
	int lenb=b1.size();
	//倒序存储并转换为整数类型 
	for(int i=0;i<lena;i++){
		a[lena-i]=a1[i]-'0';
	}
	for(int i=0;i<lenb;i++){
		b[lenb-i]=b1[i]-'0';
	}
	//做减法
	int lenc=1;
	//做减法的次数为两个高精度数中较大的位数 
	while(lenc<=lena||lenc<=lenb){
		c[lenc]+=a[lenc]-b[lenc]; //减法
		if(c[lenc]<0){            //退位
			c[lenc]+=10;      //借位
			c[lenc+1]=-1;     //前面的要-1
		}
		lenc++;
	}
	//去除前导0
	while(!c[lenc])
		lenc--;
	//把c存到c1里面
	while(lenc>=1)
		c1+=c[lenc--]+'0';
	return c1;
}
int main(){
	string a1,b1;
	cin>>a1;
	cin>>b1;
	cout<<sub(a1,b1);
	return 0;
}

3.高精度乘法

这个就有点意思了。

再来看一个竖式:

\[\ \ \ \ \ \ \ \ 1\ 5\ 3 \]

\[\times\ \ \ \ \ \ \ \ 2\ 7 \]

\[\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_ \]

\[\ \ \ \ \ \ 1\ 0\ 7\ 1 \]

\[\ \ \ 3\ 0\ 6 \]

\[\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_ \]

\[\ \ \ \ \ \ 4 \ 1\ 3\ 1 \]

乘法又是怎么算的呢?

根据日常经验,我们能很快发现:需要先将一个数拆掉,一位一位去乘另一个数,然后把结果加起来,不过加起来的时候需要乘以10^{最小位数-1}。

那事情就变得容易了起来,一个双重循环,每一次循环让被拆掉的那个数的每一位乘以另一个数,加在答案里,进位和加法差不多,不过要注意进位可能不止一位。

代码:

#include<bits/stdc++.h>
using namespace std;
int a[5005],b[5005],c[10005];
string mul(string a1,string b1){
	//如果有数为0直接返回0 
	if(a1=="0"||b1=="0")
		return "0";
	//前置工作
	string c1;
	int lena=a1.size();
	int lenb=b1.size();
	//倒序存储并转换为整数类型 
	for(int i=0;i<lena;i++){
		a[lena-i]=a1[i]-'0';
	}
	for(int i=0;i<lenb;i++){
		b[lenb-i]=b1[i]-'0';
	}
	//做乘法
	int lenc;
	for(int i=1;i<=lena;i++){
		for(int j=1;j<=lenb;j++){
			lenc=i+j-1;            //存进去的位数 
			c[lenc]+=a[i]*b[j];    //乘法 
			c[lenc+1]+=c[lenc]/10; //进位 
			c[lenc]%=10;           //取个位 
			lenc++;
		}
	}
	lenc=lena+lenb;
	//去除前导0 
	while(!c[lenc])
		lenc--;
	//把c存到c1里面
	while(lenc>=1)
		c1+=c[lenc--]+'0';
	return c1;
}
int main(){
	string a1,b1;
	cin>>a1;
	cin>>b1;
	cout<<mul(a1,b1);
	return 0;
} 

4.高精度除法之高精除以低精

没学高精度之前一直不知道为什么要把除法分成两个部分,学了之后才发现这难度根本不是一个级别的!awa

首先还是思考人是怎么列竖式做除法的。(emmm不会用\(\LaTeX\)写竖式就只能写一个很丑的竖式了www)

做除法的步骤是:

1.用现在判断到这一位之前的数除以除数,把商存在答案中;

2.计算余数;

3.循环1~2,直到判断完毕。

这应该是一个很正确的思路,但是其实它本来是对的,但是放到程序里就错了。

为什么呢?

首先除以除数(第一步)的代码是这个样子的:

c[i]=(a[i]+c[i])/b;

第二步求余数的代码是这个样子的:

c[i+1]=(a[i]+c[i])%b*10;

很容易注意到,在执行第一步操作的时候,\(c[i]\)的值会发生改变,但是求余数需要用到的\(c[i]\)却必须是原来的值。

所以在代码的实现中,要先求余数再求商。

代码:

#include<bits/stdc++.h>
using namespace std;
int a[10005],b,c[10005];
string div(string a1,int b){
	if(a1=="0")
		return "0";
	//前置工作
	string c1;
	int lena=a1.size();
	//转换为整数类型 
	for(int i=0;i<lena;i++){
		a[i+1]=a1[i]-'0';
		//printf("%d",a[i+1]);
	}
	//除法
	for(int i=1;i<=lena;i++){
		/*
		※错点:一定要先求下一位余了多少再求现在的位数,
		因为求现在的位数时c[i]的值会更新 
		*/ 
		c[i+1]=(a[i]+c[i])%b*10; //求下一位余了多少 
		c[i]=(a[i]+c[i])/b;      //计算当前位数的值 
	} 
	int lenc=1;
	while(!c[lenc])
		lenc++;
	for(int i=lenc;i<=lena;i++)
		c1+=c[i]+'0';
	return c1;
}
int main(){
	string a1;
	cin>>a1;
	scanf("%d",&b);
	cout<<div(a1,b);
	return 0;
}

5.高精度除法之高精除以高精

emmm……

直到现在我的高精除高精还是没写出来啊╮(╯▽╰)╭,因为我感觉这个程序不是bug太多,而是整个程序就是一个bug……

先看思路。

首先高精除高精是不能像低精除法那样直接除的(不然请你告诉我怎么除),这里采取的方式是:用减法模拟除法。

设置一个变量\(flag\),代表现在判断到了第几位。在这一位以及这一位之前组成的数就是被减数,除数就是减数。判断一下两个数之间还能否相减(即被减数是否还大于减数),如果不行了就判断下一位,如果还可以就继续减,直到不能减为止。

说得简单,这玩意儿实现起来超级无敌难的。

我的代码一直都是 \(\texttt{TLE}\ 10\) 分,不过思路应该是对的,也拿出来给大家看一下。

//一个bug比头发还多的高精除高精代码 
#include<bits/stdc++.h>
using namespace std;
int a[5005],b[5005],c[5005],d[5005];
//这是判断是否还能减的函数 
bool check(int flag,int lena,int lenb){
	if(flag<lenb)
		return false;
	int sum=0;//统计前面有多少个前导0 
	for(int i=lena;i>=flag;i--){
		if(!a[i])
			sum++;
		else
			break;
	}
	if(flag-sum<lenb)
		return false;
	if(flag-sum>lenb)
		return true;
	//对于位数一样的数,判断是否可减
	bool f=1;//是否完全相等 
	for(int i=lena-sum;i>=1;i--){
		if(a[i]>b[i])
			return true;
		if(a[i]!=b[i])
			f=0;
	}
	if(f)
		return true;
	else
		return false;
}
string div(string a1,string b1){
	//前置工作
	if(a1=="0")
		return "0";
	string c1;
	int lena=a1.size();
	int lenb=b1.size();
	//倒序存储并转换为整数类型 
	for(int i=0;i<lena;i++){
		a[lena-i]=a1[i]-'0';
	}
	for(int i=0;i<lenb;i++){
		b[lenb-i]=b1[i]-'0';
	}
	//开始用减法模拟除法
	int flag=lena;//指针,现在到第几位了 
	int lenc=1;
	while(flag){
		//printf("%d\n",flag);
		if(!check(flag,lena,lenb)){//现在a已经小于b,需要进行下一位的除法 
			a[flag]=0;
			flag--;
			continue;
		}else{//a还未小于b,继续减 
			memset(d,0,sizeof(d));
			int lend=flag;
			while(lend<=lena||lend<=lenb){
				d[lend]+=a[lend]-b[lend];
				if(d[lend]<0){
					d[lend]+=10;
					d[lend+1]=-1;
				}
				lend++;
			}
			lenc++;
			for(int i=flag;i<=lena;i++)
				a[i]=d[i];
		}
	}
	//去除前导0
	while(!c[lenc])
		lenc--;
	//把c存到c1里面
	while(lenc>=1)
		c1+=c[lenc--]+'0';
	return c1;
}
int main(){
	string a1,b1;
	cin>>a1;
	cin>>b1;
	cout<<div(a1,b1);
	return 0;
} 

(我那个 \(10\) 分估计是特判 \(0\) 得的?

posted @ 2021-04-13 13:41  Saiodgm  阅读(255)  评论(0编辑  收藏  举报