-1

Problem:

Fuel Injection Perfection

Commander Lambda has asked for your help to refine the automatic quantum antimatter fuel injection system for her LAMBCHOP doomsday device. It's a great chance for you to get a closer look at the LAMBCHOP - and maybe sneak in a bit of sabotage while you're at it - so you took the job gladly.

Quantum antimatter fuel comes in small pellets, which is convenient since the many moving parts of the LAMBCHOP each need to be fed fuel one pellet at a time. However, minions dump pellets in bulk into the fuel intake. You need to figure out the most efficient way to sort and shift the pellets down to a single pellet at a time.

The fuel control mechanisms have three operations:

Add one fuel pellet Remove one fuel pellet Divide the entire group of fuel pellets by 2 (due to the destructive energy released when a quantum antimatter pellet is cut in half, the safety controls will only allow this to happen if there is an even number of pellets) Write a function called solution(n) which takes a positive integer as a string and returns the minimum number of operations needed to transform the number of pellets to 1. The fuel intake control panel can only display a number up to 309 digits long, so there won't ever be more pellets than you can express in that many digits.

For example: solution(4) returns 2: 4 -> 2 -> 1 solution(15) returns 5: 15 -> 16 -> 8 -> 4 -> 2 -> 1

Test cases

Inputs: (string) n = "4" Output: (int) 2

Inputs: (string) n = "15" Output: (int) 5

my code:

def solution(n):
n = int(n)
if n == 2:
    return 1
if n % 2 != 0:
    return min(solution(n + 1), solution(n - 1)) + 1
else:
    return solution(int(n / 2)) + 1

This is the solution that I came up with with passes 4 out of 10 of the test cases. It seems to be working fine so im wondering if it is because of the extensive runtime. I thought of applying memoization but im not sure how to do it(or if it is even possible). Any help would be greatly appreciated :)

Zenith
  • 23
  • 1
  • 5

5 Answers5

2

There are several issues to consider:

First, you don't handle the n == "1" case properly (operations = 0).

Next, by default, Python has a limit of 1000 recursions. If we compute the log2 of a 309 digit number, we expect to make a minimum of 1025 divisions to reach 1. And if each of those returns an odd result, we'd need to triple that to 3075 recursive operations. So, we need to bump up Python's recursion limit.

Finally, for each of those divisions that does return an odd value, we'll be spawning two recursive division trees (+1 and -1). These trees will not only increase the number of recursions, but can also be highly redundant. Which is where memoization comes in:

import sys
from functools import lru_cache

sys.setrecursionlimit(3333)  # estimated by trial and error

@lru_cache()
def solution(n):
    n = int(n)

    if n <= 2:
        return n - 1

    if n % 2 == 0:
        return solution(n // 2) + 1

    return min(solution(n + 1), solution(n - 1)) + 1

print(solution("4"))
print(solution("15"))
print(solution(str(10**309 - 1)))

OUTPUT

> time python3 test.py
2
5
1278
0.043u 0.010s 0:00.05 100.0%    0+0k 0+0io 0pf+0w
>

So, bottom line is handle "1", increase your recursion limit, and add memoization. Then you should be able to solve this problem easily.

cdlane
  • 40,441
  • 5
  • 32
  • 81
  • This question has an iterative answer that requires only O(log n) steps to find: https://stackoverflow.com/a/39589499 – גלעד ברקן Jul 19 '21 at 03:30
  • "... And if each of those returns an odd result, we'd need to triple that to 3075 recursive operations ..." Can you explain more about why need to triple the value? I still don't get it. – Owen Lie Oct 08 '22 at 16:32
1

There are more memory- and runtime-efficient ways to solve the problem, which is what Google is testing for with their constraints. Every time you recurse a function, you put another call on the stack, or 2 calls when you recurse twice on each function call. While they seem basic, a while loop was a lot faster for me.

Think of the number in binary - when ever you have a streak of 1s >1 in length at LSB side of the number, it makes sense to add 1 (which will flip that streak to all 0s but add another bit to the overall length), then shift right until you find another 1 in the LSB position. You can solve it in a fixed memory block in O(n) using just a while loop.

rreagan3
  • 117
  • 6
1

An implementation of @rreagan3's solution, with the exception that an input of 3 should lead to a subtraction rather than an addition even through 3 has a streak of 1's on the LSB side:

def solution(n):
    n = int(n)
    count = 0
    while n > 1:
        if n & 1 == 0:
            n >>= 1
        elif n & 2 and n != 3:
            n += 1
        else:
            n -= 1 # can also be: n &= -2
        count += 1
    return count

Demo: https://replit.com/@blhsing/SlateblueVeneratedFactor

blhsing
  • 91,368
  • 6
  • 71
  • 106
1
def solution(n):
    n = int(n)
    count = 0
    while n != 1:
        if n & 1 == 0:
            n >>= 1
        elif n == 3 or ((n >> 1) & 1 == 0):
            n -= 1
        else:
            n += 1
        count += 1
    return count
ecoraegbu
  • 21
  • 2
0

If you don't want or can't use functools, you can build your own cache this way :

cache = {}


def solution_rec(n):
    n = int(n)
    if n in cache:
        return cache[n]
    else:
        if n <= 1:
            return 0
        if n == 2:
            return 1
        if n % 2 == 0:
            div = n / 2
            cache[div] = solution(div)
            return cache[div] + 1
        else:
            plus = n + 1
            minus = n - 1
            cache[plus] = solution(n + 1)
            cache[minus] = solution(n - 1)
            return min(cache[plus], cache[minus]) + 1

However, even if it runs much faster and has less recursive calls, it's still too much recursive calls for Python default configuration if you test the 309 digits limit.

it works if you set sys.setrecursionlimit to 1562.

romain-lavoix
  • 403
  • 2
  • 6
  • 20