C# 在给定部分权重算法的部分中拆分整数

C# split integer in parts given part weights algorithm

我有一个整数和一个非负权重列表,我怎样才能将整数 'split' 转换为具有相应权重的相同数量的 'buckets'?

public int[] SplitIntoBuckets(int count, int[] weights) {
    // some magic algorithm
    Debug.Assert(solution.Sum() == count);
    return solution;
}

一个简单的例子是 count = 200weights = { 25, 25, 50 } 以及解决方案 {50, 50, 100} (50+50+100 = 200)。然而,输入不一定是 'nice' 数字,另一个没有好的解决方案的例子是 count = 150 和权重 {753, 42, 95, 501}.
桶的总和必须始终等于 count 输入,算法应尽可能接近权重地在桶之间分配输入。 'close as possible' 是什么并不重要(例如,它可以是最低绝对误差、相对误差或平方误差)。
我能找到的最接近的问题是 Split evenly into buckets, however my buckets are not 'even' and Split randomly into buckets 但是权重是随机选择的 'nice' 个数字。

注意 solution[i] 等于:

round(weights[i] / weightSum * count)

存在一种极端情况,其中 weights[i] / weightSum * count 是二分之一 (x.5) 的奇数倍,这会导致 round 不必要地舍入额外的时间。例如 count = 3weights = { 1, 1 }。为了解决这个问题,我们通过从 count 中减去前面的桶的总和来计算最后一个桶。这将确保解决方案无论如何加起来 count

public int[] SplitIntoBuckets(int count, int[] weights) {
    int[] solution = new int[weights.Length];
    int weightSum = weights.Sum();
    // for every weight except the last...
    int sum = 0;
    for (int i = 0 ; i < weights.Length - 1 ; i++) {
        solution[i] = (int)Math.Round((double)weights[i] / weightSum * count);
        sum += solution[i];
    }
    // calculate the last bucket by subtracting:
    solution[weights.Length - 1] = count - sum;
    return solution;
}

我建议在跟踪精确 double 值 (v) 和四舍五入整数一 (value) 之间的差异 (diff) 时进行舍入:

public static int[] SplitIntoBuckets(int count, int[] weights) {
  if (null == weights)
    throw new ArgumentNullException(nameof(weights));
  else if (weights.Length <= 0)
    return new ArgumentOutOfRangeException(nameof(weights), "Empty weights");  

  double sum = weights.Sum(d => (double)d);

  if (sum == 0)
    throw new ArgumentException("Weights must not sum to 0", nameof(weights));

  Func<double, int> round = (double x) => (int)(x >= 0 ? x + 0.5 : x - 0.5);

  int[] result = new int[weights.Length];
  double diff = 0;

  for (int i = 0; i < weights.Length; ++i) {
    double v = count * (double)(weights[i]) / sum;
    int value = round(v);
    diff += v - value;

    if (diff >= 0.5) {
      value += 1;
      diff -= 1;
    }
    else if (diff <= -0.5) {
      value -= 1;
      diff += 1;
    }

    result[i] = value;
  }
    
  return result;
}

演示:

string demo = sstring.Join(Environment.NewLine, Enumerable
  .Range(200, 15)
  .Select(n => $"{n} = {string.Join(", ", SplitIntoBuckets(n, new int[] { 25, 25, 50 }))}"));

Console.Write(demo);
    

结果:

200 = 50, 50, 100
201 = 50, 51, 100
202 = 51, 50, 101
203 = 51, 51, 101
204 = 51, 51, 102
205 = 51, 52, 102
206 = 52, 51, 103
207 = 52, 52, 103
208 = 52, 52, 104
209 = 52, 53, 104
210 = 53, 52, 105
211 = 53, 53, 105
212 = 53, 53, 106
213 = 53, 54, 106
214 = 54, 53, 107