算法基础课02:高精度加法,高精度减法,高精度乘低精度,高精度除以低精度,前缀和,差分

本文详细介绍了大整数的高精度加法、减法、乘法、除法实现,并探讨了如何利用前缀和优化区间求和问题。此外,还讲解了差分操作在数组和矩阵上的应用,以及如何通过差分数组快速更新数组元素。这些技术在处理大规模数据和提高算法效率方面具有重要意义。
摘要由CSDN通过智能技术生成

1.高精度加法

讨论的是两个大整数相加:A+B
A,B的位数大概在10^6左右

  • 例题:
    在这里插入图片描述
  • 代码:
#include<iostream>
#include<vector>
using namespace std;

//C=A+B
vector<int> add(vector<int> &A,vector<int> &B)//加&是为了提高效率,不加就会把整个数组拷贝一遍
{
    vector<int> C;
    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];
        
        C.push_back(t%10);
        t/=10;
    }
    
    if(t) C.push_back(1);//判断最高位是否进位,如果进位了,就需要在vector容器后再加一位数
    
    return C;
}

int main()
{
    string a,b;//用字符串将数字读进来
    vector<int>A,B;//将数字存入在向量中
    
    cin>>a>>b;
    
    //按低位到高位来存储,A[0]存储的个位
    for(int i=a.size()-1;i>=0;i--) A.push_back(a[i]-'0'); 
    for(int i=b.size()-1;i>=0;i--) B.push_back(b[i]-'0'); 
    
    auto C = add(A,B);//auto 让编译器自己推断C的类型
    for(int i=C.size()-1;i>=0;i--) cout<<C[i];
    
    
    return 0;
}

在这里我们是把大整数用字符串的形式读入,然后存入vector中,但是我们是按低位到高位的顺序存入的,这样的话,当我们高位有进位的时候,就可以很方便在vector后加一位数字。如果按高位到低位的方式存入,当高位有进位时,在vector的头部是无法再往前添加一位数字的。

2.高精度减法

讨论的是两个大整数相减:A-B
A,B的位数大概在10^6左右

  • 例题:
    在这里插入图片描述
  • 代码:
#include<iostream>
using namespace std;
#include<vector>

//判断是否有A>=B
bool cmp(vector<int>&A,vector<int>&B)
{
    //A,B位数不相等
    if(A.size()!=B.size()) return A.size() > B.size();
    
    //A,B位数相等,则逐位比大小(从最高位开始比)
    for(int i=A.size()-1;i>=0;i--)
    {
        if(A[i]!=B[i])
            return A[i]>B[i];
    }
    
    //每位都相同
    return true;
}

//C=A-B
vector<int> sub(vector<int>&A,vector<int>&B)
{
    vector<int> C;
    for(int i=0,t=0;i<A.size();i++)
    {
        t=A[i]-t;
        
        //判断B的位数是否存在,因为可能存在len(B)<len(A)
        if(i<B.size()) t=t-B[i];
        
        //①当t=A[i]-B[i]-t>=0时,此时就是t;
        //②当t=A[i]-B[i]-t<0时,即t<0,需要借位,此时t=10+t;
        //两种情况可以一起表示成(t+10)%10;
        C.push_back((t+10)%10);
        
        if(t<0) t=1;//不够减,需要借位
        else t=0;
    }
    
    //去掉前导0
    //例:123-120为003,但我们需要输出的是3,所以需要去掉前面的0
    while(C.size()>1 && C.back()==0) C.pop_back();
    
    return C;
}


int main()
{
    string a,b;
    vector<int> A,B;
    
    cin>>a>>b;
    
    for(int i=a.size()-1;i>=0;i--) A.push_back(a[i]-'0');
    for(int i=b.size()-1;i>=0;i--) B.push_back(b[i]-'0');
    
    if(cmp(A,B))//如果A>B
    {
        auto C = sub(A,B);
        for(int i=C.size()-1;i>=0;i--) cout<<C[i];
    }
    else//如果A<B
    {
        auto C = sub(B,A);
        cout<<'-';
        for(int i=C.size()-1;i>=0;i--) cout<<C[i];
    }
}

3.高精度乘低精度

讨论的是一个大整数乘上一个比较小的数:A * a
A的位数len(A)小于10^6:len(A)≤10 ^6
a的数值(大小)小于等于10^9:a≤10 ^9

  • 例题:
    在这里插入图片描述
  • 代码:
#include<iostream>
using namespace std;
#include<vector>

