算法基础1.2:高精度,前缀和,差分

高精度运算:

只有C++需要高精度的学习,JAVA和python则不需要,因为JAVA是大整数类,没有位数要求,python默认数的范围就是无限大,只有C++没有大整数类。

高精度主要服务于比较大的整数加减乘除,一般来说高精度考四种。

第一种的话是两个比较大的整数相加(a和b的位数大概是10的六次方)。

第二种是两个比较大的整数相减(a和b的位数大概是10的六次方)。

第三种是一个大整数乘上一个小的整数。(大整数的长度小于10的6次方,小的整数小于等于10的9次方,是特别长的一个数。

第四种是a÷b求商和余数是多少,

比如求方案数,n个苹果,选出来若干个。问有多少种选法,答案是二的N次方,当n是1000的时候,一万的时候,那么它总共的位数肯定是3000位,就很长了。

第一个问题:大整数的存储

大整数,在C++里面代码里面是如何表示的?首先会爆掉int变量,用int变量肯定是存不下来的,大多数时候其实是把它的每一位存到数组里面去,,比方说一个大整数是123456789,一共是九位的一个整数,我们会把它的每一位存到数组里面去,存的时候就有一个问题,我们应该是高位在前呢,还是低位在前呢?假设我们有一个数组的下标是从012345678,第0位应该存一还是存九呢?其实第0位应该存九比较好。数组里边分别存的987654321,为什么把个位存到这个数组的第一个数,然后把最高位存到最后一个数呢?主要是因为两个整数相加,乘法或除法的时候,可能会有进位,就需要在高位上补上一个数,数组的话就在最后一个数,在数组的末尾补上一个数是最容易的,在数组的开头,如果想补上一个数,那就需要把整个数组全部往后平移一位啊,比较麻烦。但是如果往末尾添加一个数字的话,就很好加了,就直接往后push back 1个数就可以了,因此我们存的时候,个位是数组中第零个下标是零的,这个位置存个位,下标是一的,这个位置存十位,以此类推。和我们一般的写法是反着的,我们一般写的话会先写高位,但是在数组中会先写低位。加减乘除的大整数存储都一样。主要是因为题目里面最终答案可能是很大的,中间把答案算出来,这个过程可能会包含加减乘除。必须要这几个运算要同时进行,所以整数存储的格式就一致

高精度1.加法

高精度的加法是一个模拟人工加法的过程,与小学加法相同。用代码去模拟相加进位这个过程,假设已有A和B数组,那么A数组的A0是个位,A1是十位,以此类推到A3,B组比方只有三B2B1B0,B0存的是个位。然后我们A和B相加,首先算a0加上b0是多少,如果大于十的话,就进一位,如果小于十的话,就不进位。然后新的个位就是a+b.模十,比方说写成c0,二位再算的时候就是a1加上b1,再加上进位,算一下是不是大于十,如果大于十的话,就进位,如果不大于十的话,就不进位,新十位就是三个数的和模10,求出c1,以此类推。算每一位的时候,实际上是三个数相加,就是Ai+Bi,再加上上一位的进位,比方叫做t,上一位的进位是t,上一位如果没有进位的话t就是零啊,如果有进位的话t就是一。最开始的也可以让t=0,相当于进了一个零。每一位都是这样,如果Ai或者Bi不存在,那么我们就把它看成零。

模板题:

给定两个正整数(不含前导 0),计算它们的和。

输入格式

共两行,每行包含一个整数。

输出格式

共一行,包含所求的和。

数据范围

1≤整数长度≤100000

输入样例:
12
23
输出样例:
35

#include <iostream>
#include <vector>    //vector 是变长数组,支持随机访问,不支持在任意位置 O(1)插入。为了保证效率,元素的增删一般应该在末尾进行。
using namespace std;

vector<int> add(vector<int> &A, vector<int> &B)    //加上引用的目的是为了提高效率,如果不加引用就会把整个数组copy 1遍,如果加上引用就不会拷贝整个数组。
{
    if (A.size() < B.size()) return add(B, A);    //A的size一定大于B的size

    vector<int> C;     //C=A+B
    int t = 0;     //定义进位
    for (int i = 0; i < A.size(); i ++ )
    {
        t += A[i];
        if (i < B.size()) t += B[i];   //A[i]+B[i]+t
        C.push_back(t % 10);
        t /= 10;  //t大于10进位,否则不进位
    }

    if (t) C.push_back(t);   //如果最高位还有进位,就在C的最后加上
    return C;
}

int main()
{
    string a, b;     //用字符串读入
    vector<int> A, B;   //vector<int> a; 相当于一个长度动态变化的 int 数组,vector<int> b[233]; 相                                                                 当于第一维长 233,第二位长度动态变化的 int 数组
    cin >> a >> b;    //a='123456'
    for (int i = a.size() - 1; i >= 0; i -- ) A.push_back(a[i] - '0');   //第一步大整数存储,把每一位抠              出来放到vector中去,a.push_back(x) 把元素 x 插入到 vector a 的尾部,A=[6,5,4,3,2,1]
    for (int i = b.size() - 1; i >= 0; i -- ) B.push_back(b[i] - '0');//所有的容器都可以视作一个“前闭后开”的结构,因此逆序输出,size要减一。数组中存储的是字符类型的数字, 要变成真实整数的值需要                                                                          减去一 个字符0

    auto C = add(A, B);     //auto意为让计算机自己判断变量类型,避免过长类型的重复输入

    for (int i = C.size() - 1; i >= 0; i -- ) cout << C[i];     //输出的时候也要倒着输出,输出的时候需要先输出最高位,再输出次高位,这是人类的一个习惯。最高位是最后一位。

    cout << endl;

    return 0;
}

作者:yxc
链接:https://www.acwing.com/activity/content/code/content/39792/
来源:AcWing

高精度2.减法

一样模拟人工减法,假设A2A1A0和B2B1B0两组都三个数,首先第一个要算的是a0-b0,如果不够减,就跟前面借一位,然后加上一个十,分两种情况。每次算ai-bi,然后分两种情况,如果够减,大于等于零,就直接减,如果小于零,就跟前面借一位,就变成ai-bi加上十,这里ai-bi不够,还需要看一下上一位有没有借位啊?所以这里需要减去一个借位t,t如果为一的话,说明上一位有借位t,如果得零,说明上一位没有借位,就是ai-bi- 1个t。对于每一位而言,分两种情况来看,ai-bi-t就是我们当前这一位的值,如果这一位的值大于等于零,说明我们这一位不需要借位,直接把答案写下来,反之,如果要是小于零,那说明需要跟前面借位,借一当十过来,这一位就变成ai-bi-t+10。高精度减法的模板要保证a大于等于b,如果a小于b,就算b-a,加上一个负号就可以了,只要通过这样的变换就可以保证每次做减法的时候都一定是较大的数减去较小的数。一定不会出现负数的情况。因此最高位是一定不会再往前借位的啊,就可以少处理一些边界情况了。

模板题:

给定两个正整数(不含前导 0),计算它们的差,计算结果可能为负数。

输入格式

共两行,每行包含一个整数。

输出格式

共一行,包含所求的差。

数据范围

1≤整数长度≤1051≤整数长度≤105

输入样例:
32
11
输出样例:
21

#include <iostream>
#include <vector>

using namespace std;

bool cmp(vector<int> &A, vector<int> &B)    //判断是否有A>=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;   //如果相等,返回ture
}

