Skip to content

Instantly share code, notes, and snippets.

@teaalltr
Last active September 9, 2018 16:39
Show Gist options
  • Save teaalltr/e744c9da5ed18898fae85f29a560e90f to your computer and use it in GitHub Desktop.
Save teaalltr/e744c9da5ed18898fae85f29a560e90f to your computer and use it in GitHub Desktop.

Units formations

---- NEW: improved algorithm at the end of the document ----

Experimental data

Military units can be arranged in various formations. Here's an overview on how this may be implemented

Line formation

If you table the number of units in the first row, in the second row and so on accordingly to the number of units in the group, you'll find a pattern in the units number intervals for a given row count: namely, the length of the first interval (0...6) is 6, the second (7...15) is 9 = 6+3 + 0, the third (16...28) is 13 = (6+3) + 3+1, the fourth (maybe) (29...46) is 18 = ((6+3) + 3+1 ) + 3+2.

The value of 46 is chosen by observing that the difference between the last value for a certain row count and the first value for the following row count is always 3; in this case, with 46 units 4 rows are formed: the first 3 with 12 units and the last with 10. For the next value, 47, we guess 5 rows with 10, 10, 10, 10, 7.

Of course, since there's a limit of 40 to the number of selectable units, there's unfortunately no way to check this guess.

Formulas

If we neglect the first interval (0...6) from above, we can define a recurrence equation between the others. It looks like this:

def f(n):
    if n == 0:
    	return 9
    return f(n-1) + n + 3

In order to find the nth last value before the row count changes, we just need to sum f(i) from 0 up to n. Then, in order to get the row count for a specified numOfUnits, we can iteratively check which of the values is the upper bound of the interval where our numOfUnits lies:

def findRowCount(numOfUnits):
    # the last numOfUnits before the row count changes
    lastBeforeInc = 6  
    # iteration counter
    i = 0

    # for the first row
    if numOfUnits <= 6:
        return 1

    while numOfUnits > lastBeforeInc: # is numOfUnits greater than last bound?
        lastBeforeInc += f(i) # no, increase the bound
        i += 1

    rowCount = i + 1
    return (lastBeforeInc, rowCount)

Finally, we have to find the number of units for each row. The first row from the list we got from experimentation leads to the following formula:

rows[0] = math.ceil(numOfUnits / findRowCount(numOfUnits))

This seems to agree with experimental data and nicely extends to values over 40. Rows other from the last one have the same value of the first. The last row is simply:

rows[rowCount - 1] = numOfUnits - (rowCount-1)*rows[0]

or, if you care about performance:

rows[rowCount - 1] = numOfUnits % (rowCount-1)

Considerations : for performance sake (or just out of curiosity), you may want to find a better, non recursive function for f. It turns out that the recurrence equation can be solved to

    f(n) = (18 + 7*n + n^2)/2

using Mathematica's

        RSolve[{f[n] == f[n-1] + n + 3, f[0] == 9}, f[n], n]

We can separate 6 + f(0) + f(1) + f(2) + ... + f(n) (the formula for lastBeforeInc) and make it a function of the upper bound n:

def findNthLastBeforeInc(n): 
    return (90 + 65*n + 12*n^2 + n^3)/6

The above is the result of Mathematica's:

        6 + Sum[1/2 (18+7 i+i^2), {i, 0, n}]

The while loop in findRowCount simply becomes:

    while numOfUnits > lastBeforeInc: # numOfUnits greater than last bound?
        lastBeforeInc = findNthLastBeforeInc(i) # no, get the next bound
        i += 1

This allows us to get rid of the O(n) recursive function. However, it is a third-degree function.

The best would be to have an explicit function for n. This is easily achieved by imposing:

    findNthLastBeforeInc(n) + 1 <= numOfUnits < findNthLastBeforeInc(n + 1)

with some constraints over numOfUnits and n. So, by doing:

assumptions := 
 nIntegers && numOfUnits > 6 && numOfUnitsIntegers && n >= 0;