//C=A*b 把b当作一个整体来乘
vector<int> mul(vector<int>&A,int b)
{
    vector<int>C;
    
    int t=0;//进位
    for(int i=0;i<A.size()|| t;i++)//如果i还没循环完或进位t还没处理完,就继续做
    {
        if(i<A.size()) t+=A[i]*b;
        C.push_back(t%10);//把个位取出来
        t/=10;//取出进位
    }
    
    //前导为0
    while(C.size()>1 && C.back()==0 ) C.pop_back();
    return C;
}

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

4.高精度除以低精度

讨论的是一个大整数除以一个比较小的数:A /a,我们需要求一下商和余数。
A的位数len(A)小于10^6:len(A)≤10 ^6
a的数值(大小)小于等于10^9:a≤10 ^9

  • 例题:
    在这里插入图片描述

  • 代码

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

// A/b:商是C,余数是r
vector<int> div(vector<int>&A,int b,int &r)//是用引用将余数r传回
{
    vector<int>C;//商
    r=0;
    for(int i=A.size()-1;i>=0;i--)//除法从最高位开始计算
    {
        r=r*10+A[i];
        C.push_back(r/b);//商是r/b
        r%=b;//商后的余数
    }
    
    //C里面是从高位到低位存储的,所以顺序需要颠倒下
    reverse(C.begin(),C.end());
    
    //商里面可能存在前导为0的情况
    while(C.size()>1 && C.back()==0) C.pop_back();
    
    return C;
}

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

5.前缀和

5.1 一维前缀和

一个数组有n个元素:a1,a2,…an;
前缀和Si=a1+a2+…+ai (前i个数的和);
前缀和里面下标一定要从1开始。
①如何求Si?
for(int i=1;i<n;i++ ) S[i]=S[i-1]+a[i];
(S0要定义成0,是为了处理边界问题)

②作用:
为了快速求出原数组里面一段数【l,r】的和:Sr-Sl-1;
例如:我们求[1,10]这个区间的和,就是S10-S0,其实就是等于S10,但是为了把他写成相减的形式,我们需要用到S0,所以我们需要定义S0=0;
即定义S0=0是为了:当求区间[1,x]的和时,可以写成Sx-S0相减的形式,这样就少了一次if判断。

具体做法

首先做一个预处理,定义一个sum[]数组,sum[i]代表a数组中前i个数的和。

求前缀和运算

const int N=1e5+10;
int S[N],a[N]; //S[i]=a[1]+a[2]+a[3].....a[i];
for(int i=1;i<=n;i++)
{ 
   S[i]=S[i-1]+a[i];   
}

然后查询操作

 cin>>l>>r;
 cout<<S[r]-S[l-1]<<endl;

对于每次查询,只需执行S[r]-S[l-1] ,时间复杂度为O(1)

原理:

S[r] =a[1]+a[2]+a[3]+a[l-1]+a[l]+a[l+1]......a[r];
S[l-1]=a[1]+a[2]+a[3]+a[l-1];
S[r]-S[l-1]=a[l]+a[l+1]+......+a[r];

图解:
在这里插入图片描述

这样,对于每个询问,只需要执行 sum[r]-sum[l-1]。输出原序列中从第l个数到第r个数的和的时间复杂度变成了O(1)。

我们把它叫做一维前缀和

总结:
在这里插入图片描述

代码:

#include<iostream>
using namespace std;

const int N=1e5+10;

int n,m,a[N],S[N];

int main()
{
    ios::sync_with_stdio(false);
    //原理:让cin和标准输入输出不同步
    //作用:提高cin读取速度
    //副作用:不能再使用scanf
    
    cin>>n>>m; 
    for(int i=1;i<=n;i++) cin>>a[i];
    
    for(int i=1;i<=n;i++) S[i]=S[i-1]+a[i];//前缀和初始化
    
    while(m--)
    {
        int l,r;
        cin>>l>>r;
        cout<<S[r]-S[l-1]<<endl;//区间和计算
    }
    
    return 0;
}


5.2 二维前缀和在这里插入图片描述

在这里插入图片描述
例题:
在这里插入图片描述

代码:

#include<iostream>
using namespace std;

const int N=1e3+10;

int n,m,q,a[N][N],s[N][N];

int main()
{
    cin>>n>>m>>q;
    
    //一维前缀和下标从1开始
    //同理,二维前缀和下标从(1,1)开始
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
            cin>>a[i][j];
            
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
            s[i][j]=s[i][j-1]+s[i-1][j]-s[i-1][j-1]+a[i][j];//求前缀和
            
    while(q--)
    {
        int x1,y1,x2,y2;
        cin>>x1>>y1>>x2>>y2;
        cout<<s[x2][y2]-s[x1-1][y2]-s[x2][y1-1]+s[x1-1][y1-1]<<endl;//算子矩阵的和
        
    }
    
    return 0;
}

