关键词:高精度加法、vector(容器)、压位、__int128、快读快写
地位:算法竞赛普遍涉及,在笔试、面试是一个比较重要的考点。
Java和Python自带高精度类型;但C/C++需要手写高精度。
前置知识:long long表示的最大值是,即,略大于。
unsigned long long表示的最大值是,即。
其实在有些情况下,算法竞赛中(NOIP、CSP、NOI、蓝桥杯、XCPC等)答案会让你模。但有些题目就会刁难选手,例如求方案数、排列数、组合数。在不让带模板的算法竞赛中,高精度的加减乘除其实并不罕见,考频实际上也不低。而且如果高精度表示的位数过高(例如几百万位),可能还会有超时的风险,此时需要“压位”操作来卡时间(待会会讲到)。
表示高精度有两种方法:
Ⅰ.用数组模拟高精度,设一个int类型的数组基本上够了;
Ⅱ.用STL的vector容器(俗称“可变数组”)来模拟高精度。
y总使用的是第二种方法,因为他的存储结构特殊,计算时方便。在此介绍一下vector容器:
vector容器是C++特有的STL容器,他是一个可变数组;使用时加上vector头文件才可使用。
vector相对于普通数组的优势是不需要定义数组的长度,保证数组不会因此越界。
y总在这道题中所用的操作如下:(假设vector容器的名称是a)
a.push_back(x);//在末尾插入一个元素
a.pop();//删除末尾的元素
a.back();//返回末尾元素
a.size();//vector当前元素个数
那么如何用高精度模拟加法?
其实我们可以从小学数学的竖式计算中得到启发。
见下图:从个位进行竖式相加,如果超过10就向前一位“进一”,直到计算完为止。如果当前最高位的计算需要进一操作,那么答案的最高位一定是1。例如:7+5=14。
y总的方法是用string存储两个大整数的输入。然后转化为vector<int>类型的数组。
string是一个字符串类型,理论上可以容纳足够多的字符,所以存储的每位数字是char类型,此时需要让每位数字减'0'(即字符0)达到转换的效果。此外,要从字符串的末尾存储数字,这样处理的时候第一次被相加将从个位开始,比较方便。
之后通过自己设计函数将他们相加(返回vector<int>类型)最后输出即可。
其中存储两个整数的vector<int>容器设为a和b;存储结果的容器设为c(为读者方便,全设为小写字母,包括待会写的代码)。C/C++模拟竖式相加的原理也是从个位相加,如果每一位的相加结果大于等于10则t=1,否则t=0(t初始是0)。之后容器c从尾端插入一个数,就是下图的,表示a与b的第i位相加的和,如果后一位有进一(即t=1)需要加上t。
理解之后是不是认为很简单?实际上确实很简单,我们就先上y总的代码:
#include<iostream>
#include<vector>
using namespace std;
//手写高精度加法vector版本,&表示引用,y总解释可以让速度变快:
vector<int> add(vector<int> &a,vector<int> &b)//a+b
{
int t=0;//t表示是否进位
vector<int> ans;
//一直加到a和b的首位,可以用竖式计算模拟
for(int i=0;i<a.size()||i<b.size();i++)
{
if(i<a.size()) t+=a[i];//a还没加完
if(i<b.size()) t+=b[i];//b还没加完
ans.push_back(t%10);
t/=10;//两个数的个位相加不可能超过18
//除以10就可以表示是否进位,为下次加法铺垫
//读者可以手动模拟,t只有0和1两种情况
}
if(t) ans.push_back(1);//最高位有进位在首位添上1
return ans;//记得返回vector容器
}
int main()
{
string A,B;//C++的字符串STL容器
vector<int> a,b;
cin>>A>>B;//string用scanf很麻烦,所以用cin代替
//cin可以输入任何数据,比scanf方便,但比scanf慢一些
//cin还有一个好处:忽略字符的空格和换行,在字符操作有优势
for(int i=A.size()-1;i>=0;i--)//将个位开始存入vector容器
a.push_back(A[i]-'0');
for(int i=B.size()-1;i>=0;i--)//同上
b.push_back(B[i]-'0');
vector<int> c=add(a,b);
//注意c容器的首元素也是最高位,所以要逆序输出
for(int i=c.size()-1;i>=0;i--)
cout<<c[i];//cout也比较“万能”,但比printf慢
return 0;
}
之后再上用数组模拟高精度的代码,思想和上面的描述是一样的;但普通的数组比STL容器要快一些,不容易超时。此代码可以在AcWing的791题通过。(C/C++不能直接通过函数返回数组)
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=1e5+10;//此题目的位数不超过十万
char A[N],B[N];//char数组模拟字符串
int a[N],b[N],c[N];//a和b代表整数,c代表结果
int main()
{
scanf("%s%s",A,B);//输入字符数组不需要带地址符
//strlen代表字符长度,要加上cstring或string.h头文件
int alen=strlen(A),blen=strlen(B);
//逆序存储整数a和b,即下标为0的数是个位数,以此类推
for(int i=0,j=alen-1;i<alen;i++,j--)
a[i]=A[j]-'0';
for(int i=0,j=blen-1;i<blen;i++,j--)
b[i]=B[j]-'0';
//clen代表a和b拥有的最高位
int clen=max(alen,blen);
//下边的步骤实现和上一个代码是一样的,这里不赘述了
int t=0;
for(int i=0;i<clen;i++)
{
if(i<alen) t+=a[i];
if(i<blen) t+=b[i];
c[i]=t%10;
t/=10;
}
//注意如果最高位要进一,要补上一个1
if(t)
{
c[clen]=1;
clen++;
}
//注意c数组也是逆序存储的,要反着输出
for(int i=clen-1;i>=0;i--)
printf("%d",c[i]);
return 0;
}
我们看到上述代码的运行时间比vector要快(如果不开O2优化),但要自己实现长度、逆序存储等操作,很容易写错。所以读者应该多多调试自己的代码来提升自己的编程能力。
接下来所讲的两个知识拓展属于算法竞赛中十分重要也比较常用的”常数优化“;但目标是蓝桥杯省赛的同学,可以选学。
知识拓展一:压位
高精度加法的时间复杂度约为,其中n为位数。
压即“压缩”;压位的定义是将数组的每一个元素表示1位数转变为表示多位数。常用的压位一般是压4位或压9位。压的位数如果比较多,在一定程度上可以加快运算的速度,里面的n就可以除相应的压位位数,即“常数优化”操作;有时甚至可以卡在时间限制之内。运算的原理依然与上述所讲的一致。不过需要避开几个坑:
1.除了最高位,其他位都要打印出“前导零”。例如10123压4位,则分割成1和0123,如果不输出前导零则结果输出是1123,明显与正确答案不符。打印出前导零的请看代码中的注释。
2.如果要压缩十位以上,那么数组要开long long!
下面展示自己实现的vector高精度加法的压位代码(压9位),其他的代码参考文末链接:
#include<iostream>
#include<vector>
#include<cstring>
using namespace std;
const int mod=1e9;
vector<int> add(vector<int> &a,vector<int> &b)
{
vector<int> ans;
int t=0;
for(int i=0;i<a.size()||i<b.size();i++)
{
if(i<a.size()) t+=a[i];
if(i<b.size()) t+=b[i];
//压位的话把10换成mod就好了 压几位就mod1e几
ans.push_back(t%mod);
t/=mod;
}
if(t) ans.push_back(1);
return ans;
}
int main()
{
string A,B;
cin>>A>>B;
vector<int>a,b;
//压9位的操作
for(int i=A.size();i>0;i-=9)//i不可以等于0
{
if(i<9)//位数不足10位,即i<9
{
//substr截取从下标为i开始的连续j个字符
//用法:字符串名.substr(i,j)
//注意Java和Python有类似的函数,用法可能不同
string C=A.substr(0,i);//i=0时没有任何字符
//stoi是string to int的缩写,字符串转int函数
//类似的还有stoll,转为long long类型
a.push_back(stoi(C));
//上述这两个函数都要配上cstring头文件
}
else
{
//注意i初始设为A.size()而不减1。因为substr的特性,
//C初始会截取到下标为A.size()-1的字符,读者可以模拟
string C=A.substr(i-9,9);
a.push_back(stoi(C));
}
}
for(int i=B.size();i>0;i-=9)
{
if(i<9)
{
string C=B.substr(0,i);
b.push_back(stoi(C));
}
else
{
string C=B.substr(i-9,9);
b.push_back(stoi(C));
}
}
vector<int> c=add(a,b);
//最高位不输出前导零
printf("%d",c[c.size()-1]);
for(int i=c.size()-2;i>=0;i--)
{
//%09d表示输出带前导零的9位数
//类似的还有%02d,%03d等
printf("%09d",c[i]);
}
return 0;
}
在本题的运行时间是41ms,略慢于不压位的操作;但位数非常高时压位会有非常可观的效果。
知识拓展二:用具体数据类型表示高精度与快读快写
在C/C++中,double可以表示的数字的绝对值范围大约是:-1.79E+308 ~ +1.79E+308(Ⅵ),但大多数情况下输出却是科学计数法(例如1.2345e100)而且还会可能会有精度误差,在算法竞赛中慎用此类型表示高精度。
接下来才是要讲的重点:__int128(两个英文下划线+int128)。
__int128是一个不太常用的数据类型,我第一次在洛谷见到这类数据类型;顾名思义,该数据类型拥有128位(二进制位数),储存的最大数值是,有39位数:
在某些情况(例如一些DP(动态规划))可以代替高精度和压位,十分方便。
但__int128不可以用scanf、printf、cin、cout操作。
这似乎陷入了一种尴尬的局面。
此时我们就要另外寻找一个方法来表示__int128:快读快写。
快读快写使用getchar和putchar代替其他输入输出操作,在输入输出超过大约一百万(1e6)次时效率明显比上述四个操作快很多。之前笔者在电脑上测试过:输入输出一亿个元素时,cin和cout的耗时是39秒,scanf和printf比cin和cout快;而快读快写只需要不到1秒的时间执行完操作。不过在输入输出的个数较少时scanf和printf反而更快一些,因此能用scanf和printf就尽量使用。
也有用二进制和其他方式表示快读快写的方法,但底层原理实际上还是getchar和putchar。
scanf、printf、getchar和putchar的头文件在cstdio(stdio.h)。
一般的竞赛中,iostream会“补充”cstdio头文件;但在一些正式的竞赛(例如CSP、NOI、蓝桥杯、XCPC等)建议读者加上cstdio头文件。
这里引用了一种快读快写表示__int128的方法(Ⅶ):
#include<iostream>
#include<cstdio>
using namespace std;
//这里展示引用参数的函数,也有不引用参数的函数
void read(__int128 &a)//&表示引用,表示传进来的参数可以被函数修改
{
a=0;//初始化a
int w=1;//判断正负
char c=getchar();//读取字符
//有效字符为负号、0~9
while(c=='-'||(c>='0'&&c<='9'))
{
if(c=='-')
{
w=-1;
}
if(c>='0'&&c<='9')
{
a=a*10+c-'0';//a*10+当前数字表示从末尾插入一个数
//注意这里要减去字符0
}
c=getchar();//继续读取字符
}
a*=w;//w如果为负要加上负号
return ;//void return空值
}
void print(__int128 a)//函数名不要起printf
{
if(a<0)//判断是否为负数
{
putchar('-');
a*=-1;
}
//用递归打印每一位
if(a>=10) print(a/10);
putchar(a%10+'0');//打印当前位
}
int main()
{
__int128 num;
//如果不引用参数,则要这么写:num=read();
read(num);
print(num);
return 0;
}
不过__int128和double都有各自的局限性,还需要读者在实际应用中自行判断。
参考链接/文献:
Ⅰ.AcWing——算法基础课,2019,闫学灿 活动 - AcWing
Ⅱ.AcWing791.高精度加法 活动 - AcWing
Ⅲ.压位高精度的写法——博客园,2019,HoshizoraZ 压位高精度的写法 - HoshizoraZ - 博客园 (cnblogs.com)
Ⅳ.高精度之压位——CSDN,2021,你好世界wxx 【算法专题】高精度之压位_压位高精度-CSDN博客
Ⅴ.C++常用函数--stoi函数用法总结——CSDN,2022,白木烨 C++常用函数--stoi函数用法总结-CSDN博客
Ⅵ.双精度浮点数——百度百科,2010,百度百科 双精度浮点数_百度百科 (baidu.com)
Ⅶ.快读快写模板——CSDN,2022,Wu_while 快读快写模板-CSDN博客
感谢您的支持