Media di 3 numeri interi lunghi


103

Ho 3 numeri interi con segno molto grandi.

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

Voglio calcolare la loro media troncata. Il valore medio atteso è long.MaxValue - 1, ovvero 9223372036854775806.

È impossibile calcolarlo come:

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

Nota: ho letto tutte quelle domande sulla media di 2 numeri, ma non vedo come questa tecnica possa essere applicata alla media di 3 numeri.

Sarebbe molto facile con l'uso di BigInteger, ma supponiamo che non possa usarlo.

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

Se mi converto in double, ovviamente, perdo la precisione:

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

Se mi converto in decimal, funziona, ma supponiamo anche che non possa usarlo.

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

Domanda: esiste un modo per calcolare la media troncata di 3 numeri interi molto grandi solo con l'utilizzo del longtipo? Non considerare questa domanda come specifica per C #, solo che è più facile per me fornire esempi in C #.


1
perché non calcolare il diff medio generale e sottrarlo dal massimo?
Andreas Niedermair

6
@AndreasNiedermair Non funzionerebbe se avessi long.MinValuee long.MaxValuetra i valori.
Ulugbek Umirov

buona cattura, anzi :)
Andreas Niedermair

Sei sicuro che dobbiamo preoccuparci di questo, non dovrebbe essere gestito dal framework?
Bolu

11
C'è qualche motivo effettivo che BigIntegero decimalè escluso, o è solo per rendere la cosa difficile?
jpmc26

Risposte:


142

Questo codice funzionerà, ma non è così carino.

Per prima cosa divide tutti e tre i valori (mette a terra i valori, quindi "perdi" il resto), quindi divide il resto:

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

Si noti che l'esempio sopra non funziona sempre correttamente quando si hanno uno o più valori negativi.

Come discusso con Ulugbek, poiché il numero di commenti sta esplodendo di seguito, ecco l'attuale soluzione MIGLIORE sia per i valori positivi che per quelli negativi.

Grazie alle risposte e ai commenti di Ulugbek Umirov , James S , KevinZ , Marc van Leeuwen , gnasher729 questa è la soluzione attuale:

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 No. In matematica, (x + y + z) / 3 = x / 3 + y / 3 + z / 3.
Kris Vandermotten

4
Ho usato Z3 per dimostrare che è corretto per tutti i conteggi delle variabili compresi tra 1 e 5.
usr

5
Ovviamente questo sembra funzionare, ma il modo in cui opera il troncamento di interi ti rovinerà. f(1,1,2) == 1mentref(-2,-2,8) == 2
KevinZ

11
Si noti che a causa della semantica danneggiata dal cervello dell'operazione modulo, questo può dare un risultato diverso di uno, cioè arrotondato per eccesso piuttosto che per difetto, se sono consentiti valori negativi per le variabili. Ad esempio, se x, y sono multipli positivi di 3 e z è -2, ottieni (x+y)/3che è troppo.
Marc van Leeuwen

6
@KevinZ: ... il cui effetto deve quindi essere annullato da un programmatore che non ha mai voluto quel comportamento speciale in primo luogo. Lasciare che il programmatore specifichi il modulo invece di doverlo derivare da un resto che il compilatore potrebbe aver derivato dal modulo sembrerebbe utile.
supercat

26

NB - Patrick ha già dato un'ottima risposta . Espandendo questo potresti fare una versione generica per qualsiasi numero di interi in questo modo:

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
Questo non accadrà per long, ma per i tipi più piccoli, nota che la seconda somma può traboccare.
user541686

7

Patrick Hofman ha pubblicato un'ottima soluzione . Ma se necessario può ancora essere implementato in molti altri modi. Usando l'algoritmo qui ho un'altra soluzione. Se implementato con attenzione, potrebbe essere più veloce delle divisioni multiple nei sistemi con divisori hardware lenti. Può essere ulteriormente ottimizzato utilizzando la tecnica di divisione per costanti dalla gioia degli hacker

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;

