Как я могу вычислить a * b / c, когда и a, и b меньше, чем c, но a * b переполняется?

Предполагая, что uint является самым большим интегральным типом на моей платформе с фиксированной точкой, у меня есть:

uint func(uint a, uint b, uint c);

Что должно вернуть хорошее приближение a * b / c.

Значение c больше, чем значение a и значение b.

Итак, мы точно знаем, что значение a * b / c поместится в uint.

Однако само значение a * b превышает размер uint.

Таким образом, одним из способов вычисления значения a * b / c будет:

return a / c * b;

Или даже:

if (a > b)
    return a / c * b;
return b / c * a;

Однако значение c больше, чем значение a и значение b.

Таким образом, приведенное выше предложение просто вернет ноль.

Мне нужно уменьшить a * b и c пропорционально, но опять же - проблема в том, что a * b переполняется.

В идеале я мог бы:

  • Замените a * b на uint(-1)
  • Замените c на uint(-1) / a / b * c.

Но как бы я ни заказывал выражение uint(-1) / a / b * c, я сталкиваюсь с проблемой:

  • uint(-1) / a / b * c усекается до нуля из-за uint(-1) / a / b
  • uint(-1) / a * c / b переполняется из-за uint(-1) / a * c
  • uint(-1) * c / a / b переполняется из-за uint(-1) * c

Как я могу решить этот сценарий, чтобы найти хорошее приближение a * b / c?


Изменить 1

У меня нет таких вещей, как _umul128 на моей платформе, когда самый большой целочисленный тип — uint64. Мой самый большой тип — uint, и у меня нет поддержки чего-то большего (ни на аппаратном уровне, ни в какой-то уже существующей стандартной библиотеке).

Мой самый большой тип uint.

Редактировать 2

В ответ на многочисленные повторяющиеся предложения и комментарии:

У меня нет под рукой какого-то более крупного шрифта, который я мог бы использовать для решения этой задачи. Вот почему вступительное утверждение вопроса:

Предполагая, что uint является самым большим интегральным типом на моей платформе с фиксированной точкой

Я предполагаю, что другого типа не существует ни на уровне ПО (через некоторую встроенную стандартную библиотеку), ни на уровне оборудования.


person goodvibration    schedule 28.10.2020    source источник
comment
Что ж, либо a, либо b должны быть больше половины размера uint, поэтому, возможно, мне следует заменить большее из них, например, a на uint(-1) / a. Затем я могу исправить c пропорционально... просто мысль...   -  person goodvibration    schedule 28.10.2020
comment
@WeatherVane: это инфраструктура с фиксированной точкой.   -  person goodvibration    schedule 28.10.2020
comment
Пожалуйста, укажите это в вопросе!   -  person Weather Vane    schedule 28.10.2020
comment
@WeatherVane: Готово, спасибо.   -  person goodvibration    schedule 28.10.2020
comment
@GSerg: Нет, спасибо.   -  person goodvibration    schedule 28.10.2020
comment
Требования к производительности и требования к точности?   -  person 4386427    schedule 28.10.2020
comment
@ 4386427: Спасибо за точность.   -  person goodvibration    schedule 28.10.2020
comment
Вопрос выиграл бы от обновления с более явным ограничением, запрещающим FP, если это так. Даже моя платформа с фиксированной точкой может реализовать double.   -  person chux - Reinstate Monica    schedule 28.10.2020
comment
Что такое инфраструктура фиксированной точки? Вы пытаетесь сказать, что нет плавающей запятой?   -  person M.M    schedule 28.10.2020
comment
@M.M: Да, точно.   -  person goodvibration    schedule 28.10.2020
comment
это должно быть в C? или С++ разрешен?   -  person vmp    schedule 28.10.2020
comment
@vmp: Какое значение имеет C++ здесь??? Это чисто арифметическая задача. В любом случае у меня проблема даже не в C, а в Solidity. Единственная причина, по которой я опубликовал его C, заключается в том, что у него больше аудитории, чем у Solidity, в то время как оба языка имеют одинаковую общую природу изначальной поддержки целочисленного деления (т. е. по определению).   -  person goodvibration    schedule 28.10.2020
comment
comment
Что ж, в этом случае не будет ли размер целого числа и значение c константами времени компиляции? Почему бы вам не рассказать нам, что они собой представляют? Знание их заранее значительно упрощает задачу.   -  person KevinZ    schedule 01.11.2020
comment
@phuclv: Нет, это не так. Я опубликовал ответ на свой вопрос. Спасибо.   -  person goodvibration    schedule 01.11.2020
comment
@KevinZ: ни один из входных данных (a, b и c) не имеет постоянных значений.   -  person goodvibration    schedule 01.11.2020
comment
@goodvibration Я спросил, потому что в большинстве случаев, когда люди запрашивают арифметику с фиксированной запятой, они пытаются реализовать масштабированные десятичные дроби. В этой ситуации, даже если c не является единственной константой времени компиляции, она должна находиться в очень ограниченном наборе констант времени компиляции, для всех из которых можно жестко закодировать. Деление против известного делителя также намного быстрее, чем деление против неизвестного делителя. Тем не менее, возможно, есть альтернативный способ упростить это, если c не может быть постоянным: если вы можете пообещать, что c < sqrt(UINT_MAX), то возможен другой способ сокращения.   -  person KevinZ    schedule 01.11.2020
comment
@KevinZ: Это (c) непостоянно. Более того, я опубликовал ограниченную ситуацию, с которой пытался справиться для начала (a < c && b < c), хотя на самом деле в моей системе возможен любой сценарий. Я уже разработал решение с постоянным временем (то есть без циклов), которое вы можете увидеть ниже. Спасибо.   -  person goodvibration    schedule 01.11.2020


