Modular inverses and unsigned integers
Asked Answered
E

2

6

Modular inverses can be computed as follows (from 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;
}

However, the inputs are ints, as you can see. Would the above code work for unsigned integers (e.g. uint64_t) as well? I mean, would it be ok to replaced all int with uint64_t? I could try for few inputs but it is not feasible to try for all 64-bits combinations.

I'm specifically interested in two aspects:

  • for values [0, 264) of both a and b, would all calculation not overflow/underflow (or overflow with no harm)?

  • how would (x1 < 0) look like in unsigned case?

Ebonee answered 30/11, 2018 at 15:22 Comment(3)
Your question is already answered in this blog post. You can write that into an answer.Kidding
@Kidding Thank you, this is a very good article but it has one potential problem for "modular" inverses. pX can be less than zero, whereas in modular setting it is not and the usual way is to do if (pX < 0) pX += b. But that is signed and unsigned addition which can (or cannot?) overflow. Ideally, for modular calculations there would be no static_cast<S>. But thanks again for writing the article, if you are the author.Ebonee
No, I'm not the author; but fixing the problem you pointed out is not difficult using the bounds the author of the blog post had proven. (tl;dr: In your original code, you know that the actual value of x1 is in the range [-max(a,b)/2 .. max(a,b)/2], so if it's negative then it must be in the range [(uint) -max(a,b)/2 .. UINT_MAX] and has the top bit set, you can check whether it's true and then add b in that case). I may write an answer later.Kidding
V
5

First of all how this algorithm works? It is based on the Extended Euclidean algorithm for computation of the GCD. In short the idea is following: if we can find some integer coefficients m and n such that

a*m + b*n = 1

then m will be the answer for the modular inverse problem. It is easy to see because

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

Luckily the Extended Euclidean algorithm does exactly that: if a and b are co-prime, it finds such m and n. It works in the following way: for each iteration track two triplets (ai, xai, yai) and (bi, xbi, ybi) such that at every step

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

so when finally the algorithm stops at the state of ai = 0 and bi = GCD(a0,b0), then

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

It is done using more explicit way to calculate modulo: if

q = a / b
r = a % b

then

r = a - q * b

Another important thing is that it can be proven that for positive a and b at every step |xai|,|xbi| <= b and |yai|,|ybi| <= a. This means there can be no overflow during calculation of those coefficients. Unfortunately negative values are possible, moreover, on every step after the first one in each equation one is positive and the other is negative.

What the code in your question does is a reduced version of the same algorithm: since all we are interested in is the x[a/b] coefficients, it tracks only them and ignores the y[a/b] ones. The simplest way to make that code work for uint64_t is to track the sign explicitly in a separate field like this:

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;
}

Note that if a and b are not co-prime or when b is 0 or 1, this problem has no solution. In all those cases my code returns 0 which is an impossible value for any real solution.

Note also that although the calculated value is really the modular inverse, simple multiplication will not always produce 1 because of the overflow at multiplication over uint64_t. For example for a = 688231346938900684 and b = 2499104367272547425 the result is inv = 1080632715106266389

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

But if you do a naive multiplication of those a and inv of type uint64_t, you'll get 4042520075082636476 so (a*inv)%b will be 1543415707810089051 rather than expected 1.

Veneration answered 4/12, 2018 at 1:12 Comment(2)
Thank you! However, would it be possible to do without the extra storage for signs? Because now it is effectively uint65_t and I would like to know whether it is possible to employ just 64 unsigned bits.Ebonee
@EcirHana, yes, this is exactly like making signed int65_t from uint64_t. Maybe there is some nice property of the algorithm you can exploit or some other clever trick, but I can't come up with it from the top of my head. The bounty is still open so maybe someone else can.Veneration
C
0

The mod_inv C function :

  • return a modular multiplicative inverse of n with respect to the modulus
  • return 0 if the linear congruence has no solutions
unsigned mod_inv(unsigned n, const unsigned mod) {
    unsigned a = mod, b = a, c = 0, d = 0, e = 1, f, g;
    for (n *= a > 1; n > 1 && (n *= a > 0); e = g, c = (c & 3) | (c & 1) << 2) {
        g = d, d *= n / (f = a);
        a = n % a, n = f;
        c = (c & 6) | (c & 2) >> 1;
        f = c > 1 && c < 6;
        c = (c & 5) | (f || e > d ? (c & 4) >> 1 : ~c & 2);
        d = f ? d + e : e > d ? e - d : d - e;
    }
    return n ? c & 4 ? b - e : e : 0;
}

Examples

n =     7 and mod = 45    then res = 13    so 1 == (   13 * 7    ) % 45
n =    52 and mod = 107   then res = 35    so 1 == (   35 * 52   ) % 107
n =   213 and mod = 155   then res = 147   so 1 == (  147 * 213  ) % 155
n =   392 and mod = 45    then res = 38    so 1 == (   38 * 392  ) % 45

n = 3708141711 and mod = 4280761040 it still works...
Cyrillic answered 28/4, 2022 at 7:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.