In C / C ++ su piattaforme a 64 bit è molto più semplice con __int128

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

2
Suggerirei che un buon modo per dividere un valore senza segno a 32 bit per 3 è moltiplicare per 0x55555555L, aggiungere 0x55555555 e spostare a destra per 32. Il metodo divideby3, al confronto, sembra che richiederebbe molti passaggi discreti.
supercat

@supercat sì, conosco quel metodo. Il metodo per la gioia degli hacker è ancora più corretto ma implementerò per un'altra volta
phuclv

Non sono sicuro di cosa significhi "più corretto". Le moltiplicazioni reciproche possono in molti casi fornire direttamente valori esatti, oppure produrre valori che possono essere raffinati in uno o due passaggi. A proposito, penso che avrei dovuto suggerire di moltiplicare per 0x55555556, che avrebbe prodotto risultati esatti senza bisogno di "aggiungere". Inoltre, la tua condizione del loop è corretta? Cosa modifica H e L nel ciclo?
supercat

Per inciso, anche se non si dispone di una moltiplicazione hardware, è possibile approssimare rapidamente una x=y/3via non firmata x=y>>2; x+=x>>2; x+=x>>4; x+=x>>8; x+=x>>16; x+=x>>32;. Il risultato sarà molto vicino a x e può essere reso preciso calcolando delta=y-x-x-x;e utilizzando la regolazione xsecondo necessità.
supercat

1
@ gnasher729 Mi chiedo se possa utilizzare tale ottimizzazione nei computer a 32 bit poiché spesso non può eseguire la moltiplicazione 64x64 → 128 bit
phuclv

7

È possibile calcolare la media dei numeri in base alle differenze tra i numeri anziché utilizzare la somma.

Diciamo che x è il massimo, y è la mediana, z è il minimo (come hai). Li chiameremo max, median e min.

Controllo condizionale aggiunto secondo il commento di @ 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
Vedi il commento di @ UlugbekUmirov: non funzionerebbe se avessi long.MinValue e long.MaxValue tra i valori
Bolu

@Bolu il commento è applicabile solo a long.MinValue. Quindi ho aggiunto questo condizionale per farlo funzionare per il nostro caso.
La-comadreja

Come puoi usare la mediana quando non è stata inizializzata?
phuclv

@ LưuVĩnhPhúc, la mediana è il valore tra il minimo e il massimo.
La-comadreja

1
non è (double)(2 / 3)uguale a 0,0?
phuclv

5

Poiché C utilizza la divisione con pavimento anziché la divisione euclidea, potrebbe essere più semplice calcolare una media correttamente arrotondata di tre valori senza segno rispetto a tre valori con segno. Aggiungi semplicemente 0x8000000000000000UL a ogni numero prima di prendere la media senza segno, sottrailo dopo aver preso il risultato e usa un cast non controllato Int64per ottenere una media con segno .

Per calcolare la media senza segno, calcolare la somma dei primi 32 bit dei tre valori. Quindi calcola la somma dei 32 bit inferiori dei tre valori, più la somma dall'alto, più uno [il più uno è quello di produrre un risultato arrotondato]. La media sarà 0x55555555 volte la prima somma, più un terzo della seconda.

