【问题标题】:Need help implementing Karatsuba algorithm in C++需要帮助在 C++ 中实现 Karatsuba 算法
【发布时间】:2011-11-02 19:00:14
【问题描述】:

首先介绍一下背景:
- 我是第一次发帖,是大学学生(不是编程)。
- 这不是家庭作业问题,我只是为了好玩。
- 我的编程经验包括一个学期(3 个月)的 C++ 和一些高中 QBasic。
- 是的,我看过 GMP 和 Bignum 库;从原始代码中学习东西是非常困难的,尤其是在不了解程序员的意图的情况下。此外,我想学习如何为自己做。

我正在编写一个任意大整数的乘法函数。我使用字符数组来表示这些数字,最后用 + 或 - 作为标记(例如“12345+”、“31415-”)。

我目前正在实施 Karatsuba 算法。问题在于,使用递归和动态内存分配,该函数比简单方法慢 5 倍。
我可以使用一些提示来减少运行时间。

char* dam(char* one, char* two){            // Karatsuba method

    char* zero = intochar(0, 0);
    int size_a = char_size(one) - 1;
    int size_b = char_size(two) - 1;

    if(compare(one, zero) == 0 || compare(two, zero) == 0)
        return zero;                        // if either array is zero, product is zero
    delete[] zero;

    if(size_a < 4 && size_b < 4)            // if both numbers are 3 digits or less, just return their product
        return multiplication(one, two);
                                            // is the product negative?
    bool negative = one[size_a] == two[size_b]? false : true;
    int digits = size_a > size_b ? size_a : size_b;
    digits += digits & 1;                   // add one if digits is odd
    int size = digits / 2 + 1;              // half the digits plus sentinel

    char* a, *b;                            // a and b represent one and two but with even digits
    if(size_a != digits)
        a = pad_char(one, digits - size_a); // pad the numbers with leading zeros so they have even digits
    else
        a = copy_char(one);
    if(size_b != digits)
        b = pad_char(two, digits - size_b);
    else
        b = copy_char(two);

    char* a_left = new char[size];          // left half of number a
    char* a_rite = new char[size];          // right half of number a
    char* b_left = new char[size];
    char* b_rite = new char[size];
    memcpy(a_left, a, size - 1);
    a_left[size - 1] = a[digits];
    memcpy(a_rite, a + size - 1, size);
    memcpy(b_left, b, size - 1);
    b_left[size - 1] = b[digits];
    memcpy(b_rite, b + size - 1, size);
    delete[] a;
    delete[] b;

    char* p0 = dam(a_left, b_left);         // Karatsuba product = p1*10^n + (p0+p2-p1)*10^(n/2) + p2
    char* p2 = dam(a_rite, b_rite);
    deduct(a_left, a_rite);
    deduct(b_left, b_rite);
    char* p1 = dam(a_left, b_left);
    char* p3 = intochar(0, digits - 1);     // p3 = p0 + p2 - p1
    append(p3, p0);                         // append does naive addition
    append(p3, p2);
    deduct(p3, p1);
    delete[] a_left;
    delete[] a_rite;
    delete[] b_left;
    delete[] b_rite;

    int sum_size = 2 * digits;              // product of two numbers can have a maximum of n1 + n2 digits
    char* sum = new char[sum_size + 1];
    memset(sum, 0, sum_size);
    if(negative)
        sum[sum_size] = '-';
    else
        sum[sum_size] = '+';

    char* left = extend_char(p0, digits, false);        // extend returns a new array with trailing zeros
    char* mid = extend_char(p3, size - 1, false);
    append(sum, left);
    append(sum, mid);
    append(sum, p2);
    delete[] p0;
    delete[] p1;
    delete[] p2;
    delete[] p3;
    delete[] left;
    delete[] mid;

    return sum;}

