AcWing 算法基础课第二节基础算法2高精度、前缀和与差分

1、该系列为ACWing中算法基础课,已购买正版,课程作者为yxc
2、y总培训真的是业界良心,大家有时间可以报一下
3、为啥写在这儿,问就是oneNote的内存不够了QAQ


本节内容:高精度、前缀和与差分

一、高精度

java和python 有大整数类,可以不需要学习高精度。

1.1 应用场景

  • 高精度的考试方式有4种:

    • 1、比较大的整数相加A+BA/B 的位数在1e6
    • 2、比较大的整数相减A-BA/B 的位数在1e6
    • 3、大整数乘以小整数A*aA 的位数小于等于1e6,a的数值小于等于1e9
    • 4、大整数除以小整数A/a
  • 高精度整数以数组的形式存储,个位存在前面,高位存在后面,方便做加法和乘法的时候进位。

1.2 高精度加法

  • 高精度加法模拟的是手动相加的顺序。
  • 举例:1≤整数长度≤100000,给定两个正整数(不含前导 0),计算它们的和。
// 高精度加法
// C = A + B, A >= 0, B >= 0
#include <iostream>
#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;
    for (int i = 0; i < A.size(); i ++ )
    {
        t += A[i];
        if (i < B.size()) t += B[i];
        C.push_back(t % 10);
        t /= 10;
    }
    
    if (t) C.push_back(t); // 最高位还有进位
    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');
    
    auto c = add(A, B);
    for (int i = c.size()-1; i >=0; i -- ) printf("%d", c[i]);
    
    return 0;
}

1.3 高精度减法

  • 加减法存储方式是相同的。
  • 流程:
    • 如果A<B,计算-(B-A),可以保证计算时最高位不用借位。
    • 判断是否需要借位:
      在这里插入图片描述
// 高精度减法
// C = A - B, 满足A >= B, A >= 0, B >= 0
#include <iostream>
#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; // A与B相同
}

vector<int> sub(vector<int> &A, vector<int> &B)  // C = A - B, 满足A >= B, A >= 0, B >= 0
{
    vector<int> C;
    for (int i = 0, t = 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(例如123-120 = 003)
    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 -- ) printf("%d", c[i]);
    }
    else
    {
        auto c  = sub(B, A);
        printf("-");
        for (int i = c.size()-1; i >=0; i -- ) printf("%d", c[i]);
    }

    return 0;
}

1.3 高精度乘法

  • 高精度乘法时的当前位数和进位
    在这里插入图片描述
    在这里插入图片描述

  • 举例:123 x 12,这里的乘法把B 视为一个整体;
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    因此答案为1476;

  • 代码:

// 高精度乘低精度
// C = A * b, A >= 0, b > 0
#include <iostream>
#include <vector>

using namespace std;

vector<int> mul(vector<int> &A, int b)
{
    vector<int> C;
    int t = 0;
    
    // 要么i没有循环完,要么t不为0 (||t)(进位没有处理完).
    for (int i = 0; i < A.size() || t; i ++ )
    {
        if (i < A.size()) t += A[i] * b;
        C.push_back(t % 10);
        t /= 10;
    }
    return C;
}

int main()
{
    string a;
    int b;
    cin >> a >> b;
    
    if (b==0) 
    {
        cout << '0' << endl;
        return 0;
    }
    
    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--) printf("%d", C[i]);
    
    return 0;
}

  • 注意:相乘的循环条件,要么i没有循环完,要么t不为0(进位没有处理完)

1.4 高精度除法

  • 假设是A3A2A1A0除以b
    在这里插入图片描述
  • 除法:一般正着存数组会好算。
// 高精度除以低精度
// A / b = C ... r, A >= 0, b > 0

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

vector<int> div(vector<int> &A, int b, int &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;
    }
    // 翻转前C0是最高位,C1是最低位
    reverse(C.begin(), C.end());
    
    // 去前导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');
    
    int r;
    auto C = div(A, b, r);
    
    for (int i=C.size()-1; i>=0; i--) printf("%d", C[i]);
    cout << endl;
    cout << r << endl;
    
    return 0;
}

