当我看到这个问题时,我打算解决它,但那一刻我很忙。上周末,我可以获得一些有奖时间的空闲时间,所以我考虑了我的未决挑战。
首先,我建议你考虑上述反应。我从不使用 GMP 库,但我确信它是比手工代码更好的解决方案。此外,您可能有兴趣分析 bc 计算器的代码;它可以处理大数字,我曾经测试过我自己的代码。
好的,如果您仍然对代码感兴趣,请自己动手(仅支持 C 语言和标准 C 库)也许我可以给您一些东西。
首先,一点点理论。在基本数值理论(模算术水平)中,有一种算法可以启发我找到一个解决方案;乘法和幂算法求解a^N模 m:
Result := 1;
for i := k until i = 0
if n_i = 1 then Result := (Result * a) mod m;
if i != 0 then Result := (Result * Result) mod m;
end for;
其中 k 是二进制表示中 N 减去一个的位数,并且 n_i 是 i 二进制数。例如(N 是指数):
N = 44 -> 1 0 1 1 0 0
k = 5
n_5 = 1
n_4 = 0
n_3 = 1
n_2 = 1
n_1 = 0
n_0 = 0
当我们进行模运算时,作为整数除法,我们可能会丢失部分数字,所以我们只需要修改算法,以免遗漏相关数据。
这是我的代码(注意它是一个临时代码,对可能的计算机拱门有很强的依赖性。基本上我玩的是 C 语言的数据长度,所以要小心,因为我的数据长度不能相同):
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
enum { SHF = 31, BMASK = 0x1 << SHF, MODULE = 1000000000UL, LIMIT = 1024 };
unsigned int scaleBigNum(const unsigned short scale, const unsigned int lim, unsigned int *num);
unsigned int pow2BigNum(const unsigned int lim, unsigned int *nsrc, unsigned int *ndst);
unsigned int addBigNum(const unsigned int lim1, unsigned int *num1, const unsigned int lim2, unsigned int *num2);
unsigned int bigNum(const unsigned short int base, const unsigned int exp, unsigned int **num);
int main(void)
{
unsigned int *num, lim;
unsigned int *np, nplim;
int i, j;
for(i = 1; i < LIMIT; ++i)
{
lim = bigNum(i, i, &num);
printf("%i^%i == ", i, i);
for(j = lim - 1; j > -1; --j)
printf("%09u", num[j]);
printf("\n");
free(num);
}
return 0;
}
/*
bigNum: Compute number base^exp and store it in num array
@base: Base number
@exp: Exponent number
@num: Pointer to array where it stores big number
Return: Array length of result number
*/
unsigned int bigNum(const unsigned short int base, const unsigned int exp, unsigned int **num)
{
unsigned int m, lim, mem;
unsigned int *v, *w, *k;
//Note: mem has the exactly amount memory to allocate (dinamic memory version)
mem = ( (unsigned int) (exp * log10( (float) base ) / 9 ) ) + 3;
v = (unsigned int *) malloc( mem * sizeof(unsigned int) );
w = (unsigned int *) malloc( mem * sizeof(unsigned int) );
for(m = BMASK; ( (m & exp) == 0 ) && m; m >>= 1 ) ;
v[0] = (m) ? 1 : 0;
for(lim = 1; m > 1; m >>= 1)
{
if( exp & m )
lim = scaleBigNum(base, lim, v);
lim = pow2BigNum(lim, v, w);
k = v;
v = w;
w = k;
}
if(exp & 0x1)
lim = scaleBigNum(base, lim, v);
free(w);
*num = v;
return lim;
}
/*
scaleBigNum: Make an (num[] <- scale*num[]) big number operation
@scale: Scalar that multiply big number
@lim: Length of source big number
@num: Source big number (array of unsigned int). Update it with new big number value
Return: Array length of operation result
Warning: This method can write in an incorrect position if we don't previous reallocate num (if it's necessary). bigNum method do it for us
*/
unsigned int scaleBigNum(const unsigned short scale, const unsigned int lim, unsigned int *num)
{
unsigned int i;
unsigned long long int n, t;
for(n = 0, t = 0, i = 0; i < lim; ++i)
{
t = (n / MODULE);
n = ( (unsigned long long int) scale * num[i] );
num[i] = (n % MODULE) + t; // (n % MODULE) + t always will be smaller than MODULE
}
num[i] = (n / MODULE);
return ( (num[i]) ? lim + 1 : lim );
}
/*
pow2BigNum: Make a (dst[] <- src[] * src[]) big number operation
@lim: Length of source big number
@src: Source big number (array of unsigned int)
@dst: Destination big number (array of unsigned int)
Return: Array length of operation result
Warning: This method can write in an incorrect position if we don't previous reallocate num (if it's necessary). bigNum method do it for us
*/
unsigned int pow2BigNum(const unsigned int lim, unsigned int *src, unsigned int *dst)
{
unsigned int i, j;
unsigned long long int n, t;
unsigned int k, c;
for(c = 0, dst[0] = 0, i = 0; i < lim; ++i)
{
for(j = i, n = 0; j < lim; ++j)
{
n = ( (unsigned long long int) src[i] * src[j] );
k = i + j;
if(i != j)
{
t = 2 * (n % MODULE);
n = 2 * (n / MODULE);
// (i + j)
dst[k] = ( (k > c) ? ((c = k), 0) : dst[k] ) + (t % MODULE);
++k; // (i + j + 1)
dst[k] = ( (k > c) ? ((c = k), 0) : dst[k] ) + ( (t / MODULE) + (n % MODULE) );
++k; // (i + j + 2)
dst[k] = ( (k > c) ? ((c = k), 0) : dst[k] ) + (n / MODULE);
}
else
{
dst[k] = ( (k > c) ? ((c = k), 0) : dst[k] ) + (n % MODULE);
++k; // (i + j)
dst[k] = ( (k > c) ? ((c = k), 0) : dst[k] ) + (n / MODULE);
}
for(k = i + j; k < (lim + j); ++k)
{
dst[k + 1] += (dst[k] / MODULE);
dst[k] %= MODULE;
}
}
}
i = lim << 1;
return ((dst[i - 1]) ? i : i - 1);
}
/*
addBigNum: Make a (num2[] <- num1[] + num2[]) big number operation
@lim1: Length of source num1 big number
@num1: First source operand big number (array of unsigned int). Should be smaller than second
@lim2: Length of source num2 big number
@num2: Second source operand big number (array of unsigned int). Should be equal or greater than first
Return: Array length of operation result or 0 if num1[] > num2[] (dosen't do any op)
Warning: This method can write in an incorrect position if we don't previous reallocate num2
*/
unsigned int addBigNum(const unsigned int lim1, unsigned int *num1, const unsigned int lim2, unsigned int *num2)
{
unsigned long long int n;
unsigned int i;
if(lim1 > lim2)
return 0;
for(num2[lim2] = 0, n = 0, i = 0; i < lim1; ++i)
{
n = num2[i] + num1[i] + (n / MODULE);
num2[i] = n % MODULE;
}
for(n /= MODULE; n; ++i)
{
num2[i] += n;
n = (num2[i] / MODULE);
}
return (lim2 > i) ? lim2 : i;
}
编译:
gcc -o bgn <name>.c -Wall -O3 -lm //Math library if you wants to use log func
要检查结果,请使用直接输出 as 和输入到 bc。简单的shell脚本:
#!/bin/bash
select S in ` awk -F '==' '{print $1 " == " $2 }' | bc`;
do
0;
done;
echo "Test Finished!";
我们有一个 unsigned int 数组(4 个字节),我们在数组的每个 int 中存储一个 9 位数字( % 1000000000UL );因此 num[0] 我们将有前 9 位数字,num[1] 我们将有数字 10 到 18,num[2]... 我使用常规内存来工作,但可以使用动态内存进行改进。好的,但是它可能是数组的长度?(或者我们需要分配多少内存?)。使用 bc 计算器(bc -l 和 mathlib)我们可以确定有多少位数字:
l(a^N) / l(10) // Natural logarith to Logarithm base 10
如果我们知道数字,我们就知道我们需要的整数数量:
( l(a^N) / (9 * l(10)) ) + 1 // Truncate result
如果您使用 (2^k)^N 之类的值,您可以使用以下表达式将其解析为对数:
( k*N*l(2)/(9*l(10)) ) + 1 // Truncate result
确定整数数组的确切长度。例子:
256^800 = 2^(8*800) ---> l(2^(8*800))/(9*l(10)) + 1 = 8*800*l(2)/(9*l(10)) + 1
值 1000000000UL (10^9) 常量非常重要。像 10000000000UL (10^10) 这样的常量不起作用,因为会产生和未检测到的溢出(尝试数字 16^16 和 10^10 常量会发生什么)和更小的常量,例如 1000000000UL (10^8) 是正确的,但是我们需要预留更多的内存,做更多的步骤。10^9 是 32 位 unsigned int 和 64 位 unsigned long long int 的关键常数。
代码有两部分,乘法(简单)和乘以 2(更难)。乘法只是乘法和缩放并传播整数溢出。它需要数学中的关联属性原理来精确地执行逆原理,所以如果 k(A + B + C) 我们想要 kA + kB + kC ,其中数字将是 k*A*10^18 + k*B*10 ^9 + k C。显然,k C 操作可以生成大于 999 999 999 的数字,但绝不会大于 0xFF FF FF FF FF FF FF FF。乘法中永远不会出现大于 64 位的数字,因为 C 是 32 位的无符号整数,而 k 是 16 位的无符号短整数。在麦芽汁的情况下,我们将有这个数字:
k = 0x FF FF;
C = 0x 3B 9A C9 FF; // 999999999
n = k*C = 0x 3B 9A | 8E 64 36 01;
n % 1000000000 = 0x 3B 99 CA 01;
n / 1000000000 = 0x FF FE;
在 Mul k B 之后,我们需要从 C 的最后一次乘法中添加 0x FF FE( B = k B + (C / module) ),依此类推(我们有 18 位算术偏移量,足以保证正确的值)。
功率更复杂,但本质上是相同的问题(乘法和加法),所以我给出了一些关于代码功率的技巧:
- 数据类型很重要,非常重要
- 如果您尝试将无符号整数与无符号整数相乘,则会得到另一个无符号整数。使用显式强制转换来获得 unsigned long long int 并且不会丢失数据。
- 始终使用无符号修饰符,不要忘记它!
- Power by 2 可以直接修改当前索引前的 2 索引
- gdb 是你的朋友
我开发了另一种添加大数字的方法。最后这些我没有证明太多,但我认为它运作良好。如果它有错误,请不要对我残忍。
...就这样!
PD1:开发于
Intel(R) Pentium(R) 4 CPU 1.70GHz
Data length:
unsigned short: 2
unsigned int: 4
unsigned long int: 4
unsigned long long int: 8
它花费的数字如 256^1024:
real 0m0.059s
user 0m0.033s
sys 0m0.000s
计算 i^i 的 bucle,其中 i = 1 ... 1024:
real 0m40.716s
user 0m14.952s
sys 0m0.067s
对于像 65355^65355 这样的数字,花费的时间是疯狂的。
PD2:我的回复太晚了,但我希望我的代码有用。
PD3:对不起,用英语解释我是我最严重的障碍之一!
最后更新:我刚刚有了一个想法,即使用相同的算法但其他实现,提高响应并减少要使用的内存量(我们可以使用 unsigned int 的完全位)。秘密:n^2 = n * n = n * (n - 1 + 1) = n * (n - 1) + n。(我不会做这个新代码,但如果有人有兴趣,可能是在考试之后......)