Moyenne de 3 longs entiers


103

J'ai 3 très grands entiers signés.

long x = long.MaxValue;
long y = long.MaxValue - 1;
long z = long.MaxValue - 2;

Je veux calculer leur moyenne tronquée. La valeur moyenne attendue est long.MaxValue - 1, ce qui est 9223372036854775806.

Il est impossible de le calculer comme:

long avg = (x + y + z) / 3; // 3074457345618258600

Remarque: j'ai lu toutes ces questions sur la moyenne de 2 nombres, mais je ne vois pas comment cette technique peut être appliquée à la moyenne de 3 nombres.

Ce serait très facile avec l'utilisation de BigInteger, mais supposons que je ne puisse pas l'utiliser.

BigInteger bx = new BigInteger(x);
BigInteger by = new BigInteger(y);
BigInteger bz = new BigInteger(z);
BigInteger bavg = (bx + by + bz) / 3; // 9223372036854775806

Si je me convertis en double, alors, bien sûr, je perds la précision:

double dx = x;
double dy = y;
double dz = z;
double davg = (dx + dy + dz) / 3; // 9223372036854780000

Si je me convertis en decimal, cela fonctionne, mais supposons également que je ne puisse pas l'utiliser.

decimal mx = x;
decimal my = y;
decimal mz = z;
decimal mavg = (mx + my + mz) / 3; // 9223372036854775806

Question: Existe - t-il un moyen de calculer la moyenne tronquée de 3 très grands entiers uniquement avec l'utilisation de longtype? Ne considérez pas cette question comme spécifique à C #, mais il m'est plus facile de fournir des exemples en C #.


1
pourquoi ne pas calculer le diff moyen global et le soustraire de max?
Andreas Niedermair

6
@AndreasNiedermair ne fonctionnerait pas au cas où j'en aurais long.MinValueet long.MaxValueparmi les valeurs.
Ulugbek Umirov

bonne prise, en effet :)
Andreas Niedermair

Êtes-vous sûr que nous devons nous inquiéter à ce sujet, cela ne devrait-il pas être géré par le framework?
Bolu

11
Y a-t-il une raison réelle pour laquelle BigIntegerou decimalest exclue, ou est-ce simplement pour rendre cela difficile?
jpmc26

Réponses:


142

Ce code fonctionnera, mais n'est pas si joli.

Il divise d'abord les trois valeurs (il nivelle les valeurs, vous «perdez» donc le reste), puis divise le reste:

long n = x / 3
         + y / 3
         + z / 3
         + ( x % 3
             + y % 3
             + z % 3
           ) / 3

Notez que l'exemple ci-dessus ne fonctionne pas toujours correctement lorsqu'il a une ou plusieurs valeurs négatives.

Comme discuté avec Ulugbek, comme le nombre de commentaires explose ci-dessous, voici la meilleure solution actuelle pour les valeurs positives et négatives.

Grâce aux réponses et aux commentaires d' Ulugbek Umirov , James S , KevinZ , Marc van Leeuwen , gnasher729 , voici la solution actuelle:

static long CalculateAverage(long x, long y, long z)
{
    return (x % 3 + y % 3 + z % 3 + 6) / 3 - 2
            + x / 3 + y / 3 + z / 3;
}

static long CalculateAverage(params long[] arr)
{
    int count = arr.Length;
    return (arr.Sum(n => n % count) + count * (count - 1)) / count - (count - 1)
           + arr.Sum(n => n / count);
}

3
@DavidG Non , en mathématiques, (x + y + z) / 3 = x / 3 + y / 3 + z / 3.
Kris Vandermotten

4
J'ai utilisé Z3 pour prouver que cela était correct pour tous les comptes de variables entre 1 et 5.
usr

5
Bien sûr, cela semble fonctionner, mais la façon dont la troncature entière fonctionne vous dérangera. f(1,1,2) == 1whilef(-2,-2,8) == 2
KevinZ

