1

X can be a positive decimal or integer and Y is a positive integer. X >= 2 * Y. What is the optimal way (in terms of code performance) to accomplish this?

Context

I am creating a damage over time (DoT) batching algorithm for a game. X is the total duration of the DoT effect and Y is the desired duration (size) of each batch.

If you simply take X / Y you usually end up with a remainder, which introduces significant rounding error. So I want to find a set of numbers that adds up to the X, but wherein each number is as close as possible to Y.

Current Solution

totalDuration is X and batchTarget is Y. BatchDurations is a List of integers containing the batches (the output of the function).

public void CalcBatchRate(float ticksPerUpdate, int batchTarget)
{
    float totalDuration = Effect.Duration * ticksPerUpdate;
    float batches = totalDuration / batchTarget;
    float batchesRemainder = batches % 1;
    float batchSize = batchTarget;

    if (batchesRemainder != 0)
    {
        batches -= batchesRemainder;
        batchSize = totalDuration / batches;

        // Check if adding another batch improves batch size:
        while (Math.Abs(batchSize - batchTarget) > Math.Abs((totalDuration / (batches + 1)) - batchTarget))
        {
            batches++;
            batchSize = totalDuration / batches;
        }

        // Correct for decimals:
        float batchSizeRemainder = batchSize % 1;
        if (batchSizeRemainder != 0)
        {
            // Based on the size of the remainder, determine how many batches to round up/down:
            int roundDown = (int)Math.Round(batches * Math.Abs(1 - batchSizeRemainder), 0);
            int roundUp = (int)(batches - roundDown);

            for (int i = roundDown; i > 0; i--)
            {
                BatchDurations.Add((int)(batchSize - batchSizeRemainder));
            }

            for (int i = roundUp; i > 0; i--)
            {
                BatchDurations.Add((int)(batchSize + (1 - batchSizeRemainder)));
            }
        }
        else
        {
            for (int i = (int)batches; i > 0; i--)
            {
                BatchDurations.Add((int)batchSize);
            }
        }
    }
    else
    {
        for (int i = (int)batches; i > 0; i--)
        {
            BatchDurations.Add((int)batchSize);
        }
    }
}
synthc
  • 25
  • 5
  • 1
    You are asksing about performance, not about a basic approach. So show your solution for achieving the desired numbers (without "significant rounding error") and then define how fast you need to be. To do so specify the timing you have measured and name the needed improved timing. – Yunnosch Apr 23 '22 at 07:33
  • 1
    If however you have no solution at all yet please drop the speed aspect of your question and focus on any solution. You can show a solution of the one with "significant rounding error", maybe there is a simple way to fix that problem. – Yunnosch Apr 23 '22 at 07:35
  • 1
    I also wonder why the most obvious solution (R ceiled, then floored) is not useable for you. – Yunnosch Apr 23 '22 at 07:38
  • The obvious solution with significant rounding error that I mentioned is simply X / Y. I am working on a solution that eliminates rounding error, but may not be very fast. Could you elaborate on the R ceiled, then floored solution? – synthc Apr 23 '22 at 07:50
  • Would you say that this formula is true? `X == N * Y + R` for N the number of batches (floor X/Y) and R the remainder of X/Y. – Yunnosch Apr 23 '22 at 08:24
  • Yes, that seems right. I added my solution with no rounding error. – synthc Apr 23 '22 at 09:00
  • The interface is unclear to me. I thoght you want N numbers with a total of X. the shown function does not return anything. And if it is a solution without rounding error but you are not asking for help with achieving a certain speed improvement.... then what is your question? – Yunnosch Apr 23 '22 at 11:07
  • Sorry, this is a method that adds the batches to List BatchDurations of the parent object. The numbers added to that list are the N numbers with a total of X (totalDuration) that are as close as possible to Y (batchTarget). As for speed, I don't have a specific requirement. This is for a game, so it should be as fast as possible. – synthc Apr 23 '22 at 19:59
  • I am confused by the apparent complexity of your code. I would do `for(0..R) Add(Y+1); for (0..N-R) Add(Y);`. Did I miss something? – Yunnosch Apr 24 '22 at 10:10
  • Sorry, I don't know R. What would that solution look like in a C language (or python)? – synthc Apr 25 '22 at 02:36
  • R according to the definition we agreed on. – Yunnosch Apr 25 '22 at 05:40
  • OK, I think I understand. That's essentially what I'm doing with the roundUp / roundDown loops to distribute the remainder. Your solution seems like it would be faster, but it does not account for decimals; is that correct (what if R is a decimal)? – synthc Apr 25 '22 at 21:25
  • Please give an example where R is a decimal; I cannot think of one. – Yunnosch Apr 26 '22 at 05:44
  • X = 125; Y = 50. X / Y = 2 with a remainder of 0.5. Maybe I am not understanding your math. Could you give an actual code example? – synthc Apr 26 '22 at 09:48
  • The remainder of 125/50 is 25 not 0.5. If I just code it I cannot be sure that you understand the math. If I try to explain, I cannot be sure that you understood. And without a selected language I cannot give any actual code. – Yunnosch Apr 26 '22 at 10:11
  • 1
    I just noticed something fishy with my loops proposal.... Sorry. Will rethink. – Yunnosch Apr 26 '22 at 10:17
  • 1
    I start to worry that "Sum X and mean Y" is impossible for an integer number of batches... – Yunnosch Apr 26 '22 at 10:45
  • 1
    How about rethinking the requirements, e.g. "Sum X, N batches, batch size difference <=1". Or "Sum X, Y<= batch_size < 2 Y". – Yunnosch Apr 26 '22 at 10:48
  • 1
    Oh, right, I'm confusing integer and float modulo. If X > Y and both are integers R will never be a decimal. The problem I was running into is that due to the game speed setting changing the duration (X), sometimes X is a decimal. So two mistakes I made with my original question: 1. X may not be an integer (X could be rounded without introducing too much rounding error, if it speeds up the function significantly). 2. Y should not be the mean of batches, but rather each batch should be as close as possible to Y. I will update the question. – synthc Apr 26 '22 at 20:31

1 Answers1

1

Not sure what exactly you are trying to calculate, but here's some Python code.

def subdivide(totalDuration, batchTarget):

  print (f'Aiming to have total duration of {totalDuration} with batches of size {batchTarget}')

  nBatches = totalDuration // batchTarget
  totalError = totalDuration % batchTarget

  if totalError > batchTarget - totalError:
      nBatches = nBatches + 1
      totalError = totalError - batchTarget

  error = totalError // nBatches
  extraNum = totalError % nBatches
  regularNum = nBatches - extraNum
  regularSize = batchTarget + error
  extraSize = regularSize + 1

  checkTotalDuration = regularNum * regularSize + extraNum * extraSize

  print (f'{regularNum} batches of size {regularSize} and {extraNum} batches of size {extraSize}')
  print (f'have total duration of {checkTotalDuration}')
n. m. could be an AI
  • 112,515
  • 14
  • 128
  • 243
  • Looks to be essentially the same as my C# solution except no loops because you don't have to add the batches to a list. Also this solution does not handle a decimal value for totalDuration. This at least makes me more confident that my approach is right - thanks. – synthc Apr 27 '22 at 23:51