Ответы (4)


должен вернуть хорошее приближение к a * b / c
Мой самый большой тип - uint
и a, и b меньше, чем c

Вариант этой 32-битной проблемы:

Algorithm: Scale a, b to not overflow

SQRT_MAX_P1 as a compile time constant of sqrt(uint_MAX + 1)
sh = 0;
if (c >= SQRT_MAX_P1) {
  while (|a| >= SQRT_MAX_P1) a/=2, sh++
  while (|b| >= SQRT_MAX_P1) b/=2, sh++
  while (|c| >= SQRT_MAX_P1) c/=2, sh--
}
result = a*b/c

shift result by sh.

С n-битным uint я ожидаю, что результат будет правильным как минимум до n/2 значащих цифр.

Можно улучшить ситуацию, воспользовавшись тем, что меньшее из a,b меньше SQRT_MAX_P1. Об этом позже, если интересно.


Пример

#include <inttypes.h>

#define IMAX_BITS(m) ((m)/((m)%255+1) / 255%255*8 + 7-86/((m)%255+12))
// https://stackoverflow.com/a/4589384/2410359

#define UINTMAX_WIDTH (IMAX_BITS(UINTMAX_MAX))
#define SQRT_UINTMAX_P1 (((uintmax_t)1ull) << (UINTMAX_WIDTH/2))

uintmax_t muldiv_about(uintmax_t a, uintmax_t b, uintmax_t c) {
  int shift = 0;
  if (c > SQRT_UINTMAX_P1) {
    while (a >= SQRT_UINTMAX_P1) {
      a /= 2; shift++;
    }
    while (b >= SQRT_UINTMAX_P1) {
      b /= 2; shift++;
    }
    while (c >= SQRT_UINTMAX_P1) {
      c /= 2; shift--;
    }
  }
  uintmax_t r = a * b / c;
  if (shift > 0) r <<= shift;
  if (shift < 0) r >>= shift;
  return r;
}



#include <stdio.h>

int main() {
  uintmax_t a = 12345678;
  uintmax_t b = 4235266395;
  uintmax_t c = 4235266396;
  uintmax_t r = muldiv_about(a,b,c);
  printf("%ju\n", r);
}

Вывод с 32-битной математикой (точный ответ: 12345677)

12345600  

Вывод с 64-битной математикой

12345677  
person chux - Reinstate Monica    schedule 28.10.2020
comment
Спасибо. Я установил решение, которое работает со сложностью O(1) (без циклов). Пожалуйста, смотрите мой ответ ниже (или выше). - person goodvibration; 29.10.2020
comment
@ 4386427 Я рассмотрю больше позже. - person chux - Reinstate Monica; 29.10.2020

Вот еще один подход, использующий рекурсию и минимальное приближение для достижения высокой точности.

Сначала код, а ниже объяснение.

Код:

uint32_t bp(uint32_t a) {
  uint32_t b = 0;
  while (a!=0)
  {
    ++b;
    a >>= 1;
  };
  return b;
}

int mul_no_ovf(uint32_t a, uint32_t b)
{
  return ((bp(a) + bp(b)) <= 32);
}

