目录
📘At first:一个初学算法的萌新,如果文中有误,还请指正🤓
🎗️专栏介绍:本专栏目前基于AcWing算法基础课进行笔记的记录,包括及课上大佬讲的一些算法的模板还有自己的一些心得和理解
🕶️个人博客地址:https://blog.csdn.net/m0_73352841?spm=1010.2135.3001.5343
一、高精度
1.1 高精度加法
一般来说高精度考四种(浮点数的基本用不上):
- 两个大的整数(位数 <=10 ^ 6,极端情况)相加
- 两个大的整数相减
- 一个大整数(位数 <=10 ^ 6)乘以一个小整数(数值<=10 ^ 5,大致)
- 一个大整数除以一个小整数
以下以两个数均为正数的情况进行讨论
问题:
Q1:大整数在代码中是如何表示的?
A1:把每一位存在数组里
Q2:高位在前还是低位在前?
A2:第0位存个位,因为在计算的时候可能会进位,这时就需要在高位上补1,在数组的末尾补充是比较方便的
1.1.1 代码实现
#include <iostream>
#include <vector>
using namespace std;
vector<int> add(vector<int>& A, vector<int>& B)
//算数组表示的整数A和数组表示的整数B,返回值是数组表示的整数C
//加引用是为了提高效率,如果不加应用它就会把整个数组copy一遍,如果加上引用就不会copy整个数组了
{
vector<int> C;
int t = 0;
//记录A与B每一位相加的中间变量
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];
//用if的原因是A或B可能在相同位上有一者为0
C.push_back(t % 10);
//对t进行取余,作为数组C的低位,填入
t /= 10;
//进行进位
}
if (t) C.push_back(1);
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);
//auto,编译器会自己推断这个变量是什么类型的
//auto C等价于vector<int> C
for (int i = C.size() - 1; i >= 0; i--) printf("%d", C[i]);
return 0;
}
1.1.2 图示
以下面的数据为例
85129
65247
如图所示,A、B两组数字已录入。从个位开始,该位上A、B中都有数,所以t = A[i] + B[i] + t
,即t = 9 + 7 + 0 = 16
对t进行取余,并将其填入C中,即C.push_back(t % 10)
(图中那个跟加号类似的东西其实是t
QWQ)
然后对t进行进位操作,即t /= 10
十位上的操作,这时t
已经进位了,一开始就有一个1,所以t = 1 + 2 + 4 = 7
,将其导入C中
t进行进位,由于未大于10,所以t = t / 10 = 0
以此类推,最后得到的结果为150376
,最后倒着输出即可(因为我们表示数的大小是高位在前)
1.2 高精度减法
两数相减,要保证前者大于等于后者,即A >= B,如果小于则算B - A,再加负号,这样就能保证每次都是较大的数减较小的数,一定不会出现负数的情况,最高位不会再向前借位,少处理边界情况
以下均已两数为正数的情况进行讨论。如果运算中有负数,要判断第一个录入的字符是不是负号。另,两个数相减的话一定能转换成两个数的绝对值进行相减
1.2.1 代码实现
#include <iostream>
#include <vector>
using namespace std;
bool cmp(vector<int> &A, vector<int> &B)
//cmp函数要自己实现,判断是否有 A >= B,是的话返回true,否则返回flase
{
if (A.size() != B.size()) return A.size() > B.size();
//首先判断位数,如果位数不同,A的位数大就大,反之亦然
for (int i = A.size() - 1; i >= 0; i--)
//从高位至低位逐一比较
if (A[i] != B[i])
return A[i] > B[i];
return true;
}
vector<int> sub(vector<int>& A, vector<int>& B)
{
vector<int> C;
for (int i = 0, t = 0; i < A.size(); i++)
//这里一定保证是“A” >= “B”的,“A”的size一定大于“B”的size
{
t = A[i] - t;
if (i < B.size()) t -= B[i];
//判断B有没有这一位
C.push_back((t + 10) % 10);
//这一步解析请看图示部分
if (t < 0) t = 1;
//进位,下一位再减的时候就要多减一个1了,因为上一位借了一个
else t = 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))
//判断两个大整数谁大
{
auto C = sub(A, B);
//如果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]);
}
auto C = sub(A, B);
return 0;
}
1.2.2 图示
以下面的数据为例
56823
34987
将数字录入后开始进行每个位上的相减运算,从个位开始t = A[i] - B[i] - t
,即t = 3 - 7 - 0 = -4
。这时候返回给C的数,也即我们前面的代码C.push_back((t + 10) % 10 )
所以返回给C的是6。这时,由于t = -4 < 0
,所以赋给t = 1
,也即我们的进位操作。下一次,A对应的位就要多减1了
十位上的操作,这时t
已经进位了,一开始就有一个1,所以t = 2 - 1 - 8 = -7
,将其导入C中( C.push_back((t + 10) % 10)
)
t
进行进位,由于未大于0,所以t = 1
以此类推,最后得到的结果为21836
,最后倒着输出即可(因为我们表示数的大小是高位在前)
关于while (C.size() > 1 && C.back() == 0) C.pop_back()
这步操作,是为了去掉前导的0
如图中这里例子,运算完录入我们C中的数据就是1 0 0
,这两个0便是前导0,我们要出去它们,所以便有了这步操作
1.3 高精度乘法
以下图为例,A0~A5表示每一位,b表示那个较小整数。注意,我们这里是拿整个b去和A中的每一位去乘的
个位上的数应当是(A0 * b)
取10的余数,即,而进位则是(A0 * b) / 10
。同理,十位上的数就是(A1 * b + t ) % 10
,进位为(A1 * b + t) / 10
,这里t
代指(A1 * b + t) / 10
,即进位
1.3.1 代码实现
#include <iostream>
#include <vector>
using namespace std;
vector<int> mul(vector<int> &A, int b)
{
vector<int> C;
int t = 0;
//最开始的进位是没有的,为0
for (int i = 0; i < A.size() || t ; i++)
//要么是i没有循环完,要么是t不为0,只要t不是0就一直做,把最后一次也包含了
{
if (i < A.size()) t += A[i] * b;
//判断i是不是在A的范围里
//上一位的进位加上这一位和b的积
C.push_back(t % 10);
//当前这一位的结果
t /= 10;
//进位
}
return C;
}
int main()
{
string a;
//a很长,用字符串来存
int b;
//b很短,用int来存
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]);
}
1.3.2 图示
以下面的数据为例
5398
21
首先t = 8 * 21 = 168
,个位上的数就是t % 10 = 8
t
进行进位,即t /= 10 = 16
再确定十位上的数字,t = (9 * 21 + 16) = 205
,则十位上的数为t % 10 = 5
t
进行进位,即t /= 10 = 20
以此类推,最后得到的结果为113358
,最后倒着输出即可(因为我们表示数的大小是高位在前)
1.4 高精度除法
以下图为例模拟运算过程。A0~A3表示A中的每一位,b表示那个较小的整数
最开始余数就是A3(见代码实现部分的具体解析),我们用r
表示。拿他去除以b,商用C3
表示。余数A3模b等价于A3 - b * C3
,即下一位的余数,我们用r'
表示。由于到了下一位,上一位的余数就需要先乘以10再做除法,即r' * 10 + A2
。再把这当作一个整体除以b,得到商C2
,以此类推进行运算
1.4.1 代码实现
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
vector<int> div(vector<int>& A, int b, int &r)
//除了返回商(用C表示),还会返回一个余数,用r表示,是引用
/*除法是从最高位开始算的,之前的加、减、乘都是从最低位开始算的,如果只有除法的话,正着存会好一些
但是一般的题目里几种运算是混合的,所以这里也倒着来存,但是运算我们是从最高位开始算的*/
{
vector<int> C;
r = 0;
//一开始余数是0
for (int i = A.size() - 1; i >= 0; i--)
{
r = r * 10 + A[i];
//每一次余数等于余数乘以10加上这一位
C.push_back(r / b);
r %= b;
}
reverse(C.begin(), C.end());
//由于我们开头说的,此时C中下标0位置上存的是最高位,要把它们逆过来
while (C.size() > 1 && C.back() == 0) C.pop_back();
return C;
}
int main()
{
string a;
//a很长,用字符串来存
int b;
//b很短,用int来存
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 << r << endl;
//第一行输出商,第二行输出余数
return 0;
}
1.4.2 图示
我们以下面的数据为例
7521
65
一开始r = 0
,先进行最高位上的运算,r = r * 10 + A[i]
,即r = 0 * 10 + 7 = 7
,然后返回给C的数是7 / 65 = 0
然后r %= b = 7
仍为7,为下一位运算做准备
下一位,r = r * 10 + A[i] = 75
,返回给C的数为75 / 65 = 1
然后r %= b = 10
以此类推,最后得到的结果是商为115
,余数为46
二、前缀和与差分
前缀和与差分是一对逆运算
2.1 前缀和
2.1.1 一维前缀和
如果我们有一个长度为n的数组,Si表示原数组中前i
数的和,这就是前缀和。前缀和中一定要让下标从1开始(后面解释)
问题
Q1:Si如何求?
A1:大致实现过程:
for (i = 1; i <= n; i ++ )
s[i] = s[i - 1] + a[i]
S[i - 1]是a数组里面前i-1
个数的和,加上第i
个数,就可以得到前i
个数的和了,就是Si 。还有边界S0要定义成0(后面解释)
Q2:前缀和数组的作用是什么?
A2:能快速的求出原数组中一段的和。比如要求原数组中从l
到r
这一段数组的和,如果没有前缀和数组,就需要循环一遍,再做运算。而有了前缀和数组从l
到r
这一段数的和就是Sr - Sl-1 。只需要一次运算就可以算出任意区间的和,这是前缀和最大的应用,基本上也是唯一的应用
Q3:为什么下标要从1开始、要定义S0为0?
A3:其实下标从1开始就是为了能定义出S0 。从1开始S0就不需要对应任何变量了,就可以把它定义为0
这有什么好处?主要是为了处理边界,比如要求[1,10]
这段和,就要算S10 - S0,那我们就需要用到S0这个东西
我们为了统一,让所有l
到r
都用这样一个同样的公式,那我们就把S0定义成0(那其实上个例子就是在求S10),也就是可以少考虑一个特殊判断
2.1.1.1 代码实现
下面以一道例题为例
题目描述
输入一个长度为n
的整数序列
接下来再输入m
个询问,每个询问输入一对l
,r
。
对于每个询问,输出原序列中从第l
个数到第r
个数的和。
输入格式
第一行包含两个整数n
和m
第二行包含n
个整数,表示整数数列
接下来m
行,每行包含两个整数l
和r
,表示一个询问的区间范围。
输出格式
共m行,每行输出一个询问的结果。
数据范围
1 <= l <= r <= n,
l <= 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;
//n为数组内元素个数
//m为询问个数
int a[N], s[N];
//a[N]为原数组
//s[N]为前缀和数组
int main()
{
scanf_s("%d%d", &n, &m);
/*
由于我用的Visual Studio的版本比较新,scanf被认为是不安全的
可能造成数据溢出,就只好换成scanf_s()
*/
for (int i = 1; i <= n; i++) scanf_s("%d", &a[i]);
//注意这里也是从1开始给数组a赋值的
for (int i = 1; i <= n; i++)s[i] = s[i - 1] + a[i];
//前i个数的和等于前i-1个数加上第i的数
while (m -- )
{
int l, r;
scanf_s("%d%d", &l, &r);
//输入的数比较大,用scanf,比cin快差不多1倍
printf("%d\n", s[r] - s[l - 1]);
//前r个数的和减去前l-1个数的和,就等于l到r的和
}
return 0;
}
2.1.1.2 图示
其实这里有一个地方要说明,就是:
for (int i = 1; i <= n; i++)s[i] = s[i - 1] + a[i];
注意,这里是对数组s中每一位上赋予前面所有项的和。其实每轮循环输出数组s
每一位上的值就是:
2.1.2 二维前缀和
如果想快速求出一个子矩阵的和,也可以用前缀和的思想来做。如图,如果小矩形和虚线矩形的交点是aij的话,那这个虚线围起来的所有元素的和就是Sij
那么小矩形中的元素和该怎么求?
我们用坐标分别表示小矩形的两个坐标,用Sx2y2表示虚线矩形中的元素和
那么小矩形中元素和就等于图中虚线矩形的元素和(Sx2y2)减去绿色矩形中的元素和(Sx2y1-1)、红色矩形中的元素和(Sx1-1y2),再加上重叠减去的部分(Sx1-1y1-1),即:
Sx2y2 - Sx2y1-1 - Sx1-1y2 + Sx1-1y1-1
问题:
Q1:如何把aij(aka x2,y2)算出来?
A1二重循环,第一个循环i
从1至n, 第二个循环j
从1至m
Q2:怎么算Sij(aka Sx2y2)?
A2:
Sij = Sij-1 + si-1j - Si-1j-1 + aij
2.1.2.1 代码实现
下面以一道题为例
Q:
题目描述
输入一个n
行m
列的整数矩阵,再输入q
个询问,每个询问包含四个整数x1
,y1
,x2
,y2
,表示一个子矩阵的左上角坐标和右下角坐标
对于每个询问输出子矩阵中所有数的和
输入格式
第一行包含三个整数n
,m
,q
接下来n
行,每行包含m
个整数,表示整数矩阵
接下来q行,每行包含四个整数x1
,y1
,x2
,y2
,表示一组询问
输出格式
共q
行,每行输出一个询问的结果
数据范围
1 <= n,m <= 1000,
1 <= q <= 100000,
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>
const int N = 1010;
int n, m, q;
//n、m边长
//q是询问次数
int a[N][N], s[N][N];
//a[N][N]为原数组
//s[N][N]为前缀和数组
int main()
{
scanf_s("%d%d%d", &n, &m, &q);
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= m; j ++ )
scanf_s("%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];
//求前缀和,即Sij
while (q -- )
//m个询问
{
int x1, y1, x2, y2;
scanf_s("%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.1.2.2 图示
我们先把样例矩阵画出来
然后画出前缀和数组
然后看第一组坐标(1,1),(2,2)
,对应我们前缀和中的17
其余的类似,比如第二组坐标(2,1),(3,4)
就是41 - 14 = 27
2.2 差分
2.2.1 一维差分
构造使得a数组是b数组的前缀和,b数组就成为a数组的差分,a就是b的前缀和
问题:
Q1:差分有什么用?
A1:假设已经构造出b数组,如果想求原数组就只需要对b数组求一遍前缀和就可以求出原数组
前缀和主要是能帮助我们快速处理一种操作
定一个区间[l,r]
,让我们在a数组的这个区间内所有的数加上一个C(al+C、al+1+C、…、ar+C ),这些操作完成后,如果想求原的a数组的话,只需要把b数组扫描一遍,求一下b数组的前缀和,就可以求出原来的a数组
Q2:当我们对a数组这样操作后(给定区间加C),对B数组会有什么影响?
A2:全部加C,a数组又是b数组的前缀和,因此,就要让bl+C,效果就是,算al的时候,从b1一直加到bl
bl+C,al也会自动加上C。但是我们只要lr这个区间内加上C,r+1后的数不能加上C,所以就让br+1-C就好了
主要作用就是可以给原数组中间的某个区间全部加上一个固定的值
2.2.1.1 代码实现
下面以一道题为例
题目描述
输入一个长度为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;
//n为数组内元素个数
//m为操作个数
int a[N], b[N];
//a[N]是原数组
//b[]为差分数组
void insert(int l, int r, int c)
//构造插入函数
{
b[l] += c;
b[r + 1] -= c;
}
int main()
{
scanf_s("%d%d", &n, &m);
for (int i = 1; i <= n; i ++ ) scanf_s("%d", &a[i]);
for (int i = 1; i <= n; i ++ ) insert(i, i, a[i]);
//构造差分数组
while ( m-- )
//m个操作
{
int l, r, c;
scanf_s("%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.1.2 图示
先将数录入原数组中
然后是构造分差数组b,即b[l] += c; b[r + 1] -= c;
,(这时原矩阵就是差分矩阵的前缀和了)
第一步操作,根据之前的分析,差分数组中只要在一处增加,最后的前缀和都会增加,所以其实只需要在第一个位置上加1和r + 1处
减1就可以了,即b[l] += c; b[r + 1] -= c;
然后进行完剩下两步操作得到的差分数组为:
然后把数组b变成自己的前缀和输出,即for (int i = 1; i <= n; i ++ ) b[i] += b[i - 1];
2.2.2 二维差分
假设原矩阵是aij,对其构造差分矩阵bij。构造也是一样的,就是满足原矩阵是差分矩阵的前缀和就行
在一维差分中是在一段上加上一个值,而在二维差分中是在一个子矩阵中加上一个值
如图,bx1y1 += c 的效果就是把整个虚线围成矩形都加上C。但是“L”形中的部分也多加了C
那就需要减去绿色的部分,即bx2+1y1 -= c,还有红色部分,即bx1y2+1 -= c,再加上多减的(红绿重叠部分),即b~x2+1y2+1 += c
2.2.2.1 代码实现
下面以一道题为例
题目描述
输入一个n行m列的整数矩阵,再输入q个操作,每个操作包含五个整数x1,y1,x2,y2,c,其中(x1,y1)和(x2,y2)表示一个子矩阵的左上角和右下角
每个操作都要将选中的子矩阵中的每个元素的值加上c
请你将进行完所有操作后的矩阵输出
输入格式
第一行包含整数n,m,q
接下来n行,每行包含m个整数,表示整数矩阵
接下来q行,每行包含5个整数x1,y1,x2,y2,c,表示一个操作
输出格式
共n行,每行m个整数,表示所有操作进行完毕后的最终矩阵
数据范围
1 <= n,m <= 1000,
1 <= q <= 100000,
1 <= x1 <= x2 <= n,
1 <= y1 <= y2 <= m,
输入样例:
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;
//n、m为边长
//m是操作个数
int a[N][N], b[N][N];
//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_s("%d%d%d", &n, &m, &q);
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
scanf_s("%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 -- )
//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 ++ )
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;
}
2.2.2.2 图示
先将数录入矩阵中
然后是构造分差矩阵b(这时原矩阵就是差分矩阵的前缀和了),即:
b[x1][y1] += c;
b[x2 + 1][y1] -= c;
b[x1][y2 + 1] -= c;
b[x2 + 1][y2 + 1] += c;
然后进行三步操作,最后有差分矩阵b:
然后把差分矩阵b变成自己的前缀和输出,即:
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];