【问题标题】:Average of 3 long integers3 个长整数的平均值
【发布时间】:2014-07-19 22:18:31
【问题描述】:

我有 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

问题:有没有办法仅使用long 类型来计算3 个非常大的整数的截断平均值?不要认为这个问题是特定于 C# 的,只是我更容易在 C# 中提供示例。

【问题讨论】:

  • 为什么不计算总体平均差异并从最大值中减去?
  • @AndreasNiedermair 如果我的值中有long.MinValuelong.MaxValue,则不会工作。
  • 确实不错:)
  • 你确定我们需要担心这个,这不应该由框架来处理吗?
  • 排除BigIntegerdecimal是否有任何实际原因,还是只是为了让它变得困难?

标签: c# .net long-integer


【解决方案1】:

这段代码可以工作,但不是很漂亮。

它首先将所有三个值相除(它使值下限,因此您“丢失”余数),然后除以余数:

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

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

正如与 Ulugbek 讨论的那样,由于 cmets 的数量在下面呈爆炸式增长,因此这是当前正值和负值的最佳解决方案。

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

【讨论】:

  • @DavidG 没有。在数学方面,(x + y + z) / 3 = x / 3 + y / 3 + z / 3.
  • 我使用 Z3 来证明这对于 1 到 5 之间的所有变量计数都是正确的。
  • 当然这看起来可行,但整数截断的操作方式会让你搞砸。 f(1,1,2) == 1f(-2,-2,8) == 2
  • 请注意,由于模运算的脑损伤语义,如果允许变量的负值,这可能会给出一个差一的结果,即向上舍入而不是向下舍入。例如,如果 x,y 是 3 的正倍数,而 z 是 -2,你会得到 (x+y)/3,这太多了。
  • @KevinZ: ...其效果必须由一开始就不想出现这种特殊情况的程序员来撤销。让程序员指定模数而不是必须从编译器可能从模数派生的余数派生它似乎很有帮助。
【解决方案2】:

NB - Patrick 已经给了a great answer。对此进行扩展,您可以为任意数量的整数做一个通用版本,如下所示:

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;

【讨论】:

  • long 不会发生这种情况,但对于较小的类型,请注意第二个总和可能会溢出。
【解决方案3】:

帕特里克霍夫曼有posted a great solution。但如果需要,它仍然可以通过其他几种方式实现。使用算法here 我有另一个解决方案。如果仔细实施,它可能比硬件除数较慢的系统中的多个除法更快。它可以通过黑客的喜悦使用divide by constants技术进一步优化

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;

【讨论】:

  • 我建议将 32 位无符号值除以 3 的好方法是乘以 0x55555555L,加上 0x55555555,然后右移 32。相比之下,您的 divideby3 方法看起来好像这将需要许多离散的步骤。
  • @supercat 是的,我知道这种方法。黑客高兴的方法更正确,但我会再次实现
  • 我不确定“更正确”是什么意思。在许多情况下,倒数乘法可以直接产生精确的值,或者产生可以在一两个步骤中细化的值。顺便说一句,我想我应该建议乘以 0x55555556,这样就可以在不需要“添加”的情况下产生准确的结果。另外,您的循环条件是否正确?是什么修改了循环中的 H 和 L?
  • 顺便说一句,即使没有硬件乘法,也可以通过x=y&gt;&gt;2; x+=x&gt;&gt;2; x+=x&gt;&gt;4; x+=x&gt;&gt;8; x+=x&gt;&gt;16; x+=x&gt;&gt;32; 快速逼近无符号x=y/3。结果将非常接近 x,并且可以通过计算 delta=y-x-x-x; 并根据需要使用调整 x 来获得精确。
  • @gnasher729 我想知道它是否可以在 32 位计算机中使用该优化,因为它通常无法进行 64x64→128 位乘法
【解决方案4】:

您可以根据数字之间的差异而不是使用总和来计算数字的平均值。

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

