4

In Javascript, I am attempting to iterate through an array of indices indexArray. This array can be any length, but for ease of explanation, I will use length 3 indexArray.length = 3.

I have an external function: getResult(indexArray) which returns an integer.

I would like to recursively loop through indexArray calling getResult(indexArray) and incrementing each index until a condition has been met: getResult(indexArray) > 5

The initial array is always an array of 1s: indexArray = [1, 1, 1]. I would like the indices to increment from the last element to the 0th element in the following manner: [1,1,1], [1,1,2], [1,1,3], [1,1,4] *Condition Met* [1,2,1], [1,2,2], [1,2,3], ... And so on until the condition has been met for every combination over the integers.

I found this stackoverflow question which helped me formulate my initial attempts, however as I do not know the constraints on my indices, for loops do not work for my use case.

Here is what I have attempted so far:

numVars = 3;
currentIndices = Array(numVars).fill(1);
end = false

function callManyTimes(indices, func) {
    doCallManyTimes(indices, func, [], 0);
}

function doCallManyTimes(indices, func, args, index) {
    if (indices.length == 0) {
        func(args);
        end = true;
    } else {
        var rest = indices.slice(1);

        args[index] = 1;
        while(!end){
            currentIndices[index] = args[index]
            currentVal = func(currentIndices);

            if(currentVal > 5){
                doCallManyTimes(rest, func, args, index + 1)
            }
              
            ++args[index];
        }
    }
}

function getResult(indices){
    res = indices.reduce((partialSum, a) => partialSum + a, 0);
    console.log(currentIndices);
    return res;
}

callManyTimes(currentIndices, getResult);

And the output I get (from logging the indices on each loop) is:

[ 1, 1, 1 ]
[ 2, 1, 1 ]
[ 3, 1, 1 ]
[ 4, 1, 1 ]
[ 4, 1, 1 ]
[ 4, 1, 1 ]
[ 4, 1, 1 ]

Clearly I'm doing something wrong with my looping condition, but I can't seem to understand why the recursion is not iterating through the different indices. Or why it repeats its final state 4 times. If someone could help clear this up, it would be much appreciated. Thanks!

EDIT: My Expected/Desired Output is:

[1, 1, 1]
[1, 1, 2]
[1, 1, 3]
[1, 1, 4] //condition met here
[1, 2, 1]
[1, 2, 2]
[1, 2, 3] //condition met here
[1, 3, 1]
[1, 3, 2] //condition met here
[1, 4, 1] //condition met here
[2, 1, 1]
[2, 1, 2]
[2, 1, 3] //condition met here
[2, 2, 1]
[2, 2, 2] //condition met here
[2, 3, 1] //condition met here
[3, 1, 1]
[3, 1, 2] //condition met here
[3, 2, 1] //condition met here
[4, 1, 1] //condition met here

EDIT #2: Reduced, but accurate condition-calculating logic

Instead of the line currentVal > 5, I am storing the previous value (previousVal) of currentVal, and checking the following: (currentVal > 0) && ((currentVal/previousVal) > .99). If both of those conditions are met, I'd like to increment the array of indices as previously described. Below is a rough version of my getResults(indices) function after ripping out extraneous steps in order to keep the logic as close to original as possible.

const BigNumber = require("bignumber.js");

function getResults(indices){
    sum = BigNumber(0);

    for(i = 0; i < indices.length; i++){
        fiveTerm = indices.length - 1 - i;
        fiveValue = BigNumber(5).pow(fiveTerm);

        var threeValue;
        if(i == 0){
            threeValue = BigNumber(1);
        } else {
            threeIndices = indices.slice(-i);
            threeIndexSum = threeIndices.reduce((partialSum, a) => partialSum + a, 0);
            threeValue = BigNumber(3).pow(threeIndexSum);
        }

        currentTerm = fiveValue.times(threeValue);
        sum = sum.plus(currentTerm);

    }

    indexSum = indices.reduce((partialSum, a) => partialSum + a, 0);
    divisor = (BigNumber(3).pow(indexSum)).minus(BigNumber(5).pow(indices.length))

    result = sum.div(divisor);

    console.log("Indices: " + indices + "\nResult: " + result.toFixed())
    return result;
}