vector<int> sub(vector<int> &A, vector<int> &B)
{
    vector<int> C;
    for (int i = 0, t = 0; i < A.size(); i ++ )  //A的size一定大于B的size
    {
        t = A[i] - t;
        if (i < B.size()) t -= B[i];    //B有对应的一位才减去B
        C.push_back((t + 10) % 10);   //t一定是个位数,A-B大于等于0返回t,小于0则返回t+10,                                                                  (t+10)%10可包 含两种情况
        if (t < 0) t = 1; //如果t小于0表示有借位,等于1
        else t = 0;
    }

    while (C.size() > 1 && C.back() == 0) C.pop_back();  //减法有一个问题,比方说123-120,答案是三,a有多少位c就有多少位,所以返回的其实是003,需要把前导零去掉,所以需要一个while循环,只要c的最后一位,首先c的size肯定得大于一。如果它只有一位,是零的话,不能把它减掉,要给它留上一位零,并且最后一位如果等于零的话,就把最后一位去掉。
pop_back()意为删除 vector 的最后一个元素。

    return C;
}

int main()
{
    string a, b;      //这个题并没有考虑很复杂的情况,比方说如果有负数怎么做?如果有负数,不是模板的问题,其实是数输出的问题,需要判断一下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');

    vector<int> C;

    if (cmp(A, B)) C = sub(A, B);  //判断A是否大于B,大于则A-B,小于则-(B-A)
    else C = sub(B, A), cout << '-';

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

    return 0;
}