【讨论】:

  • 查看@UlugbekUmirov 的评论:如果值中有 long.MinValue 和 long.MaxValue 则不起作用
  • @Bolu 该注释仅适用于 long.MinValue。所以我添加了这个条件以使其适用于我们的案例。
  • 没有初始化的中位数怎么用?
  • @LưuVĩnhPhúc,中位数是最小值和最大值之间的值。
  • (double)(2 / 3) 不等于 0.0?
【解决方案5】:

由于 C 使用的是取整除法而不是欧几里得除法,因此计算三个无符号值的正确舍入平均值可能比计算三个有符号值更容易。只需在取无符号平均值之前将 0x8000000000000000UL 添加到每个数字,然后在取结果后减去它,然后使用未经检查的强制转换回 Int64 以获得有符号平均值。

要计算无符号平均值,请计算三个值的前 32 位的总和。然后计算三个值的底部 32 位的总和,加上上面的总和,再加上一个 [加一是产生一个四舍五入的结果]。平均值将是第一个总和的 0x55555555 倍,再加上第二个总和的三分之一。

32 位处理器的性能可以通过生成三个“和”值来提高,每个值都是 32 位长,因此最终结果是 ((0x55555555UL * sumX)&lt;&lt;32) + 0x55555555UL * sumH + sumL/3;通过用((sumL * 0x55555556UL) &gt;&gt; 32) 替换sumL/3 可能会进一步增强它,尽管后者将取决于JIT 优化器[它可能知道如何用乘法替换除以3,并且它的代码实际上可能比显式乘法运算]。

【讨论】:

  • 加上0x8000000000000000UL后溢出不影响结果吗?
  • @LưuVĩnhPhúc 没有溢出。转到my answer 进行实施。不过,拆分为 2 个 32 位 int 是不必要的。
  • @KevinZ:将每个值拆分为高低 32 位部分比将其拆分为除以三的商和余数更快。
  • @LưuVĩnhPhúc:与有符号值在语义上表现得像数字并且不允许在合法的 C 程序中溢出不同,无符号值通常表现得像包装抽象代数环的成员,所以包装语义很好已定义。
  • 元组代表-3、-2、-1。在将 0x8000U 添加到每个值之后,这些值应分成两半:7F+FF 7F+FE 7F+FD。添加上半部分和下半部分,得到 17D + 2FA。将上半部分的总和与下半部分的总和相加,得到 477。将 17D 乘以 55,得到 7E81。将 477 除以三得到 17D。将 7E81 添加到 17D 产生 7FFE。减去 8000 得到 -2。
【解决方案6】:

您可以使用这样一个事实,即您可以将每个数字写为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 );
    }
}

【讨论】:

    【解决方案7】:

    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(),并消除所有可能的边缘情况。

    【讨论】:

    【解决方案8】:

    如果你知道你有 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);
    }
    

    【讨论】:

    • 这与帕特里克霍夫曼的解决方案相同,如果不是比最终版本更正确
    【解决方案9】:

    此函数以两个除法计算结果。它应该很好地推广到其他除数和字长。

    它的工作原理是计算双字加法结果,然后计算除法。

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

    【讨论】:

      【解决方案10】:

      数学

      (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 代码已修复,应使用双精度类型进行处理
      • 这就是我想要避免的——double 的使用,因为在这种情况下我们会失去精度。
      【解决方案11】:

      我也尝试过,并提出了一个更快的解决方案(尽管只有大约 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;
      }
      

      这是whole code,包括测试和基准,results 没有那么令人印象深刻。

      【讨论】:

        【解决方案12】:

        试试这个:

        long n = Array.ConvertAll(new[]{x,y,z},v=>v/3).Sum()
             +  (Array.ConvertAll(new[]{x,y,z},v=>v%3).Sum() / 3);
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2021-05-18
          • 1970-01-01
          • 2018-01-21
          • 2018-09-26
          • 2019-12-16
          • 1970-01-01
          相关资源
          最近更新 更多