4
\$\begingroup\$

I am doing one kata on codewars:

Consider a sequence u where u is defined as follows:

The number u(0) = 1 is the first one in u. For each x in u, then y = 2 * x + 1 and z = 3 * x + 1 must be in u too. There are no other numbers in u. Ex: u = [1, 3, 4, 7, 9, 10, 13, 15, 19, 21, 22, 27, ...]

1 gives 3 and 4, then 3 gives 7 and 10, 4 gives 9 and 13, then 7 gives 15 and 22 and so on...

Task: Given parameter n the function dbl_linear (or dblLinear...) returns the element u(n) of the ordered (with <) sequence u.

Example: dbl_linear(10) should return 22

Note: Focus attention on efficiency

So far I did this algorithm but it's too slow and it times out. I do not really know how to refactor it. Can anyone help me? Maybe some another concept how to approach to solve it?

function dblLinear(n) {
   let arrOfResult = [1];
   const set = new Set();
   set.add(1);
   let nextInvoker = 0;

    while (arrOfResult.length <= n) {
       let y = 2 * arrOfResult[nextInvoker] + 1;
       let z = 3 * arrOfResult[nextInvoker] + 1;

       set.add(y);
       set.add(z);


       arrOfResult = Array.from(set).sort((a, b) => a - b);

       nextInvoker++;
  }

   arrOfResult.forEach(element => {
       let y = 2 * element + 1;
       let z = 3 * element + 1;

       set.add(y);
       set.add(z);
   });

   const result = Array.from(set).sort((a, b) => a - b);

   return result[n];    
}
\$\endgroup\$
  • \$\begingroup\$ @ernesto this code does not compile because it's missing a } but anyway, sorting inside the while loop is unnecessary and incredibly slow (not to mention that you're building an Array from a Set, each time): directly insert items in the right position and drop set all together. \$\endgroup\$ – Adriano Repetti Jun 4 '18 at 8:48
  • \$\begingroup\$ @AdrianoRepetti i put sorting after (below) while loop and it's even slower :). \$\endgroup\$ – Ernesto Jun 4 '18 at 8:55
  • \$\begingroup\$ @ernesto put the missing } to make clear where things start/ends but the point is the you do not need sorting at all. When inserting you already know where to (binary search for the greatest number smaller than the one you want to insert). Also note that the final size of the array is known in advance then you do not need to create/destroy/copy arrays (and you can preallocate array new Array(the_size_you_need). With slightly more complicate code you don't even need to search but even doing it you'll greatly gain in performance compared to now. \$\endgroup\$ – Adriano Repetti Jun 4 '18 at 9:24
  • \$\begingroup\$ More optimisations are possible, for example you do not need to deal with anything > n and you can simply continue plus I have the gut feeling that algorithm itself should be rewritten to be even simpler but I'm kind of lazy then... \$\endgroup\$ – Adriano Repetti Jun 4 '18 at 9:28
8
\$\begingroup\$

As I understand it, the algorithm employed is as follows:

  • set is a set of numbers, all of which are in the sequence
  • arrOfResult is set in order
  • Invariant: the first nextInvoker + 1 elements of arrOfResult are complete: i.e. they form a prefix of the sequence with no gaps
  • We loop, extending set/arrOfResult by two elements each time until we have n elements, so the prefix is of length n/2 (plus or minus a couple: I haven't worked through the exact details)
  • We then form the closure under the two operations \$x \to 2x+1\$ and \$x \to 3x+1\$ and claim that the prefix of that closure contains the first n elements with no gaps.

The claim of that last point seems to me to need a comment showing why it's true.

But more importantly, the way in which the algorithm works seems to repeat a lot of work.

       arrOfResult = Array.from(set).sort((a, b) => a - b);

sorts the same elements as the previous time round the loop, plus two more, with a full \$\Omega(n \lg n)\$ sort.

An easy way to get an asymptotic improvement would be to use a suitable priority queue. A standard binary heap would probably be sufficient. It would also simplify the main method: just three variables (the heap, a counter, and the previous element in order to detect duplicates) and a single loop.

In fact, if you really optimise that loop it's not even necessary to fully implement the binary heap.

function dbl_linear(n) {
    var prev = 0,
        heap = [1];
    while (true) {
        if (heap[0] === prev) {
            // Standard heap pop: move last element to replace first and downheap
            heap[0] = heap.pop();
        }
        else {
            if (n-- === 0) return heap[0];

            // We want to pop x and then insert 2x+1 and 3x+1
            // Since 3x+1 is guaranteed to be larger than anything in the heap,
            // we can optimise its insertion. Then we can combine the pop and the
            // other insertion into one assignment and a downheap.
            prev = heap[0];
            heap.push(3 * prev + 1);
            heap[0] = 2 * prev + 1;
        }

        // Push heap[0] down until we restore the heap property
        downheap(heap);
    }
}

downheap not included.


The clever way of tackling a problem about a sequence of integers if you're allowed access to the Internet is to use the Online Encyclopedia of Integer Sequences to see what's known about it. Searching for the prefix given in the problem statement gives two results: the desired sequence with and without duplicates. Since you clearly interpret the (IMO ambiguous) problem statement as being without duplicates, look at the notes on the sequence without duplicates:

...
a(n+1) = A007448(a(n)); giving also the record values of A007448 and their positions. - Reinhard Zumkeller, Jul 14 2010
...

A007448 is

Knuth's sequence (or Knuth numbers): a(n+1) = 1 + min( 2*a(floor(n/2)), 3*a(floor(n/3)) )

I would be happy to leave it there, but since some commentators doubted my observation that this gave a simple and efficient solution, two implementations.

A naïve implementation (ignoring everything from Zumkeller's comment after the semicolon) would be

function dbl_linear(n) {
    var cache = {0 : 1},
        a = 1,
        knuth = function(n) {
            return cache[n] = cache[n] || 1 + Math.min(2 * knuth(Math.floor((n - 1) / 2)),
                                                       3 * knuth(Math.floor((n - 1) / 3)));
        }
    while (n--) a = knuth(a);
    return a;
}

But taking into account the observations made after the semicolon we can optimise quite heavily. This is slightly less simple (although a binary search is still simpler than a heap), but more efficient, and easily argued to run in \$O(n \lg n)\$ time:

function dbl_linear(n) {
    // a(n+1) = A007448(a(n)); giving also the record values of A007448 and their positions.
    // - Reinhard Zumkeller, Jul 14 2010

    // A007448(n+1) = 1 + min(2 * A007448(n/2), 3 * A007448(n/3))

    var A002977 = [1],
        i,
        ai = 1;

    function A007448(x) {
        // Find the smallest element of A002977 which is larger than x by binary chop
        // Invariant: A002977[lb] <= x < A002977[ub]
        // To ensure the invariant we need a special case
        if (x === 0) return 1;

        var lb = 0,
            ub = A002977.length - 1,
            mid;
        while (ub - lb > 1) {
            mid = lb + ((ub - lb) >> 1);
            if (A002977[mid] > x) ub = mid;
            else lb = mid;
        }
        return A002977[ub];
    }

    for (i = 1; i <= n; i++) {
        ai = 1 + Math.min(2 * A007448(Math.floor((ai - 1) / 2)),
                          3 * A007448(Math.floor((ai - 1) / 3)));
        A002977[i] = ai;
    }

    return ai;
}

Performance measured on tio.run using Spidermonkey. Times based on a single run, so take with a pinch of salt. They are at least illustrative.

n    | original code | sineemore's code | heap   | naïve Knuth | optimised Knuth
100  |        0.034s |           0.038s | 0.028s |      0.026s |          0.029s
10^3 |        0.185s |           0.031s | 0.028s |      0.032s |          0.031s
10^4 |       15.2s   |           0.147s | 0.031s |      0.042s |          0.040s
10^5 |       timeout |           8.3s   | 0.042s |      0.268s |          0.051s
10^6 |       timeout |          timeout | 0.195s |      4.4s   |          0.155s
10^7 |    not tested |       not tested | 2.4s   |    segfault |          1.9s
\$\endgroup\$
  • 1
    \$\begingroup\$ a very efficient and simple algorithm - if you're a methematician :-P \$\endgroup\$ – t3chb0t Jun 4 '18 at 10:35
  • 1
    \$\begingroup\$ @t3chb0t, no amphetamines required ;) \$\endgroup\$ – Peter Taylor Jun 4 '18 at 10:37
  • 1
    \$\begingroup\$ @t3chb0t There are good programming-challenges that don't rely on math but they are rare. I agree that math focused problems are not very good for learning how to program because most of the time you can solve those without programming anything at all. \$\endgroup\$ – yuri Jun 4 '18 at 10:58
  • 2
    \$\begingroup\$ @JackAidley, I'm aiming more to teach how to fish than to give a fish. \$\endgroup\$ – Peter Taylor Jun 4 '18 at 13:48
  • 2
    \$\begingroup\$ @JackAidley, of course it's not faster than solving the original problem, because it solves the original problem. But I think it's quite obvious that a closed recurrence is simpler than one which involves testing set membership. In any case, I've added a naïve implementation (which is already much faster than the final code given in the other answer, and considerable simpler) and an optimised implementation (which is faster still, and about equally complicated at sineemore's code). \$\endgroup\$ – Peter Taylor Jun 5 '18 at 15:18
7
\$\begingroup\$

This is not a math baked answer, expect nested loops and heavy memory usage, you were warned

Uniqueness

Being rather oldfansined I tend to skip most new JavaScript features like Set and const, etc. To guranty array uniquness I'll use seen = {} as a lookup object.

Every time we are adding y or z values to array we'll do seen[y] = true and seen[z] = true.

if (!seen[y]) {
    seen[y] = true;
    // inserting `y` to queue
}

// inserting `z` to queue
seen[z] = true;

Order

So I bet the sort thing is the root of all evil in this task. Calling sort in a loop drasticly decreases perfomance. So in place of sorting array every time after insert we will try to keep the array order while inserting.

Every z value will always be the highest number, so we'll queue.push(z). To determine the right index for y we'll loop over current queue.

for (var i = 0; i < queue.length; i++) {
    if (queue[i] > y) break;
}
queue.splice(i, 0, y);

The splice call inserts y at given index i. This way the queue stays in order.

But wait, we are always looping from 0. Let's make this a bit more efficient. We can remember the last i value to start from it next time.

for (var i = last; i < queue.length; i++) {
    if (queue[i] > y) break;
}
queue.splice(i, 0, y);
last = i;

(this gave me great perfomance increase)

Memory

Our lookup object seen and array queue may use lots of space.

var x = queue.shift();
delete seen[x];

The shift call removes the first array element and returns it. delete removes object property. This way our queue and lookup object will contain only unprocessed values.

Example rewrite:

function dblLinear(n) {

  var queue = [1];
  var length = 0;
  var seen = {};
  var last = 0;

  while (length < n) {
    var x = queue.shift();
    delete seen[x];
    var y = 2 * x + 1;
    var z = 3 * x + 1;
    if (!seen[y]) {
      seen[y] = true;
      for (var i = last; i < queue.length; i++) {
        if (queue[i] > y) break;
      }
      queue.splice(i, 0, y);
      last = i;
    }
    seen[z] = true;
    queue.push(z);
    last--;
    length++;
  }

  return queue[0];
}


// Usage

function testing(a, b) {
  if (a !== b) console.log(a, '!==', b)
  else console.log('passed');
}

testing(dblLinear(10), 22);
testing(dblLinear(20), 57);
testing(dblLinear(30), 91);
testing(dblLinear(50), 175);
testing(dblLinear(100), 447);

Best run on codewars gave me 7218ms.

\$\endgroup\$
  • \$\begingroup\$ Nice, I find they should include some hints about what might be useful like splice or shift etc. After all this is so that you learn something. \$\endgroup\$ – t3chb0t Jun 4 '18 at 11:56
  • \$\begingroup\$ @t3chb0t, I've added links to MDN. \$\endgroup\$ – sineemore Jun 4 '18 at 12:08
  • \$\begingroup\$ I didn't mean you to include links but codewars hints about solving the challange, sorry ;-] \$\endgroup\$ – t3chb0t Jun 4 '18 at 12:10
2
\$\begingroup\$

We have two functions:
\$Y(x) = 2 * x + 1\$
\$Z(x) = 3 * x + 1\$

We need to apply these two functions to every value x in result

I simply kept two running indices of result representing which x in result we have calculated so far for both the \$Y\$ function and \$Z\$ function. I called these values yIndex and zIndex.

I then calculate Y(result[yIndex]) and Z(result[zIndex]) which I called yNext and zNext. I then append the smallest value of these two values to our result and then increment the yIndex or zIndex depending on which function gave the smaller value (or both if they are equal). Repeat until you reach desired n.

This gives \$O(n)\$ time.

+----------+--------+--------+-------+-------+-------------+----------------------------------------------------------------------------------------------+
| currentN | yIndex | zIndex | yNext | zNext |   result    |                                           comment                                            |
+----------+--------+--------+-------+-------+-------------+----------------------------------------------------------------------------------------------+
|        0 |      0 |      0 |     1 |     1 | [1]         | Base case, yNext = zNext = 1, increment both, append result                                      |
|        1 |      1 |      1 |     3 |     4 | [1,3]       | calculate new yNext and zNext, yNext < zNext, increment yIndex only, append yNext            |
|        2 |      2 |      1 |     7 |     4 | [1,3,4]     | calculate new yNext using result[yIndex of the previous row], zNext < yNext, increment zIndex only, append zNext |
|        3 |      2 |      2 |     7 |    10 | [1,3,4,7]   | ... yNext < zNext, increment yIndex                                                          |
|        4 |      3 |      2 |     9 |    10 | [1,3,4,7,9] | ...                                                                                          |
+----------+--------+--------+-------+-------+-------------+----------------------------------------------------------------------------------------------+

Here is the algorithm:

const tail = xs => xs[xs.length - 1]

function dblLinear (n) {
  const results = []
  const YZ = {
    yIndex: 0,
    zIndex: 0,
    get yNext () { return (results[YZ.yIndex] || 0) * 2 + 1 },
    get zNext () { return (results[YZ.zIndex] || 0) * 3 + 1 }
  }

  do {
    results.push(Math.min(YZ.yNext, YZ.zNext))
    YZ.yIndex += tail(results) === YZ.yNext
    YZ.zIndex += tail(results) === YZ.zNext
  } while (results.length < n)

  return results.pop()
}

Performance test between this implementation and Peter Taylor's

​​​​​1031926810​​​​​ at ​​​dblLinear(10000000)​​​ ​quokka.js:52:0​

​​​​​1: 1248.006ms​​​​​

​​​​​1031926810​​​​​ at ​​​dbl_linear2(10000000)​​​ ​quokka.js:55:0​

​​​​​2: 27667.439ms​​​​​
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.