算法基础课笔记(二):高精度、前缀和和差分

这是第二节课,单纯从高精度也就是大数运算的角度来说,我记得之前做课设时候的数字加密器,用Java做直接就有BigDecimal类,可以做大数运算,相对而言c/c++就没这么便利了。前缀和和差分我好像是闻所未闻,大概就是这么个情况。

高精度

一般来说,超大数字用的不多,long在平时场景处理数据时候已经够用了,但是一旦用到了,c/c++没有相关处理机制,就很难受。目前的处理思路是把数字按位塞进数组里,然后再设计算法进行运算。涉及的有两个大数相加、相减,大数和一个较小的数相乘,大数除以一个较小的数。

加法

先上模板:

#include <iostream>
#include <string>
#include <vector>
using namespace std;

vector<int> add(vector<int> &A,vector<int> &B)
{
    //判断一下两个数的长度,用长的加上短的
    if(A.size() < B.size()) return add(B,A);
    vector<int> C;
    //进位的存储
    int t = 0;
    //A[i] + B[i] + t,和10取余是当前位,除以10得到的是进位
    for(int i = 0;i < A.size();i++)
    {
        t += A[i];
        //很明显若B短,之后就变成了A[i] + t
        if(i < B.size()) t += B[i];
        C.push_back(t % 10);
        t /= 10;
    }
    //最后若t不为0,说明还有进位,要塞进去
    if(t) C.push_back(t);
    return C;
}

int main()
{
    vector<int> A,B;
    string a,b;
    cin>>a>>b;
    for(int i = a.size() - 1;i >= 0;i--)
    {
        A.push_back(a[i] - '0');
    }
    for(int j = b.size() - 1;j >= 0;j--)
    {
        B.push_back(b[j] - '0');
    }
    auto C = add(A,B);
    for(int i = C.size() - 1;i >= 0;i--)
    {
        cout<<C[i];
    }
    return 0;
}

算法实现比较简单,记得最后判断t的值来确定最后还用不用进位,处理数据的部分也是要注意的点,要把高位放进下标大的位置,从平常来看就是把数字倒着放入数组里。

容器的使用相比数组的好处就是可以直接用size()成员函数来判断长度也不用担心数组固定大小带来的各种问题,string的元素减'0'可以变成数字倒是之前没见过的,之前用c做主要语言时候基本是用c_str()成员函数把string类型转成c类型的字符串(就是字符数组),然后再用atoi()函数转成数字,学到了。

减法

先上模板:

#include <iostream>
#include <string>
#include <vector>
using namespace std;

bool cmp(vector<int> &A,vector<int> &B)
{
    //当两个数字不等长的时候,肯定是长的大
    if(A.size() != B.size()) return A.size() > B.size();
    //等长的时候,比较的时候,从高位开始比较,高位大的就大
    for(int i = A.size() - 1;i >= 0;i--)
        if(A[i] != B[i])
            return A[i] > B[i];
    return true;
}

void trimZero(vector<int> &A)
{
    while (A.back() == 0 && A.size() > 1) A.pop_back();
}


//保证A的长度大于等于B的长度,且两个数字都不小于0
vector<int> sub(vector<int> &A,vector<int> &B)
{
    vector<int> C;
    int t = 0;
    for(int i = 0;i < A.size();i++)
    {
        t = A[i] - t;
        if(i < B.size()) t -= B[i];
        C.push_back((t + 10) % 10);
        if(t < 0) t = 1;
        else t = 0;
    }
    //当数字的是高位有0的时候,把0删掉
    trimZero(C);
    return C;
}

int main()
{
    vector<int> A,B,C;
    string a,b;
    cin>>a>>b;
    for(int i = a.size() - 1;i >= 0;i--)
    {
        A.push_back(a[i] - '0');
    }
    for(int j = b.size() - 1;j >= 0;j--)
    {
        B.push_back(b[j] - '0');
    }
    //如果数据合法那么去0这一步完全可以去掉
    //trimZero(A), trimZero(B);
    if(cmp(A,B))
    {
        C = sub(A,B);
    }
    else
    {
        C = sub(B,A);
        cout<<"-";
    }
    for(int i = C.size() - 1;i >= 0;i--)
    {
            cout<<C[i];
    }
    return 0;
}

主要注意的问题是,对应位数相减,如果从最高位开始有0,那么就要把0去掉,不然输出结果就会很奇怪。模板解决的只有大的减小的,且两个数都非负的情况,如果是其他情况可以通过结合加法的模板来计算,像是负数-正数可以转化成-(正数+正数),完全可以用加法模板来做。

做这个题的时候,我的模板还是没有背熟,竟然在main()函数处理数据的部分都能写错,导致出现了段错误,每次出现段错误都只能自己慢慢查找老半天,丢人了。

乘法

这里的乘法指的是大数乘以一个较小的数字,单纯来说

#include <iostream>
#include <vector>
#include <string>
using namespace std;

