3个长整数的平均值


103

我有3个非常大的有符号整数。

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

我想计算他们的平均值。预期平均值long.MaxValue - 19223372036854775806

不可能将其计算为:

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

问题:是否有一种方法仅使用longtype 可以计算3个非常大的整数的截断平均值?不要认为该问题是C#特有的,对我而言,使用C#提供示例更加容易。


1
为什么不计算总体平均差异并减去最大值?
Andreas Niedermair 2014年

6
@AndreasNiedermair会在情况下,如果我没有工作,long.MinValuelong.MaxValue值之间。
Ulugbek Umirov

确实不错:)
Andreas Niedermair 2014年

您确定我们需要为此担心吗,框架不应该处理吗?
博卢2014年

11
是否有任何实际的原因BigInteger还是decimal被排除在外,或者是它只是使这个硬的缘故?
jpmc26 2014年

Answers:


142

这段代码可以用,但是效果不是很好。

它首先对所有三个值进行除法(将这些值作为底数,因此您“损失”了余数),然后对余数进行除法:

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

请注意,以上示例在具有一个或多个负值时并不总是正常工作。

正如与Ulugbek讨论的那样,由于注释的数量在下面爆炸,因此这是当前正值和负值的BEST解决方案。

感谢Ulugbek UmirovJames SKevinZMarc van Leeuwengnasher729的回答和评论,这是当前的解决方案:

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) == 1f(-2,-2,8) == 2
KevinZ 2014年

11
请注意,由于模运算的语义受损,如果允许变量的值为负,则得出的结果可能相差一个,即四舍五入而不是向下取整。例如,如果x,y是3的正数倍,而z是-2,则得到的(x+y)/3值太大。
马克·范·吕文

6
@KevinZ:...其效果必须由一开始就从不希望特殊情况的程序员撤销。让程序员指定模数,而不必从编译器可能已经从模数中导出的余数中导出模数似乎很有帮助。
超级猫

26

注意:Patrick已经给出了一个很好的答案。对此进行扩展,您可以为任意数量的整数创建通用版本,如下所示:

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,但是对于较小的类型,请注意第二个和会溢出。
user541686

7

帕特里克·霍夫曼(Patrick Hofman)发布了一个很好的解决方案。但是,如果需要,它仍然可以通过其他几种方式来实现。在这里使用算法我有另一个解决方案。如果实施得当,它可能比硬件除数慢的系统中的多个部门更快。可以通过使用除法常数技术进一步改善黑客的喜好

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;

在64位平台上的C / C ++中,使用它要容易得多 __int128

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

2
我建议将32位无符号值除以3的一种好方法是乘以0x55555555L,再加上0x55555555,然后右移32。相比之下,divideby3方法似乎需要很多离散的步骤。
超级猫2014年

@supercat是的,我知道该方法。黑客高兴的方法更加正确,但我将再次实现
phuclv 2014年

我不确定“更正确”是什么意思。在许多情况下,倒数相乘可以直接得出精确值,或者可以一阶或两步精化得出值。顺便说一句,我想我应该建议乘以0x55555556,这样就可以产生准确的结果,而无需“加”。另外,您的循环条件正确吗?是什么修改了循环中的H和L?
2014年

顺便说一句,即使没有硬件乘法,也可以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 2014年

尚未初始化的中位数如何使用?
phuclv

@LưuVĩnhPhúc,中位数是最小值和最大值之间的值。
La-comadreja 2014年

1
(double)(2 / 3)等于0.0吗?
phuclv

5

因为C使用下位除法而不是欧几里得除法,所以计算三个无符号值的适当取整平均值可能要比三个有符号值更容易。在取无符号平均值之前,只需在每个数字上加上0x8000000000000000UL,在取结果后减去它,然后使用未经检查的强制转换为Int64以获得有符号平均值。

要计算无符号平均值,请计算三个值的高32位之和。然后,计算三个值的低32位之和,再加上上方的总和,再加上一个[加一将得出舍入结果]。平均值将是第一个总和的0x55555555乘以第二个总和的三分之一。

通过产生三个“ sum”值(每个值均为32位长)可以提高32位处理器的性能,因此最终结果为((0x55555555UL * sumX)<<32) + 0x55555555UL * sumH + sumL/3;它可能可以通过替换sumL/3为来进一步增强((sumL * 0x55555556UL) >> 32),尽管后者将取决于JIT优化器[它可能知道如何用乘法替换3除,并且它的代码实际上可能比显式乘法操作更有效。


添加0x8000000000000000UL后,溢出不会影响结果吗?
phuclv 2014年

@LưuVĩnhPhúc没有溢出。转到我的答案以获取实施。拆分为2 32位int是不必要的。
KevinZ 2014年

@KevinZ:将每个值拆分为高32位和低32位部分要快于将其拆分为3分频商和余数。
超级猫

1
@LưuVĩnhPhúc:与有符号的值在语义上像数字一样,并且不允许在合法的C程序中溢出,无符号的值通常在行为上像是包装的抽象代数环的成员,因此包装的语义得到了很好的定义。
2014年

1
元组表示-3,-2,-1。将0x8000U添加到每个值之后,应将这些值分成两半:7F + FF 7F + FE 7F + FD。将上下两半相加,得出17D + 2FA。将上半部分和下半部分相加得出477。将17D乘以55得到7E81。用477除以3得到17D。将7E81添加到17D中,得到7FFE。从中减去8000,得到-2。
超级猫

5

我用supercat的修正修补Patrick Hofman的解决方案,为您提供以下内容:

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

这总是给出均值的floor(),并消除所有可能的边缘情况。


1
我将AvgN转换为Z3代码,并证明对于所有合理的输入大小(例如1 <= args.Length <= 5和位向量大小为6)都是正确的。这个答案是正确的。
usr 2014年

很好的回答凯文。感谢您的贡献!meta.stackoverflow.com/a/303292/993547
Patrick Hofman

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

这与帕特里克·霍夫曼(Patrick Hofman)的解决方案相同,即使最终版本的
准确性

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
Ulugbek Umirov 2014年

@UlugbekUmirov代码已修复,应使用双
精度

1
那就是我要避免的-的用法double,因为在这种情况下我们将失去精度。
Ulugbek Umirov 2014年

0

试试这个:

long n = Array.ConvertAll(new[]{x,y,z},v=>v/3).Sum()
     +  (Array.ConvertAll(new[]{x,y,z},v=>v%3).Sum() / 3);
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.