6.差分(前缀和的逆运算)

6.1 一维差分

在这里插入图片描述
在这里插入图片描述

差分就是就是构造一个差分数组,使得a[]是b[]的差分。

只要有b[],我们就可以用O(n)的时间得到a[],对b[]求前缀和即可。
前缀和主要是帮我们解决下面这种操作:
我们有这样一堆操作,在a[]给定的区间[l,r]上,将每个数都+c,即:al + c,al+1 + c,…,ar + c。
我们有很多这样的操作,如果用暴力的方法来做循环就需要O(n),但是如果用差分来做就可做到O(1)。
这些操作完成后,如果我们想求原来的a[],只需要将b[]求前缀和即可。

假设我们要对a[]的区间[l,r]上所有数都+c,那我们对a[]的差分数组b[]如何操作才能达到这个效果呢?
由于a[]是B[]的前缀和,所以我们需要让bl + c,这样算al 的时候是从b1加到bl,bl加上了c,就相当于al加上了c,一直到al+1,al+2,…,an全部会自动加上c。
但是我们只需要区间[l,r]的数加上c,[r+1]后面的数不能加上c,所以我们要让br+1 - c

在这里插入图片描述

所以我们想让a[]里的某个区间的数全+c,我们只需要在b[]里面修改两个数就可以了,即bl + c , br+1 - c。本来我们循环一次需要O(n)的复杂度,我们利用差分就将复杂度降到了O(1)。
这样差分的作用就有了,即用O(1)的复杂度给原数组的某个区间内的所有数加上一个固定值

例题:
在这里插入图片描述

代码1(代码可读性比较好):

//差分 时间复杂度 o(m)
#include<iostream>
using namespace std;
const int N = 1e5 + 10;
int a[N], b[N];
int main()
{
    int n, m;
    cin>>n>>m;
    for (int i = 1; i <= n; i++)
    {
       cin>>a[i];
       b[i] = a[i] - a[i - 1];      //构建差分数组
    }
    int l, r, c;
    while (m--)
    {
        cin>>l>>r>>c;
        b[l] += c;     //将序列中[l, r]之间的每个数都加上c
        b[r + 1] -= c;
    }
    for (int i = 1; i <= n; i++)
    {
        a[i] = b[i] + a[i - 1];    //前缀和运算
        cout<<a[i]<<' ';
    }
    return 0;
}


我觉得这位大佬讲的很通俗易懂!!!!文章链接

代码2(y总代码):

#include <iostream>

using namespace std;

const int N = 100010;
int n, m;
int a[N], b[N];

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

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i++) scanf("%d", &a[i]);

    for (int i = 1; i <= n; i++) insert(i, i, a[i]);

    while (m--) {
        int l, r, c;
        scanf("%d%d%d", &l, &r, &c);
        insert(l, r, c);
    }  

    for (int i = 1; i <= n; i++) b[i] += b[i - 1];
    for (int i = 1; i <= n; i++) printf("%d ", b[i]);

    return 0;

}

y总的代码我没理解为什么要使用在[i,i]之间插入a[i],没理解为什么说不需要构造,直接进行插入操作?
其实关于查分那一块,整体逻辑是这样的,我以一维数组的差分为例:
因为在代码中a和b两个数组都是全局变量,全局变量有一个特性就是在定义时编译器会自动将数组中的元素全部初始化为0。而a和b两个等长的0数组是满足差分关系的,也就是满足b是a的差分数组,a是b的前缀和数组。
也就是将a和b均为0数组的状态看做是初始状态,但是我们的a数组的实际初始状态并不是全0的怎么办呢?y总的代码中是将a的实际初始状态看做是在全0的初始状态下对a进行插入而得到,此时在a插入的同时对b矩阵的状态进行更新,得到实际a的初始状态下对应的b矩阵。
接下来就是题目要求的对a矩阵的一个子矩阵进行插入操作,在这里我们不直接对a进行更新,而是对b矩阵进行跟新,最终根据得到的b矩阵来反求出a矩阵。
整个过程大概就是这样。

y总说,可以这样理解:你可以理解成a数组本来是直接赋值,然后求其差分数组b。后来变成了先赋值成0,然后求差分数组b,然后再插入相应的值,这两种方式得到的差分数组是相同的。

6.2 二维差分