vector<int> mul(vector<int> &A,int b)
{
    vector<int> C;

    int t = 0;
    for(int i = 0;i < A.size();i++)
    {
        t += A[i] * b;
        C.push_back(t % 10);
        //进位
        t / = 10;
    }
    //当t不为0,说明进行到最后还有进位,要直接放入
    if(t) C.push_back(t);
    //其实如果上一步执行了,这一步基本不会执行了,这一步是为了除去从高位开始出现的0的
    while(C.size() > 1 && C.back() == 0) C.pop_back();
    return C;
}

int main()
{
    string a;
    vector<int> A,B;
    int b;
    cin >>a>>b;
    for(int i = a.size() - 1;i >= 0;i--)
    {
        A.push_back(a[i] - '0');
    }
    B = mul(A,b);
    for(int i = B.size() - 1;i >= 0;i--)
    {
        cout<<B[i];
    }
    return 0;
}

整个算法不少内容都和前两个算法的思想差不多,可以参考。

除法

一个大数除以一个较小的数字:

//A / b = C ... r,A >= 0,b > 0
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
using namespace std;

vector<int> div(vector<int> &A,int b,int &r)
{

    r = 0;
    vector<int> C;
    //除法的处理是从高位开始的所以要倒着处理
    for(int i = A.size() - 1;i >= 0;i--)
    {
        r = r * 10 + A[i];
        C.push_back(r / b);
        r %= b;
    }
    //存放的顺序颠倒了,要反转一下
    reverse(C.begin(),C.end());
    //去掉高位可能存在的0
    while(C.size() > 1 && C.back() == 0) C.pop_back();
    return C;
}

int main()
{
    vector<int> A,C;
    string a;
    int b;
    cin>>a>>b;
    for(int i = a.size() - 1;i >= 0;i--)
    {
        A.push_back(a[i] - '0');
    }
    //r用来存储余数
    int r = 0;
    C = div(A,b,r);
    for(int i = C.size() - 1;i >= 0;i--)
    {
        printf("%d",C[i]);
    }
    printf("\n");
    printf("%d",r);
    return 0;
}

具体处理,main()的部分基本和乘法的差别不大,函数部分要注意的地方就是除法是从高位开始处理的,其他的问题不是太大。

前缀和

前缀和简单的来说就是一个数列的前n项和作为一个新的数列S来存储的话,举例:S[5]就是数列的前五个项的和,此处特殊定义S[0] = 0,前缀和可以处理一些数组中的连续的数字之和这类的问题,例如要求一个数列第3到第5个数字的和,可以用前缀和表示成S[5] - S[2],特殊地,可以用S[5] - S[0]解决第1个到第5个数字和的问题。

前缀和的实现可以用递归来做,基于S[i] = S[i - 1] + a[i]:

//计算前缀和
int calcS(vector<int> &A, unsigned int n)
{
    if(n == 0) return 0;
    else return calcS(A,n - 1) + A[n - 1];
}

递归做法在处理大量数据的时候总会遇见各种超时,空间不够的问题,这个只能改写成栈的方式解决。

差分

差分可以说是前缀和的逆运算,试想一下,假设一个数列A,可以获得一个前缀和数列B;反过来说,数列B的差分数列就是数列A。

差分数列A的特点就是B[i] = A[1] + … + A[i],如果要构造的话,可以令A[1] = B[1],A[2] = B[2] - B[1],A[3] = B[3] - B[2]…但是实际应用中,构造差分数列并不重要,重要的是应用这个差分的性质来解题。

假定一种情况:对于数组B的从下标l到下标r的所有的数字都加上一个c,如果是通常暴力做法,遍历一遍数组然后挨个往上加,那么就是O(n)的时间复杂度。但是还有另外一种方法:假设数组B有一个差分数组A,那么差分数组的前n项和等于B对应下标n的元素,那么,对A[l]加上一个c,那么根据性质B[l]往后的每个数都加上了一个c,但是要使B[r + 1]开始的值不要再加c了,那么就要对A[r + 1]加上一个c来抵消掉之前加上的c带来的影响。这样只修改了两个数字,时间复杂度是O(1)。

以下是上面提出问题的代码实现:

#include <iostream>
using namespace std;
const int N = 1e6 + 10;
int n,m;
int group[N],group2[N];//group2是group的差分数列

void insert(int l,int r,int c)
{
    group2[l] += c;
    group2[r + 1] -= c;
}

int main()
{
    scanf("%d%d",&n,&m);
    for(int i = 1;i <= n;i++)
    scanf("%d",&group[i]);
    //怎么构造出差分数列呢?
    for(int i = 1;i <= n;i++)
    insert(i,i,group[i]);
    while(m--)
    {
        int l,r,c;
        scanf("%d%d%d",&l,&r,&c);
        insert(l,r,c);
    }
    //计算一遍group2的前缀和
    for(int i = 1;i <= n;i++)
    group2[i] += group2[i - 1];
    for(int i = 1;i <= n;i++)
    printf("%d ",group2[i]);
    return 0;
}

二重差分的操作类似,若要对B的子矩阵都加上一个c,实际上操作就是对差分矩阵A的对应子矩阵的四角元素进行操作,左上和右下是加,左下和右上是减。