1.5 高精度x高精度

高精度 X 高精度
#include <iostream>
#include <vector>

using namespace std;

vector<int> mul(vector<int> &A, vector<int> &B) {
    vector<int> C(A.size() + B.size() + 7, 0); // 初始化为 0,C的size可以大一点

    for (int i = 0; i < A.size(); i++)
        for (int j = 0; j < B.size(); j++)
            C[i + j] += A[i] * B[j];

    int t = 0;
    for (int i = 0; i < C.size(); i++) { // i = C.size() - 1时 t 一定小于 10
        t += C[i];
        C[i] = t % 10;
        t /= 10;
    }

    while (C.size() > 1 && C.back() == 0) C.pop_back(); // 必须要去前导 0,因为最高位很可能是 0
    return C;
}

int main() {
    string a, b;
    cin >> a >> b; // a = "1222323", b = "2323423423"

    vector<int> 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');

    auto C = mul(A, B);

    for (int i = C.size() - 1; i >= 0; i--)
        cout << C[i];

    return 0;
}

二、前缀和与差分

前缀和与差分是一对逆运算。

2.1 前缀和

2.1.1 一维前缀和

  • 如果有一个数组a1...an,其前缀和数组si定义为si = a1+ a2+ a3+...+ai,注意前缀和的下标从1开始,s0 = 0
  • 如何求si ?从前往后for循环一遍。for (i=1; i<=n; i++) { s[i] = s[i-1] + a[i] }
  • si的作用:快速的求出原数组中一段数的和,如求出a[l]a[r]的和,只需计算s[r] - s[l-1]
  • s0 = 0可以处理边界,例如计算[1,10]的前缀和,是s10 - s0 = s10,可以用相减的方式来表示,不需要特判。
 一维前缀和
S[i] = a[1] + a[2] + ... a[i]
a[l] + ... + a[r] = S[r] - S[l - 1]
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 100010;

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

int main()
{
	// ios::sync_with_stdio(false);
	
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i ++ ) scanf("%d", &a[i]);
    
    // 前缀和的初始化
    for (int i = 1; i <= n; i ++ ) s[i] = s[i-1] + a[i]; 
    
    while (m -- )
    {
        int l, r;
        scanf("%d%d", &l, &r);
        printf("%d\n", s[r] - s[l-1]); // 区间和的计算
    }
    
    return 0;
}
  • ios::sync_with_stdio(false);cin和标准输入输出不同步,提高cin的读取速度,但是会干扰文件操作,且不能再使用scanf
  • 数据规模大于100,0000建议用scanf,否则用cin

2.1.2 二维前缀和

  • 一维前缀和,可以快速的求出区间的和;二维前缀和,可以快速的求出子矩阵的和;
  • sij表示左上角所有区域的和;
    在这里插入图片描述
  • 求出子矩阵的和:s(x2, y2 )- s(x2, y1-1) - s(x1-1, y2) + s(x1-1, y1-1)
    在这里插入图片描述
  • 计算二维前缀和矩阵元素sij(已知其左上角的前缀和):for (i: 1~n ) { for (j: 1~m) { sij = s(i-1, j) + s(i, j-1) - s(i-1, j-1) + aij }}(红色加绿色面积减去重叠的面积加上aij元素面积)
二维前缀和
S[i, j] = 第i行j列格子左上部分所有元素的和
以(x1, y1)为左上角,(x2, y2)为右下角的子矩阵的和为 S[x2, y2] - S[x1 - 1, y2] - S[x2, y1 - 1] + S[x1 - 1, y1 - 1]
#include <iostream>
#include <cstring>
#include <algorithm>

const int N = 1010;
int n, m, q;

int a[N][N], s[N][N];