【问题讨论】:

    标签: c++ multiplication largenumber


    【解决方案1】:

    Karatsuba 是一个很好的算法,而且编程起来并不难。如果你只是为了好玩,那么在 base 10 中工作甚至不是一个坏主意——它会大大减慢你的速度,但也会减慢幼稚的实现,所以你仍然有比较这两种方法的基础。

    但是,您绝对必须放弃在递归树的每个节点上动态分配和释放工作空间的想法。你只是买不起。您必须在计算开始时分配所需的工作空间,并智能地处理您的指针,以便树的每一层都获得所需的工作空间,而无需分配它。

    此外,在每个级别都测试负面产品是没有意义的。只需在顶层执行此操作,并在计算期间仅使用正数。

    不是说它与你的问题有关,而是每当我看到类似的东西时

    bool negative = one[size_a] == two[size_b]? false : true;
    

    我的心微微一缩。想想所有那些浪费的像素!我恭敬地建议:

    bool negative = one[size_a] != two[size_b] ;
    

    【讨论】:

    • 感谢您的建议。虽然我不知道如何实际实现... prog 的介绍。我上完的课就没有动态记忆了。我几乎必须自己学习整个概念,因此到处都是疯狂的分配(嘿,至少我记得释放它们 LOL)。我会相应地更改否定标志。
    • @sth128:自学动态内存?胆子大!这值得做得好!你会想找到一个关于structs/classes的教程,然后是一个关于使用std::string/std::vector的教程
    • @Mooing:你不需要所有这些。当您进入/离开 recusive 调用时,您只需将全局指针增加/减少适当的量。不需要结构/类,不需要 std::whatever。
    • 不,但他们会难以置信地为他的工作提供便利。从技术上讲,他甚至不需要堆。
    【解决方案2】:

    您使用拼写出的十进制值会产生大量开销。 Karatsuba 乘法只会击败相对于机器寄存器大小巨大的数字的长乘法,并且您确实希望每个基元乘法都能做尽可能多的工作。

    我建议你重新设计你的数据结构,这样:

    if(size_a < 4 && size_b < 4)
        return multiplication(one, two);
    

    可以变成这样:

    if(size_a == 1 && size_b == 1)
        return box(int64_t(one[0]) * two[0]);
    

    one[0] 的类型可能是int32_t。这就是 GMP 对其mp_limb_t 数组所做的事情。

    【讨论】:

    • 我希望这就是multiplication(one, two); 在幕后所做的。如果不是,他的“nieve”方法可能也在使用“乘法”函数(对于整个事情),这意味着这并不能解释缓慢。
    • 恐怕我不知道 int32_t 是什么...它与常规(32 位)signed int 有何不同?另外,在您的情况下, one[0] 会超过 9 吗?目前我用 char 数组的每个字节表示一个数字。
    • @Mooing Duck:我的幼稚函数“乘法”执行个位数移位和加法。基本上,找到 a[i] * b[j] 然后加到总和上。天真的方法意味着“一个 5 岁的孩子会做什么”......
    • @sth128: int32_t 与您计算机上的 signed int 相同。但是,signed int 不能保证在所有计算机上都是 32 位。 int32_t 是。
    • @sth128:Zach 建议使用int/int64_t 的数组,而不是代表十进制数字的字符。您正在以 10 为基数做所有事情。他建议使用基数 4294967296 或更高。他是对的,它MUCH 更快。虽然对于较新的编码器,我会推荐基数 1000000000。
    【解决方案3】:

    我想放缓实际上可能是分配。用本地固定大小的缓冲区替换它们,我想你会看到一个不错的速度提升。或者使用自定义池分配器。但我认为,如果你很狡猾,大部分都可以在原地完成。

    此外,如果您将字符串的长度传递给函数,则每次都为查找长度节省了一次迭代。

    (也拼写为“正确”)。

    【讨论】:

    • 我不能使用固定大小(你的意思是静态的吗?)数组。 K 方法规定每次迭代将这些数字分成两半。同样使用静态数组会任意限制我可以相乘的数字的大小。至于“right”,我选择了“rite”,因为它与“left”的字母#相同,所以代码看起来更好......
    • 您可以尝试使用可变大小的堆栈数组,例如"char a_left[size];" 我不记得这在技术上是否是有效的 C++98,但它适用于很多编译器。
    • C++ 不能用变量整数声明静态数组,除非它是常量。但是 C++ 不能改变常量变量的值。在任何情况下,MS Visual C++ 编译器(我正在使用的)都不会这样做......
    • @sth128:MSVC 将使用_alloca 函数来完成。
    • @sth128:我刚刚为我的 bignum 对象编写了一个 karatsuba 算法,并做了一些快速的数学运算,据估计,当我有超过 2000 个十进制数字时,它只比 nieve 快。 big-O 表示法的问题在于它忽略了常量,使得算法比较变得棘手。
    【解决方案4】:

    写“函数慢 5 倍”是什么意思? Karatsuba 渐近地更快,而不仅仅是更快。这意味着即使是 Karatsuba 的玩具实现最终也会比简单的乘法更快。你用 10000 位数字测试过速度吗?

    我知道 GMP 代码不容易阅读……但请看this table extracted from the code。 它为(针对不同的 CPU)提供了 Toom-2,2 (Karatsuba) 的阈值。简而言之,在 GMP 中实现 Karatsuba 并不比对小于 320 位(10 x 32 位寄存器)的操作数的幼稚实现快。

    关于您的代码的一些问题:

    • 你真的需要 char *a, *b 吗?在开始计算之前删除它们! ;-)
    • 您确定需要将符号复制到 {a,b}_{left,rite} 吗?您是否检查了带有负数操作数的结果是否正确?
    • 考虑非常不平衡的操作数...如果 size_a * 2

    下一步将是Toom,对吧?

    【讨论】:

    • 我用阶乘函数测试了速度。速度变慢是由于内存分配,而不是位数。 *a*b' 不是真的必要,我只是懒得弄清楚填充。 size_a * 2 &lt; size_b 情况正在由 if(compare(a, zero) == 0 语句处理。现在我只是在寻找一种方法来分配我一开始需要的所有内存,而不是即时分配。在我弄清楚 a) 如何优化内存和 b) 如何实际使用 FFT 之前,我认为我不会很快实施 Toom。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-02-08
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多