11
Notez qu'en raison de la sémantique cérébrale endommagée de l'opération modulo, cela peut donner un résultat qui est décalé de un, à savoir arrondi vers le haut plutôt que vers le bas, si des valeurs négatives pour les variables sont autorisées. Par exemple, si x, y sont des multiples positifs de 3 et z vaut -2, vous obtenez (x+y)/3ce qui est trop.
Marc van Leeuwen

6
@KevinZ: ... dont l'effet doit ensuite être annulé par un programmeur qui n'a jamais voulu ce comportement de cas particulier en premier lieu. Laisser le programmeur spécifier le module plutôt que d'avoir à le dériver d'un reste que le compilateur peut avoir dérivé du module semblerait utile.
supercat

26

NB - Patrick a déjà donné une excellente réponse . En développant là-dessus, vous pouvez créer une version générique pour n'importe quel nombre d'entiers comme ceci:

long x = long.MaxValue;
long y = long.MaxValue - 1;
long z = long.MaxValue - 2;

long[] arr = { x, y, z };
var avg = arr.Select(i => i / arr.Length).Sum() 
        + arr.Select(i => i % arr.Length).Sum() / arr.Length;

1
Cela ne se produira pas pour long, mais pour les types plus petits, notez que la deuxième somme peut déborder.
user541686

7

Patrick Hofman a publié une excellente solution . Mais si nécessaire, il peut toujours être mis en œuvre de plusieurs autres manières. En utilisant l'algorithme ici, j'ai une autre solution. S'il est mis en œuvre avec soin, il peut être plus rapide que les multiples divisions dans les systèmes avec des diviseurs matériels lents. Il peut être encore optimisé en utilisant la technique de division par constantes du plaisir des hackers

public class int128_t {
    private int H;
    private long L;

    public int128_t(int h, long l)
    {
        H = h;
        L = l;
    }

    public int128_t add(int128_t a)
    {
        int128_t s;
        s.L = L + a.L;
        s.H = H + a.H + (s.L < a.L);
        return b;
    }

    private int128_t rshift2()  // right shift 2
    {
        int128_t r;
        r.H = H >> 2;
        r.L = (L >> 2) | ((H & 0x03) << 62);
        return r;
    }

    public int128_t divideby3()
    {
        int128_t sum = {0, 0}, num = new int128_t(H, L);
        while (num.H || num.L > 3)
        {
            int128_t n_sar2 = num.rshift2();
            sum = add(n_sar2, sum);
            num = add(n_sar2, new int128_t(0, num.L & 3));
        }

        if (num.H == 0 && num.L == 3)
        {
            // sum = add(sum, 1);
            sum.L++;
            if (sum.L == 0) sum.H++;
        }
        return sum; 
    }
};

int128_t t = new int128_t(0, x);
t = t.add(new int128_t(0, y));
t = t.add(new int128_t(0, z));
t = t.divideby3();
long average = t.L;

En C / C ++ sur les plates-formes 64 bits, c'est beaucoup plus facile avec __int128

int64_t average = ((__int128)x + y + z)/3;

2
Je suggérerais qu'un bon moyen de diviser une valeur non signée 32 bits par 3 est de multiplier par 0x55555555L, d'ajouter 0x55555555 et de décaler vers la droite par 32. Votre méthode divideby3, par comparaison, semble nécessiter de nombreuses étapes discrètes.
supercat

@supercat oui je connais cette méthode. La méthode par le plaisir du hacker est encore plus correcte mais je vais l'implémenter pour une autre fois
phuclv

Je ne suis pas sûr de ce que signifie «plus correct». Les multiplications réciproques peuvent dans de nombreux cas donner directement des valeurs exactes, ou bien des valeurs qui peuvent être affinées en une ou deux étapes. BTW, je pense que j'aurais dû suggérer de multiplier par 0x55555556, ce qui donnerait alors des résultats exacts sans avoir besoin d'un "ajout". De plus, la condition de votre boucle est-elle correcte? Qu'est-ce qui modifie H et L dans la boucle?
supercat

