7.AcWing791.高精度加法(AcWing算法基础课二刷)

关键词:高精度加法、vector(容器)、压位、__int128、快读快写

地位:算法竞赛普遍涉及,在笔试、面试是一个比较重要的考点。

Java和Python自带高精度类型;但C/C++需要手写高精度。

前置知识:long long表示的最大值是9223372036854775807,即2^{^{63}}-1,略大于9e18

unsigned long long表示的最大值是18446744073709551615,即2^{^{64}}-1

其实在有些情况下,算法竞赛中(NOIP、CSP、NOI、蓝桥杯、XCPC等)答案会让你模1e9+7。但有些题目就会刁难选手,例如求方案数、排列数、组合数。在不让带模板的算法竞赛中,高精度的加减乘除其实并不罕见,考频实际上也不低。而且如果高精度表示的位数过高(例如几百万位),可能还会有超时的风险,此时需要“压位”操作来卡时间(待会会讲到)。
表示高精度有两种方法:

Ⅰ.用数组模拟高精度,设一个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_{i}+b_{i}+t,表示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优化),但要自己实现长度、逆序存储等操作,很容易写错。所以读者应该多多调试自己的代码来提升自己的编程能力。

接下来所讲的两个知识拓展属于算法竞赛中十分重要也比较常用的”常数优化“;但目标是蓝桥杯省赛的同学,可以选学。

知识拓展一:压位

高精度加法的时间复杂度约为O\left ( n \right ),其中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位(二进制位数),储存的最大数值是2^{127}-1,有39位数:

170141183460469231731687303715884105727

在某些情况(例如一些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博客

感谢您的支持

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值