uint32_t f(uint32_t a, uint32_t b, uint32_t c)
{
  if (mul_no_ovf(a, b))
  {
    return (a*b) / c;
  }

  uint32_t m = c / b;
  ++m;
  uint32_t x = m*b - c;
  // So m * b == c + x where x < b and m >= 2

  uint32_t n = a/m;
  uint32_t r = a % m;
  // So a*b == n * (c + x) + r*b == n*c + n*x + r*b where r*b < c

  // Approximation: get rid of the r*b part
  uint32_t res = n;
  if (r*b > c/2) ++res;

  return res + f(n, x, c);
}

Пояснение:

The multiplication a * b can be written as a sum of b

a * b = b + b + .... + b

Since b < c we can take a number m of these b so that (m-1)*b < c <= m*b, like

(b + b + ... + b) + (b + b + ... + b) + .... + b + b + b
\---------------/   \---------------/ +        \-------/
       m*b        +        m*b        + .... +     r*b
     \-------------------------------------/
            n times m*b

so we have

a*b = n*m*b + r*b

where r*b < c and m*b > c. Consequently, m*b is equal to c + x, so we have

a*b = n*(c + x) + r*b = n*c + n*x + r*b

Divide by c :

a*b/c = (n*c + n*x + r*b)/c = n + n*x/c + r*b/c

The values m, n, x, r can all be calculated from a, b and c without any loss of 
precision using integer division (/) and remainder (%).

The approximation is to look at r*b (which is less than c) and "add zero" when r*b<=c/2
and "add one" when r*b>c/2.

So now there are two possibilities:

1) a*b = n + n*x/c

2) a*b = (n + 1) + n*x/c

So the problem (i.e. calculating a*b/c) has been changed to the form

MULDIV(a1,b1,c) = NUMBER + MULDIV(a2,b2,c)

where a2,b2 is less than a1,b2. Consequently, recursion can be used until 
a2*b2 no longer overflows (and the calculation can be done directly).
person 4386427    schedule 28.10.2020
comment
Спасибо. Я установил решение, которое работает со сложностью O(1) (без циклов). Пожалуйста, смотрите мой ответ ниже (или выше). - person goodvibration; 29.10.2020

Я установил решение, которое работает со сложностью O(1) (без циклов):

typedef unsigned long long uint;

typedef struct
{
    uint n;
    uint d;
}
fraction;

uint func(uint a, uint b, uint c);
fraction reducedRatio(uint n, uint d, uint max);
fraction normalizedRatio(uint a, uint b, uint scale);
fraction accurateRatio(uint a, uint b, uint scale);
fraction toFraction(uint n, uint d);
uint roundDiv(uint n, uint d);

uint func(uint a, uint b, uint c)
{
    uint hi = a > b ? a : b;
    uint lo = a < b ? a : b;
    fraction f = reducedRatio(hi, c, (uint)(-1) / lo);
    return f.n * lo / f.d;
}

fraction reducedRatio(uint n, uint d, uint max)
{
    fraction f = toFraction(n, d);
    if (n > max || d > max)
        f = normalizedRatio(n, d, max);
    if (f.n != f.d)
        return f;
    return toFraction(1, 1);
}

fraction normalizedRatio(uint a, uint b, uint scale)
{
    if (a <= b)
        return accurateRatio(a, b, scale);
    fraction f = accurateRatio(b, a, scale);
    return toFraction(f.d, f.n);
}

fraction accurateRatio(uint a, uint b, uint scale)
{
    uint maxVal = (uint)(-1) / scale;
    if (a > maxVal)
    {
        uint c = a / (maxVal + 1) + 1;
        a /= c; // we can now safely compute `a * scale`
        b /= c;
    }
    if (a != b)
    {
        uint n = a * scale;
        uint d = a + b; // can overflow
        if (d >= a) // no overflow in `a + b`
        {
            uint x = roundDiv(n, d); // we can now safely compute `scale - x`
            uint y = scale - x;
            return toFraction(x, y);
        }
        if (n < b - (b - a) / 2)
        {
            return toFraction(0, scale); // `a * scale < (a + b) / 2 < MAXUINT256 < a + b`
        }
        return toFraction(1, scale - 1); // `(a + b) / 2 < a * scale < MAXUINT256 < a + b`
    }
    return toFraction(scale / 2, scale / 2); // allow reduction to `(1, 1)` in the calling function
}

fraction toFraction(uint n, uint d)
{
    fraction f = {n, d};
    return f;
}

uint roundDiv(uint n, uint d)
{
    return n / d + n % d / (d - d / 2);
}

Вот мой тест:

#include <stdio.h>

