1

I am struggling with a randomize algorithm which I'm unable to solve.

Below is the randomize criteria.

  1. User type a random integer number, i.e 28
  2. User type a factor number, i.e 1.2
  3. If user key in 28 on point #1 and 1.2 for point #2, then there should be a total of 28 randomize number generated and the sums of the 28 randomize number must equals to 29.2
  4. For each randomize number must be the value between 0.01 to 9.99, maximum two decimals.

I have done my code and it meet criteria 1-3, but I can't seems to meet criteria #4. If a lot of high randomize number generated upfront, the ending iteration would not be enough to generate at least 0.01. It was always 0.00. Anything i missed out?

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace RandomizeAlgo
{
    public static class Extend
    {
        public static double RandomNumberBetween(this Random random, double minValue, double maxValue)
        {
            var next = random.NextDouble();

            return minValue + (next * (maxValue - minValue));
        }

        public static double ToFloor(this double value)
        {
            return Math.Floor(value * 100) / 100;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            var rnd = new Random();
            var totalPeople = 28;
            var factor = 1.2;
            var shouldHaveTotalNumber = totalPeople + factor;

            var defaultMin = 0.01;
            var defaultMax = 9.99;

            while (true)
            {
                var iteration = 0;
                var balance = shouldHaveTotalNumber;
                var listOfRandomizedNumber = new List<double>();
                for (var i = 1; i <= totalPeople; i++)
                {
                    var randomizeResult = 0.00;
                    if (i == totalPeople)
                    {
                        randomizeResult = balance;
                    }
                    else if (balance >= defaultMax)
                    {
                        randomizeResult = rnd.RandomNumberBetween(defaultMin, defaultMax);
                        randomizeResult = randomizeResult.ToFloor();
                    }
                    else
                    {
                        randomizeResult = rnd.RandomNumberBetween(defaultMin, balance);
                        randomizeResult = randomizeResult.ToFloor();
                    }
                    listOfRandomizedNumber.Add(randomizeResult);

                    Console.WriteLine(string.Format("{0:0.00}", randomizeResult));

                    balance = balance - randomizeResult;
                }

                iteration++;
                //Assertion
                var totalSumNumberGenerated = listOfRandomizedNumber.Sum().ToString("0.00");
                if (totalSumNumberGenerated != shouldHaveTotalNumber.ToString("0.00"))
                {
                    throw new InvalidOperationException("Total #"+ iteration + " iteration is: " + totalSumNumberGenerated + " . Invalid Randomize Number. Sum does not match");
                }
                else
                {
                    Console.WriteLine("Iteration #"+ iteration + " successfully generated");
                }
            }
        }
    }
}
TJ.
  • 241
  • 2
  • 14
  • Add a few examples what the difference is between total and totalSumNumber. – rene Oct 20 '18 at 06:59
  • @rene , I have modify the thread and make it more clearer. Basically if let's say user key in 2 for "Total People", and "Factor Number" is 1.2, meaning there should be 2 numbers generated and the sum of two number should be 3.2. – TJ. Oct 20 '18 at 07:10
  • I guess in the last iteration you shouldn't generate a random number, but calculate it instead? – Lennart Stoop Oct 20 '18 at 07:17
  • That is not what I asked. I hoped you would run it and then tell us what the actual values are in `shouldHaveTotalNumber` and `totalSumNumberGenerated`. Run it a couple of times. Edit the results you got in the question. – rene Oct 20 '18 at 07:19
  • Looks like there is also missing a break statement. Needs an if that looks at iteration? – rene Oct 20 '18 at 07:22
  • And you may also want to re-calculate the minvalue & maxvalue of the randomization (within the current boundaries): this will mitigate the case where for example the first 2 numbers are 0.1 and 0.1, and the last number would exceed maxvalue to complete the sum – Lennart Stoop Oct 20 '18 at 07:38
  • 1
    @rene , I found a bug in my code and i updated the code. Basically right now I can meet criteria #1 to #3 . One problem with my code is that it is difficult to meet criteria #4. There is high possibility that the first few randomize number is a high value. Assume 3 randomize number, and total sum of it must be 4.2. If the first randomize is 4.19, then the 2nd and 3rd number became 0.01 and 0.00 . Which fails criteria #4 – TJ. Oct 20 '18 at 07:40
  • @LennartStoop Yes i notice that as well. I will try to think what you have said. I have updated my code in this thread and right now i'm unable to successfully meet criteria #4. – TJ. Oct 20 '18 at 07:44

2 Answers2

1

Randomize proportions of the sum, not magnitudes

Since you don't seem to care about distribution, how about a different approach. Generate a list of random numbers in any range, compute their sum, then compute a constant that will scale the sum to the target. For example, if the actual sum was 10, but the target sum was 20, the scaling value is 2. Then multiply every element by 2.

Another way to look at it is that you are generating a series of numbers that represents the proportion that element contributes to the sum, rather than the magnitude itself.

This approach would always get the list you need in one iteration, except for one pesky thing: your range and precision requirements. By enforcing a precision to two decimals we could round ourselves off target, so we will need to double check and then iterate a few times until it works out.

Also, don't forget floating point comparisons must use a tolerance, in this case 0.005.

public static List<double> GetTheList(int count, double target)
{
    var random = new Random();
    var iteration = 0;

    while (true)
    {
        iteration++;

        //Start with a list of random numbers of any range
        var list = Enumerable.Range(1, count).Select( i => random.NextDouble() );

        //Take the sum
        var sum = list.Sum();

        //Determine a scaling factor to make the sum hit the target
        var scale = target / sum;

        //Scale all of the numbers, and round them off
        var results = list.Select( n => Math.Round(n * scale, 2) ).ToList();

        //Check to see if rounding errors put you off target
        if (Math.Abs(results.Sum() - target) > 0.005) continue;

        //Ensure bounds
        if (results.Min() < 0.01 || results.Max() > 99.9) continue;

        //The list matches all the criteria, so return it
        Console.WriteLine("{0} iterations executed.", iteration);
        return results;
    }
}

Example on DotNetFiddle

John Wu
  • 50,556
  • 8
  • 44
  • 80
  • I did copy your code and did an infinity loop to check your random number. I ran for over 1 minute, possible few thousand iteration and i found out that there are no occurrence of any value over 5.00 to 9.99 . – TJ. Oct 20 '18 at 11:08
  • This seems to me logical. If the sum of 28 double is 29.2, the average is 1,043. so in the algorithm above it will hit the limits many times and goes into the next iteration. Most likely the likelyhood that you will have any number above 5 will be like winning the lotto. I add to this the algorithm makes perfect sense. – Aldert Oct 20 '18 at 12:00
  • @TJ. I didn't see any requirements in your post regarding [distribution](http://mathworld.wolfram.com/StatisticalDistribution.html). The results match all of the specifications that you wrote and are completely random. If there is an additional requirement, please amend your post. – John Wu Oct 20 '18 at 23:43
0

Here's an implementation I've made, but first allow me to explain the theory behind it. The idea is that instead of simply creating random numbers that up to a certain number we say we want random numbers in pairs that add up to the average per number multiplied by two. this way the numbers while random have a sticky effect to the average and therefore the target number.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace RandomizeAlgo {

    class Program {

        public static void Main(string[] args) {
            int people = 0;
            double factor = 0.0;
            getInputs(ref people, ref factor);
            Random r = new Random();
            List<double> randoms = new List<double>();
            double total = factor + people;
            double divisor = Math.Round(total / people, 2);
            double counter = 0.0;
            if (people % 2 == 1) {
                randoms.Add(Math.Round(Math.Min(Math.Min(r.NextDouble() + 0.01, 0.99) * 10 / people, divisor), 2));
                total -= randoms[0];
                divisor = Math.Round(total / (people - 1), 2);
                counter += randoms[0];
            } for (int i = randoms.Count; i != people; i += 2) {
                if (i == people - 2) {
                    double finals = Math.Round(Math.Min(r.NextDouble() + 0.01, 0.99) * divisor, 2);
                    randoms.Add(finals);
                    randoms.Add(Math.Round((people + factor) - (counter + finals), 2));
                } randoms.AddRange(getRandomPair(divisor, r, ref counter));
            } counter = 0.0;
            for (int i = 0; i != people; i++) {
                counter += randoms[i];
                Console.WriteLine("+ " + randoms[i].ToString() + " = " + counter.ToString());
            } Console.ReadLine();
        }

        public static void getInputs(ref int people, ref double factor) {
            while (true) {
                Console.Clear();
                Console.Write("Integer: ");
                try { people = Convert.ToInt32(Console.ReadLine()); break; }
                catch { Console.WriteLine("ERROR: Must be an integer."); Console.ReadLine(); }
            } while (true) {
                Console.Clear();
                Console.Write("Double: ");
                try { factor = Convert.ToDouble(Console.ReadLine()); break; }
                catch { Console.WriteLine("ERROR: Must be an double."); Console.ReadLine(); }
            }
        }

        public static double[] getRandomPair(double divisor, Random r, ref double counter) {
            double[] parts = { Math.Round(Math.Min(r.NextDouble() + 0.01, 0.99) * divisor, 2), 0 };
            parts[1] = (divisor * 2) - parts[0];
            counter = counter + parts[0] + parts[1];
            return parts;
        }
    }
}
Naate
  • 115
  • 10