Incidemment, même si on n'a pas de multiplication matérielle, on peut rapidement se rapprocher d'un x=y/3via non signé x=y>>2; x+=x>>2; x+=x>>4; x+=x>>8; x+=x>>16; x+=x>>32;. Le résultat sera très proche de x, et peut être rendu précis en calculant delta=y-x-x-x;et en utilisant des ajustements xsi nécessaire.
supercat

1
@ gnasher729 Je me demande s'il peut utiliser cette optimisation dans les ordinateurs 32 bits car il ne peut souvent pas faire de multiplication 64x64 → 128 bits
phuclv

7

Vous pouvez calculer la moyenne des nombres en fonction des différences entre les nombres plutôt qu'en utilisant la somme.

Disons que x est le maximum, y est la médiane, z est le minimum (comme vous l'avez fait). Nous les appellerons max, médian et min.

Vérificateur conditionnel ajouté selon le commentaire de @ UlugbekUmirov:

long tmp = median + ((min - median) / 2);            //Average of min 2 values
if (median > 0) tmp = median + ((max - median) / 2); //Average of max 2 values
long mean;
if (min > 0) {
    mean = min + ((tmp - min) * (2.0 / 3)); //Average of all 3 values
} else if (median > 0) {
    mean = min;
    while (mean != tmp) {
        mean += 2;
        tmp--;
    }
} else if (max > 0) {
    mean = max;
    while (mean != tmp) {
        mean--;
        tmp += 2;
    }
} else {
    mean = max + ((tmp - max) * (2.0 / 3));
}

2
Voir le commentaire de @ UlugbekUmirov: ne fonctionnerait pas si j'ai long.MinValue et long.MaxValue parmi les valeurs
Bolu

@Bolu le commentaire ne s'applique qu'à long.MinValue. J'ai donc ajouté ce conditionnel pour que cela fonctionne pour notre cas.
La-comadreja

Comment pouvez-vous utiliser la médiane quand elle n'a pas été initialisée?
phuclv

@ LưuVĩnhPhúc, la médiane est la valeur entre le minimum et le maximum.
La-comadreja

1
n'est pas (double)(2 / 3)égal à 0,0?
phuclv

5

Étant donné que C utilise la division par étage plutôt que la division euclidienne, il peut être plus facile de calculer une moyenne correctement arrondie de trois valeurs non signées que de trois signées. Ajoutez simplement 0x8000000000000000UL à chaque nombre avant de prendre la moyenne non signée, soustrayez-la après avoir pris le résultat et utilisez une conversion non cochée vers Int64pour obtenir une moyenne signée.

Pour calculer la moyenne non signée, calculez la somme des 32 premiers bits des trois valeurs. Ensuite, calculez la somme des 32 bits inférieurs des trois valeurs, plus la somme d'en haut, plus un [le plus un donne un résultat arrondi]. La moyenne sera 0x55555555 fois la première somme, plus un tiers de la seconde.

Les performances sur les processeurs 32 bits peuvent être améliorées en produisant trois valeurs "somme" dont chacune est longue de 32 bits, de sorte que le résultat final est ((0x55555555UL * sumX)<<32) + 0x55555555UL * sumH + sumL/3; il pourrait éventuellement être amélioré en remplaçant sumL/3par ((sumL * 0x55555556UL) >> 32), bien que ce dernier dépende de l'optimiseur JIT [il pourrait savoir comment remplacer une division par 3 par une multiplication, et son code pourrait en fait être plus efficace qu'une opération de multiplication explicite].


Après avoir ajouté 0x8000000000000000UL, le débordement n'affecte-t-il pas le résultat?
phuclv

@ LưuVĩnhPhúc Il n'y a pas de débordement. Allez à ma réponse pour une implémentation. La division en 2 32 bits int était cependant inutile.
KevinZ

@KevinZ: Il est plus rapide de diviser chaque valeur en une partie supérieure et inférieure de 32 bits que de la diviser en un quotient divisé par trois et un reste.
supercat