作者:yxc
链接:https://www.acwing.com/activity/content/code/content/39793/
来源:AcWing
 

高精度3.乘法

 人工乘法,比方说a是123,小b不一定是一位数,比方说12,先算个位,假设答案是c0c1c2c3,c0等于三乘上12模十得六,进位比方是t1,t2,t3,t1是3×12整除十得三,c1等于2×12加上进位的t1模十得七,t2=27除10,c2等于1×12加上t2的进位模十得4,t3等于14÷10进位得一,c3就直接等于一,所以答案是1476。注意乘法是把较小的数b看成一个整体和a乘,而不是一位一位乘。

模板题:

给定两个非负整数(不含前导 0) A和 B,请你计算 A×B 的值。

输入格式

共两行,第一行包含整数 A,第二行包含整数 B。

输出格式

共一行,包含 A×B 的值。

数据范围

1≤A的长度≤100000,
0≤B≤10000

输入样例:
2
3
输出样例:
6

#include <iostream>
#include <vector>

using namespace std;


vector<int> mul(vector<int> &A, int b)   //C=高精度的A乘上低精度的b
{
    vector<int> C;

    int t = 0;
    for (int i = 0; i < A.size() || t!=0; i ++ )
    {
        if (i < A.size()) t += A[i] * b;
        C.push_back(t % 10);
        t /= 10;    //进位
    }

    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 -- ) printf("%d", C[i]);

    return 0;
}

作者:yxc
链接:https://www.acwing.com/activity/content/code/content/39794/
来源:AcWing

高精度4.除法

人工除法,列数式,比方说1234除上11,首先从第零位开始算,因为计算机不能直接看到第0位不够,所以第零位应该商零,余数是一,把这个余数乘以十。加上下一位数字2,然后下一位是12÷11为一,所以商一余一,把这个余数乘以十,再加上三得13啊,下一位再商一余二,二乘10加4为24,商2余2,最后余数是二。除法也是一个高精度的整数除上一个低精度的整数,假设我们的高精度数是a3a2a1a0除上一个b,每一次就判断一下当前这个余数,这个余数最一开始就只有a3,就直接求a3除b的余数,然后商到这就是最高位c3然后除完之后,余数会变成a3减去一个b×c3,等价于a3除上b的一个余数是多少,余数假设是r1,那么r1乘上十加上下一位a2就会得到a1a2前两位的一个余数,把前两位的一个余数整除b就可以得到c2,同理,再把这个数模上b,得到下一个余数r2,补上下一位r2a1,等于r2乘上十加上a1,因为a的值一直补到余数的后面一位,其实就是r乘10加a1,然后再把这个数除以b求一个商,就是c1,然后再求一下这个数对于b的余数r3,然后最后算一下r3a0这个数除b商是c0,然后最后再算r3a0模b的余数r4

模板题:

给定两个非负整数(不含前导 0) A,B,请你计算 A/B的商和余数。

输入格式

共两行,第一行包含整数 A,第二行包含整数 B。

输出格式

共两行,第一行输出所求的商,第二行输出所求余数。

数据范围

1≤A的长度≤100000,
1≤B≤10000,
B 一定不为 0

输入样例:
7
2
输出样例:
3
1

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

using namespace std;    //除法是从最高位开始算的,加法减法乘法都是从最低位开始算,所以如果题目里面只有除法的话啊,正着存数可能会好一些,但是一般情况下,题目里加减乘除这几种运算是混合的,为了能让这几种运算统一起来,在算的时候没有任何的问题。那么除法存储的时候,也是按照前面的存储方式倒着来存。

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;
    }
    reverse(C.begin(), C.end());  //注意第一位c0存的是最高位,c1存的是最低位。跟前面定义大整数的存法是逆过来的,所以这里需要reverse 1遍,加上算法的头文件。
    while (C.size() > 1 && C.back() == 0) C.pop_back();     //去前导0
    return C;
}

int main()
{
    string a;
    vector<int> A;

    int B;
    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;
}

作者:yxc
链接:https://www.acwing.com/activity/content/code/content/39795/
来源:AcWing
 

前缀和

如果有一个数组,从a1开始一直到an,长度为n,前缀和数组:Si定义成a 1+a 2一直加到ai,Si表示的是原数组前i个数的和。注意前缀和里面一定要让下标从一开始。

第一个问题  如何求Si?可以递推一遍,从前往后for循环一遍,Si等于Si- 1加上ai,定义边界s0为零

第二个问题   前缀和数组的作用是什么?作用只有一个,就是能快速的求出来原数组里面一段数的和。比方说求一下原数组当中从l到r这一段数的和也就是算一下al,加上al+1一直加到ar。如果没有前缀和数组的话,需要循环一遍,时间复杂度是On的。如果有前缀和数组,那就是从l到r这一段的数的和等于Sr啊减去Sl- 1,sr表示的是前r个数的和。等于a 1+a 2加到al- 1,al+0加到ar,Sl- 1是前l- 1个数的和,a1加到al- 1,两个数组相减就会消掉a1到al- 1,剩下的就是al一直加到ar,因此可以用一次运算就可以算出来任意一段区间内所有数的和。是前缀和的最大的应用,而且也是基本上是唯一的应用啊。

为什么下标从一开始要定义s0,首先下标原数组下标从一开始就是为了能定义出来s0,s0可以不对应任何的变量,就可以把它定义成零了,把s0定义成零之后有什么好处呢?主要是为了处理边界问题。比方说,想求从1到10这一段和的话,那么我们就要算S10-S0,把s0定成零,那么它其实就等于s10。也就是说,当想求某一段前缀和的时候,并不是某一个区间的和,而是一个特殊的区间的前缀和的时候也可以用相减的方式来做了。用一个相减的方式来统一的处理所有的情况,少一个if判断啊,如果从一到x的话,那么也可以写成Sx-S0

模板题:

输入一个长度为 n的整数序列。

接下来再输入 m个询问,每个询问输入一对 l,r。

对于每个询问,输出原序列中从第 l个数到第 r个数的和。

输入格式

第一行包含两个整数 n 和 m。

第二行包含 n 个整数,表示整数数列。

接下来 m 行,每行包含两个整数 l 和 r,表示一个询问的区间范围。

输出格式

共 m行,每行输出一个询问的结果。

数据范围

1≤l≤r≤n
1≤n,m≤100000,
−1000≤数列中元素的值≤1000

输入样例:
5 3
2 1 3 6 4
1 2
1 3
2 4
输出样例:
3
6
10

#include <iostream>

using namespace std;

const int N = 100010;

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

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 ++ ) 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;
}

作者:yxc
链接:https://www.acwing.com/activity/content/code/content/39796/
来源:AcWing

以下推广到二维前缀和,如果说想去快速的求出来某一个矩阵中子矩阵的和,也可以用前缀和的思想来做,假设原数组是aij,Sij表示的是以aij左上角所有区域元素的和,是两个方向的前缀和。

假设想求的小矩阵左上角坐标是x1y1,右下角坐标是x2y2,

想求一下这个矩形内部的所有元素,类似于一维前缀和的思想,等于整个的面积先减去红色区域的面积,x是行,y是列,再减去绿颜色小方框的面积啊,上面这小三角被减了两次,需要把它加上一次啊,要求的这个x1y1,x2y2之间的面积,就应该是四项的组合,整个面积减去两个矩形面积再加上多减的面积

还有一个问题是如何推导Si j,可以递推一下,两重for循环就for i从一到n,然后j从一到m,再推导一下x2y2左上所有元素的和

模板题:

输入一个 n 行 m 列的整数矩阵,再输入 q 个询问,每个询问包含四个整数 x1,y1,x2,y2,表示一个子矩阵的左上角坐标和右下角坐标。

对于每个询问输出子矩阵中所有数的和。

输入格式

第一行包含三个整数 n,m,q。

接下来 n 行,每行包含 m 个整数,表示整数矩阵。

接下来 q行,每行包含四个整数 x1,y1,x2,y2,表示一组询问。

输出格式

共 q 行,每行输出一个询问的结果。

数据范围