int main()
{
    scanf("%d%d%d", &n, &m, &q);
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= m; j ++ )
            scanf("%d", &a[i][j]);
            
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= m; j ++ )
            s[i][j] = s[i-1][j] + s[i][j-1] - s[i-1][j-1] + a[i][j]; // 求前缀和
    
    while(q --)
    {
        int x1, y1, x2, y2;
        scanf("%d%d%d%d", &x1, &y1, &x2, &y2);
        
        printf("%d\n", s[x2][y2] - s[x1-1][y2] - s[x2][y1-1] + s[x1-1][y1-1]); // 算子矩阵的和
    }
    
    
    return 0;
}

2.2 差分

2.2.1 一维差分

  • 差分是前缀和的逆运算;
  • 假设有数组a1,a2,...,an要构造b1, b2,...,bn,使得ai = b1+b2+...+bi,则b称为a的差分(b2+a1 = a2),a称为b的前缀和。(同理的表述:b1 = a1,b2=a2-a1,b3=a3-a2,…,bn = an-a(n-1)
  • 差分的用处:
    • 1、只要有b数组,就可以用O(n)的时间得到数组a;
    • 2、利用差分在O(1)的时间内,让前缀和A数组[l,r]范围内的数都加c( 假设给定一个数组 A ,其差分数组为 B, 如果对A数组的某个区间 [l, r]上每个数都加一个数c, 其等价于 B 数组中 B[l] += c,且 B[r + 1] -=c。因为A[l]表示B[l]的前缀和,则如果B[l]多加一个cB[l] += c),则A[l], A[l+1], …, A[r], A[r + 1], …, A[n] 都将多加一个c。而我们只需要 [l, r] 上加c,所以对于 A[r+1, n]区间上的值再减去 c,即对应于 B[r + 1] -= c)
    • 其实可以直接按照原始的方法,依次遍历前缀和A数组的 [l, r] 区间,并执行 A[i] += c,但每次的操作的时间复杂度都是O(n)。使用insert差分方法,则可以将每次操作转换为一个公式即可,即时间复杂度变为 O(1)。修改前缀和的某个连续区间范围内的值,可以转化为只需要修改其差分数组的某两个值。 差分能够优化的方式在于,不用每次操作都进行循环,可完成每轮操作修改其差分数组的两个值后,统一进行循环。
      在这里插入图片描述
  • 3、当其前缀和数组为[a1, a2, ... , an]时,想要得到差分数组b的值,可以通过模拟在n位全零数组B[0, 0, ... ,0]B[i, i]中插入a[i]得到,即insert(i, i, a[i]);,且进行了n次插入操作。例如:前缀和数组为{1 2 2 1 2 1}经过插入得到差分数组{1 1 0 -1 1 -1 }。同时该insert操作可以让前缀和数组a的某一段+c,并让两种功能都通过同一个函数实现。
 B[i] = a[i] - a[i - 1]   // (i>1)
 给区间[l, r]中的每个数加上c:B[l] += c, B[r + 1] -= c (a[r]之后的不能加上c)
  • 举例:输入一个长度为 n 的整数序列。接下来输入 m 个操作,每个操作包含三个整数 l,r,c,表示将序列中 [l,r] 之间的每个数加上 c。请你输出进行完所有操作后的序列。
#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);
    // 前缀和和差分数组下标都是从1开始,为了方便
    for (int i = 1; i <= n; i ++ ) scanf("%d", &a[i]);
    
    // 这里的insert求的是a的差分数组,刚开始差分数组b的初始化全为0
    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;
}

2.2.2 二维差分

  • 给差分矩阵的子矩阵都+c:

    • b(x1, y1)+c的效果是,这个点右下角所有值都+c
      在这里插入图片描述

    • b(x1, y2+1)-c的效果是,这个点右边所有值都-c
      在这里插入图片描述

    • b(x2+1, y1)-c的效果是,这个点下面所有值都-c
      在这里插入图片描述
      在这里插入图片描述

    • 因此可以转化为:
      在这里插入图片描述

  • 因此前缀和矩阵的a(i,j)可以想象为给差分矩阵的1x1大小的子矩阵进行插入操作得到的。

