В середньому 3 довгих цілих числа


103

У мене є 3 дуже великих підписаних цілих числа.

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

Я хочу порахувати їх усечену середню. Очікуване середнє значення є long.MaxValue - 1, яке є 9223372036854775806.

Неможливо його обчислити як:

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

Примітка. Я читав усі ці питання щодо середнього числа 2, але не бачу, як цю техніку можна застосувати до середнього числа 3.

З використанням цього було б дуже просто BigInteger, але припустимо, я не можу ним користуватися.

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

Якщо я переходжу double, то, звичайно, я втрачаю точність:

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

Якщо я конвертую decimal, він працює, але також припустимо, що я не можу ним користуватися.

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

Запитання: Чи існує спосіб обчислити усечену середню кількість 3 дуже великих цілих чисел лише з використанням longтипу? Не вважайте це питання специфічним для C #, просто мені простіше надати зразки в C #.


1
чому б не обчислити загальний середній розріз і підсумувати, що від max?
Андреас Нідермайр

6
@AndreasNiedermair не працюватиме, якщо я маю long.MinValueі long.MaxValueсеред значень.
Улугбек Уміров

хороший улов, дійсно :)
Андреас Нідермайр

Ви впевнені, що нам потрібно про це хвилюватися, чи не слід цим керувати рамки?
Болу

11
Чи є якась фактична причина, яка BigIntegerабо decimalвиключається, чи це просто заради того, щоб зробити це важким?
jpmc26

Відповіді:


142

Цей код буде працювати, але це не дуже.

Спочатку він ділить усі три значення (він перекриває значення, тож ви 'втрачаєте' залишок), а потім ділиться залишок:

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

Зауважте, що вищевказаний зразок не завжди працює належним чином при наявності одного або декількох негативних значень.

Як обговорювалося з Ulugbek, оскільки кількість коментарів вибухає нижче, ось поточне найкраще рішення як позитивних, так і негативних значень.

Завдяки відповідям та коментарям Ulugbek Umirov , James S , KevinZ , Marc van Leeuwen , gnasher729 це поточне рішення:

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 Ні , в математиці, (x + y + z) / 3 = x / 3 + y / 3 + z / 3.
Кріс Вандермоттен

4
Я використовував Z3, щоб довести це правильним для всіх підрахунків змінних між 1 і 5.
usr

5
Звичайно, це, здається, спрацьовує, але те, як діє ціле укорочення, розчарує вас. f(1,1,2) == 1покиf(-2,-2,8) == 2
КевінЗ

11
Зауважте, що через пошкоджену мозком семантику модульної операції це може давати результат, який вимикається на один, а саме округлений, а не вниз, якщо дозволені негативні значення для змінних. Наприклад, якщо x, y додатні кратні 3, а z дорівнює -2, ви отримуєте (x+y)/3це занадто багато.
Марк ван Левенен

6
@KevinZ: ... ефект якого повинен бути скасований програмістом, який ніколи не хотів в першу чергу такої поведінки. Дозволити програмісту вказати модуль, а не виводити його з залишку, який компілятор, можливо, отриманий з модуля, здається корисним.
supercat

26

NB: Патрік вже дав чудову відповідь . Розширюючи це, ви можете зробити загальну версію для будь-якої кількості таких чисел:

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
Це не відбудеться long, але для менших типів зауважте, що друга сума може переповнюватись.
користувач541686

7

Патрік Хофман виклав чудове рішення . Але в разі потреби це все одно можна реалізувати кількома іншими способами. Використовуючи алгоритм тут, я маю інше рішення. Якщо ретельно впроваджуватись, це може бути швидше, ніж численні підрозділи в системах з повільними апаратними дільниками. Це можна додатково оптимізувати, використовуючи техніку розділення за константами від захоплення хакером

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;

У C / C ++ на 64-бітних платформах це набагато простіше __int128

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

2
Я б припустив, що хороший спосіб ділити 32-бітне непідписане значення на 3 - це множення на 0x55555555L, додавання 0x55555555 і зсув праворуч на 32. Ваш метод розділення3, для порівняння, виглядає так, ніби він зажадає багатьох дискретних кроків.
supercat

@supercat Так, я знаю цей метод. Метод від захоплення хакером ще правильніше, але я реалізую інший раз
phuclv

Я не впевнений, що означає "правильніше". Взаємне множення може у багатьох випадках отримувати точні значення безпосередньо, або в іншому випадку значення виходу, які можна уточнити в один або два етапи. BTW, я думаю, я мав би запропонувати помножити на 0x55555556, що дало б точні результати, не потребуючи "додавання". Також, чи правильний ваш стан циклу? Що модифікує H і L у циклі?
supercat

Між іншим, навіть якщо у вас немає апаратного множення, можна швидко наблизити неподписаний x=y/3через x=y>>2; x+=x>>2; x+=x>>4; x+=x>>8; x+=x>>16; x+=x>>32;. Результат буде дуже близький до x, і його можна зробити точним, обчислюючи delta=y-x-x-x;та використовуючи коригування xза потребою.
суперкат

1
@ gnasher729 Цікаво, чи може вона використовувати оптимізацію в 32-розрядних комп'ютерах, оскільки часто не може розмножувати 64x64 → 128 біт
phuclv

7

Ви можете обчислити середнє число на основі різниці між числами, а не використовувати суму.

Скажімо, x - макс, y - медіана, z - хв (як у вас є). Ми їх назвемо макс, медіаною та хв.

Додано умовна перевірка відповідно до коментаря @ 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
Дивіться коментар @ UlugbekUmirov: Не вдалося б у випадку, якщо я маю long.MinValue і long.MaxValue серед значень
Bolu