1≤n,m≤1000
1≤q≤200000,
1≤x1≤x2≤n
1≤y1≤y2≤m
−1000≤矩阵内元素的值≤1000

输入样例:
3 4 3
1 7 2 4
3 6 2 8
2 1 2 3
1 1 2 2
2 1 3 4
1 3 3 4
输出样例:
17
27
21

#include <iostream>

using namespace std;

const int N = 1010;

int n, m, q;
int 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", &s[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];  //求前缀和

    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;
}

作者:yxc
链接:https://www.acwing.com/activity/content/code/content/39797/
来源:AcWing

差分

差分是前缀和的逆运算,假设给定我们的原数组是a1a2一直到an,要构造b数组,b1b2一直到bn,满足一个条件使ai=b 1+b 2加到bi,使得a数组是b数组的前缀和,构造方式是b 1=a 1,b2=a2-a1,b3.=a3-a2,一直到bn=an-an- 1。首先b 1=a 1,然后b 2+b 1等于a2,然后b 3+b 2+b 1等于a3,一维很好构造,假想一个b数组使得a数组是它的前缀和,那么b数组就称为a数组的差分。就b就称为a的差分。那a就称为b的前缀和。

那么差分有什么用呢?-假设已经构造出B数组,如果想求原数组的话。只要对b数组求一遍前缀和就可以了,因此只要有b这个数组,就可以用On的时间得到a数组,主要是帮助我们能快速的处理这样一种操作,我们有这样一堆操作,每堆操作的话是给定一个区间l到r,然后让我们在a数组的lr区间所有的数全部加上一个c,如果一般来做,就要循环一遍,时间On,但是如果用差分来做,就可以做到O1。这些操作完成后,如果想求原来的a数组,就只需要把b数组扫描一遍,求一下b数组的前缀和。

假设要在a数组从l到r这个区间里面所有数全部加上c,a数组是b数组的前缀和,因此需要让bl+c,算al的时候,是从b1一直加到bl,只要给bl+c,那么al就会自动加上c,同样al+1也会自动加上c,一直到an全部会自动加上c,因为从al到an里面每个变量在求前缀和的时候都会把这个bl+1遍,因此只要bl+c,那么al到an就会全部加上c,但是只让al到ar里面的数加上c,r+1之后的数不能加上c,所以就再打一个补丁就好了,就让br+1-c就可以了

后面这一段加c-c抵消了,就是零没有变,那么后面没有任何影响,也是零。所以说当想让a数组的某一个区间内的所有数全部加上c的话,只需要在b数组里面修改两个数就可以了,本来我们需要循环一遍,需要On的时间复杂度,现在只需要修改两个数,就只需要O1的时间复杂度了,差分的作用就有了,就可以用O1的时间,来给原数组中间的某一个连续区间全部加上一个固定的值

可以假定原数组a初始的时候全部是零,那么我们的差分数组也就全部都是零,但是数组不是零,已经给出a1到an,可以看成进行了n次插入操作。第一次是让原数组当中的1到1这个区间加上a1,第二次是让原数组之当中的2到2这个区间加上a2,一直到第n次是让原数组当中的n到n这个区间啊加上an。所以可以发现差分就不需要构造了,总结出来差分就只有一个操作,如果想给al到ar区间整段加一个数,就让bl+c,br+1-c就可以了。同样二维差分也是一样的道理。

模板题:

输入一个长度为 n的整数序列。

接下来输入 m个操作,每个操作包含三个整数 l,r,c,表示将序列中 [l,r][之间的每个数加上 c。

请你输出进行完所有操作后的序列。

输入格式

第一行包含两个整数 n和 m。

第二行包含 n 个整数,表示整数序列。

接下来 m行,每行包含三个整数 l,r,c,表示一个操作。

输出格式

共一行,包含 n个整数,表示最终序列。

数据范围

1≤n,m≤100000,
1≤l≤r≤n
−1000≤c≤1000
−1000≤整数序列中元素的值≤1000

输入样例:
6 3
1 2 2 1 2 1
1 3 1
3 5 1
1 6 1
输出样例:
3 4 5 3 4 2

#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;
}

作者:yxc
链接:https://www.acwing.com/activity/content/code/content/39799/
来源:AcWing
 

二维差分