给以(x1, y1)为左上角,(x2, y2)为右下角的子矩阵中的所有元素加上c:
S[x1, y1] += c, S[x2 + 1, y1] -= c, S[x1, y2 + 1] -= c, S[x2 + 1, y2 + 1] += c
  • 举例:输入一个 nm 列的整数矩阵,再输入 q 个操作,每个操作包含五个整数 x1,y1,x2,y2,c,其中 (x1,y1)(x2,y2) 表示一个子矩阵的左上角坐标和右下角坐标。每个操作都要将选中的子矩阵中的每个元素的值加上 c。请你将进行完所有操作后的矩阵输出。
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 1010;
int n, m, q;
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()
{
    scanf("%d%d%d", &n, &m, &q);
    
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= m; j ++ )
            scanf("%d", &a[i][j]);
    
    // 得到原来的差分数组
    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;
        scanf("%d%d%d%d%d", &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 ++ )
            b[i][j] += b[i-1][j] + b[i][j-1] - b[i-1][j-1];
            
    for (int i = 1; i <= n; i ++ )
    {
        for (int j = 1; j <= m; j ++ )
            printf("%d ", b[i][j]);
        puts("");
    }
        
    return 0;
}
  • 代码中a和b两个数组都是全局变量,全局变量有一个特性就是在定义时编译器会自动将数组中的元素全部初始化为0。而a和b两个等长的0数组是满足差分关系的,也就是满足b是a的差分数组,a是b的前缀和数组
  • 代码中是将a的实际初始状态看做是在全0的初始状态下对a进行插入而得到,此时在a插入的同时对b矩阵的状态进行更新,得到实际a的初始状态下对应的b矩阵。
  • 接下来就是题目要求的对a矩阵的一个子矩阵进行插入操作,在这里我们不直接对a进行更新,而是对b矩阵进行跟新,最终根据得到的b矩阵来反求出a矩阵。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: acwing算法基础课是一门针对算法学习的在线课程,在这门课程中,学生可以系统地学习和掌握算法基础知识,提高编程水平。为了方便学生学习,acwing提供了网盘服务。 acwing算法基础课网盘是一个用于存储课程资源的平台。通过这个网盘,学生可以下载课程讲义、代码模板以及补充材料等。这些资源都经过精心整理,供学生们参考和学习。 网盘中的资源是按照课程章节进行分类的,学生可以根据自己的学习需要,选择性地下载所需的资料。同时,网盘还提供了搜索功能,方便学生快速定位和获取所需资料。 acwing算法基础课网盘的使用对于学生们的学习非常有帮助。通过下载和学习这些资源,学生们可以更好地理解课程内容,加深对算法的理解。此外,学生们还可以通过研究代码模板,学习优秀的编程思想和技巧,提高自己的编程能力。 总之,acwing算法基础课网盘是一项非常便利和实用的服务,为学生们提供了更加全面和深入的学习资源,帮助他们更好地掌握和运用算法知识。 ### 回答2: acwing算法基础课是一门优质的算法学习资源,其中的课程内容丰富多样,涵盖了算法基础知识、数据结构、动态规划、图论等等。很多学习者都认为这门课程对他们的算法学习有很大的帮助。 网盘是指以网络为媒介,提供文件存储和下载服务的云存储平台。acwing算法基础课也提供了网盘服务,方便学习者下载课程资料并进行学习。 通过acwing算法基础课网盘,学习者可以方便地获取到课程的各种学习资料,包括讲义、习题集、代码示例等。这些资料可以帮助学习者更好地理解和掌握课程的内容。此外,网盘还提供了上传和分享功能,学习者可以将自己的学习心得、代码等资料分享给其他学习者,促进学习者之间的互相学习和交流。 acwing算法基础课网盘的优点不仅仅是方便快捷的下载和分享功能,还包括安全可靠的存储环境。学习者可以放心地将自己的学习资料上传到网盘进行备份,减少数据丢失的风险。同时,网盘还提供了多种存储空间容量的选择,满足学习者不同的需求。 总的来说,acwing算法基础课网盘为学习者提供了方便、安全和多样化的学习资源下载和分享服务,为学习者的算法学习和进步提供了有力的支持。如果你对算法感兴趣,我推荐你去尝试一下这门精彩的课程!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值