1、该系列为ACWing中算法基础课,已购买正版,课程作者为yxc
2、y总培训真的是业界良心,大家有时间可以报一下
3、为啥写在这儿,问就是oneNote的内存不够了QAQ
ACwing C++ 算法笔记2 基础算法
本节内容:高精度、前缀和与差分
一、高精度
java和python 有大整数类,可以不需要学习高精度。
1.1 应用场景
-
高精度的考试方式有4种:
- 1、比较大的整数相加
A+B
,A/B
的位数在1e6 - 2、比较大的整数相减
A-B
,A/B
的位数在1e6 - 3、大整数乘以小整数
A*a
,A
的位数小于等于1e6,a
的数值小于等于1e9 - 4、大整数除以小整数
A/a
- 1、比较大的整数相加
-
高精度整数以数组的形式存储,个位存在前面,高位存在后面,方便做加法和乘法的时候进位。
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)
,可以保证计算时最高位不用借位。 - 判断是否需要借位:
- 如果A<B,计算
// 高精度减法
// 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]
多加一个c
(B[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
- 举例:输入一个
n
行m
列的整数矩阵,再输入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矩阵。