diff --git a/工作日志/2021年3月27日-今日计划.md b/工作日志/2021年3月27日-今日计划.md index ecdbfb85..8041f1b6 100644 --- a/工作日志/2021年3月27日-今日计划.md +++ b/工作日志/2021年3月27日-今日计划.md @@ -4,9 +4,10 @@ - [x] 双指针环的入接点总结 - [x] 字符串分割与格式化方法总结 - [x] 位运算与补码总结 -- [ ] 大数运算 +- [x] 大数运算 - [ ] n个骰子的点数 - [ ] 约瑟夫问题 +- [ ] 手写所有排序 diff --git a/算法/A类:基本算法/11 递归与迭代.md b/算法/A类:基本算法/11 递归与迭代.md index 958095f1..7abf1555 100644 --- a/算法/A类:基本算法/11 递归与迭代.md +++ b/算法/A类:基本算法/11 递归与迭代.md @@ -88,12 +88,12 @@ $$ * 多分支递归:当一个问题可以分为多个相同的子问题的时候。在同一层进行多分支递归。每一层**多次调用递归函数**。如树和图的遍历问题,有多个下层子节点。 ## 3 递归的实现 -### 递归前的预处理 +### 递归问题的分析 1. 提取重复的逻辑,缩小问题规模。 2. 只有写出**反向递推关系式**,才知道递归的参数、递归的返回值和递归的执行关系。 -### 递归的实现步骤 +### 递归算法的步骤 1. 设计**递归的参数**。首先确定每一次递归,上一层需要提供给下一层的内容。相当于上层---->下层的接口。 2. 设计**递归的返回值**。设计递归需要返回的内容,即提供给上一层的内容。相当于下层---->上层的接口。 diff --git a/算法/A类:基本算法/3.4.cpp b/算法/A类:基本算法/3.4.cpp new file mode 100644 index 00000000..e69de29b diff --git a/算法/A类:基本算法/4.2 大整数乘法问题.md b/算法/A类:基本算法/4.2 大整数乘法问题.md index b6902099..0fe5cda1 100644 --- a/算法/A类:基本算法/4.2 大整数乘法问题.md +++ b/算法/A类:基本算法/4.2 大整数乘法问题.md @@ -1,38 +1,333 @@ # 大整数乘法 -## 1 大整数乘法-分治法 +> 参考文献 +> * [大数乘法的问题及其高效的算法](https://blog.csdn.net/u010983881/article/details/77503519) ### 问题描述 -设u 和v 是两个n 位的整数(二进制),传统的乘法算法需要(n2) 数字相乘来计算u 和v 的乘积。 +* 题目描述: 输出两个不超过100位的大整数的乘积。 +* 输入: 输入两个大整数,如1234567 和 123 +* 输出: 输出乘积,如:151851741 +* 数字以字符串的形式给出。 +``` +求 1234567891011121314151617181920 * 2019181716151413121110987654321 的乘积结果 +``` + +### 问题分析 +* 所谓大数相乘(Multiplication algorithm),就是指数字比较大,相乘的结果超出了基本类型的表示范围,所以这样的数不能够直接做乘法运算。参考了很多资料,包括维基百科词条Multiplication algorithm,才知道目前大数乘法算法主要有以下几种思路: + + * 模拟小学乘法:最简单的乘法竖式手算的累加型; + * 分治乘法:最简单的是Karatsuba乘法,一般化以后有Toom-Cook乘法; + * 快速傅里叶变换FFT:(为了避免精度问题,可以改用快速数论变换FNTT),时间复杂度O(N lgN lglgN)。具体可参照Schönhage–Strassen algorithm; + * 中国剩余定理:把每个数分解到一些互素的模上,然后每个同余方程对应乘起来就行; + * Furer’s algorithm:在渐进意义上FNTT还快的算法。不过好像不太实用,本文就不作介绍了。大家可以参考维基百科Fürer’s algorithm + + +## 1 大数乘法-模拟小学乘法 + +### 算法设计 +* 模拟乘法手算累加 +``` + 7 8 9 6 5 2 +× 3 2 1 1 +----------------- + 7 8 9 6 5 2 <---- 第1趟 + 7 8 9 6 5 2 <---- 第2趟 + .......... <---- 第n趟 +----------------- + ? ? ? ? ? ? ? ? <---- 最后的值用另一个数组表示 +``` +* 如上所示,乘法运算可以分拆为两步: + 1. 首先将数位反置。个位数在低位数组,高位数在高位数组。 + 2. 是将乘数与被乘数逐位相乘将逐位相乘得到的结果,对应累计相加起来。r[i+j]+=a[i]*b[j]位置. + 3. 因为每个位置代表1位数。循环一边处理进位。/10的结果加到下一位。%10的结果保留到本位。 + +> 这个方法,简单易于解。只需要几行代码即可。 + +### 算法分析 + +* 时间复杂度O(n^2) +* 空间复杂度O(n) + +### 算法实现 + +```C++ +#include +#include +#include +using namespace std; + +vector mul_mul(vector a,vector b); +// 以字符串形式给出。还是得把这个形式改成整数数组比较好 +int main(){ + string a="123456789"; + string b="987654321"; + // 将字符串转换为整形数组。C的方法可以使用memset分配空间C++直接pushback + // 将大数数组反向存储。个位存在最开始的位置。方便数组增长。 + vector aa; + for(int i=a.size()-1;i>=0;i--){ + aa.push_back((int)(a[i]-'0')); + } + vector bb; + for(int i=b.size()-1;i>=0;i--){ + bb.push_back((int)(b[i]-'0')); + } + vector result = mul_mul(aa,bb); + + // 将最终的整数数组转换成字符串。并输出 + string res; + for(int i=result.size()-1;i>=0;i--){ + res+=to_string(result[i]); + } + // 去除前边多余的0 + res = res.substr(res.find_first_not_of('0')); + cout< mul_mul(vector a,vector b){ + vector a_b(a.size()+b.size()+1,0); + // 小学乘法 + for(int i=0;i `ac*bd`在暴力乘法当中,需要四次乘法运算。`ad,ab,cd,cb`。但是通过分治法。可以使得ad+bc在一次乘法中实现。即`(a+c)*(b+d)-ac-bd=ad+cb`.降低了乘法运算的数量。 ### 算法原理 -* 把每个整数分为两部分,每部分为n/2位,则u 和v 可重写为u = w2n/2 + x 和v = y2n/2 +z +![](image/2021-03-28-03-20-48.png) -* u 和v 的乘积可以计算为: -``` - uv = (w2n/2 + x)(y2n/2 +z) = wy2n + (wz + xy) 2n/2 + xz -``` -用2^n做乘法运算相当于简单地左移n 位,它需要θ( n) 时间。 +1. 我们假设要相乘的两个数是x * y。我们可以把x,y写成: +$$ +x = a * 10^{n/2} + b +\\ +y = c * 10^{n/2} + d +$$ +2. 这里的n是数字的位数。如果是偶数,则a和b都是n/2位的。如果n是奇数,则你可以让a是n/2+1位,b是n/2位。(例如a = 12,b = 34;a = 123,b = 45),那么x*y就变成了: +$$ +x*y = (a * 10^{n/2}+b) * (c * 10^{n/2}+d) +$$ +3. 进一步计算, +$$ +x*y = a * c * 10^n + (a * d + b * c) * 10^{n/2} + bd +$$ -* 考虑用以下恒等式计算wz + xy。 -``` -wz + xy = (w + x) ( y + z) – wy – xz -``` -* 由于wy 和 xz 不需要做二次计算,结合以上二式,仅需3次乘法运算,即 -``` -uv = wy2n + (( w + x) ( y + z) – wy –xz) 2n/2 + xz -``` +4. 对比之前的计算过程。结果已经呼之欲出了。这里唯一需要注意的两点就是: +> `(a * d + b * c)`的计算为了防止两次乘法,应该使用之前的计算这些乘法在算法里应该是递归实现的,数字很大时,先拆分,然后拆分出来的数字还是很大的话,就继续拆分,直到`a * b`已经是一个非常简单的小问题为之。这也是分治的思想。 + +### 算法设计 + +> 该方法比较复杂。 + +1. 首先根据手推的运算规则。发现在计算过程中需要实现四种计算。分别是:乘法计算、加法计算、减法计算、位移计算(乘以10的指数n。表示在末位添加n个零) +2. 分别实现。三种计算方式。并对高位的零进行处理。但是应该至少保留1位数(这位数如果是零。则表示该数字为零。) +3. 然后实现递归乘法运算。分以下几种情况实现 + 1. 递归的接口:两个大数。 + 2. 递归的返回值:大数乘积的结果。 + 3. 递归的处理:将大数分为两部分。分别实现`a*c,b*d,(a+b)*(c+d)`实现递归。 + 4. 递归的终止条件。相乘的两个数只有1位数字。结果如果为零,应该保留1位零。 + 5. 递归前的处理:数字对齐。在高位添加零。使得两个数字对齐。 + 6. 递归后的处理:将成绩的结果进行加法计算、减法计算、唯一计算。得到最后的结果 + +4. 难点在于:进行分治前:高位补零进行对齐。返回结果前去除高位多余的零,如果是0,则至少保留1位。 ### 算法效率 -* 此方法产生以下递推式 +* 时间复杂度:$O(n^{log_2 3}) = O(n^{1.59})$ +* 空间复杂度O(nlog n) + +### 算法实现 + +```C++ +#include +#include +#include +using namespace std; + +vector mul_mul(vector& a, vector& b); +// 大数加法 +vector mul_divided(vector x, vector y); +vector mul_sum(vector a, vectorb); +vector mul_minus(vectora, vectorb); +vector mul_pow(vectora, int n); +// 以字符串形式给出。还是得把这个形式改成整数数组比较好 +int main() { + string a = "99999999"; + string b = "77777777"; + // 将字符串转换为整形数组。C的方法可以使用memset分配空间C++直接pushback + // 将大数数组反向存储。个位存在最开始的位置。方便数组增长。 + vector aa; + for (int i = a.size() - 1; i >= 0; i--) { + aa.push_back((int)(a[i] - '0')); + } + vector bb; + for (int i = b.size() - 1; i >= 0; i--) { + bb.push_back((int)(b[i] - '0')); + } + //vector result = mul_mul(aa,bb); + vector result = mul_divided(aa, bb); + // 将最终的整数数组转换成字符串。并输出 + string res; + for (int i = result.size() - 1; i >= 0; i--) { + res += to_string(result[i]); + } + // 去除前边多余的0 + // res = res.substr(res.find_first_not_of('0')); + cout << res << endl; + return 0; +} + +// 模拟小学竖式。反转的字符串。个位在前边。 +vector mul_mul(vector& a, vector& b) { + vector a_b(a.size() + b.size(), 0); + // 小学乘法,i位置×j位置。在i+j位置保存结果。最后处理进位。 + for (int i = 0; i < a.size(); i++) { + for (int j = 0; j < b.size(); j++) { + a_b[i + j] += a[i] * b[j]; + } + } + // 对a_b中的数值进行处理(进位处理) + for (int i = 0; i < a_b.size(); i++) { + a_b[i + 1] += a_b[i] / 10; + a_b[i] %= 10; + } + //去除高位的0 + int i = a_b.size() - 1; + while (a_b[i] == 0) { + a_b.pop_back(); + i--; + } + return a_b; +} + +// 分治法。 +//反转的字符串。个位在前边。 +//假设等长只有等长的数据划分才会方便。 +// 如果等长情况下的处理 +//关于是相乘结果是0的情况应该特殊处理。 +vector mul_divided(vector x, vector y) { + //对x,y短的进行补齐.高位补齐0 + while (x.size() < y.size()) { + x.push_back(0); + } + while (y.size() < x.size()) { + y.push_back(0); + } + // 只在末端做一次乘法 + vector result(x.size() + y.size()); + if (x.size() == 1 && y.size() == 1) { + result[0] = x[0] * y[0]; + result[1] = result[0] / 10; + result[0] = result[0] % 10; + //去除高位的0 + int i = result.size() - 1; + //但是保留唯一的0;用来表示这是一个0 + while (result.size()>0 && result[i] == 0) { + result.pop_back(); + i--; + } + return result; + } + + int n = x.size() / 2; + // 分割成a,b,c,d四个部分 + vector b(x.begin(), x.begin() + n); + vector a(x.begin() + n, x.end()); + vector d(y.begin(), y.begin() + n); + vector c(y.begin() + n, y.end()); + + // 进行递归 + vector mul1 = mul_divided(a, c); + vector mul2 = mul_divided(b, d); + vector mul_ab_cd = mul_divided(mul_sum(a, b), mul_sum(c, d)); + + // 加法。个位在前边。 + vector sum_ac_bd = mul_sum(mul1, mul2); + vector mul3 = mul_minus(mul_ab_cd, sum_ac_bd); + + + result = mul_sum(mul_sum(mul_pow(mul1, 2 * n), mul2), mul_pow(mul3, n)); + return result; +} + +// 大数加法。 +vector mul_sum(vector a, vector b) { + vector result(max(a.size(), b.size()) + 1, 0); + for (int i = 0; i < result.size() - 1; i++) { + if (i < a.size()) { + result[i] += a[i]; + } + if (i < b.size()) { + result[i] += b[i]; + } + // 处理进位 + result[i + 1] += result[i] / 10; + result[i] %= 10; + } + //去除高位的0 + int i = result.size() - 1; + while (result.size() > 0 && result[i] == 0) { + result.pop_back(); + i--; + } + return result; +} +// 大数减法(结果不可能是负数) +vector mul_minus(vector a, vectorb) { + vector result(a.size(), 0); + // 做减法 + for (int i = 0; i < a.size(); i++) { + if (i < b.size()) { + result[i] += a[i] - b[i]; + } + else { + result[i] += a[i]; + } + // 处理借位 + if (result[i] < 0) { + result[i] += 10; + result[i + 1] -= 1; + } + } + //去除高位的0 + int i = result.size() - 1; + while (result.size() > 0 && result[i] == 0) { + result.pop_back(); + i--; + } + return result; +} +// 向右移位 +vector mul_pow(vector a, int n) { + vector result(n, 0); + result.insert(result.end(), a.begin(), a.end()); + //去除高位的0 + int i = result.size() - 1; + while (result.size() > 0 && result[i] == 0) { + result.pop_back(); + i--; + } + return result; +} ``` -T (n) = d 若n = 1 - = 3T(n/2) +bn 若 n>1 -``` -* 定理得出 -$$ -T(n) =Θ(n^log 3) = O(n^1.59) -$$ \ No newline at end of file diff --git a/算法/A类:基本算法/4.2.cpp b/算法/A类:基本算法/4.2.cpp new file mode 100644 index 00000000..091dc963 --- /dev/null +++ b/算法/A类:基本算法/4.2.cpp @@ -0,0 +1,170 @@ +#include +#include +#include +using namespace std; + +vector mul_mul(vector& a, vector& b); +// 大数加法 +vector mul_divided(vector x, vector y); +vector mul_sum(vector a, vectorb); +vector mul_minus(vectora, vectorb); +vector mul_pow(vectora, int n); +// 以字符串形式给出。还是得把这个形式改成整数数组比较好 +int main() { + string a = "999999999999999999999999999999999999999999"; + string b = "777777777777777777777777777777777777777777777777"; + // 将字符串转换为整形数组。C的方法可以使用memset分配空间C++直接pushback + // 将大数数组反向存储。个位存在最开始的位置。方便数组增长。 + vector aa; + for (int i = a.size() - 1; i >= 0; i--) { + aa.push_back((int)(a[i] - '0')); + } + vector bb; + for (int i = b.size() - 1; i >= 0; i--) { + bb.push_back((int)(b[i] - '0')); + } + // vector result = mul_mul(aa,bb); + vector result = mul_divided(aa, bb); + // 将最终的整数数组转换成字符串。并输出 + string res; + for (int i = result.size() - 1; i >= 0; i--) { + res += to_string(result[i]); + } + // 去除前边多余的0 + // res = res.substr(res.find_first_not_of('0')); + cout << res << endl; + return 0; +} + +// 模拟小学竖式。反转的字符串。个位在前边。 +vector mul_mul(vector& a, vector& b) { + vector a_b(a.size() + b.size(), 0); + // 小学乘法,i位置×j位置。在i+j位置保存结果。最后处理进位。 + for (int i = 0; i < a.size(); i++) { + for (int j = 0; j < b.size(); j++) { + a_b[i + j] += a[i] * b[j]; + } + } + // 对a_b中的数值进行处理(进位处理) + for (int i = 0; i < a_b.size(); i++) { + a_b[i + 1] += a_b[i] / 10; + a_b[i] %= 10; + } + //去除高位的0 + int i = a_b.size() - 1; + while (a_b[i] == 0) { + a_b.pop_back(); + i--; + } + return a_b; +} + +// 分治法。 +//反转的字符串。个位在前边。 +//假设等长只有等长的数据划分才会方便。 +// 如果等长情况下的处理 +//关于是相乘结果是0的情况应该特殊处理。 +vector mul_divided(vector x, vector y) { + //对x,y短的进行补齐.高位补齐0 + while (x.size() < y.size()) { + x.push_back(0); + } + while (y.size() < x.size()) { + y.push_back(0); + } + // 只在末端做一次乘法 + vector result(x.size() + y.size()); + if (x.size() == 1 && y.size() == 1) { + result[0] = x[0] * y[0]; + result[1] = result[0] / 10; + result[0] = result[0] % 10; + //去除高位的0 + int i = result.size() - 1; + //但是保留唯一的0;用来表示这是一个0 + while (result.size()>0 && result[i] == 0) { + result.pop_back(); + i--; + } + return result; + } + + int n = x.size() / 2; + // 分割成a,b,c,d四个部分 + vector b(x.begin(), x.begin() + n); + vector a(x.begin() + n, x.end()); + vector d(y.begin(), y.begin() + n); + vector c(y.begin() + n, y.end()); + + // 进行递归 + vector mul1 = mul_divided(a, c); + vector mul2 = mul_divided(b, d); + vector mul_ab_cd = mul_divided(mul_sum(a, b), mul_sum(c, d)); + + // 加法。个位在前边。 + vector sum_ac_bd = mul_sum(mul1, mul2); + vector mul3 = mul_minus(mul_ab_cd, sum_ac_bd); + + + result = mul_sum(mul_sum(mul_pow(mul1, 2 * n), mul2), mul_pow(mul3, n)); + return result; +} + +// 大数加法。 +vector mul_sum(vector a, vector b) { + vector result(max(a.size(), b.size()) + 1, 0); + for (int i = 0; i < result.size() - 1; i++) { + if (i < a.size()) { + result[i] += a[i]; + } + if (i < b.size()) { + result[i] += b[i]; + } + // 处理进位 + result[i + 1] += result[i] / 10; + result[i] %= 10; + } + //去除高位的0 + int i = result.size() - 1; + while (result.size() > 0 && result[i] == 0) { + result.pop_back(); + i--; + } + return result; +} +// 大数减法(结果不可能是负数) +vector mul_minus(vector a, vectorb) { + vector result(a.size(), 0); + // 做减法 + for (int i = 0; i < a.size(); i++) { + if (i < b.size()) { + result[i] += a[i] - b[i]; + } + else { + result[i] += a[i]; + } + // 处理借位 + if (result[i] < 0) { + result[i] += 10; + result[i + 1] -= 1; + } + } + //去除高位的0 + int i = result.size() - 1; + while (result.size() > 0 && result[i] == 0) { + result.pop_back(); + i--; + } + return result; +} +// 向右移位 +vector mul_pow(vector a, int n) { + vector result(n, 0); + result.insert(result.end(), a.begin(), a.end()); + //去除高位的0 + int i = result.size() - 1; + while (result.size() > 0 && result[i] == 0) { + result.pop_back(); + i--; + } + return result; +} diff --git a/算法/A类:基本算法/5 动态规划.md b/算法/A类:基本算法/5 动态规划.md index 4e672dcf..6c0102da 100644 --- a/算法/A类:基本算法/5 动态规划.md +++ b/算法/A类:基本算法/5 动态规划.md @@ -17,17 +17,8 @@ 2. **无后效性**:即某阶段状态一旦确定,就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响以前的状态,只与当前状态有关。 3. **有重叠子问题**:即子问题之间是**不独立**的,一个子问题在下一阶段决策中可能被多次使用到。(该性质并不是动态规划适用的必要条件,但是如果没有这条性质,动态规划算法同其他算法相比就不具备优势) -### 基本步骤 -1. 找出最优解的性质,刻画其结构特征。 -2. 递推地定义最优值。需要给出状态转移变量 -$$ -f(n)=f(n-1)+f(n-2) -$$ -3. 以自底向上的方式计算出最优解。 -4. 根据计算最优值时的到的信息,构造最有解。 - ### 要素 -* 最有子结构 +* 最优子结构 * 重叠子问题 * 备忘录方法(矩阵表格) @@ -44,15 +35,25 @@ $$ * 顺序解法和逆序解法只表示行进方向的不同或始端的颠倒。但用动态规划方法求最优解时,都是在行进方向规定后,均要逆着这个规定的行进方向,从最后一段向前逆推计算,逐段找出最优途径 -### 构建动态规划模型 +## 2 构建动态规划模型 -1. 正确选择状态变量xk, 使它既能描述过程的状态,又要满足无后效性。动态规划中的状态与一般所说的状态概念是不同的,它必须具有三个特性: +### 动态规划问题的分析 +1. 找出**最优解的性质**,刻画其结构特征。 +2. 递推地定义最优值。需要给出**状态转移变量** +$$ +f(n)=f(n-1)+f(n-2) +$$ +3. 以自底向上的方式**计算出最优解**。 +4. 根据计算最优值时的到的信息,**构造最有解**。 + +### 动态规划算法的步骤 +1. 正确选择**状态变量**xk, 使它既能描述过程的状态,又要满足无后效性。动态规划中的状态与一般所说的状态概念是不同的,它必须具有三个特性: * 要能够用来描述受控过程的演变特征。 * 要满足无后效性。 所谓无后效性是指:如果某段状态给定,则在这段以后过程的发展不受前面各阶段状态的影响。 * 可知性。即是规定的各段状态变量的值,由直接或间接都是可以知道的。 -2. 确定决策变量uk及每段的允许决策集合Dk(xk)={uk} -3. 写出状态转移方程如果给定第k段状态变量xk的值,则该段的决策变量uk一经确定,第k+1段状态变量xk+1的值也就完全确定。 -4. 列出指标函数Vk,n 关系,并要满足递推性。 +2. 确定**决策变量**uk及每段的允许决策集合Dk(xk)={uk} +3. 写出**状态转移方程**如果给定第k段状态变量xk的值,则该段的决策变量uk一经确定,第k+1段状态变量xk+1的值也就完全确定。 +4. 列出**指标函数**Vk,n 关系,并要满足递推性。 ### 动态规划最关键的部分 * 确定规模的增长方向。一般在动态规划问题中。规模可变的并不只有一个。比如在正则表达式与字符串的匹配问题中。字符串的规模可以变化,正则表达式的规模也可以变化。 diff --git a/算法/A类:基本算法/5.12 n个骰子的点数.md b/算法/A类:基本算法/5.12 n个骰子的点数.md new file mode 100644 index 00000000..e69de29b diff --git a/算法/A类:基本算法/image/2021-03-28-03-17-52.png b/算法/A类:基本算法/image/2021-03-28-03-17-52.png new file mode 100644 index 00000000..72fced9b Binary files /dev/null and b/算法/A类:基本算法/image/2021-03-28-03-17-52.png differ diff --git a/算法/A类:基本算法/image/2021-03-28-03-20-48.png b/算法/A类:基本算法/image/2021-03-28-03-20-48.png new file mode 100644 index 00000000..2ff60795 Binary files /dev/null and b/算法/A类:基本算法/image/2021-03-28-03-20-48.png differ