1

I'm trying to do as the title suggests. Take in a value like 100 with a part number like 5. Then split 100 into 5 parts that add up to 100. Each part being random. So a result would be like 20, 25, 5, 40, 10. It would return a list/array. This is the code I'm currently using thanks to a post here from 10+ years ago.

        List<int> a = new List<int>();
        a = Enumerable.Repeat(0, numOfStats - 1)        // Seq with (n-1) elements...
                          .Select(x => Random.Range(1, points))  // ...mapped to random values
                          .Concat(new[] { 0, points })
                          .OrderBy(x => x)
                          .ToArray()
                          .ToList();

        return a.Skip(1).Select((x, i) => x - a[i]).ToList();

numStats is the division number and points is the total value that will be split.

The only problem is that I need to make sure each part is no more than a certain number. So each part would be max 30 for example. Anyone know how I can edit this to make sure there is a clamp on the parts?

Austin
  • 13
  • 1
  • 3
  • A different approach to randomness with constraints is to just do rejection sampling. That is, just do the simple kind of sampling you already know, and just throw out a sample if it doesn't meet the criteria and try again. For the problem at hand, I estimate the acceptance rate is about 5%, so you'll have to try about 20 times on the average to get an acceptable sample. No big deal, in most cases -- 20 times a very small number of microseconds is still pretty fast. – Robert Dodier Jul 22 '22 at 04:10
  • Anyway, rejection sampling is very simple, so you can get something working right away, and then relax and spend more time on a more complex method if you want. – Robert Dodier Jul 22 '22 at 04:10
  • Which part values are acceptable? Integers >= 1? Or >= 0? Or even fractions? – Klaus Gütter Jul 22 '22 at 04:24

1 Answers1

0

Give up on trying to do it in one line (and program defensively, there are quite a few edge cases)

EDIT Added SplitValue2() (an improvement over SplitValue()) and Shuffle()

static List<int> SplitValue(int value, int nParts, int maxPart)
{
  if (maxPart < value / nParts) throw new Exception("Not possible");
  var rng = new Random();
  var lst = new List<int>();
  var total = 0;
  //  Initial random allocation
  for (var i = 0; i < nParts; i++)
  {
    var part = rng.Next(Math.Min(maxPart + 1, value - total)); // upper bound is exclusive
    lst.Add(part);
    total += part;
    //  Need more room
    if (total == value && i + 1 < nParts)
      for (var j = i; j >= 0; j--)
      {
        if (lst[i] > 0)
        {
          lst[i] -= 1;
          total--;
        }
      }
  }
  //  Top-up
  for (var i = 0; i < nParts && total < value; i++)
  {
    var topup = Math.Min(maxPart - lst[i], value - total);
    lst[i] += topup;
    total += topup;
  }
  if (total != 100) throw new Exception("Failed");
  return lst;
}

static List<int> SplitValue2(int valueToSplit, int nParts, int maxPart)
{
  var result = new int[nParts];
  var prng = new Random();
  if (maxPart < valueToSplit / nParts) throw new Exception("Not possible");
  var remaining = valueToSplit;
  while (remaining > 0)
  {
    for (var i = 0; i < nParts && remaining > 0; i++)
    {
      var next = prng.Next(0, Math.Min(maxPart - result[i], remaining) + 1);
      result[i] += next;
      remaining -= next;
    }
  }
  return Shuffle(result.ToList());
}

static List<int> Shuffle(List<int> list)
{
  if (list == null) throw new Exception("nothing to do");
  var cpy = new List<int>(list);
  var prng = new Random();
  var ret = new List<int>();
  var len = cpy.Count;
  if (len == 0) return ret;
  var lenRem = len;
  while (lenRem > 1)
  {
    var select = prng.Next(lenRem);
    ret.Add(cpy[select]);
    cpy.RemoveAt(select);
    lenRem--;
  }
  ret.Add(cpy[0]);
  return ret;
}

Console.WriteLine("Split 1");
//Console.WriteLine(string.Join(',', SplitValue(100,5,10)));
Console.WriteLine(string.Join(',', SplitValue(100,5,20)));
Console.WriteLine(string.Join(',', SplitValue(100,5,30)));
Console.WriteLine(string.Join(',', SplitValue(100,5,70)));
Console.WriteLine(string.Join(',', SplitValue(100,5,70)));
Console.WriteLine(string.Join(',', SplitValue(100,5,150)));
Console.WriteLine("\nSplit 2");
//Console.WriteLine(string.Join(',', SplitValue2(100,5,10)));
Console.WriteLine(string.Join(',', SplitValue2(100,5,20)));
Console.WriteLine(string.Join(',', SplitValue2(100,5,30)));
Console.WriteLine(string.Join(',', SplitValue2(100,5,70)));
Console.WriteLine(string.Join(',', SplitValue2(100,5,70)));
Console.WriteLine(string.Join(',', SplitValue2(100,5,150)));

I don't claim that this is bug-free, you will need to test (and curious to see what other ideas are offered)

Sample output

Split 1
20,20,20,20,20
30,30,15,15,10
69,27,2,2,0
44,24,22,1,9
85,9,6,0,0

Split 2
20,20,20,20,20
21,30,11,25,13
3,4,64,4,25
8,10,56,13,13
3,0,1,86,10
AlanK
  • 1,827
  • 13
  • 16
  • this seems to be working! thank you so much. So fast :) Though I'm having trouble figuring out what the "Failed" exception at the end is for? – Austin Jul 22 '22 at 03:01
  • @MitchWheat No it's clamped at 70 as the test intends. – AlanK Jul 22 '22 at 07:05
  • @Austin I was catching conditions under which it appeared to be terminating normally but the parts did not add up. It's how I found the bug/shortcoming that led to "topping up". You may want to replace it with an 'Assert'; definitely keep some check(s) while you fiddle with the algorithm. (O and if this answers your question satisfactorily, I'd appreciate if you marked it as such - for the brownie points;-) – AlanK Jul 22 '22 at 07:10
  • @Austin If this answers your question satisfactorily, I'd appreciate if you marked it as such ("for the brownie points, like, upvote, subscribe and such";-). I haven't checked it out but MitchWheat 's answer looks tidy at first glance. – AlanK Jul 22 '22 at 07:18
  • seems like i can't get around the descending, but if I shuffle I think that's ok. – Austin Jul 22 '22 at 13:28
  • @MitchWheat why is it more complicated? – Austin Jul 23 '22 at 03:39
  • @Austin, I added `Shuffle()`. It is primitive but effective. Added `SplitValue2()` which is a lot tidier and comparable to `SplitValue()` in terms of efficiency. If you need to improve from here I would suggest looking at a better quality random number generator. Thanks for an enjoyable question. – AlanK Jul 23 '22 at 05:37