I hope this edit is helpful for clarification regarding the conditions around which I'd like the iterators to increment.

  • can you please paste the exact output you are expecting? – Mulan Aug 06 '22 at 19:34
  • @Mulan I have added an edit with the exact expected output – sorsumIntel Aug 06 '22 at 19:54
  • Yes, your understanding is correct. Ideally, that line should be excluded, however, for this exercise logging the inputs that hit the condition is helpful. The result<=5 is a simplified dummy condition to help me understand the inner workings as my real condition is significantly more complicated, and unrelated to the problem at hand. – sorsumIntel Aug 06 '22 at 21:38

2 Answers2

1

disciplined recursion

Recursion in a functional heritage and so using it with functional style yields the best results. This means avoiding things like mutation, variable reassignment, and other side effects wherever possible. The result is a dramatic reduce in code, complexity, and cognitive load. Functions are smaller, do less, easier to write and test, and more reusable in other parts of your program.

To write getResult(limit, k) we can use inductive reasoning -

  1. If the number of remaining choices k is zero, no choices remain. Yield the empty solution, []
  2. (inductive) k is positive. If the limit is negative, the solution has gone out of bounds. Stop iteration.
  3. (inductive) k is positive and the limit is in bounds, 0 or greater. For all n of 1..limit, for all solutions of the subproblem getResult(limit - n, k - 1), prepend n to the solution and yield.

function *getResult(limit, k) {
  if (k == 0) yield []                  // 1
  if (limit < 0) return                 // 2
  for (let n = 1; n <= limit; n++)      // 3
    for (const solution of getResult(limit - n, k - 1))
      yield [n, ...solution]
}

for (const sln of getResult(6, 3))
  console.log(String(sln))
.as-console-wrapper { min-height: 100%; top: 0; }
1,1,1
1,1,2
1,1,3
1,1,4
1,2,1
1,2,2
1,2,3
1,3,1
1,3,2
1,4,1
2,1,1
2,1,2
2,1,3
2,2,1
2,2,2
2,3,1
3,1,1
3,1,2
3,2,1
4,1,1

Let's see k = 4 -

for (const sln of getResult(6, 4))
  console.log(String(sln))
1,1,1,1
1,1,1,2
1,1,1,3
1,1,2,1
1,1,2,2
1,1,3,1
1,2,1,1
1,2,1,2
1,2,2,1
1,3,1,1
2,1,1,1
2,1,1,2
2,1,2,1
2,2,1,1
3,1,1,1

And k = 5 -

for (const sln of getResult(6, 5))
  console.log(String(sln))
1,1,1,1,1
1,1,1,1,2
1,1,1,2,1
1,1,2,1,1
1,2,1,1,1
2,1,1,1,1

And k = 6 -

for (const sln of getResult(6, 6))
  console.log(String(sln))
1,1,1,1,1,1

collect all items into array

To collect all possible results into an array, use Array.from -

const arr = Array.from(getResult(4,2))
console.log(arr)
[[1,1],[1,2],[1,3],[2,1],[2,2],[3,1]]

without generators

Above we use generators to yield the solutions. Generators are a good fit for combinatorics because generally the solution space is quite large and the caller can begin consuming the result immediately, as each solution is generated. Additionally the generators can be paused/resumed/cancelled at any time, allowing the caller to avoid vast amounts of wasted computations when an early result can be determined. If you want to force the caller to handle an entire array of possibilities, of course that's possible, but maybe not recommended. Notice the similarities reasoning and structure for both programs -

const range = (min, max) =>
  min > max
    ? []
    : [min, ...range(min + 1, max)]

const getResult = (limit, k) =>
  k == 0                             // 1
    ? [[]]
: limit < 0                          // 2
    ? []
