第一章 基础算法(二)包含高精度以及前缀和差分

188 阅读7分钟

第一章 基础算法(二)

一、高精度

一般来说,高精度会考有四种:(都是大数)

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)运算:模拟人工运算的过程

image-20220304170514195.png

  • A0 + B0 + t (每一位上 ,t = 0 , 1);

(3)ACWING 791 高精度加法

image-20220304170759963.png

 #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:高精度减法

image-20220305154911190.png

image-20220305154729542.png

  • 通过这样的交换,可以保证我们一定是较大的数,减去较小的数,可以保证最高位一定不会再往前借位的;
  • 比较:同位的话,比较最高位,不行的话,依次往下比;
  • 上一位借过位的话,t 为 1, 否则为 0;

image-20220305155339746.png

 #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 高精度乘法

image-20220306142136163.png

image-20220306142657097.png

 #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 高精度除法

高精度整数 / 低精度整数

image-20220307103954916.png

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

image-20220307115859029.png

二、 前缀和与差分

若有长度为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开始;    

image-20220307155423687.png

(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)如果想快速求出某一个子矩阵的和?也可以用前缀和的思想

image-20220307163551166.png

image-20220307164303260.png

image-20220307164438948.png

  • 画个格子用格子比较好理解
  • 一个点代表一个格子
  • image-20220307212806908.png
  • image-20220307213557376.png

(3)ACWING 796. 子矩阵的和

image-20220307174531988.png

 #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)差分其实是前缀和的逆运算

image-20220307213914690.png

  • a 称前缀,b 称差分
  • 对b数组求一遍前缀和,即可得到a数组,O(n);
  • 应用:快速处理,例如:[l, r]区间内,所有的数都加 + c;
  • image-20220309091632500.png
  • 暴力做法,时间复杂度 O(n), 差分做法 为 O(1);
  • image-20220309091955729.png
  • image-20220309092008917.png
  • 可以这么理解:在[1, 1]区间插入 a[1], 在[2, 2]区间插入a[2],......;
  • image-20220309103920449.png

(5)ACWING 797 差分 :【一维数组差分】

image-20220309085810676.png

#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

image-20220309111718307.png

image-20220309112515117.png

image-20220309112546828.png

(7)ACWING 798 差分矩阵

image-20220309112845001.png

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