7
votes

Comment puis-je calculer a * b / c lorsque a et b sont tous deux plus petits que c, mais que a * b déborde?

En supposant que uint est le plus grand type intégral sur ma plate-forme en virgule fixe, j'ai:

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

Ce qui doit renvoyer une bonne approximation de a * b / c .

La valeur de c est supérieure à la fois à la valeur de a et à la valeur de b .

Nous savons donc avec certitude que la valeur de a * b / c rentrerait dans un uint .

Cependant, la valeur de a * b elle-même dépasse la taille d'un uint .

Donc, une façon de calculer la valeur de a * b / c serait:

return a / c * b;

Ou même:

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

Cependant, la valeur de c est supérieure à la fois à la valeur de a et à la valeur de b .

Ainsi, la suggestion ci-dessus renverrait simplement zéro.

J'ai besoin de réduire a * b et c proportionnellement, mais encore une fois - le problème est que a * b déborde.

Idéalement, je serais capable de:

  • Remplacez a * b par uint(-1)
  • Remplacez c par uint(-1) / a / b * c .

Mais peu importe comment uint(-1) / a / b * c l'expression uint(-1) / a / b * c , je rencontre un problème:

  • uint(-1) / a / b * c est tronqué à zéro à cause de uint(-1) / a / b
  • uint(-1) / a * c / b déborde à cause de uint(-1) / a * c
  • uint(-1) * c / a / b déborde à cause de uint(-1) * c

Comment aborder ce scénario pour trouver une bonne approximation de a * b / c ?


Modifier 1

Je n'ai pas de choses telles que _umul128 sur ma plate-forme, lorsque le plus grand type d'intégrale est uint64 . Mon plus grand type est uint , et je n'ai aucun support pour quelque chose de plus grand que cela (ni au niveau HW, ni dans une bibliothèque standard préexistante).

Mon plus gros type est uint .

Modifier 2

En réponse à de nombreuses suggestions et commentaires en double:

Je n'ai pas de "type plus grand" sous la main, que je puisse utiliser pour résoudre ce problème. C'est pourquoi la déclaration liminaire de la question est:

En supposant que uint est le plus grand type intégral sur ma plate-forme en virgule fixe

Je suppose qu'aucun autre type n'existe, ni sur la couche SW (via une bibliothèque standard intégrée) ni sur la couche HW.


19 commentaires

Eh bien, a ou b doit être plus grand que "la moitié de la taille de uint ", alors peut-être que je devrais remplacer le plus grand d'entre eux, par exemple, a par uint(-1) / a . Ensuite, je peux fixer c proportionnellement ... juste une pensée ...


@WeatherVane: C'est une infrastructure en virgule fixe.


Veuillez mettre cela dans la question!


@WeatherVane: C'est fait, merci.


@GSerg: Non, ce n'est pas le cas, merci.


Exigences de performance versus exigences de précision?


@ 4386427: Précision privilégiée, merci.


Comment puis-je détartrer x par n / d, lorsque x * n déborde? , Comment puis-je multiplier et diviser des entiers 64 bits avec précision? , Comment multiplier un entier 64 bits par une fraction en C ++ tout en minimisant l'erreur? , (a * b) / c MulDiv et gestion du débordement de multiplication intermédiaire


La question bénéficierait d'une mise à jour avec une restriction plus explicite interdisant FP si c'est le cas. Même une "ma plate-forme en virgule fixe" pourrait implémenter le double .


Qu'est-ce qu'une "infrastructure en virgule fixe"? Essayez-vous de dire qu'il n'y a pas de virgule flottante?


@MM: Oui, exactement.


doit-il être en C? ou C ++ est autorisé?


@vmp: Comment le C ++ ferait-il une différence ici ??? C'est un problème purement arithmétique. En tout cas, mon problème n'est même pas en C, c'est en Solidity. La seule raison pour laquelle je l'ai publié en C est parce qu'il y a un public plus large que celui de Solidity, alors que les deux langues partagent la même nature commune de prendre en charge la division entière de manière native (c'est-à-dire par définition).


Est-ce que cela répond à votre question? Méthode rapide pour multiplier un entier par une fraction appropriée sans flotteurs ni débordement


Eh bien, dans ce cas, la taille de l'entier et de la valeur de c ne serait-elle pas des constantes de temps de compilation? Pourquoi ne nous dites-vous pas ce qu'ils sont? Les connaître à l'avance simplifie beaucoup le problème.


@phuclv: Non, ce n'est pas le cas. J'ai posté une réponse à ma propre question. Merci.


@KevinZ: Aucune des entrées ( a , b et c ) n'est de valeur constante.


@goodvibration J'ai demandé parce que la plupart des gens qui demandent l'arithmétique à virgule fixe essaient d'implémenter des décimales mises à l'échelle. Dans cette situation, même si c n'est pas une constante de temps de compilation unique, il doit être dans un ensemble très fini de constantes de temps de compilation, qui peuvent toutes être codées en dur. La division contre un diviseur connu est également beaucoup plus rapide que la division contre un diviseur inconnu. Cela dit, il existe peut-être une autre façon de simplifier cela si c ne peut pas être constant: si vous pouvez promettre que c < sqrt(UINT_MAX) , alors il y a un autre raccourci possible.