Reduce[{findNthLastBeforeInc[n] + 1 <= numOfUnits < findNthLastBeforeInc[n + 1], assumptions}, n]

we find that (for numOfUnits >= 16), n must lie between 0 and the first root of a third-degree polynomial, namely:

        Root[90 - 6 numOfUnits + 65 x + 12 x^2 + x^3 &, 1]

For numOfUnits == 15, n is 0. Since we're looking for natural values, we just need the ceil() of this function of numOfUnits. The root can be shown explicitly using ToRadicals[]. Unfortunately, it turns out to be a very complex function, unsuitable for real-time computing. The plot of the values looks like that of a square-root-like function. The function

        a*x^(1/6) + b*x^(1/3) + c*x + d

with parameters

{a -> -7.622169906683245, 
b -> 3.6545500464975955, 
c -> -0.002901063925120857, 
d -> 4.129231118574412}

found using

    FindFit[values, a x^(1/6) + b x^(1/3) + c*x + d, {a, b, c, d}, x]

may be a good choice.

Finally, we rewrite our findRowCount as

import math

def findRowCount(numOfUnits):
    if numOfUnits <= 6:
        return 1
    if 7 <= numOfUnits and numOfUnits <= 15:
        return 2

    a = -7.622169906683245
    b = 3.6545500464975955
    c = -0.002901063925120857
    d = 4.129231118574412
    approx = a*pow(numOfUnits, 1.0/6) + b*pow(numOfUnits, 1.0/3) + c*numOfUnits + d
    rowCount = math.ceil(approx) + 1
    return (approx, rowCount)

Improved algorithm

If you look back at the experimental data, an educated guess may be that the points where the slopes change may be expressed out as 5*i - (i-1): for the n-th point we have

        Sum[(5 i - (i - 1)), {i, 1, n}] + 1

Mathematica solves this as

        1 + 3 n + 2 n^2

The first few values are 6, 15, 28, 45, 66, which prove to be correct.

Now we must find where our numOfUnits is greater or equal to the above

        Reduce[1 + 3 n + 2 n^2 <= N, n, Integers]

And of course we take the upper bound of the interval found, namely

        -(3/4) + 1/4 Sqrt[1 + 8 N]

We need the next integer, so we Ceil the above,

        Ceiling[-(3/4) + 1/4 Sqrt[1 + 8 N]]

The expression found is the rowCount of above, in a saner form.

Again, the first row is

        rows[0] = math.ceil(numOfUnits / findRowCount(numOfUnits))

and the other rows are unchanged.

The final Python code may be:

import math

def findRowCount(numOfUnits):
    return math.ceil((math.sqrt(1 + 8*numOfUnits) - 3) / 4)

Performance considerations

Turns out that the expression is equivalent to

    -Quotient[N, Quotient[3 - Sqrt[1+8*N], 4]]

which contains less divisions and Quotient is directly mapped to the IDIV Intel x86 instruction.

More, it is equal to

    -Quotient[N, Quotient[3 - Sqrt[BitShiftLeft[N, 3]], 4]]

which not contains the addition and uses a left shift to do the multiplication, thus being faster. Or one may use the following

    -Quotient[N, Quotient[2 - Floor[Sqrt[BitShiftLeft[N, 3]]], 4]]

where Floor[Sqrt[n]] is the integer square root function and there exist plenty of optimized algorithms to do it.

BONUS: Hackish stuff

The general formula is M*i - (i-u) with some constraints on M and u. This gives us:

        Ceiling[N / Ceiling[(1-M-2*u + Sqrt[(-1+M)*(-9+M+ 8*N) + 4*(-1+M) u+4 u^2]) / (2*(-1+M))]]

The constraints on N, M and u are (N | M | u) ∈ ℤ && u ≥ 1 && M ≥ 2 && N ≥ 2. This is useful if we're not satisfied with the default game behavior 😄

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment