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?
CodePudding user response:
Give up on trying to do it in one line (and program defensively, there are quite a few edge cases)
Here is some working code
If you need good random distribution you may need to make the "Need more room" and "Top-up" bits smarter
In particular note that the returned list is generally descending when the maxPart
is large (because the max parameter to Random.Next()
reduces as the algorithm progresses). To fix this do a shuffle at the end, just before before return
ing. Shuffling is worth learning about, too much to go into here. This is a good start About 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;
}
//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)));
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
20,20,20,20,20
30,24,19,13,14
66,26,8,0,0
46,27,5,21,1
26,42,18,11,3
CodePudding user response:
public List<int> SplitValue(int value, int numParts, int maxPartValue)
{
Random rnd = new Random();
int remainingValue = value;
int part;
if (value / numParts > maxPartValue)
throw new ArgumentException("Not possible");
if (maxPartValue > value - numParts)
throw new ArgumentException("Not possible");
List<int> Parts = new List<int>();
for (int loop = 1; loop < numParts; loop )
{
part = rnd.Next(Math.Max(1, remainingValue - (numParts - loop)*maxPartValue), Math.Min(remainingValue / (numParts - loop), maxPartValue) 1);
Parts.Add(part);
remainingValue -= part;
}
Parts.Add(value - Parts.Sum());
return Parts;
}