1
@ LưuVĩnhPhúc: Contrairement aux valeurs signées qui se comportent sémantiquement comme des nombres et ne sont pas autorisées à déborder dans un programme C légitime, les valeurs non signées se comportent généralement comme les membres d'un anneau algébrique abstrait enveloppant, donc la sémantique d'encapsulation est bien définie.
supercat

1
Le tuple représente -3, -2, -1. Après avoir ajouté 0x8000U à chaque valeur, les valeurs doivent alors être divisées en deux: 7F + FF 7F + FE 7F + FD. Ajouter les moitiés supérieure et inférieure, ce qui donne 17D + 2FA. Ajouter la somme de la moitié supérieure à la somme de la moitié inférieure donnant 477. Multipliez 17D par 55, ce qui donne 7E81. Divisez 477 par trois pour obtenir 17D. Ajoutez 7E81 à 17D pour obtenir 7FFE. Soustrayez 8000 de cela et obtenez -2.
supercat

5

Patcher Patrick Hofman 'solution s avec supercat ' correction de, je vous donne les éléments suivants:

static Int64 Avg3 ( Int64 x, Int64 y, Int64 z )
{
    UInt64 flag = 1ul << 63;
    UInt64 x_ = flag ^ (UInt64) x;
    UInt64 y_ = flag ^ (UInt64) y;
    UInt64 z_ = flag ^ (UInt64) z;
    UInt64 quotient = x_ / 3ul + y_ / 3ul + z_ / 3ul
        + ( x_ % 3ul + y_ % 3ul + z_ % 3ul ) / 3ul;
    return (Int64) (quotient ^ flag);
}

Et le cas des N éléments:

static Int64 AvgN ( params Int64 [ ] args )
{
    UInt64 length = (UInt64) args.Length;
    UInt64 flag = 1ul << 63;
    UInt64 quotient_sum = 0;
    UInt64 remainder_sum = 0;
    foreach ( Int64 item in args )
    {
        UInt64 uitem = flag ^ (UInt64) item;
        quotient_sum += uitem / length;
        remainder_sum += uitem % length;
    }

    return (Int64) ( flag ^ ( quotient_sum + remainder_sum / length ) );
}

Cela donne toujours le plancher () de la moyenne et élimine tous les cas de bord possibles.


1
J'ai traduit le code AvgN en code Z3 et j'ai prouvé que c'était correct pour toutes les tailles d'entrée raisonnables (par exemple 1 <= args.Length <= 5 et la taille du vecteur de bits de 6). Cette réponse est correcte.
usr

Merveilleuse réponse Kevin. Merci pour votre contribution! meta.stackoverflow.com/a/303292/993547
Patrick Hofman

4

Vous pouvez utiliser le fait que vous pouvez écrire chacun des nombres comme y = ax + b, où xest une constante. Chacun aserait y / x(la partie entière de cette division). Chaque b serait y % x(le reste / modulo de cette division). Si vous choisissez cette constante de manière intelligente, par exemple en choisissant la racine carrée du nombre maximum comme constante, vous pouvez obtenir la moyenne des xnombres sans avoir de problèmes de débordement.

La moyenne d'une liste arbitraire de nombres peut être trouvée en trouvant:

( ( sum( all A's ) / length ) * constant ) + 
( ( sum( all A's ) % length ) * constant / length) +
( ( sum( all B's ) / length )

%désigne modulo et /désigne la partie «entière» de la division.

Le programme ressemblerait à quelque chose comme:

class Program
{
    static void Main()
    {
        List<long> list = new List<long>();
        list.Add( long.MaxValue );
        list.Add( long.MaxValue - 1 );
        list.Add( long.MaxValue - 2 );

        long sumA = 0, sumB = 0;
        long res1, res2, res3;
        //You should calculate the following dynamically
        long constant = 1753413056;

        foreach (long num in list)
        {
            sumA += num / constant;
            sumB += num % constant;
        }

        res1 = (sumA / list.Count) * constant;
        res2 = ((sumA % list.Count) * constant) / list.Count;
        res3 = sumB / list.Count;

        Console.WriteLine( res1 + res2 + res3 );
    }
}

4

Si vous savez que vous avez N valeurs, pouvez-vous simplement diviser chaque valeur par N et les additionner?

long GetAverage(long* arrayVals, int n)
{
    long avg = 0;
    long rem = 0;

    for(int i=0; i<n; ++i)
    {
        avg += arrayVals[i] / n;
        rem += arrayVals[i] % n;
    }

    return avg + (rem / n);
}

c'est exactement la même chose que la solution de Patrick Hofman, sinon moins correcte que la version finale
phuclv

2

Je l'ai également essayé et j'ai trouvé une solution plus rapide (bien que seulement d'un facteur 3/4). Il utilise une seule division

public static long avg(long a, long b, long c) {
    final long quarterSum = (a>>2) + (b>>2) + (c>>2);
    final long lowSum = (a&3) + (b&3) + (c&3);
    final long twelfth = quarterSum / 3;
    final long quarterRemainder = quarterSum - 3*twelfth;
    final long adjustment = smallDiv3(lowSum + 4*quarterRemainder);
    return 4*twelfth + adjustment;
}

smallDiv3est la division par 3 en utilisant la multiplication et en travaillant uniquement pour les petits arguments

private static long smallDiv3(long n) {
    assert -30 <= n && n <= 30;
    // Constants found rather experimentally.
    return (64/3*n + 10) >> 6;
}

Voici l' ensemble du code comprenant un test et un benchmark, les résultats ne sont pas si impressionnants.


1

Cette fonction calcule le résultat en deux divisions. Il devrait bien se généraliser à d'autres diviseurs et tailles de mots.

Il fonctionne en calculant le résultat de l'addition de deux mots, puis en élaborant la division.

Int64 average(Int64 a, Int64 b, Int64 c) {
    // constants: 0x10000000000000000 div/mod 3
    const Int64 hdiv3 = UInt64(-3) / 3 + 1;
    const Int64 hmod3 = UInt64(-3) % 3;

    // compute the signed double-word addition result in hi:lo
    UInt64 lo = a; Int64 hi = a>=0 ? 0 : -1;
    lo += b; hi += b>=0 ? lo<b : -(lo>=UInt64(b));
    lo += c; hi += c>=0 ? lo<c : -(lo>=UInt64(c));

    // divide, do a correction when high/low modulos add up
    return hi>=0 ? lo/3 + hi*hdiv3 + (lo%3 + hi*hmod3)/3
                 : lo/3+1 + hi*hdiv3 + Int64(lo%3-3 + hi*hmod3)/3;
}

0

Math

(x + y + z) / 3 = x/3 + y/3 + z/3

(a[1] + a[2] + .. + a[k]) / k = a[1]/k + a[2]/k + .. + a[k]/k

Code

long calculateAverage (long a [])
{
    double average = 0;

    foreach (long x in a)
        average += (Convert.ToDouble(x)/Convert.ToDouble(a.Length));

    return Convert.ToInt64(Math.Round(average));
}

long calculateAverage_Safe (long a [])
{
    double average = 0;
    double b = 0;

    foreach (long x in a)
    {
        b = (Convert.ToDouble(x)/Convert.ToDouble(a.Length));

        if (b >= (Convert.ToDouble(long.MaxValue)-average))
            throw new OverflowException ();

        average += b;
    }

    return Convert.ToInt64(Math.Round(average));
}

pour l'ensemble de {1,2,3}la réponse est 2, mais votre code reviendra 1.
Ulugbek Umirov

Code @UlugbekUmirov corrigé, devrait utiliser des types doubles pour le traitement
Khaled.K

1
C'est ce que je veux éviter - l'utilisation de double, car nous allons perdre de la précision dans un tel cas.
Ulugbek Umirov le

0

Essaye ça:

long n = Array.ConvertAll(new[]{x,y,z},v=>v/3).Sum()
     +  (Array.ConvertAll(new[]{x,y,z},v=>v%3).Sum() / 3);
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.