模逆和无符号整数

时间:2018-11-30 15:22:28

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

模逆可以如下计算(来自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)吗?我的意思是,用int替换所有uint64_t可以吗?我可以尝试输入很少的内容,但尝试使用所有64位组合都是不可行的。

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

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

  • (x1 < 0)在未签名的情况下会是什么样?

1 个答案:

答案 0 :(得分:2)

首先该算法如何工作?它基于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| <= b|yai|,|ybi| <= 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,这对于任何实际的解决方案都是不可能的值。

还要注意,尽管计算出的值实际上是模逆,但由于在uint64_t上的乘法运算会溢出,因此简单乘法不会总是产生1。例如,对于a = 688231346938900684b = 2499104367272547425,结果为inv = 1080632715106266389

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

但是,如果对类型a的{​​{1}}和inv进行幼稚乘法,则会得到uint64_t,因此4042520075082636476将是{{ 1}},而不是预期的(a*inv)%b