【问题标题】:Modular inverses and unsigned integers模逆和无符号整数
【发布时间】:2018-11-30 15:22:28
【问题描述】:

模逆可以计算如下(来自Rosetta Code):

#include <stdio.h>

int mul_inv(int a, int b)
{
    int b0 = b, t, q;
    int x0 = 0, x1 = 1;
    if (b == 1) return 1;
    while (a > 1) {
        q = a / b;
        t = b, b = a % b, a = t;
        t = x0, x0 = x1 - q * x0, x1 = t;
    }
    if (x1 < 0) x1 += b0;
    return x1;
}

但是,如您所见,输入是 ints。上面的代码是否也适用于无符号整数(例如uint64_t)?我的意思是,可以用uint64_t 替换所有int 吗?我可以尝试一些输入,但尝试所有 64 位组合是不可行的。

我对两个方面特别感兴趣:

  • 对于 ab 的值 [0, 2^64),所有计算都不会溢出/下溢(或溢出而无害)吗?

  • (x1 &lt; 0) 在未签名的情况下会是什么样子?

【问题讨论】:

  • 您的问题已在this blog post 中得到解答。您可以将其写入答案。
  • @user202729 谢谢,这是一篇非常好的文章,但它有一个“模块化”逆的潜在问题。 pX 可以小于零,而在模块化设置中则不是,通常的方法是使用if (pX &lt; 0) pX += b。但这是可以(或不能?)溢出的有符号和无符号加法。理想情况下,对于模块化计算,不会有static_cast&lt;S&gt;。但再次感谢您撰写本文,如果您是作者的话。
  • 不,我不是作者;但是使用博客文章的作者已经证明的范围来解决您指出的问题并不困难。 (tl;dr: 在您的原始代码中,您知道 x1 的实际值在 [-max(a,b)/2 .. max(a,b)/2] 范围内,所以如果它是负数,那么它必须在 [(uint) -max(a,b)/2 .. UINT_MAX] 范围内并且设置了最高位,您可以检查它是否为真,然后在这种情况下添加 b)。稍后我可能会写一个答案。

标签: math integer-overflow greatest-common-divisor modular-arithmetic


【解决方案1】:

首先这个算法是如何工作的?它基于Extended Euclidean algorithm 计算GCD。简而言之,这个想法如下:如果我们能找到一些整数系数 mn 这样

a*m + b*n = 1

那么m 将是模逆问题的答案。很容易看到,因为

a*m + b*n = a*m (mod b)

幸运的是,扩展欧几里得算法正是这样做的:如果 ab 是互质数,它会找到这样的 mn。它的工作方式如下:对于每次迭代,跟踪两个三元组 (ai, xai, yai)(bi, xbi, ybi),以便在每一步

ai = a0*xai + b0*yai
bi = a0*xbi + b0*ybi

所以当算法最终停止在ai = 0bi = GCD(a0,b0)的状态时,那么

1 = GCD(a0,b0) = a0*xbi + b0*ybi

使用更明确的方式来计算模数:if

q = a / b
r = a % b

然后

r = a - q * b

另一个重要的事情是,可以证明对于正的ab 在每一步|xai|,|xbi| &lt;= b|yai|,|ybi| &lt;= a。这意味着在计算这些系数期间不会出现溢出。不幸的是,负值是可能的,此外,在每个方程的第一个之后的每一步,一个是正的,另一个是负的。

您问题中的代码所做的是同一算法的简化版本:因为我们感兴趣的是x[a/b] 系数,它只跟踪它们并忽略y[a/b] 系数。使该代码适用于 uint64_t 的最简单方法是在单独的字段中显式跟踪符号,如下所示:

typedef struct tag_uint64AndSign {
    uint64_t  value;
    bool isNegative;
} uint64AndSign;


uint64_t mul_inv(uint64_t a, uint64_t b)
{
    if (b <= 1)
        return 0;

    uint64_t b0 = b;
    uint64AndSign x0 = { 0, false }; // b = 1*b + 0*a
    uint64AndSign x1 = { 1, false }; // a = 0*b + 1*a

    while (a > 1)
    {
        if (b == 0) // means original A and B were not co-prime so there is no answer
            return 0;
        uint64_t q = a / b;
        // (b, a) := (a % b, b)
        // which is the same as
        // (b, a) := (a - q * b, b)
        uint64_t t = b; b = a % b; a = t;

        // (x0, x1) := (x1 - q * x0, x0)
        uint64AndSign t2 = x0;
        uint64_t qx0 = q * x0.value;
        if (x0.isNegative != x1.isNegative)
        {
            x0.value = x1.value + qx0;
            x0.isNegative = x1.isNegative;
        }
        else
        {
            x0.value = (x1.value > qx0) ? x1.value - qx0 : qx0 - x1.value;
            x0.isNegative = (x1.value > qx0) ? x1.isNegative : !x0.isNegative;
        }
        x1 = t2;
    }

    return x1.isNegative ? (b0 - x1.value) : x1.value;
}

请注意,如果ab 不是互质数或b 为0 或1,则此问题无解。在所有这些情况下,我的代码都会返回 0,这对于任何真正的解决方案来说都是不可能的。

还请注意,虽然计算的值实际上是模逆,但简单的乘法并不总是产生 1,因为在乘法时会溢出 uint64_t。例如a = 688231346938900684b = 2499104367272547425 的结果是inv = 1080632715106266389

a * inv = 688231346938900684 * 1080632715106266389 = 
= 743725309063827045302080239318310076 = 
= 2499104367272547425 * 297596738576991899 + 1 =
= b * 297596738576991899 + 1

但是,如果您对uint64_t 类型的ainv 进行简单的乘法运算,您将得到4042520075082636476,因此(a*inv)%b 将是1543415707810089051,而不是预期的1

【讨论】:

  • 谢谢!但是,没有额外的标志存储空间是否可行?因为现在它实际上是uint65_t,我想知道是否可以只使用 64 个无符号位。
  • @EcirHana,是的,这就像从uint64_t 制作签名的int65_t。也许您可以利用算法的一些不错的属性或其他一些巧妙的技巧,但我无法从头顶想出它。赏金仍然开放,所以也许其他人可以。
猜你喜欢
  • 1970-01-01
  • 2013-10-02
  • 2015-02-17
  • 1970-01-01
  • 1970-01-01
  • 2013-06-02
  • 2013-10-27
  • 1970-01-01
相关资源
最近更新 更多