@Bolu коментар застосовний лише до long.MinValue. Тому я додав це умовне, щоб він працював у нашому випадку.
La-comadreja

Як можна використовувати медіану, коли вона не була ініціалізована?
phuclv

@ LưuVĩnhPhúc, медіана - це значення між мінімумом та максимумом.
La-comadreja

1
не (double)(2 / 3)дорівнює 0,0?
phuclv

5

Оскільки C використовує флорированний поділ, а не евклідовий поділ, можливо, простіше обчислити правильно округлене середнє значення трьох неподписаних значень, ніж три підписаних. Просто додайте 0x8000000000000000UL до кожного числа перед тим, як взяти непідписане середнє значення, відніміть його після отримання результату та скористайтеся неперевіреною ланкою назад, Int64щоб отримати підписане середнє значення.

Щоб обчислити непідписане середнє значення, обчисліть суму верхніх 32 біт трьох значень. Потім обчисліть суму нижнього 32 біта трьох значень плюс сума зверху плюс одне [плюс один - щоб отримати округлий результат]. Середній показник буде в 0x55555555 разів перевищує першу суму плюс третину другої.

Продуктивність 32-бітних процесорів може бути підвищена шляхом створення трьох значень "сума", кожне з яких становить 32 біти, так що кінцевий результат є ((0x55555555UL * sumX)<<32) + 0x55555555UL * sumH + sumL/3; можливо, це може бути додатково покращено заміною sumL/3на ((sumL * 0x55555556UL) >> 32), хоча останній залежатиме від оптимізатора JIT [він може знати, як замінити поділ на 3 на множення, і його код може бути фактично більш ефективним, ніж явна операція множення].


Після додавання 0x8000000000000000UL чи не впливає перелив на результат?
phuclv

@ LưuVĩnhPhúc Переливу немає. Перейдіть до моєї відповіді на реалізацію. Розщеплення на 2 32 бітові int було непотрібним.
KevinZ

@KevinZ: Розбиття кожного значення на верхню і нижню 32-бітну частину швидше, ніж розділення його на частку на поділ на три та решту.
supercat

1
@ LưuVĩnhPhúc: На відміну від підписаних значень, які ведуть себе семантично як числа та не дозволяють переповнювати законну програму C, неподписані значення зазвичай поводяться як члени обгорткового абстрактного алгебраїчного кільця, тому семантика обгортання добре визначена.
суперкат

1
Кортеж представляє -3, -2, -1. Після додавання 0x8000U до кожного значення значення слід розділити навпіл: 7F + FF 7F + FE 7F + FD. Додайте верхню і нижню половини, отримуючи 17D + 2FA. Додайте суму верхньої половини до суми нижньої половини, отримуючи 477. Помножте 17D на 55, отримавши 7E81. Розділіть 477 на три, отримавши 17D. Додайте 7E81 до 17D, отримуючи 7FFE. Відніміть 8000 від цього і отримайте -2.
supercat

5

Patching Патрика Хофман «S рішення з SUPERCAT корекція» с, я даю вам наступне:

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

І випадок 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 ) );
}

Це завжди дає слово () середнього значення та усуває всі можливі крайові випадки.


1
Я перевів AvgN в код Z3 і довів це правильним для всіх розумних розмірів вводу (наприклад, 1 <= args.Length <= 5 і розмір бітвектора 6). Ця відповідь правильна.
usr

Чудова відповідь Кевін. Дякуємо за ваш внесок! meta.stackoverflow.com/a/303292/993547
Патрік Хофман

4

Ви можете використати той факт, що ви можете записати кожне з чисел як y = ax + b, де xє постійною. Кожне aбуло б y / x(ціла частина цього поділу). Кожен b буде y % x(решта / модуль цього поділу). Якщо ви обираєте цю константу розумним способом, наприклад, вибравши квадратний корінь максимального числа в якості константи, ви можете отримати середнє xчисло чисел, не маючи проблем із переповненням.

Середнє значення довільного списку чисел можна знайти, знайшовши:

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

де %позначає модуль і /позначає «цілу» частину поділу.

Програма виглядала б приблизно так:

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

Якщо ви знаєте, що у вас є N значень, чи можете ви просто розділити кожне значення на N та підсумовувати їх разом?

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

це точно так само, як рішення Патріка Хофмана, якщо не менш правильне, що остаточна версія
phuclv

2

Я також спробував це і придумав швидше рішення (хоча лише на коефіцієнт приблизно 3/4). Він використовує єдиний поділ

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

де smallDiv3є поділ на 3, використовуючи множення і працюючи лише для малих аргументів

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

Тут є весь код, включаючи тест і орієнтир, результати не такі вражаючі.


1

Ця функція обчислює результат у двох поділах. Він повинен добре узагальнити інші роздільники та розміри слів.

Він працює, обчислюючи результат додавання подвійних слів, а потім відпрацьовуючи поділ.

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

Математика

(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

Код

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

набір {1,2,3}відповіді є 2, але ваш код повернеться 1.
Улугбек Уміров

Код @UlugbekUmirov виправлено, для обробки слід використовувати подвійні типи
Khaled.K

1
Цього я хочу уникати - використання double, оскільки ми втрачаємо точність у такому випадку.
Улугбек Уміров

0

Спробуйте це:

long n = Array.ConvertAll(new[]{x,y,z},v=>v/3).Sum()
     +  (Array.ConvertAll(new[]{x,y,z},v=>v%3).Sum() / 3);
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.