第一章 基础算法(二)
一、高精度
一般来说,高精度会考有四种:(都是大数)
A + B : A, B位数10^6
A - B : A, B位数10^6
A * a : len(A) <= 10^6 a <= 10^9| a <= 10000(a 这里是指数值,不是位数)
A / a
A * | / B
(1)问题一:大整数在代码中是如何表示的,如何存储?
- int 变量肯定存储不了大整数;
- 我们是将大整数的每一位存储在数组里面;
// 例如 123456789, 我们将它放到数组里面
- 假设有一个数组 下标 0 1 2 3 4 5 6 7 8
- 我们将 个位放在第0位,即 9 在第0位; 【为什么这样做会比较好呢?】
- 存完之后 数组下标从0 - 8 位分别对应9 8 7 6 5 4 3 2 1 (将最高位存在了数组最后一个数)
- 因为我们两个整数相加(运算)会产生进位,那么进位的话我们就需要在高位补上一个数,补上一个数的话,在数组的末尾补上一个数是容易的;在数组的开头补上一个数字的话,就需要将数组所有都往后平移一位,再补,就很麻烦;
- 加减乘除中大整数的存储都是一样的;
(2)运算:模拟人工运算的过程
- A0 + B0 + t (每一位上 ,t = 0 , 1);
(3)ACWING 791 高精度加法
#include <bits/stdc++.h>
#include <vector>
using namespace std;
const int N = 1e6 + 10;
// C = A + B 加 & 是为了提高效率,不然,在调用的时候就会把数组复制一遍, 浪费时间
vector<int> add (vector<int> &A, vector<int> &B)
{
vector<int> C;
int t = 0; // 进位, 一开始为0
// 只要A, B没有走完就一直加
for (int i = 0; i < A.size() || i < B.size(); ++i)
{
// 如果 A 还没有结束, 就加
if (i < A.size()) t += A[i];
// 同理
if (i < B.size()) t += B[i];
//t = t + Ai + Bi 进位加上 A,B的每一位
// 理解一下最后这两步
C.push_back(t % 10); // 取模C的当前位, 由“新 ”t 取模 保留个位
t /= 10; //如果t > 10 , t 就要进位 为 1, 否则就是 0
}
// 结束后,如果最高位还有进位的话,就要加 1
if (t) C.push_back(1);
return C;
}
//用数组表示整数的时候,倒着来表示第0位表示个位,第一位表示十位。。。, 最高位在最后;
int main ()
{
string a, b;
vector<int> A, B;
cin >> a >> b; // 读入两个大整数, 假设 a = "123456"
// 再将这两个大整数存储到数组中 存储的时候,记得要-'0',变成数,而不是字符
for (int i = a.size() - 1; i >= 0; --i) A.push_back(a[i] - '0'); // A = [6, 5, 4, 3, 2, 1]
for (int i = b.size() - 1; i >= 0; --i) B.push_back(b[i] - '0');
auto C = add (A, B); // auto 编译器会自动推断这个变量是什么类型的
for (int i = C.size() - 1; i >= 0; --i) printf ("%d", C[i]);
return 0;
}
(4)ACWING 792:高精度减法
- 通过这样的交换,可以保证我们一定是较大的数,减去较小的数,可以保证最高位一定不会再往前借位的;
- 比较:同位的话,比较最高位,不行的话,依次往下比;
- 上一位借过位的话,t 为 1, 否则为 0;
#include <bits/stdc++.h>
#include <vector>
using namespace std;
//如果遇上输入两个,或者其中之一为负整数
//符号单独处理,计算数值部分不变 ,具体是需要自己去调整的,模板是可以用的
//但在这里,我们先不考虑负整数的情况
//判断是否有A >= B
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];
}
}
// 两个数相等也可以 A>=B 返回 true
return true;
}
vector<int> sub(vector<int> &A, vector<int> &B)
{
vector<int> C; // 最终结果
// A 是最大的,循环在A没有走完之前
for (int i = 0, t = 0; i < A.size(); ++i)
{
// 综合式子, t = A[i] - B[i] - t, t是进位
t = A[i] - t;
// 若B还没有越界,则减B[i]
if (i < B.size()) t -= B[i];
//对当前这位的处理 : 考虑两种情况,当t(t = A[i] - B[i] - t) >= 0 , 当前位就是此t本身,否则,要 + 10
//将两种情况合二为一的写
C.push_back((t + 10) % 10);
if (t < 0) t = 1; // < 0 说明向当前为借位了,等下要减1
else t = 0;
}
// 但是减法还需要注意一点,例如 123 - 120 = 3,但是这样push_back 会变成 003 ,要做对0的处理
// 若是结果只有一位,就算是 0 也不可以去掉
// 且注意存储也是倒着存的,若是0 0 0 3 0, 在C中就是0 3 0 0 0,所以
while (C.size() > 1 && C.back() == 0) C.pop_back(); // 去掉前导0
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');
// 比较一下A, B,由大数减小数,可保证最高位一定不会再往前借位
if (cmp(A, B))
{
vector<int> C = sub(A, B);
for (int i = C.size() - 1; i >= 0; --i) printf ("%d", C[i]);
}
else
{
vector<int> C = sub(B, A);
printf ("-");
for (int i = C.size() - 1; i >= 0; --i) printf ("%d", C[i]);
}
return 0;
}
(5)ACWING 793 高精度乘法
#include <bits/stdc++.h>
#include <vector>
using namespace std;
vector<int> mul(vector<int> &A, int b) {
vector<int> C;
int t = 0; // 进位
// 当A还有值时时候,以及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;
// 注意,如果b为0, 则会有一排0, 对这个特殊数据进行处理
if (b == 0) printf ("0");
else {
vector<int> A;
for (int i = a.size() - 1; i >= 0; --i) {
A.push_back(a[i] - '0');
}
vector<int> C = mul(A, b);
for (int i = C.size() - 1; i >= 0; --i) printf ("%d", C[i]);
}
return 0;
}
- 注意 若 b = 0, 这一组数据
(6)ACWING 794 高精度除法
高精度整数 / 低精度整数
#include <bits/stdc++.h>
#include <vector>
#include <algorithm>
using namespace std;
//除法除了返回商外,还会返回余数
//A / b, 商是C, 余数是r
vector<int> div(vector<int> &A, int b, int &r)
{
// 注意一下:前面的 + - * 都是从最低位开始,但是除法是从最高位开始算,但是main函数中存储的话,统一倒着存
vector<int> C;
r = 0; // 余数开始是0
// 余数从最高位开始算起
for (int i = A.size() - 1; i >= 0; --i)
{
// 从最高位开始算起,然后把个位留出来,再加上这一位的个位
r = r * 10 + A[i];
// 当前位 商 余数 整除 b
C.push_back(r / b);
// 下一位余数 商完之后的余数
r %= b;
}
// 与前面的不同,算完后,最高位就在前面,再翻转一下(因为在main函数中,统一了输出)
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; //余数
vector<int> 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;
}
二、 前缀和与差分
若有长度为n的数组, a1, a2, a3…an
前缀和si(元素中前i个数和) = a1 + a2 + a3 +… + ai 【前缀和中,一定要让下标从1 开始】
- 前缀和是一个公式,思想
1. 如何求si
s0 = 0;
for (i = 1; i <= n; ++i) s[i] = s[i - 1] + ai;
2. 作用
快速的求出原数组中一段数的和
例如:求[l, r]
- 若无前缀和,就要循环一遍数组 O(n)
- 依靠前缀和,则可以Sr - S(l-1) ; [l -1]是下标
- 这样,我们就可以用一次运算,算出任意一段区间的数和;
3. 为什么要让下标从1开始,以及为什么要定义S0?
- 下标从1开始,定义S0 = 0;
- 令S0 = 0,为了处理边界
- 例如:当我们求[1, 10]这段区间的数和,(即求S10)S10 - S0(S0 = 0), 就是为了 统一公式;
- 通俗一点:如果想求的不是[l, r], 而是[1, x] ,也可以用 Sr - Sl-1 ,这个公式,即Sx - S0,(S0 = 0,无影响);
- 也因此下标通常从1开始;
(1)ACWING 795. 前缀和
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 10;
int a[N];
int s[N];
int n, m, l, r;
int main ()
{
//ios::sync_with_stdio(false); // 提高cin的读取速度,不能使用scanf
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--)
{
scanf ("%d%d", &l, &r);
printf ("%d\n", s[r] - s[l - 1]); // 区间和的计算
}
return 0;
}
(2)如果想快速求出某一个子矩阵的和?也可以用前缀和的思想
-
画个格子用格子比较好理解
-
一个点代表一个格子
(3)ACWING 796. 子矩阵的和
#include <bits/stdc++.h>
using namespace std;
const int N = 1010;
int n, m, q;
int a[N][N], s[N][N];
int main()
{
// first: 输入矩阵规模以及问题个数
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];
}
}
int x1, x2, y1, y2;
while (q--)
{
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;
}
(4)差分其实是前缀和的逆运算
-
a 称前缀,b 称差分
-
对b数组求一遍前缀和,即可得到a数组,O(n);
-
应用:快速处理,例如:[l, r]区间内,所有的数都加 + c;
-
暴力做法,时间复杂度 O(n), 差分做法 为 O(1);
-
可以这么理解:在[1, 1]区间插入 a[1], 在[2, 2]区间插入a[2],…;
(5)ACWING 797 差分 :【一维数组差分】
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 10;
int n, m;
int a[N], b[N]; // 初始全为0
void insert (int l, int r, int c)
{
b[l] += c;
b[r + 1] -= c;
}
// 插入操作就相当构造了差分数组, 如果初始全为0就很好理解
int main ()
{
int n, m; // 输入n个序列, m次操作
scanf ("%d%d", &n, &m);
for (int i = 1; i <= n; ++i)
{
scanf ("%d", &a[i]);
}
// 构造差分 b 数组
for (int i = 1; i <= n; ++i)
{
insert(i, i, a[i]);
}
int l, r, c;
// 完成m次操作
while (m--)
{
scanf ("%d%d%d", &l, &r, &c);
// 对于区间的修改,时间复杂度就变成了O(1), 而不用遍历数组了
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;
}
- 差分不需要考虑如何构造,只需要考虑如何更新即可;
(6)二维差分:给子矩阵加上C
(7)ACWING 798 差分矩阵
#include <bits/stdc++.h>
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]);
}
}
//把a矩阵里面的每个数,插到空数组里面 b
for (int i = 1; i <= n; ++i)
{
for (int j = 1; j <= m; ++j)
{
insert (i, j, i, j, a[i][j]);
}
}
int x1, x2, y1, y2, c;
// 做q次操作
while (q--)
{
scanf ("%d%d%d%d%d", &x1, &y1, &x2, &y2, &c);
insert (x1, y1, x2, y2, c); // 完成+c操作
}
// 求所有q次操作更新后的新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];
}
}
for (int i = 1; i <= n; ++i)
{
for (int j = 1; j <= m; ++j)
{
printf ("%d ", b[i][j]);
}
puts("");
}
return 0;
}