: range(1, limit).flatMap(n =>       // 3
    getResult(limit -n, k - 1).map(solution =>
      [n, ...solution]
    )
  )

console.log(getResult(6, 3))
.as-console-wrapper { min-height: 100%; top: 0; }
[
  [1,1,1],
  [1,1,2],
  [1,1,3],
  [1,1,4],
  [1,2,1],
  [1,2,2],
  [1,2,3],
  [1,3,1],
  [1,3,2],
  [1,4,1],
  [2,1,1],
  [2,1,2],
  [2,1,3],
  [2,2,1],
  [2,2,2],
  [2,3,1],
  [3,1,1],
  [3,1,2],
  [3,2,1],
  [4,1,1]
]
Mulan
  • 129,518
  • 31
  • 228
  • 259
  • This solution is rather elegant. I've been spending some time revising how I think about these problems. The part I'm having the most difficulty with is how to rework this answer if the condition changes. In the question I asked, the condition was Sum(Indices) <= 5. However, for my real condition, it's a very complex calculation that sometimes takes minutes to calculate, and uses all of the currentIndices as variables. Is there a way to revamp the "limit" logic to take into account an external function using the current iteration's indices? Something like `if(checkCondition(indices))` – sorsumIntel Aug 07 '22 at 00:22
  • Maybe you could provide your real input? By simplifying the question we sometimes leave out details that need to be addressed in the real problem. I’m happy to take a look at it this afternoon. – Mulan Aug 07 '22 at 05:55
  • I have included additional information regarding the real input and conditions (under EDIT #2). I'm not sure that the conditions can be rolled into the recursion in the way you initially illustrated, as they are slightly convoluted. Also, I really appreciate the help, thanks! – sorsumIntel Aug 07 '22 at 08:18
  • Mmm, ya I don't completely understand what you are trying to do. I'm trying to visualize it, but since the provided program isn't working as intended, I can't make out what your actual intentions are. By real input, I mean include the real input _data_ along with the expected output for that data. It would help to "zoom out" a bit, ie what are the contents of `indices`? What are they indexes of? What's the big picture? – Mulan Aug 07 '22 at 13:54
  • (con't) As engineers, when we get stuck on a problem, we often forget to share the original context. For example I might ask "my corn granules are uneven in size? how to fix?" What type of grinding process am I using? What is the moisture content of the kernels? What is the application of the granules? Am I making muffins or am I making biodiesel fuel? The two contexts are completely different and advice for either could vary immensely. See also [XY problem](https://en.wikipedia.org/wiki/XY_problem). – Mulan Aug 07 '22 at 14:05
1

First I wanted to use recursion. But I found myself keep doing for loop and thinking like human counting, so I decided to adapt another answer from the same question page.

Explanation:

We start from the position of right most digit. Evaluating the getResult function then increasing counting until we hit a value > 5 (or another stop condition). Then we reset digit on our position, and move position left one step (like going from counting 099 to 100). We keep doing it until we are passed the left most position. No recursion.

var numVars = 3;
var currentIndices = Array(numVars).fill(1);

function loop(arr, getResult, stopCondition) {
  var pos;
  do {
    var value = getResult(arr);
    print(arr)
    pos = arr.length - 1;
    arr[pos]++;
    while (pos >= 0 && stopCondition(value)) {
      arr[pos] = 1;
      pos--;
      if (pos >= 0) {
        arr[pos]++;
        value = getResult(arr);
        if (stopCondition(value)) {
          print(arr)        
        }
      }
    }
  } while (pos >= 0);
}

function getResult(indices) {
  return indices.reduce((partialSum, a) => partialSum + a, 0);
}

function print(indices) {
  console.log("" + indices);
}

function stopCondition(value) {
  return value > 5;
}

loop(currentIndices, getResult, stopCondition);
.as-console-wrapper {
  max-height: 100% !important;
  top: 0;
}
IT goldman
  • 14,885
  • 2
  • 14
  • 28