Trung bình của 3 số nguyên dài


103

Tôi có 3 số nguyên có dấu rất lớn.

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

Tôi muốn tính toán trung bình cắt ngắn của họ. Giá trị trung bình mong đợi là long.MaxValue - 1, là 9223372036854775806.

Không thể tính nó là:

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

Lưu ý: Tôi đã đọc tất cả những câu hỏi đó về trung bình của 2 số, nhưng tôi không thấy kỹ thuật đó có thể áp dụng cho trung bình của 3 số như thế nào.

Nó sẽ rất dễ dàng với việc sử dụng BigInteger, nhưng hãy giả sử tôi không thể sử dụng nó.

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

Nếu tôi chuyển đổi thành double, tất nhiên, tôi sẽ mất độ chính xác:

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

Nếu tôi chuyển đổi thành decimal, nó hoạt động, nhưng cũng giả sử rằng tôi không thể sử dụng nó.

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

Câu hỏi: Có cách nào để tính trung bình bị cắt ngắn của 3 số nguyên rất lớn chỉ với việc sử dụng longkiểu không? Đừng coi câu hỏi đó là C # cụ thể, chỉ cần tôi cung cấp mẫu trong C # sẽ dễ dàng hơn.


1
tại sao không tính toán khác biệt trung bình tổng thể và lấy giá trị đó từ tối đa?
Andreas Niedermair

6
@AndreasNiedermair Sẽ không hoạt động nếu tôi có long.MinValuelong.MaxValuegiữa các giá trị.
Ulugbek Umirov

bắt tốt, thực sự :)
Andreas Niedermair

Bạn có chắc chúng ta cần phải lo lắng về điều này, điều này có nên được xử lý bởi framework không?
Bolu

11
Có lý do thực tế nào đó BigIntegerhoặc decimalbị loại trừ, hay chỉ vì mục đích khó khăn?
jpmc 26

Câu trả lời:


142

Mã này sẽ hoạt động, nhưng nó không đẹp.

Đầu tiên, nó chia cả ba giá trị (nó xếp tầng các giá trị, vì vậy bạn 'mất' phần còn lại), và sau đó chia phần còn lại:

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

Lưu ý rằng mẫu trên không phải lúc nào cũng hoạt động bình thường khi có một hoặc nhiều giá trị âm.

Như đã thảo luận với Ulugbek, vì số lượng bình luận đang bùng nổ bên dưới, đây là giải pháp TỐT NHẤT hiện tại cho cả giá trị tích cực và tiêu cực.

Nhờ câu trả lời và nhận xét của Ulugbek Umirov , James S , KevinZ , Marc van Leeuwen , gnasher729 , đây là giải pháp hiện tại:

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 Không. Trong toán học , (x + y + z) / 3 = x / 3 + y / 3 + z / 3.
Kris Vandermotten

4
Tôi đã sử dụng Z3 để chứng minh điều này đúng cho tất cả các số lượng biến từ 1 đến 5.
usr

5
Tất nhiên điều này có vẻ hiệu quả, nhưng cách thức hoạt động của việc cắt xén số nguyên sẽ khiến bạn khó chịu. f(1,1,2) == 1trong khif(-2,-2,8) == 2
KevinZ

11
Lưu ý rằng do ngữ nghĩa của hoạt động mô-đun bị tổn thương não, điều này có thể đưa ra kết quả bị sai lệch, cụ thể là làm tròn lên thay vì xuống, nếu giá trị âm cho các biến được cho phép. Ví dụ: nếu x, y là bội số dương của 3 và z là -2, bạn nhận được (x+y)/3là quá nhiều.
Marc van Leeuwen

6
@KevinZ: ... hiệu ứng của nó sau đó phải được hoàn tác bởi một lập trình viên, người không bao giờ muốn hành vi trường hợp đặc biệt đó ngay từ đầu. Để người lập trình chỉ định modulus thay vì phải lấy nó từ một phần còn lại mà trình biên dịch có thể đã bắt nguồn từ modulus sẽ có vẻ hữu ích.
supercat

26

NB - Patrick đã đưa ra một câu trả lời tuyệt vời . Mở rộng về điều này, bạn có thể tạo một phiên bản chung cho bất kỳ số nguyên nào như vậy:

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
Điều này sẽ không xảy ra đối với long, nhưng đối với các loại nhỏ hơn, hãy lưu ý rằng tổng thứ hai có thể tràn.
user541686,

7

Patrick Hofman đã đăng một giải pháp tuyệt vời . Nhưng nếu cần, nó vẫn có thể được thực hiện theo một số cách khác. Sử dụng thuật toán ở đây tôi có một giải pháp khác. Nếu được thực hiện cẩn thận, nó có thể nhanh hơn so với nhiều bộ chia trong hệ thống có bộ chia phần cứng chậm. Nó có thể được tối ưu hóa hơn nữa bằng cách sử dụng kỹ thuật chia theo hằng số từ niềm vui của 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;