Le prestazioni sui processori a 32 bit potrebbero essere migliorate producendo tre valori di "somma" ciascuno dei quali è lungo 32 bit, in modo che il risultato finale sia ((0x55555555UL * sumX)<<32) + 0x55555555UL * sumH + sumL/3; potrebbe essere ulteriormente migliorato sostituendolo sumL/3con ((sumL * 0x55555556UL) >> 32), sebbene quest'ultimo dipenda dall'ottimizzatore JIT [potrebbe sapere come sostituire una divisione per 3 con un moltiplicatore, e il suo codice potrebbe effettivamente essere più efficiente di un'operazione di moltiplicazione esplicita].


Dopo aver aggiunto 0x8000000000000000UL, l'overflow non influisce sul risultato?
phuclv

@ LưuVĩnhPhúc Non c'è overflow. Vai alla mia risposta per un'implementazione. Tuttavia, la suddivisione in 2 int a 32 bit non era necessaria.
KevinZ

@KevinZ: dividere ogni valore in una parte superiore e inferiore a 32 bit è più veloce che dividerlo in un quoziente di divisione per tre e resto.
supercat

1
@ LưuVĩnhPhúc: A differenza dei valori con segno che si comportano semanticamente come numeri e non sono autorizzati a traboccare in un programma C legittimo, i valori senza segno generalmente si comportano come membri di un anello algebrico astratto avvolgente, quindi la semantica di avvolgimento è ben definita.
supercat

1
La tupla rappresenta -3, -2, -1. Dopo aver aggiunto 0x8000U a ciascun valore, i valori dovrebbero quindi essere divisi a metà: 7F + FF 7F + FE 7F + FD. Aggiungi le metà superiore e inferiore, ottenendo 17D + 2FA. Aggiungi la metà superiore alla metà inferiore ottenendo 477. Moltiplica 17D ​​per 55 ottenendo 7E81. Dividi 477 per tre ottenendo 17D. Aggiungi 7E81 a 17D ​​ottenendo 7FFE. Sottrai 8000 da quello e ottieni -2.
supercat

5

Applicando la patch alla soluzione di Patrick Hofman con la correzione di supercat , ti do quanto segue:

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

E il caso dell'elemento N:

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

Questo dà sempre il floor () della media ed elimina ogni possibile caso limite.


1
Ho tradotto il codice AvgN in Z3 e ho dimostrato che è corretto per tutte le dimensioni di input ragionevoli (ad esempio 1 <= args.Length <= 5 e dimensione bitvector di 6). Questa risposta è corretta.
usr

Splendida risposta Kevin. Grazie per il tuo contributo! meta.stackoverflow.com/a/303292/993547
Patrick Hofman

4

Potresti usare il fatto che puoi scrivere ciascuno dei numeri come y = ax + b, dove xè una costante. Ciascuno asarebbe y / x(la parte intera di quella divisione). Ogni b sarebbe y % x(il resto / modulo di quella divisione). Se scegli questa costante in modo intelligente, ad esempio scegliendo come costante la radice quadrata del numero massimo, puoi ottenere la media dei xnumeri senza avere problemi di overflow.

La media di un elenco arbitrario di numeri può essere trovata trovando:

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

dove %denota modulo e /denota la parte "intera" della divisione.

Il programma sarebbe simile a:

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

Se sai di avere N valori, puoi semplicemente dividere ogni valore per N e sommarli insieme?

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

questa è esattamente la stessa della soluzione di Patrick Hofman, se non meno corretta della versione finale
phuclv

2

L'ho anche provato e ho trovato una soluzione più veloce (anche se solo di un fattore circa 3/4). Utilizza una singola divisione

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

dove smallDiv3è la divisione per 3 usando la moltiplicazione e lavorando solo per piccoli argomenti

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

Ecco l' intero codice compreso un test e un benchmark, i risultati non sono così impressionanti.


1

Questa funzione calcola il risultato in due divisioni. Dovrebbe generalizzarsi bene ad altri divisori e dimensioni delle parole.

Funziona calcolando il risultato dell'addizione di due parole, quindi elaborando la divisione.

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

Matematica

(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

Codice

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

per l'insieme della {1,2,3}risposta è 2, ma il tuo codice tornerà 1.
Ulugbek Umirov

@UlugbekUmirov codice corretto, dovrebbero essere utilizzati tipi doppi per l'elaborazione
Khaled.K

1
Questo è ciò che voglio evitare: l'uso di double, poiché in questo caso perderemo la precisione.
Ulugbek Umirov

0

Prova questo:

long n = Array.ConvertAll(new[]{x,y,z},v=>v/3).Sum()
     +  (Array.ConvertAll(new[]{x,y,z},v=>v%3).Sum() / 3);
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.