@KevinZ: Il ( c ) n'est pas constant. De plus, j'ai posté une situation restreinte que j'essayais de gérer au départ ( a < c && b < c ), alors qu'en fait tout scénario est possible sur mon système. J'ai déjà développé une solution à temps constant (c'est-à-dire pas de boucles), que vous pouvez voir ci-dessous. Merci.


4 Réponses :


-1
votes

Vous pouvez annuler les facteurs premiers comme suit:

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


3 commentaires

Je ne recherche pas de solution basée sur GCD. Premièrement, parce que cela donne de très mauvaises performances dans le pire des cas. Deuxièmement, parce qu'il n'est pas garanti de résoudre le problème (par exemple, chaque fois que le GCD est 1).


si le GCD est un, le résultat ne rentrera pas dans la variable ...


@vmp Je ne pense pas que votre déclaration soit vraie



2
votes

doit renvoyer une bonne approximation de a * b / c
Mon plus grand type est uint
a et b sont tous deux plus petits que c

Variante sur ce problème 32 bits :

12345677  

Avec un uint n-bit, je m'attends à ce que le résultat soit correct à au moins environ n/2 chiffres significatifs.

Pourrait améliorer les choses en tirant parti du plus petit de a,b étant inférieur à SQRT_MAX_P1 . Plus à ce sujet plus tard si vous êtes intéressé.


Exemple

12345600  

Sortie avec math 32 bits (la réponse précise est 12345677)

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

Sortie avec math 64 bits

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.

2 commentaires

Merci. J'ai mis en place une solution qui fonctionne en complexité O(1) (pas de boucles). Veuillez voir ma réponse ci-dessous (ou ci-dessus).


@ 4386427 Je reviendrai plus tard.



1
votes

Voici une autre approche qui utilise la récursivité et une approximation minimale pour obtenir une précision élevée.

D'abord le code et ci-dessous une explication.

Code:

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).

Explication:

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


1 commentaires

Merci. J'ai mis en place une solution qui fonctionne en complexité O(1) (pas de boucles). Veuillez voir ma réponse ci-dessous (ou ci-dessus).



0
votes

J'ai mis en place une solution qui fonctionne en complexité O(1) (pas de boucles):

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

Voici mon test:

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


7 commentaires

Je m'y attendais un peu ... Vous rappelez-vous que je vous ai posé une question sur "la précision par rapport à la performance" et votre réponse était "Précision favorisée, merci". Et maintenant, vous publiez une solution O (1) avec une précision "médiocre". Il semble donc que vous vouliez réellement la performance plutôt que la précision ;-)


@ 4386427: Oui, mais cela permet également d'obtenir de la précision, donc ...


Précision ... eh bien, "une précision suffisante" dépend de votre application (c'est pourquoi j'ai demandé) et c'est peut-être la précision dont vous avez besoin. Alors ça va :-) Voici juste un exemple sélectionné "au hasard" basé sur 32 bits non signé: a: 12345678 b: 4235266395 c: 4292973296. Correct answer: 12179725 Recursive answer: 12179725 O(1) answer: 12134037 Donc le O (1) l'approche est désactivée d'environ 45000 ou ~ 0,4%, ce qui peut être suffisamment bon pour votre application


@ 4386427: Ouais, tu as raison. Lorsque vous avez initialement posé la question «performance vs précision», j'ai supposé que toute solution de performance dans le pire des cas serait toujours O(1) (c'est-à-dire plusieurs opérations bien sûr, mais pas en fonction de la longueur de l'entrée). J'ai donc immédiatement répondu avec «la précision préférée à la performance». Mais ensuite j'ai reçu plusieurs réponses "while", que je ne peux malheureusement pas vraiment autoriser dans mon système. Merci!


Citation: "J'ai supposé que toute solution de performance dans le pire des cas serait toujours O (1)" Eh bien, si nous voulons être stricts à ce sujet, nous ne pouvons pas vraiment parler de complexité big-O ici car il n'y a rien ici qui peut devenir infini. La limite supérieure est le nombre de bits dans votre type non signé donc il y a une limite supérieure pour le temps d'exécution qui rend l'algo O (1) malgré l'utilisation de boucles / récursions. À titre d'exemple: dans mon algorithme récursif, la valeur a est au moins divisée par 2 entre les appels récursifs. Par conséquent, vous ne pouvez pas obtenir plus d'appels récursifs que le nombre de bits. Donc dans big-O ce serait O (1).


@ 4386427: Oui, mais la récursivité a beaucoup d'autres implications (non algorithmiques), et même si elle n'est pas dictée par le standard du langage C, je voudrais éviter d'utiliser la récursivité dans le cadre de ma solution. C'est généralement le cas dans les plates-formes à faibles ressources, telles que RT / embarqué, ainsi que dans les plates-formes où l'optimisation du compilateur (en particulier le déroulement de boucle) peut être appliquée. Ma plateforme n'est même pas C, c'est Solidity, qui en un sens est similaire à ce que je viens de décrire.


Et juste au cas où vous vous poseriez la question maintenant, la seule raison pour laquelle j'ai posté cette question en C est parce que le public est plus large que celui de Solidity, alors que les deux langues partagent la même nature commune de prise en charge native de la division entière (c'est-à-dire, par définition).