Подтвердить что ты не робот

Способы умножения по модулю с примитивными типами

Есть ли способ для сборки, например. (853467 * 21660421200929) % 100000000000007 без библиотек BigInteger (обратите внимание, что каждый номер соответствует 64-битовому целому числу, но результат умножения не соответствует)?

Это решение кажется неэффективным:

int64_t mulmod(int64_t a, int64_t b, int64_t m) {
    if (b < a)
        std::swap(a, b);
    int64_t res = 0;
    for (int64_t i = 0; i < a; i++) {
        res += b;
        res %= m;
    }
    return res;
}
4b9b3361

Ответ 1

Вы должны использовать Русское Крестьянское умножение. Он использует повторное удвоение для вычисления всех значений (b*2^i)%m и добавляет их, если установлен i -й бит a.

uint64_t mulmod(uint64_t a, uint64_t b, uint64_t m) {
    int64_t res = 0;
    while (a != 0) {
        if (a & 1) res = (res + b) % m;
        a >>= 1;
        b = (b << 1) % m;
    }
    return res;
}

Он улучшает ваш алгоритм, потому что он занимает O(log(a)) время, а не O(a) время.

Предостережения: unsigned и работает только в том случае, если m равно 63 бит или меньше.

Ответ 2

Ответ Keith Randall хорош, но, как он сказал, предостережение в том, что оно работает, только если m равно 63 бит.

Вот модификация, которая имеет два преимущества:

  • Он работает, даже если m - 64 бит.
  • Не нужно использовать операцию modulo, которая может быть дорогостоящей на некоторых процессорах.

(Обратите внимание, что строки res -= m и temp_b -= m полагаются на 64-разрядное беззнаковое целочисленное переполнение для получения ожидаемых результатов. Это должно быть хорошо, так как непознанное целочисленное переполнение корректно определено на C и С++. причина важна для использования целочисленных типов без знака.)

uint64_t mulmod(uint64_t a, uint64_t b, uint64_t m) {
    uint64_t res = 0;
    uint64_t temp_b;

    /* Only needed if b may be >= m */
    if (b >= m) {
        if (m > UINT64_MAX / 2u)
            b -= m;
        else
            b %= m;
    }

    while (a != 0) {
        if (a & 1) {
            /* Add b to res, modulo m, without overflow */
            if (b >= m - res) /* Equiv to if (res + b >= m), without overflow */
                res -= m;
            res += b;
        }
        a >>= 1;

        /* Double b, modulo m */
        temp_b = b;
        if (b >= m - b)       /* Equiv to if (2 * b >= m), without overflow */
            temp_b -= m;
        b += temp_b;
    }
    return res;
}

Ответ 3

Улучшение алгоритма повторного удвоения состоит в том, чтобы проверить, сколько бит сразу можно вычислить без переполнения. Ранняя проверка выхода может быть выполнена для обоих аргументов - ускорение (маловероятное?) Событие N не является простым.

например. 100000000000007 == 0x00005af3107a4007, который позволяет рассчитывать 16 (или 17) бит за каждую итерацию. Фактическое количество итераций будет 3 с примером.

// just a conceptual routine
int get_leading_zeroes(uint64_t n)
{
   int a=0;
   while ((n & 0x8000000000000000) == 0) { a++; n<<=1; }
   return a;
}

uint64_t mulmod(uint64_t a, uint64_t b, uint64_t n)
{
     uint64_t result = 0;
     int N = get_leading_zeroes(n);
     uint64_t mask = (1<<N) - 1;
     a %= n;
     b %= n;  // Make sure all values are originally in the proper range?
     // n is not necessarily a prime -- so both a & b can end up being zero
     while (a>0 && b>0)
     {
         result = (result + (b & mask) * a) % n;  // no overflow
         b>>=N;
         a = (a << N) % n;
     }
     return result;
}

Ответ 4

Вы можете попробовать что-то, что разбивает умножение на дополнения:

// compute (a * b) % m:

unsigned int multmod(unsigned int a, unsigned int b, unsigned int m)
{
    unsigned int result = 0;

    a %= m;
    b %= m;

    while (b)
    {
        if (b % 2 != 0)
        {
            result = (result + a) % m;
        }

        a = (a * 2) % m;
        b /= 2;
    }

    return result;
}

Ответ 5

Оба метода работают для меня. Первый из них такой же, как у вас, но я изменил ваши номера на принудительный ULL. Во-вторых, используется ассемблерная нотация, которая должна работать быстрее. Существуют также алгоритмы, используемые в криптографии (криптография на основе RSA и RSA, в основном, я думаю), как уже упоминалось об уменьшении Montgomery, но я думаю, что для их выполнения потребуется некоторое время.

#include <algorithm>
#include <iostream>

__uint64_t mulmod1(__uint64_t a, __uint64_t b, __uint64_t m) {
  if (b < a)
    std::swap(a, b);
  __uint64_t res = 0;
  for (__uint64_t i = 0; i < a; i++) {
    res += b;
    res %= m;
  }
  return res;
}

__uint64_t mulmod2(__uint64_t a, __uint64_t b, __uint64_t m) {
  __uint64_t r;
  __asm__
  ( "mulq %2\n\t"
      "divq %3"
      : "=&d" (r), "+%a" (a)
      : "rm" (b), "rm" (m)
      : "cc"
  );
  return r;
}

int main() {
  using namespace std;
  __uint64_t a = 853467ULL;
  __uint64_t b = 21660421200929ULL;
  __uint64_t c = 100000000000007ULL;

  cout << mulmod1(a, b, c) << endl;
  cout << mulmod2(a, b, c) << endl;
  return 0;
}

Ответ 6

Я могу предложить улучшение вашего алгоритма.

Фактически вы вычисляете a * b итеративно, добавляя каждый раз b, делая по модулю после каждой итерации. Лучше добавлять каждый раз b * x, тогда как x определяется так, что b * x не будет переполняться.

int64_t mulmod(int64_t a, int64_t b, int64_t m)
{
    a %= m;
    b %= m;

    int64_t x = 1;
    int64_t bx = b;

    while (x < a)
    {
        int64_t bb = bx * 2;
        if (bb <= bx)
            break; // overflow

        x *= 2;
        bx = bb;
    }

    int64_t ans = 0;

    for (; x < a; a -= x)
        ans = (ans + bx) % m;

    return (ans + a*b) % m;
}

Ответ 7

a * b % m равно a * b - (a * b/m) * m

Используйте арифметику с плавающей точкой для приближения a * b/m. Приближение оставляет значение достаточно малым для обычных 64-битных целочисленных операций, от m до 63 бит.

Этот метод ограничен значением double, которое обычно составляет 52 бита.

uint64_t mod_mul_52(uint64_t a, uint64_t b, uint64_t m) {
    uint64_t c = (double)a * b / m - 1;
    uint64_t d = a * b - c * m;

    return d % m;
}

Этот метод ограничен значением long double, которое обычно составляет 64 бита или больше. Целочисленная арифметика ограничена 63 битами.

uint64_t mod_mul_63(uint64_t a, uint64_t b, uint64_t m) {
    uint64_t c = (long double)a * b / m - 1;
    uint64_t d = a * b - c * m;

    return d % m;
}

Эти методы требуют, чтобы a и b были меньше, чем m. Чтобы обработать произвольные a и b, добавьте эти строки перед вычислением c.

a = a % m;
b = b % m;

В обоих методах последняя операция % может быть сделана условной.

return d >= m ? d % m : d;