Trong C / C ++ trên nền tảng 64 bit, nó dễ dàng hơn nhiều với __int128

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

2
Tôi đề xuất rằng một cách hay để chia giá trị 32-bit không dấu cho 3 là nhân với 0x55555555L, thêm 0x55555555 và dịch chuyển sang phải cho 32. Phương pháp chia 3 của bạn, bằng cách so sánh, có vẻ như nó sẽ yêu cầu nhiều bước rời rạc.
supercat

@supercat vâng, tôi biết phương pháp đó. Phương pháp theo sự thích thú của hacker thậm chí còn đúng hơn nhưng tôi sẽ thực hiện vào lúc khác
phuclv 30/05

Tôi không chắc "đúng hơn" nghĩa là gì. Các phép nhân đối ứng trong nhiều trường hợp có thể mang lại giá trị chính xác trực tiếp hoặc nếu không, các giá trị mang lại có thể được tinh chỉnh trong một hoặc hai bước. BTW, tôi nghĩ tôi nên đề xuất nhân với 0x55555556, sau đó sẽ mang lại kết quả chính xác mà không cần "thêm". Ngoài ra, điều kiện vòng lặp của bạn có đúng không? Điều gì thay đổi H và L trong vòng lặp?
supercat

Ngẫu nhiên, ngay cả khi một nhân không có phần cứng, người ta có thể nhanh chóng tính gần đúng một không dấu x=y/3qua x=y>>2; x+=x>>2; x+=x>>4; x+=x>>8; x+=x>>16; x+=x>>32;. Kết quả sẽ rất gần với x và có thể được thực hiện chính xác bằng máy tính delta=y-x-x-x;và sử dụng điều chỉnh xkhi cần thiết.
mèo

1
@ gnasher729 Tôi ngạc nhiên nếu nó có thể sử dụng tối ưu hóa trong các máy tính 32-bit vì nó thường không thể làm 64x64 → 128 bit nhân
phuclv

7

Bạn có thể tính giá trị trung bình của các con số dựa trên sự khác biệt giữa các con số thay vì sử dụng tổng.

Giả sử x là cực đại, y là trung vị, z là cực tiểu (như bạn có). Chúng tôi sẽ gọi chúng là max, median và min.

Trình kiểm tra có điều kiện được thêm vào theo nhận xét của @ 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
Xem bình luận của @ UlugbekUmirov: Sẽ không hoạt động trong trường hợp nếu tôi có long.MinValue và dài.MaxValue giữa các giá trị
Bolu

@Bolu nhận xét chỉ áp dụng cho long.MinValue. Vì vậy, tôi đã thêm điều kiện này để làm cho nó hoạt động cho trường hợp của chúng tôi.
La-comadreja

Làm thế nào bạn có thể sử dụng trung vị khi nó chưa được khởi tạo?
phuclv

@ LưuVĩnhPhúc, trung vị là giá trị giữa giá trị nhỏ nhất và lớn nhất.
La-comadreja

1
không (double)(2 / 3)bằng 0,0?
phuclv

5

Vì C sử dụng phép chia có dấu hoa chứ không phải phép chia Euclid, nên có thể dễ dàng tính giá trị trung bình làm tròn đúng của ba giá trị không dấu hơn là ba giá trị có dấu. Chỉ cần thêm 0x8000000000000000UL vào mỗi số trước khi lấy giá trị trung bình chưa có dấu, trừ đi sau khi lấy kết quả và sử dụng phép ép kiểu chưa được kiểm tra Int64để lấy giá trị trung bình có dấu.

Để tính giá trị trung bình không dấu, hãy tính tổng 32 bit trên cùng của ba giá trị. Sau đó, tính tổng 32 bit dưới cùng của ba giá trị, cộng với tổng từ trên, cộng một [cộng một là để mang lại kết quả làm tròn]. Trung bình sẽ là 0x55555555 lần tổng đầu tiên, cộng với một phần ba của thứ hai.

Hiệu suất trên bộ xử lý 32 bit có thể được nâng cao bằng cách tạo ra ba giá trị "tổng", mỗi giá trị dài 32 bit, để kết quả cuối cùng là ((0x55555555UL * sumX)<<32) + 0x55555555UL * sumH + sumL/3; nó có thể được nâng cao hơn nữa bằng cách thay thế sumL/3bằng ((sumL * 0x55555556UL) >> 32), mặc dù cái sau sẽ phụ thuộc vào trình tối ưu hóa JIT [nó có thể biết cách thay thế một phép chia cho 3 bằng một phép nhân và mã của nó thực sự có thể hiệu quả hơn một phép toán nhân rõ ràng].


