大数乘法(分治思想)
代码如下:
#include <stdlib.h>
#include <cstring>
#include <iostream>
using namespace std;
#define M 100
char sa[1000];
char sb[1000];
typedef struct _Node { // 面向过程的语言 面向对象 其实封装性 C语言可以完成这个思想 函数指针赋值
int s[M];
int l; // 长度
int c; // 补位
} Node, *pNode;
void cp(pNode src, pNode des, int st, int l) {
int i, j;
for (i = st, j = 0; i < st + l; i++, j++) {
des->s[j] = src->s[i];
}
des->l = l;
des->c = st + src->c;
}
void add(pNode pa, pNode pb, pNode ans) {
int i, cc, k, palen, pblen, len;
int ta, tb;
pNode temp;
//保证pa是高次幂
if ((pa->c < pb->c)) {
temp = pa;
pa = pb;
pb = temp;
}
ans->c = pb->c; //结果的幂取最少的幂
cc = 0;
palen = pa->l + pa->c; //pa的长度
pblen = pb->l + pb->c; //pb的长度
if (palen > pblen) //选取最长的长度
len = palen;
else
len = pblen;
k = pa->c - pb->c; //k是幂差,len是最长的位数
for (i = 0; i < len - ans->c; i++) {
if (i < k)
ta = 0;
else
ta = pa->s[i - k];
if (i < pb->l)
tb = pb->s[i];
else
tb = 0;
if (i >= pa->l + k)
ta = 0;
ans->s[i] = (ta + tb + cc) % 10;
cc = (ta + tb + cc) / 10;
}
if (cc)
ans->s[i++] = cc;
ans->l = i;
}
void mul(pNode pa, pNode pb, pNode ans) {
int i, cc, w;
int ma = pa->l >> 1, mb = pb->l >> 1; // 位运算 右移1位 ,相当于是除以2 右移两位是除以4 右移n位 除以 2^n
Node ah, al, bh, bl;
Node t1, t2, t3, t4, z;
pNode temp;
if (!ma || !mb) { // 除以2为0 则为证明前面至少有一个字符串为1
if (!ma) { //如果pa是一位数,则和pb交换
temp = pa;
pa = pb;
pb = temp;
}
ans->c = pa->c + pb->c;
w = pb->s[0]; //pb可能为一位数
cc = 0;
for (i = 0; i < pa->l; i++) {
//pa为2位数以上
ans->s[i] = (w * pa->s[i] + cc) % 10; // 取余运算: 取出个位数 23 % 10 =3
cc = (w * pa->s[i] + cc) / 10; // 取余运算: 取出十位上的数 23 / 10 = 2
}
if (cc)
ans->s[i++] = cc; // 升幂
ans->l = i;
return;
}
cp(pa, &ah, ma, pa->l - ma); //高位升幂 12 升幂
cp(pa, &al, 0, ma); //低位幂不变 3
cp(pb, &bh, mb, pb->l - mb);
cp(pb, &bl, 0, mb);
mul(&ah, &bh, &t1);
mul(&ah, &bl, &t2);
mul(&al, &bh, &t3);
mul(&al, &bl, &t4);
add(&t3, &t4, ans);
add(&t2, ans, &z);
add(&t1, &z, ans);
}
int main() {
Node ans, a, b;
// C++ 输出函数
cout << "输入大整数 a:" << endl;
// 输入函数
cin >> sa;
cout << "输入大整数 b:" << endl;
cin >> sb;
# 求字符串的长度
a.l = strlen(sa);
b.l = strlen(sb);
int z = 0, i;
# 字符串反转的过程
for (i = a.l - 1; i >= 0; i--)
a.s[z++] = sa[i] - '0';
a.c = 0;
z = 0;
for (i = b.l - 1; i >= 0; i--)
b.s[z++] = sb[i] - '0';
b.c = 0;
mul(&a, &b, &ans);
cout << "最终结果为:";
for (i = ans.l - 1; i >= 0; i--)
cout << ans.s[i];
cout << endl;
return 0;
}
该算法与Karatsuba
算法的算法思想类似,接下来先介绍一下Karatsuba
算法
Karatsuba算法
Karatsuba
算法是一种快速乘法算法,由Anatolii Alexeevitch Karatsuba
在1960年发现。这个算法基于分治策略,可以比传统的长乘法(也就是小学时我们学到的按位乘法)更高效地执行大数乘法运算。它主要通过减少乘法操作的次数来提升计算速度,尤其是在处理非常大的数字时。
基本思想
考虑两个大整数X
和Y
,我们可以将它们表示为:
X = A * B^m + B
Y = C * B^m + D
其中,A
和C
是X
和Y
的高位部分,B
和D
是低位部分,B^m
表示基数B
的m
次幂,通常B
取10,并且根据实际情况选择m
值,使得A
、B
、C
、D
大致位于原数的中间位置。
在传统乘法中,X
* Y
会被展开成四个乘积:A*C, A*D, B*C, B*D
。然而,Karatsuba
观察到只需进行三次乘法就可以得到最终结果:
- 计算
A * C
- 计算
B * D
- 计算
(A + B) * (C + D)
,得到的结果中减去第1步和第2步的结果,即(A+B)*(C+D) - A*C - B*D = A*D + B*C
Karatsuba算法的步骤
给定两个大整数X
和Y
,执行以下步骤:
-
分割数字:将
X
和Y
分别分割成两部分,X = A*10^n + B
与Y = C*10^n + D
。 -
递归地计算乘积:
- 计算
A*C
- 计算
B*D
- 计算
(A+B)*(C+D)
- 计算
-
组合结果:利用这些中间结果计算:
X
*Y
X*Y = A*C*10^(2*n) + ((A+B)*(C+D) - A*C - B*D)*10^n + B*D
效率分析
标准的长乘法的时间复杂度是O(n^2)
,其中n
是数字的位数。而Karatsuba算法将这个复杂度降低到了O(n^log2(3)) ≈ O(n^1.585)
,显著提高了大数乘法的效率。
应用场景
Karatsuba算法特别适合于大整数的乘法运算,在计算机科学中广泛应用,尤其是在密码学、大数运算库等领域。随着需要处理的数据量不断增加,这种快速乘法算法变得尤为重要。
总结
Karatsuba算法是分治策略的一个经典应用,它通过减少必须执行的乘法次数,实现了比传统方法更快的乘法运算。这种算法不仅是计算机科学的一个重要成就,也是对如何优化问题解决方案的深刻洞察。
回到我们的代码中来,我们分析一下改代码的算法的实现:
这段代码是一个实现大整数乘法的程序,采用分治策略以及类似于Karatsuba算法的思路进行优化,提高了大数乘法的效率。通过将大数拆分成较小的部分,递归地计算这些小部分的乘积,然后合并结果来得到最终的乘积。下面我将逐步解释关键函数和过程中的数学概念和细节。
数据结构 _Node
首先定义了一个_Node
结构体用于表示大整数。其中,s[M]
是用来存储数字的数组,每个位置存储一位数字;l
表示当前数字的长度(即位数);c
是补位信息,用于在进行乘法操作时处理幂次问题。
函数 cp
cp(pNode src, pNode des, int st, int l)
函数的作用是将源节点src
中从st
开始的l
长度的数字复制到目标节点des
中。这里主要用于分割大整数的高低位。同时更新目标节点的长度l
和补位c
。
函数 add
add(pNode pa, pNode pb, pNode ans)
函数实现了两个大整数的加法操作,考虑了进位和位数不等的情况。结果保存在ans
中。这在合并乘法的中间结果时非常重要。
函数 mul
mul(pNode pa, pNode pb, pNode ans)
是核心函数,实现了大整数的乘法。如果其中一个数字长度为1,直接计算乘法并处理进位即可。如果长度大于1,则使用分治策略:
- 将两个大整数
pa
和pb
分别从中点分割为高位(ah
,bh
)和低位(al
,bl
)。 - 然后分别计算:
- 高位与高位的乘积(
mul(&ah, &bh, &t1)
) - 高位与低位的乘积(
mul(&ah, &bl, &t2)
和mul(&al, &bh, &t3)
) - 低位与低位的乘积(
mul(&al, &bl, &t4)
)
- 高位与高位的乘积(
- 最后,将这些中间结果正确合并到最终结果中。这个合并操作需要注意幂的处理和加法操作。
这种分治方法的关键思想是将一个大规模问题分解成较小的问题,这些较小的问题更容易解决,最后再将它们的解组合起来得到原问题的解。通过递归应用这个策略,可以有效减少乘法操作的总次数,从而提高算法的效率。
主函数 main
在main
函数中,首先读入两个表示大整数的字符串,然后将它们转换成_Node
类型,确保数字是逆序存储的(因为数学运算是从最低位开始进行的)。之后调用mul
函数计算乘积,并将结果打印出来。
有了一个总体的认识,我们来看看一些细节的部分,首先是核心的部分:
cp(pa, &ah, ma, pa->l - ma); //高位升幂 12 升幂
cp(pa, &al, 0, ma); //低位幂不变 3
cp(pb, &bh, mb, pb->l - mb);
cp(pb, &bl, 0, mb);
mul(&ah, &bh, &t1);
mul(&ah, &bl, &t2);
mul(&al, &bh, &t3);
mul(&al, &bl, &t4);
add(&t3, &t4, ans);
add(&t2, ans, &z);
add(&t1, &z, ans);
这部分代码是实现大整数乘法的核心:
分割操作
- 高位和低位的分割
cp(pa, &ah, ma, pa->l - ma);
:将pa
的高位部分复制到ah
中。这里,ma
是pa
的长度除以2后取得的中点位置,所以pa->l - ma
表示高位部分的长度。通过这一步,我们得到了pa
的高位部分。cp(pa, &al, 0, ma);
:将pa
的低位部分复制到al
中。这里从索引0开始,直到ma
结束,表示低位部分。- 类似地,
pb
也被分割为高位部分bh
和低位部分bl
。
通过这种方式,大整数pa
和pb
都被分割成了两部分,即它们各自的高位和低位。这样做的目的是为了应用分治策略,将原来的大数乘法问题转化为较小数的乘法问题。
乘法操作
- 分别计算乘积
mul(&ah, &bh, &t1);
:计算pa
和pb
的高位部分的乘积,并把结果存储在t1
中。mul(&ah, &bl, &t2);
:计算pa
的高位部分与pb
的低位部分的乘积,并把结果存储在t2
中。mul(&al, &bh, &t3);
:计算pa
的低位部分与pb
的高位部分的乘积,并把结果存储在t3
中。mul(&al, &bl, &t4);
:计算pa
和pb
的低位部分的乘积,并把结果存储在t4
中。
通过上述操作,我们分别得到了四个乘积:两个高位的乘积t1
、一个高位和一个低位的乘积t2
和t3
、两个低位的乘积t4
。
合并操作
- 合并乘积
add(&t3, &t4, ans);
:首先,将t3
和t4
的结果相加,得到的中间结果存储在ans
中。add(&t2, ans, &z);
:然后,将t2
与上一步的结果ans
相加,得到的中间结果存储在z
中。add(&t1, &z, ans);
:最后,将t1
与上一步的结果z
相加,最终结果存储在ans
中。
这个合并过程是基于Karatsuba乘法的关键步骤,正确地将分治策略中得到的部分乘积组合起来得到最终的乘积结果。
细节:在该算法中是怎么实现升幂操作
升幂的含义
首先,我们需要理解“升幂”在大数乘法上下文中的含义。给定两个大数A和B,它们的乘积C可以看作是若干部分乘积的和,这些部分乘积根据其权重(即在结果中代表的位数)不同被“升幂”。例如,如果A和B都是两位数,那么它们的乘积可以表示为(A的高位×B的高位)×10^2 + ((A的高位×B的低位 + A的低位×B的高位)×10^1 + (A的低位×B的低位),其中每个括号内的乘积需要根据它们代表的位数进行相应的升幂操作。
实现升幂
在这个算法中,升幂实际上是通过以下几点隐式实现的:
- 结果合并:在
add
函数中进行结果合并时,由于我们是按照从低位到高位的顺序进行加法运算的,每个乘积部分的位置已经事先通过切割点的选择正确设定。比如mul(&ah, &bh, &t1);
得到的乘积t1
实际上代表了最高位的部分,因此在最后合并回最终结果时,它自然成为了最终乘积的最高位部分。 - 索引与补位:各部分乘积之间的相对位置(即它们的升幂信息)通过它们在数组中的索引和补位
c
来隐式地处理。每个节点的c
属性记录了该节点所代表的数字相对于原始数据的偏移量,即它们的升幂状态。当执行add
操作时,通过比较和调整这些偏移量来正确地把几个部分的结果加在一起。 - 大小调整:通过递归地对原始数字进行分割,然后再将这些部分的结果逐步合并,每次合并都隐含了升幂操作。这是因为我们在合并较小部分的乘积结果时,会根据这些部分在原始数字中的位置来确定它们在最终结果中的位置(即升幂)。
总的来说,这个算法中的升幂操作是通过控制乘积部分的相对位置(通过递归分割和结果合并),以及使用节点的补位属性c
来隐式管理的,而不是通过显式的数学公式或操作来实现的。这种方法有效地支持了大数乘法的分治策略,同时保持了算法的效率和简洁性。
细节:add函数逐步分析
函数原型
void add(pNode pa, pNode pb, pNode ans)
pa
和pb
分别指向参与加法运算的两个大整数。ans
用于存储加法运算的结果。
步骤解析
- 确保
pa
是高次幂:如果pa
的补位(pa->c
)小于pb
的补位(pb->c
),则交换pa
和pb
。这样做是为了确保pa
代表的数字在加法运算中作为基准,从而简化后续计算。 - 初始化结果的幂次:
ans->c
被设置为pb->c
,即结果的幂次取两者之间较小的幂次,这有助于后续正确地对齐数字进行加法运算。 - 确定运算长度:
- 计算
pa
和pb
的实际长度,包括其补位(即palen = pa->l + pa->c
,pblen = pb->l + pb->c
)。 - 选取
palen
和pblen
中较大值作为加法运算的范围,确保所有有效数字都会被处理。
- 计算
- 进行逐位加法:
- 遍历每一位,根据当前索引
i
及补位差k = pa->c - pb->c
,从pa
和pb
中取出相应位的数字进行加法。如果某一数组已经没有更多的位可用,则这部分的值视为0。 - 将当前位的加法结果加上前一位的进位
cc
,并计算当前位的值以及新的进位值。这里使用%
和/
运算符来分别获取加法结果的个位和进位。 - 更新结果数组
ans->s[i]
和进位cc
。
- 遍历每一位,根据当前索引
- 处理最后的进位:如果在完成所有位的加法后仍有进位(
cc != 0
),则需要将该进位添加到结果的下一位。 - 确定结果长度:更新结果数字
ans
的长度ans->l
,确保它正确反映了加法运算后数字的真实长度。
在add
方法中确定升幂和补0
在add
函数中,升幂和补0(乘以10^n)主要通过对参与加法操作的两个数的补位c
进行比较和调整来实现:
- 补位的意义:
c
表示当前数字相对于某个基准的偏移量,或可以理解为该数字需要向左移动的位数(即低位补0的个数)。 - 确定运算长度和操作:通过计算两个数的实际长度(包含补位)和它们之间的补位差异,
add
函数能够正确地对这两个数进行对齐,并逐位相加。更高的补位意味着一个数在加法运算中“看起来”更小(因为它相当于被除以了10^n),从而实现了升幂的效果。
细节:cp
函数逐步解释
cp
函数的作用是将源节点的一部分复制到目标节点,同时更新目标节点的长度l
和补位c
。
void cp(pNode src, pNode des, int st, int l) {
int i, j;
for (i = st, j = 0; i < st + l; i++, j++) {
des->s[j] = src->s[i];
}
des->l = l;
des->c = st + src->c;
}
- 参数说明:
src
:源节点指针,表示待复制的大整数。des
:目标节点指针,用于存放复制结果。st
:开始复制的起始索引,相对于src->s
数组。l
:需要复制的长度。
- 执行步骤:
- 循环复制:从
src->s[st]
开始,复制l
长度的数字到des->s
中。这里使用两个变量i
(遍历src
)和j
(遍历des
),确保正确地复制了指定范围内的数字。 - 更新长度:将
des->l
设置为复制的长度l
。 - 更新补位:
des->c
被更新为st + src->c
。这一步是关键,它考虑了从源节点复制的起始点st
,将其加上原节点的补位src->c
,得到新节点的补位。这相当于在数字上进行了升幂操作。例如,如果从src
的第3位开始复制(假设每一位代表一个十进制位),那么des
相对于src
实际上就是乘以了10^3。
- 循环复制:从
cp
函数怎么实现升幂
在cp
函数中,升幂是通过更新目标节点的补位c
来实现的。当我们从源节点的非零起始位置复制数据时,这个起始位置(加上源节点原有的补位)直接转化为目标节点的补位,相当于给目标节点的数字值乘以了相应的10的幂次(因为每个增加的补位都是向数字的右端添加一个零)。这样,cp
函数不仅复制了数字的一部分,还隐式地实现了升幂操作,为后续的数学运算提供了方便。
如果觉得文章对您有帮助,请帮忙点赞或者收藏,如果在文章中发现什么错误或不准确的地方,欢迎与我交流。