3

I'm trying to implement a recursive solution to the largest palindrome product problem

What I'm trying to do is start both numbers at 999 and iterate down to 100 for num1 and then restart num1 at 999 and iterate num2 down by 1.

The goal is basically to mimic a nested for-loop.

def largest_palindrome_prod(num1 = 999, num2 = 999, largest_so_far = 0)
  prod = num1 * num2
  largest_so_far = prod if prod > largest_so_far && check_pal(prod)

  if num2 == 100
    return largest_so_far
  elsif num1 == 100
    largest_palindrome_prod(num1 = 999, num2 -= 1, largest_so_far)
  else
    largest_palindrome_prod(num1 -= 1, num2, largest_so_far)
  end
end

#I know this function works, just here for reference
def check_pal(num)
  num = num.to_s if num.is_a? Integer
  if num.length < 2
    true
  else
    num[0] == num[-1] ? check_pal(num[1..-2]) : false
  end
end

rb:10:inlargest_palindrome_prod': stack level too deep`

I'm getting this error which is referring to the else statement in the largest_palindrome_prod function, but I can't figure out wast could be causing the stack error.

ray
  • 5,454
  • 1
  • 18
  • 40
user392500
  • 37
  • 8
  • 2
    Doesn't your recursion get to about 900 deep, which I would think is far more than enough to raise a stack level too deep exception. Note that once you find that `num1*num2` is a palindrome there's no point calling the method with `num1` decremented by `1` and `num2` unchanged, as that cannot lead to a larger palindrome. – Cary Swoveland Jul 17 '19 at 05:29

3 Answers3

4

You don't have an infinite recursion bug. The stack is just running out of space because of the size of your input. To prove this, you can run your same function with the range of 2-digit numbers, instead of the 3-digit ones. It returns fine, which shows that there is no flaw with your logic.

How to get around this? Two options.

Option 1: You could simply not use recursion here (just use a regular nested loop instead)

Option 2: Keep your same code and enable tail call optimization:

# run_code.rb

RubyVM::InstructionSequence.compile_option = {
  tailcall_optimization: true,
  trace_instruction: false
}

require './palindrome_functions.rb'
puts largest_palindrome_prod
# => 906609 

Note, for a reason I don't fully understand, the tail call optimization must be enabled in a different file than the code being run. So if you simply moved the compile_option line to the palindrome_functions.rb file, it wouldn't work.

I cant really give you a full explanation of tail call optimization (look it up on Wikipedia) but from my understanding, its a heavy optimization for recursive functions that only works when the recursive call is at the end of the function body. Your function meets this criteria.

max pleaner
  • 26,189
  • 9
  • 66
  • 118
  • The reason why you need to do that in another file : YARV needs to compile the current file before executing all of its instruction so it's not aware of your compile option until it compiled everything. – user11659763 Jul 17 '19 at 07:07
  • @maxpleaner: Ist this way to enable tail recursion optimization an *official* feature of Ruby, and if yes, where is it documented? I wonder why it is not enabled automatically, as it is in quite some under languages (Erlang, Haskell,....). – user1934428 Jul 17 '19 at 07:18
  • @user1934428 I'd assume it is a feature of "official" ruby because it works for me with just a standard ruby installation. I don't know where the docs are, I just googled to find that snippet to enable it. I do see [this ruby forum post](https://bugs.ruby-lang.org/issues/6602) about enabling it by default – max pleaner Jul 17 '19 at 07:58
  • "Ist this way to enable tail recursion optimization an *official* feature of Ruby" – No, it is not. It is a private internal implementation detail of some specific versions of YARV. It is not part of Ruby. There is no guarantee that it will work on other Ruby implementations than YARV. There is no guarantee that it will work on future versions of YARV. In fact, *everything* in the `RubyVM` namespace is considered to by a private internal implementation detail of a specific version of a specific implementation, and pretty much by definition non-portable. – Jörg W Mittag Jul 18 '19 at 19:58
2

@maxpleaner has answered your question and has shown how you can use recursion that avoids the stack level error. He also mentioned the option (which I expect he favours) of simply looping, rather than employing recursion. Below is one looping solution. The following method is used in the search1.

def check_ranges(range1, range2 = range1)
  range1.flat_map do |n|
    [n].product((range2.first..[n, range2.last].min).to_a)
  end.map { |x,y| x*y }.
      sort.
      reverse_each.
      find do |z|
        arr = z.digits
        arr == arr.reverse
      end              
end

Let's first find the largest palindrome of the product of two numbers between 960 and 999 (if there are any):

check_ranges(960..999)
  #=> nil 

There are none. Note that this calculation was very cheap, requiring the examination of only 40*40/2 #=> 800 products. Next, find the largest palindrome that is equal to the product of two numbers between 920 and 999.

check_ranges(920..999)
  #=> 888888

Success! Note that this method re-checks the 800 products we checked earlier. It makes more sense to examine only the cases represented by the following two calls to brute_force:

check_ranges(960..999, 920..959)
  #=> 888888 
check_ranges(920..959)
  #=> 861168 

The first call computes 40*40 #=> 1600 products; the second, 800 products.

Of course, we have not yet necessarily found the largest product that is a palindrome. We do, however, have a lower bound on the largest product, which we can use to advantage. Since

888888/999
  #=> 889

we infer that if the product of two numbers is larger than 888888, both of those numbers must be at least 889. We therefore need only check:

check_ranges(889..999, 889..919)
  #=> 906609 
check_ranges(889..919)
  #=> 824428 

We are finished. This tells us that 906609 is the largest product of two 3-digit numbers that is a palindrome.

The question does not ask what are the two numbers whose product is the largest palindrome, but we can easily find them:

(889..999).to_a.product((889..919).to_a).find { |x,y| x*y == 906609 }
  #=> [993, 913] 
993*913
  #=> 906609

Moreover, let:

a = (889..999).to_a.product((889..919).to_a).map { |x,y| x*y }.
      sort.
      reverse

Then:

a.index { |n| n == 906609 }
  #=> 84

tells us that only the largest 84 elements of this sorted group of 111*31 #=> 3441 products had to be examined before a palindrome (906609) was found.

All of this needs to be organized into a method. Though challenging for a newbie, it should be a good learning experience.

1. It would be useful to test which is faster, arr = z.digits; arr == arr.reverse or s = z.to_s; s == s.reverse.

Cary Swoveland
  • 106,649
  • 6
  • 63
  • 100
0

@maxpleaner already answered, @Cary Swoveland already showed one brute force way using ranges and product. I'd like to show another brute force using a nested loop, easier to follow (IMO):

n = 9999

res = [0]
bottom = 10**(n.digits.size - 1)

n.downto(bottom) do |k|
  k.downto(bottom) do |j|
    # puts "#{k}, #{j}"
    res = [k, j, k * j] if check_pal(k * j) && k * j > res.last
  end
end

res
#=> [9999, 9901, 99000099]


I guess it can be optimized further, for example, using
n.downto(n*99/100) do |k|
  k.downto(k*99/100) do |j|

Returned [99979, 99681, 9966006699] in 0.7 seconds.


Not required, but this increases the speed:
def check_pal(num)
  word = num.to_s
  word.reverse == word
end
iGian
  • 11,023
  • 3
  • 21
  • 36