int main()
{
    uint a = (uint)(-1) / 3;            // 0x5555555555555555
    uint b = (uint)(-1) / 2;            // 0x7fffffffffffffff
    uint c = (uint)(-1) / 1;            // 0xffffffffffffffff
    printf("0x%llx", func(a, b, c));    // 0x2aaaaaaaaaaaaaaa
    return 0;
}
person goodvibration    schedule 29.10.2020
comment
Я как бы ожидал этого... Помните, я спрашивал вас о точности по сравнению с производительностью, и вы ответили, что предпочтение отдается точности, спасибо. И теперь вы публикуете решение O (1) с плохой точностью. Итак, похоже, вы действительно хотели производительности, а не точности ;-) - person 4386427; 29.10.2020
comment
@4386427: Да, но это также обеспечивает точность, так что... - person goodvibration; 29.10.2020
comment
Точность... ну, достаточная точность зависит от вашего приложения (поэтому я и спросил), и это может быть та точность, которая вам нужна. Тогда все в порядке :-) Вот только один случайно выбранный пример, основанный на 32-битном беззнаковом: a: 12345678 b: 4235266395 c: 4292973296. Correct answer: 12179725 Recursive answer: 12179725 O(1) answer: 12134037 Таким образом, подход O(1) отличается на ~ 45000 или ~ 0,4%, что может быть достаточно хорошо для вашего приложения. - person 4386427; 29.10.2020
comment
@ 4386427: Да, ты прав. Когда вы изначально задали вопрос о «производительности и точности», я предположил, что любое решение для наихудшей производительности все равно будет O(1) (т. е. наверняка несколько операций, но не в зависимости от длины ввода). Поэтому я сразу же ответил: «Точность предпочтительнее производительности». Но затем я получил несколько ответов while, которые я, к сожалению, не могу разрешить в своей системе. Спасибо! - person goodvibration; 29.10.2020
comment
Цитата: Я предположил, что любое решение с наихудшей производительностью все равно будет O(1). Ну, если мы хотим быть строгими в этом отношении, мы не можем говорить здесь о большой O-сложности, поскольку здесь нет ничего, что могло бы расти бесконечно. Верхний предел - это количество битов в вашем беззнаковом типе, поэтому существует верхний предел времени выполнения, который делает алгоритм O (1), несмотря на использование циклов/рекурсий. В качестве примера: в моем рекурсивном алгоритме значение a как минимум делится на 2 между рекурсивными вызовами. Следовательно, вы не можете получить больше рекурсивных вызовов, чем количество битов. Так что в большом O это будет O (1). - person 4386427; 29.10.2020
comment
@ 4386427: Да, но у рекурсии есть много других (неалгоритмических) последствий, и хотя это не продиктовано стандартом языка C, я бы хотел избежать использования рекурсии как части моего решения. Обычно это происходит на платформах с низким уровнем ресурсов, таких как RT/embedded, а также на платформах, где можно применить оптимизацию компилятора (в частности, развертывание циклов). Моя платформа даже не C, это Solidity, что в некотором смысле похоже на то, что я только что описал. - person goodvibration; 29.10.2020
comment
И на всякий случай, если вам сейчас интересно, единственная причина, по которой я разместил этот вопрос на C, заключается в том, что для него существует большая аудитория, чем для Solidity, в то время как оба языка имеют одинаковую общую природу изначальной поддержки целочисленного деления (т.е. по определению). - person goodvibration; 29.10.2020

Вы можете отменить простые множители следующим образом:

uint gcd(uint a, uint b) 
{
    uint c;
    while (b) 
    {
        a %= b;
        c = a;
        a = b;
        b = c;
    }
    return a;
}


uint func(uint a, uint b, uint c)
{
    uint temp = gcd(a, c);
    a = a/temp;
    c = c/temp;

    temp = gcd(b, c);
    b = b/temp;
    c = c/temp;

    // Since you are sure the result will fit in the variable, you can simply
    // return the expression you wanted after having those terms canceled.
    return a * b / c;
}
person vmp    schedule 28.10.2020
comment
Не ищите решение на основе GCD. Во-первых, потому что это дает довольно плохую производительность в худшем случае. Во-вторых, потому что это не гарантирует решения проблемы (например, когда НОД равен 1). - person goodvibration; 28.10.2020
comment
если НОД один, то результат не помещается в переменную... - person vmp; 28.10.2020
comment
@vmp Я не думаю, что ваше утверждение верно - person Andrea Nardi; 28.10.2020