C++中2个64位数字的乘法
Multiplication of 2 64-digit numbers in C++
我实现了 karatsuba 乘法算法。我想以这种方式改进它,我可以将 2 个 64 位数字相乘,但我不知道该怎么做。我得到一个提示,这两个数字都包含 2 的幂的位数,但它没有给我任何提示。你能给任何其他提示吗?数学提示或算法改进提示。
#include <iostream>
#include <math.h>
using namespace std;
int getLength(long long value);
long long multiply(long long x, long long y);
int getLength(long long value)
{
int counter = 0;
while (value != 0)
{
counter++;
value /= 10;
}
return counter;
}
long long multiply(long long x, long long y)
{
int xLength = getLength(x);
int yLength = getLength(y);
// the bigger of the two lengths
int N = (int)(fmax(xLength, yLength));
// if the max length is small it's faster to just flat out multiply the two nums
if (N < 10)
return x * y;
//max length divided and rounded up
N = (N / 2) + (N % 2);
long long multiplier = pow(10, N);
long long b = x / multiplier;
long long a = x - (b * multiplier);
long long d = y / multiplier;
long long c = y - (d * N);
long long z0 = multiply(a, c);
long long z1 = multiply(a + b, c + d);
long long z2 = multiply(b, d);
return z0 + ((z1 - z0 - z2) * multiplier) + (z2 * (long long)(pow(10, 2 * N)));
}
int main()
{
long long a;
long long b;
cin >> a;
cout << '\n';
cin >> b;
cout << '\n' << multiply(a, b) << endl;
return 0;
}
这里有一个提示:
(A + kB) * (C + kD) = AC + k(BC + AD) + k^2(BD)
如果 k
是您保留数字的基数的幂,这会有所帮助。例如,如果 k
是 1'000'000'000 并且您的数字是以 10 为基数,然后通过简单地移动数字(添加零)来完成乘以 k
。
无论如何,请考虑将您的 64 位数字分成两部分,每部分 32 位,然后像上面那样进行数学运算。要计算 AC
、BC
、AD
和 BD
,您需要将一对 32 位数字相乘,计算方法类似。
由于您的位数是 2 的幂,因此您可以继续将数字分成两半,直到达到可管理的数字大小(例如 1 位数)。
顺便说一句,从你的问题中不清楚你是在谈论 64 位还是 64 位十进制数字。如果您要查找的只是 64 位数字的乘法,只需执行以下操作:
// I haven't actually run this code, so...
typedef unsigned long long u64;
u64 high32 (u64 x) {return x >> 32;}
u64 low32 (u64 x) {return x & 0xFFFFFFFF;}
u64 add_with_carry (u64 a, u64 b, u64 * carry)
{
u64 result = a + b;
*carry = (result < a ? 1 : 0);
return result;
}
void mul (u64 a, u64 b, u64 * result_low, u64 * result_high)
{
u64 a0 = low32(a), a1 = high32(a);
u64 b0 = low32(b), b1 = high32(b);
u64 a0b0 = a0 * b0;
u64 a0b1 = a0 * b1;
u64 a1b0 = a1 * b0;
u64 a1b1 = a1 * b1;
u64 c0 = 0, c1 = 0;
u64 mid_part = add_with_carry(a0b1, a1b0, &c1);
*result_low = add_with_carry(a0b0, (low32(mid_part) << 32, &c0);
*result_high = high32(mid_part) + a1b1 + (c1 << 32) + c0; // this won't overflow
}
这个实现与上面概述的想法相同。由于在标准C/C++中,乘法的结果中我们最多可以有64位有意义的位,那么我们一次只能将两个32位的数相乘。这就是我们在这里所做的。
最终结果将是 128 位,我们 return 在两个无符号 64 位数字中。我们通过 4 个 32 位乘法和一些加法来进行 64 位乘 64 位乘法。
附带说明一下,这是编写汇编通常比 C 容易得多的少数情况之一。例如,在 x64 汇编中,这实际上是一个 mul
指令,它乘以两个64 位无符号整数和 return 两个 64 位寄存器中的 128 位结果。
即使你没有 64 位到 128 位的乘法指令,仍然用汇编写这个更容易(因为 adc
或类似的指令,例如上面的整个代码只是两个mov
s、四个 mul
s、四个 add
s 和四个 adc
s 在普通 x86 程序集中。)即使你不想用汇编写,你应该检查你的编译器的 "intrinsics". 它可能有一个用于大乘法的(但你会是 platform-dependent.)
要在较低位宽的算术上应用 Karatsuba 或任何其他乘法,您需要将数字分成更小的 "digits"。在做任何其他事情之前,您需要访问这些 "digits" 所以这里是如何做的:
你得到了数字 1234
并想将它分成 10^1
位所以
1234 = 1*1000 + 2*100 + 3*10 + 4
你可以获得这样的数字:
x=1234;
a0=x%10; x/=10; // 4
a1=x%10; x/=10; // 3
a2=x%10; x/=10; // 2
a3=x%10; x/=10; // 1
如果你想要 10^2
个数字,那么:
x=1234;
a0=x%100; x/=100; // 34
a1=x%100; x/=100; // 12
现在的问题是,要做到这一点,您需要对全部数字进行除法,而您没有。如果您将数字作为字符串,那么它很容易完成,但假设您没有。计算机基于二进制计算,因此使用 2 的幂作为 "digits" 的基数是个好主意,因此:
x = 1234 = 0100 1101 0010 bin
现在如果我们想要 2^4=16
个基本数字,那么:
a0=x%16; x/=16; // 0010
a1=x%16; x/=16; // 1101
a2=x%16; x/=16; // 0100
现在如果你意识到除以 2 的幂只是右移,余数可以表示为 AND 那么:
a0=x&15; x>>=4; // 0010
a1=x&15; x>>=4; // 1101
a2=x&15; x>>=4; // 0100
移位可以堆叠到任何位宽数字,所以现在您已经拥有了您需要的一切。但这还不是全部,如果你 select 作为 "digit" 例如 2^8
即 BYTE
那么你可以使用指针来代替,例如:
DWORD x=0x12345678; // 32 bit number
BYTE *db=(BYTE*)(&x); // 8bit pointer that points to x
a0=db[0]; // 0x78
a1=db[1]; // 0x56
a2=db[2]; // 0x34
a3=db[3]; // 0x12
因此您可以直接访问数字或从数字重建 x:
DWORD x; // 32 bit number
BYTE *db=(BYTE*)(&x); // 8bit pointer that points to x
db[0]=0x78;
db[1]=0x56;
db[2]=0x34;
db[3]=0x12;
// here x should be 0x12345678
请注意,顺序取决于平台 MSB 或 LSB 的第一顺序。现在你可以应用乘法了。例如 32*32=64 位在 16 位乘法上完成是这样用天真的 O(n^2)
方法完成的:
x(a0+a1<<16) * y(b0+b1<<16) = a0*b0 + a0*b1<<16 + a1*b0<<16 + a1*b1<<32
其中a0,a1,b0,b1为操作数的位数。请注意,每个 ai*bj
乘法的结果都是 2 位宽,因此您需要将其拆分为数字并存储到由移位寻址的结果数字中。请注意,添加会导致溢出到更高的数字。要处理这个问题,您需要使用至少两倍的算术宽度进行加法(16*16 位乘法 -> 32 位加法)或使用进位标志。遗憾的是,除了在 C++ 中使用汇编之外,您无法访问进位标志。幸运的是它可以被模拟见:
- Cant make value propagate through carry
现在您可以构建 Karatsuba 或更高级的乘法以获取更多信息,请参阅:
- Fast bignum square computation
我实现了 karatsuba 乘法算法。我想以这种方式改进它,我可以将 2 个 64 位数字相乘,但我不知道该怎么做。我得到一个提示,这两个数字都包含 2 的幂的位数,但它没有给我任何提示。你能给任何其他提示吗?数学提示或算法改进提示。
#include <iostream>
#include <math.h>
using namespace std;
int getLength(long long value);
long long multiply(long long x, long long y);
int getLength(long long value)
{
int counter = 0;
while (value != 0)
{
counter++;
value /= 10;
}
return counter;
}
long long multiply(long long x, long long y)
{
int xLength = getLength(x);
int yLength = getLength(y);
// the bigger of the two lengths
int N = (int)(fmax(xLength, yLength));
// if the max length is small it's faster to just flat out multiply the two nums
if (N < 10)
return x * y;
//max length divided and rounded up
N = (N / 2) + (N % 2);
long long multiplier = pow(10, N);
long long b = x / multiplier;
long long a = x - (b * multiplier);
long long d = y / multiplier;
long long c = y - (d * N);
long long z0 = multiply(a, c);
long long z1 = multiply(a + b, c + d);
long long z2 = multiply(b, d);
return z0 + ((z1 - z0 - z2) * multiplier) + (z2 * (long long)(pow(10, 2 * N)));
}
int main()
{
long long a;
long long b;
cin >> a;
cout << '\n';
cin >> b;
cout << '\n' << multiply(a, b) << endl;
return 0;
}
这里有一个提示:
(A + kB) * (C + kD) = AC + k(BC + AD) + k^2(BD)
如果 k
是您保留数字的基数的幂,这会有所帮助。例如,如果 k
是 1'000'000'000 并且您的数字是以 10 为基数,然后通过简单地移动数字(添加零)来完成乘以 k
。
无论如何,请考虑将您的 64 位数字分成两部分,每部分 32 位,然后像上面那样进行数学运算。要计算 AC
、BC
、AD
和 BD
,您需要将一对 32 位数字相乘,计算方法类似。
由于您的位数是 2 的幂,因此您可以继续将数字分成两半,直到达到可管理的数字大小(例如 1 位数)。
顺便说一句,从你的问题中不清楚你是在谈论 64 位还是 64 位十进制数字。如果您要查找的只是 64 位数字的乘法,只需执行以下操作:
// I haven't actually run this code, so...
typedef unsigned long long u64;
u64 high32 (u64 x) {return x >> 32;}
u64 low32 (u64 x) {return x & 0xFFFFFFFF;}
u64 add_with_carry (u64 a, u64 b, u64 * carry)
{
u64 result = a + b;
*carry = (result < a ? 1 : 0);
return result;
}
void mul (u64 a, u64 b, u64 * result_low, u64 * result_high)
{
u64 a0 = low32(a), a1 = high32(a);
u64 b0 = low32(b), b1 = high32(b);
u64 a0b0 = a0 * b0;
u64 a0b1 = a0 * b1;
u64 a1b0 = a1 * b0;
u64 a1b1 = a1 * b1;
u64 c0 = 0, c1 = 0;
u64 mid_part = add_with_carry(a0b1, a1b0, &c1);
*result_low = add_with_carry(a0b0, (low32(mid_part) << 32, &c0);
*result_high = high32(mid_part) + a1b1 + (c1 << 32) + c0; // this won't overflow
}
这个实现与上面概述的想法相同。由于在标准C/C++中,乘法的结果中我们最多可以有64位有意义的位,那么我们一次只能将两个32位的数相乘。这就是我们在这里所做的。
最终结果将是 128 位,我们 return 在两个无符号 64 位数字中。我们通过 4 个 32 位乘法和一些加法来进行 64 位乘 64 位乘法。
附带说明一下,这是编写汇编通常比 C 容易得多的少数情况之一。例如,在 x64 汇编中,这实际上是一个 mul
指令,它乘以两个64 位无符号整数和 return 两个 64 位寄存器中的 128 位结果。
即使你没有 64 位到 128 位的乘法指令,仍然用汇编写这个更容易(因为 adc
或类似的指令,例如上面的整个代码只是两个mov
s、四个 mul
s、四个 add
s 和四个 adc
s 在普通 x86 程序集中。)即使你不想用汇编写,你应该检查你的编译器的 "intrinsics". 它可能有一个用于大乘法的(但你会是 platform-dependent.)
要在较低位宽的算术上应用 Karatsuba 或任何其他乘法,您需要将数字分成更小的 "digits"。在做任何其他事情之前,您需要访问这些 "digits" 所以这里是如何做的:
你得到了数字 1234
并想将它分成 10^1
位所以
1234 = 1*1000 + 2*100 + 3*10 + 4
你可以获得这样的数字:
x=1234;
a0=x%10; x/=10; // 4
a1=x%10; x/=10; // 3
a2=x%10; x/=10; // 2
a3=x%10; x/=10; // 1
如果你想要 10^2
个数字,那么:
x=1234;
a0=x%100; x/=100; // 34
a1=x%100; x/=100; // 12
现在的问题是,要做到这一点,您需要对全部数字进行除法,而您没有。如果您将数字作为字符串,那么它很容易完成,但假设您没有。计算机基于二进制计算,因此使用 2 的幂作为 "digits" 的基数是个好主意,因此:
x = 1234 = 0100 1101 0010 bin
现在如果我们想要 2^4=16
个基本数字,那么:
a0=x%16; x/=16; // 0010
a1=x%16; x/=16; // 1101
a2=x%16; x/=16; // 0100
现在如果你意识到除以 2 的幂只是右移,余数可以表示为 AND 那么:
a0=x&15; x>>=4; // 0010
a1=x&15; x>>=4; // 1101
a2=x&15; x>>=4; // 0100
移位可以堆叠到任何位宽数字,所以现在您已经拥有了您需要的一切。但这还不是全部,如果你 select 作为 "digit" 例如 2^8
即 BYTE
那么你可以使用指针来代替,例如:
DWORD x=0x12345678; // 32 bit number
BYTE *db=(BYTE*)(&x); // 8bit pointer that points to x
a0=db[0]; // 0x78
a1=db[1]; // 0x56
a2=db[2]; // 0x34
a3=db[3]; // 0x12
因此您可以直接访问数字或从数字重建 x:
DWORD x; // 32 bit number
BYTE *db=(BYTE*)(&x); // 8bit pointer that points to x
db[0]=0x78;
db[1]=0x56;
db[2]=0x34;
db[3]=0x12;
// here x should be 0x12345678
请注意,顺序取决于平台 MSB 或 LSB 的第一顺序。现在你可以应用乘法了。例如 32*32=64 位在 16 位乘法上完成是这样用天真的 O(n^2)
方法完成的:
x(a0+a1<<16) * y(b0+b1<<16) = a0*b0 + a0*b1<<16 + a1*b0<<16 + a1*b1<<32
其中a0,a1,b0,b1为操作数的位数。请注意,每个 ai*bj
乘法的结果都是 2 位宽,因此您需要将其拆分为数字并存储到由移位寻址的结果数字中。请注意,添加会导致溢出到更高的数字。要处理这个问题,您需要使用至少两倍的算术宽度进行加法(16*16 位乘法 -> 32 位加法)或使用进位标志。遗憾的是,除了在 C++ 中使用汇编之外,您无法访问进位标志。幸运的是它可以被模拟见:
- Cant make value propagate through carry
现在您可以构建 Karatsuba 或更高级的乘法以获取更多信息,请参阅:
- Fast bignum square computation