如果扩展到二维,我们需要让二维数组被选中的子矩阵中的每个元素的值加上c,是否也可以达到O(1)的时间复杂度。答案是可以的,考虑二维差分。

a[][]数组是b[][]数组的前缀和数组,那么b[][]是a[][]的差分数组

原数组: a[i][j]

我们去构造差分数组: b[i][j]

使得a数组中a[i][j]是b数组左上角(1,1)到右下角(i,j)所包围矩形元素的和。

如何构造b数组呢?

我们去逆向思考

同一维差分,我们构造二维差分数组目的是为了 让原二维数组a中所选中子矩阵中的每一个元素加上c的操作,可以由O(n*n)的时间复杂度优化成O(1)

已知原数组a中被选中的子矩阵为 以(x1,y1)为左上角,以(x2,y2)为右上角所围成的矩形区域;

始终要记得,a数组是b数组的前缀和数组,比如对b数组的b[i][j]的修改,会影响到a数组中从a[i][j]及往后的每一个数。

假定我们已经构造好了b数组,类比一维差分,我们执行以下操作
来使被选中的子矩阵中的每个元素的值加上c

b[x1][y1] + = c;

b[x1,][y2+1] - = c;

b[x2+1][y1] - = c;

b[x2+1][y2+1] + = c;

每次对b数组执行以上操作,等价于

for(int i=x1;i<=x2;i++)
  for(int j=y1;j<=y2;j++)
    a[i][j]+=c;

我们画个图去理解一下这个过程:
在这里插入图片描述
b[x1][ y1 ] +=c ; 对应图1 ,让整个a数组中蓝色矩形面积的元素都加上了c。
b[x1,][y2+1]-=c ; 对应图2 ,让整个a数组中绿色矩形面积的元素再减去c,使其内元素不发生改变。
b[x2+1][y1]- =c ; 对应图3 ,让整个a数组中紫色矩形面积的元素再减去c,使其内元素不发生改变。
b[x2+1][y2+1]+=c; 对应图4,让整个a数组中红色矩形面积的元素再加上c,红色内的相当于被减了两次,再加上一次c,才能使其恢复。

在这里插入图片描述
我们将上述操作封装成一个插入函数:

void insert(int x1,int y1,int x2,int y2,int c)
{     //对b数组执行插入操作,等价于对a数组中的(x1,y1)到(x2,y2)之间的元素都加上了c
    b[x1][y1]+=c;
    b[x2+1][y1]-=c;
    b[x1][y2+1]-=c;
    b[x2+1][y2+1]+=c;
}

我们可以先假想a数组为空,那么b数组一开始也为空,但是实际上a数组并不为空,因此我们每次让以(i,j)为左上角到以(i,j)为右上角面积内元素(其实就是一个小方格的面积)去插入 c=a[i][j],等价于原数组a中(i,j) 到(i,j)范围内 加上了 a[i][j] ,因此执行n*m次插入操作,就成功构建了差分b数组.
这叫做曲线救国。

 for(int i=1;i<=n;i++)
  {
      for(int j=1;j<=m;j++)
      {
          insert(i,j,i,j,a[i][j]);    //构建差分数组
      }
  }

总结:
在这里插入图片描述

例题:
在这里插入图片描述
代码:

#include<iostream>
using namespace std;
const int N = 1e3 + 10;
int a[N][N], b[N][N];
void insert(int x1, int y1, int x2, int y2, int c)
{
    b[x1][y1] += c;
    b[x2 + 1][y1] -= c;
    b[x1][y2 + 1] -= c;
    b[x2 + 1][y2 + 1] += c;
}
int main()
{
    int n, m, q;
    cin >> n >> m >> q;
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= m; j++)
            cin >> a[i][j];
            
    //假设a[][]初始全为0
    for (int i = 1; i <= n; i++)
    {
        for (int j = 1; j <= m; j++)
        {
            insert(i, j, i, j, a[i][j]);      //构建差分数组
        }
    }
    while (q--)
    {
        int x1, y1, x2, y2, c;
        cin >> x1 >> y1 >> x2 >> y2 >> c;
        insert(x1, y1, x2, y2, c);
    }
    for (int i = 1; i <= n; i++)
    {
        for (int j = 1; j <= m; j++)
        {
             //将二维前缀和a[i][j]计算出来存在b[][]中
            b[i][j]=b[i - 1][j] + b[i][j - 1] - b[i - 1][j - 1]+b[i][j]; 
            cout<<b[i][j]<<' ';
        }
        cout<<endl;
    }
    
    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值