Sau khi thêm 0x8000000000000000UL không tràn có ảnh hưởng đến kết quả không?
phuclv

@ LưuVĩnhPhúc Không có tràn. Đi tới câu trả lời của tôi để triển khai. Tuy nhiên, việc tách thành 2 int 32 bit là không cần thiết.
KevinZ,

@KevinZ: Chia từng giá trị thành phần trên và phần dưới 32 bit nhanh hơn là chia nó thành thương số chia cho ba và phần dư.
supercat

1
@ LưuVĩnhPhúc: Không giống như các giá trị có dấu hoạt động theo ngữ nghĩa như số và không được phép tràn trong một chương trình C hợp pháp, các giá trị không dấu thường hoạt động giống như các thành viên của một vòng đại số trừu tượng bao bọc, vì vậy ngữ nghĩa của gói được xác định rõ.
supercat

1
Bộ giá trị đại diện cho -3, -2, -1. Sau khi đã thêm 0x8000U vào mỗi giá trị, các giá trị sau đó sẽ được chia đôi: 7F + FF 7F + FE 7F + FD. Thêm nửa trên và nửa dưới, tạo ra 17D ​​+ 2FA. Cộng tổng của nửa trên với tổng của nửa dưới sẽ được 477. Nhân 17D với 55 được 7E81. Chia 477 cho ba được 17D. Thêm 7E81 vào 17D tạo ra 7FFE. Lấy số đó trừ đi 8000 và được -2.
supercat

5

Vá giải pháp của Patrick Hofman với hiệu chỉnh của supercat , tôi cung cấp cho bạn những điều sau:

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

Và trường hợp phần tử 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 ) );
}

Điều này luôn cung cấp cho tầng () của giá trị trung bình và loại bỏ mọi trường hợp cạnh có thể xảy ra.


1
Tôi đã dịch mã AvgN sang Z3 và đã chứng minh điều này đúng cho tất cả các kích thước đầu vào hợp lý (ví dụ: 1 <= args.Length <= 5 và bitvector size là 6). Câu trả lời này là chính xác.
usr

Câu trả lời tuyệt vời Kevin. Cảm ơn sự đóng góp của bạn! meta.stackoverflow.com/a/303292/993547
Patrick Hofman

4

Bạn có thể sử dụng thực tế là bạn có thể viết mỗi số dưới dạng y = ax + b, trong đó xlà một hằng số. Mỗi asẽ là y / x(phần nguyên của phép chia đó). Mỗi b sẽ là y % x(phần còn lại / mô đun của phép chia đó). Nếu bạn chọn hằng số này một cách thông minh, chẳng hạn bằng cách chọn căn bậc hai của số lớn nhất làm hằng số, bạn có thể lấy giá trị trung bình của các xsố mà không gặp vấn đề với tràn.

Giá trị trung bình của một danh sách các số tùy ý có thể được tìm thấy bằng cách tìm:

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

trong đó %biểu thị modulo và/ biểu thị phần 'toàn bộ' của phép chia.

Chương trình sẽ giống như sau:

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ếu bạn biết bạn có N giá trị, bạn có thể chỉ cần chia mỗi giá trị cho N và cộng chúng với nhau?

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

điều này cũng giống như giải pháp của Patrick Hofman, nếu không muốn nói là đúng hơn là phiên bản cuối cùng
phuclv

2

Tôi cũng đã thử nó và đưa ra một giải pháp nhanh hơn (mặc dù chỉ bằng hệ số khoảng 3/4). Nó sử dụng một bộ phận duy nhất

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

đâu smallDiv3là phép chia cho 3 bằng phép nhân và chỉ hoạt động với các đối số nhỏ

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

Đây là toàn bộ mã bao gồm một bài kiểm tra và một điểm chuẩn, kết quả không phải là ấn tượng.


1

Hàm này tính toán kết quả thành hai lần chia. Nó sẽ tổng quát tốt cho các ước số và kích thước từ khác.

Nó hoạt động bằng cách tính toán kết quả cộng từ kép, sau đó tính toán phép chia.

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

môn Toán

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

cho tập hợp {1,2,3}câu trả lời là 2, nhưng mã của bạn sẽ trả về 1.
Ulugbek Umirov

@UlugbekUmirov mã số cố định, nên sử dụng các loại tăng gấp đôi để xử lý
Khaled.K

1
Đó là điều tôi muốn tránh - việc sử dụng double, vì chúng ta sẽ mất độ chính xác trong trường hợp như vậy.
Ulugbek Umirov

0

Thử cái này:

long n = Array.ConvertAll(new[]{x,y,z},v=>v/3).Sum()
     +  (Array.ConvertAll(new[]{x,y,z},v=>v%3).Sum() / 3);
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.