【问题标题】:Stack level too deep in recursion for largest palindrome product question (Project Euler)最大回文乘积问题的递归堆栈级别太深(Project Euler)
【发布时间】:2019-07-17 00:33:18
【问题描述】:

我正在尝试对最大的回文乘积problem实现递归解决方案

我想要做的是从 999 开始两个数字,然后将 num1 向下迭代到 100,然后在 999 处重新启动 num1,并将 num2 向下迭代 1。

目标基本上是模仿嵌套的 for 循环。

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':堆栈级别太深`

我收到这个错误,它指的是最大回文函数中的 else 语句,但我不知道浪费可能导致堆栈错误。

【问题讨论】:

  • 你的递归深度不会达到 900 左右,我认为这足以引发堆栈级别太深的异常。请注意,一旦您发现num1*num2 是回文数,那么在num1 递减1num2 不变的情况下调用该方法是没有意义的,因为这不会导致更大的回文数。

标签: ruby recursion stack-overflow palindrome


【解决方案1】:

您没有无限递归错误。由于输入的大小,堆栈空间不足。为了证明这一点,您可以使用 2 位数字而不是 3 位数字范围运行相同的函数。它返回正常,这表明您的逻辑没有缺陷。

如何解决这个问题?两种选择。

选项 1:你不能在这里使用递归(只需使用常规的嵌套循环)

选项 2:保留相同的代码并启用尾调用优化:

# run_code.rb

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

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

注意,由于我不完全理解的原因,尾调用优化必须在与正在运行的代码不同的文件中启用。因此,如果您只是将 compile_option 行移动到 palindrome_functions.rb 文件中,它将不起作用。

我真的不能给你一个关于尾调用优化的完整解释(在 Wikipedia 上查找),但据我了解,它是对递归函数的重度优化,只有在递归调用结束时才有效 em> 的函数体。您的函数符合此条件。

【讨论】:

  • 您需要在另一个文件中执行此操作的原因:YARV 需要在执行其所有指令之前编译当前文件,因此在编译所有内容之前它不知道您的编译选项。
  • @maxpleaner:这种启用尾递归优化的方法是 Ruby 的官方特性吗?如果是,它在哪里记录?我想知道为什么它没有自动启用,因为它在相当多的语言(Erlang,Haskell,....)中。
  • @user1934428 我认为它是“官方” ruby​​ 的一个功能,因为它只适用于我的标准 ruby​​ 安装。我不知道文档在哪里,我只是用谷歌搜索找到那个 sn-p 来启用它。我确实看到 this ruby forum post 关于默认启用它
  • “这种方式来启用尾递归优化是 Ruby 的官方特性吗?”——不,不是。它是 YARV 某些特定版本的私有内部实现细节。它不是 Ruby 的一部分。不能保证它可以在 YARV 之外的其他 Ruby 实现上工作。不能保证它可以在未来版本的 YARV 上运行。事实上,RubyVM 命名空间中的 everything 被认为是特定实现的特定版本的私有内部实现细节,并且根据定义几乎是不可移植的。
【解决方案2】:

@maxpleaner 已经回答了您的问题,并展示了如何使用递归来避免堆栈级错误。他还提到了简单循环而不是使用递归的选项(我希望他喜欢)。以下是一种循环解决方案。搜索中使用了以下方法1

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

让我们首先找到 960 到 999 之间的两个数(如果有的话)乘积的最大回文数:

check_ranges(960..999)
  #=> nil 

没有。请注意,这种计算非常便宜,只需要检查40*40/2 #=&gt; 800 产品。接下来,找到等于 920 和 999 之间的两个数的乘积的最大回文数。

check_ranges(920..999)
  #=> 888888

成功!请注意,此方法会重新检查我们之前检查过的800 产品。只检查以下两个对brute_force 的调用所代表的案例更有意义:

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

第一次调用计算40*40 #=&gt; 1600产品;第二个,800 产品。

当然,我们还不一定找到最大的产品是回文。然而,我们确实对最大的产品有一个下限,我们可以利用它来获得优势。自从

888888/999
  #=> 889

我们推断,如果两个数字的乘积大于888888,那么这两个数字都必须至少为 889。因此我们只需要检查:

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

我们已经完成了。这告诉我们906609 是两个 3 位数字的最大乘积,即回文数。

这个问题不是问两个数的乘积是最大回文数是多少,但我们可以很容易地找到它们:

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

此外,让:

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

然后:

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

告诉我们,在找到回文 (906609) 之前,必须检查这个已排序的 111*31 #=&gt; 3441 产品组中最大的 84 元素。

所有这些都需要组织成一个方法。虽然对新手来说具有挑战性,但应该是一次很好的学习体验。

1.测试哪个更快,arr = z.digits; arr == arr.reverses = z.to_s; s == s.reverse 会很有用。

【讨论】:

    【解决方案3】:

    @maxpleaner 已经回答,@Cary Swoveland 已经展示了一种使用rangesproduct 的蛮力方式。我想展示另一种使用嵌套循环的蛮力,更容易理解(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]
    


    我想它可以进一步优化,例如,使用
    n.downto(n*99/100) do |k|
      k.downto(k*99/100) do |j|
    

    在 0.7 秒内返回 [99979, 99681, 9966006699]


    不是必需的,但这会提高速度:
    def check_pal(num)
      word = num.to_s
      word.reverse == word
    end
    

    【讨论】:

      猜你喜欢
      • 2015-09-08
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-09-27
      • 1970-01-01
      • 2012-03-04
      • 2013-10-05
      相关资源
      最近更新 更多