假设有一个原矩阵aij,然后构造差分矩阵bij,满足原矩阵是差分矩阵的前缀和,构造一个b矩阵使得aij存的是aij左上角所有bij的和,对差分的话不用考虑如何构造,因为可以先假定初始的时候aij都等于零,那bij显然等于零,全零就是一种差分方式吧,然后再对于aij遍历一下每一个值,再插一遍就可以了,只需要考虑如何去更新就可以了,类比一维差分,二维差分是给其中的一个子矩阵加上一个值c,假设左上x1y1右下x2y2,然后首先让bx1y1加等于c,因为a数组是b数组的前缀和,所以说个点右下角的所有的点就会全部加上c。但是l型的这部分点,不能加上c,那么就要减去,首先bx2+1y1减等于c,绿色的矩阵减c,然后bx1y2+1减等于c,就是让红色的矩形减c,但是右下部分加了一个c,减了两个c。是负c,因此还要给它补成零对吧啊,因此bx2+1y2。加等于c啊,补上c就可以了,如果想给x1y1x2y2之间的这个矩形加上c的话。就可以转化成给四个值分别加上c或者减上c操作一遍就可以了。本来需要枚举一下整个矩阵里面所有元素,现在就只需要除以四个数就可以了,On的时间复杂度变成了o1了,初始化很简单,假定aij=0,bij就是零。然后再把aij当中的每一个元素依次再插进去,比方说aij,要把这个值插进去,也可以把它想象成插了一个长和宽都是一的一个子矩阵,这个子矩阵,左上角是ij,右下角也是ij,这是一种边界情况,相当于给它插了一个1×1的一个小矩阵,然后,这个小矩阵里面的元素加的aij,那么它也是一个子矩阵,也可以用这样一个公式来算,因此初始化和后面这个操作是一样的,最后求原来aij的值。就是求一下b数组的前缀和就可以了。

bij是空想出来的,先假定出来差分数组bij满足的性质是aij是b数组的前缀和,所以b数组就是a数组的前缀和的逆运算,假设前缀和是一个函数,fa操作一遍就可以得到a的一个前缀和了,假设前缀和用函数f来表示,那么给定数组a,现在要构造一个数组b,假想一个数字b使得a=fb,是一个逆运算,所以b可以看成是函数逆a,然后函数的逆具体是怎么构造?可以不用管,因为可以发现构造可以用修改操作来做,只需要假设一开始所有数组都是零,那么b数组显然就是零,那么a数组原来有值的话,就把每一个值依次插进去就可以了。

模板题:

输入一个 n 行 m 列的整数矩阵,再输入 q 个操作,每个操作包含五个整数 x1,y1,x2,y2,c,其中 (x1,y1)和 (x2,y2)表示一个子矩阵的左上角坐标和右下角坐标。

每个操作都要将选中的子矩阵中的每个元素的值加上 c。

请你将进行完所有操作后的矩阵输出。

输入格式

第一行包含整数 n,m,q。

接下来 n行,每行包含 m个整数,表示整数矩阵。

接下来 q行,每行包含 55 个整数 x1,y1,x2,y2,c,表示一个操作。

输出格式

共 n行,每行 m个整数,表示所有操作进行完毕后的最终矩阵。

数据范围

1≤n,m≤1000
1≤q≤100000,
1≤x1≤x2≤n
1≤y1≤y2≤m
−1000≤c≤1000
−1000≤矩阵内元素的值≤1000

输入样例:
3 4 3
1 2 2 1
3 2 2 1
1 1 1 1
1 1 2 2 1
1 3 2 3 2
3 1 3 4 1
输出样例:
2 3 4 1
4 3 4 1
2 2 2 2

#include <iostream>

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 ++ )//先假定a数组是空的,但实际上可能是有值的,比方说123456789,那么其实可以把它看成是插入了九个数,第一个数是在左上角这个1×1的小矩形里面,把这个数加上一,然后第二步是把中间这个小矩形加上二,然后下个小矩形加上三。所以说如果有n*m个数的话,相当于进行了n×m次插入操作就可以变成初始是aij的情况。

        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;                  //q为次数
        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 ++ )          //求前缀和
            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;
}

作者:yxc
链接:https://www.acwing.com/activity/content/code/content/39800/
来源:AcWing

end..............................................................................................................................................

  • 49
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值