【问题标题】:What is a faster way to iterate through large numbers in Ruby?在 Ruby 中迭代大量数字的更快方法是什么?
【发布时间】:2018-06-09 11:47:05
【问题描述】:

我正在学习 Ruby,目前这是我的练习:

使用任意一对数,证明整数 n 是完美幂。如果没有对,则返回 nil。

在数学中,完美幂是一个正整数,可以表示为另一个正整数的整数幂。更正式地说,如果存在自然数 m > 1,并且 k > 1 使得 mk = n

,则 n 是完美幂

目前这是我的代码:

def pp(n)
# [m,k] m^k

idx1=2
  while idx1 <n
  idx2=2
    while idx2<n
        if idx1**idx2 == n
          ans = [idx1,idx2]
          break
        end
     idx2 +=1
    end
    idx1 +=1
  end
return ans

end

我想这样写,这样对于给定的随机巨大数字,我的 repl.it 不会超时。

提前谢谢你!

                                      ###Edit###

在 Python 的上下文中提到了(How to make perfect power algorithm more efficient?)这个问题。尽管我试图理解它并翻译语法,但我做不到。我希望问这个问题也能像它帮助我一样帮助那些学习 Ruby 的人。

【问题讨论】:

  • 这是一个O(n^2) 算法 - 注意"number of loops" increases as n increases! [如何] 时间复杂度 是如何快速 提高的?对于指针,将“算法完美功率”视为搜索。
  • 你能提供一些很好的测试例子吗?
  • How to make perfect power algorithm more efficient? 的可能重复项 - 其中一个答案给出了大大改进的实现及其背后的推理。在 ctheory 上可能有更多 .. mathy .. 答案。虽然代码是 Python,但它很容易适应 Ruby
  • @user2864740 我不认为它是 O(n^2)。 idx1**idx2 最多有 n^2 次计算,这不是 O(1) 操作。
  • Ruby 风格通常规定为常量、模块和类名保留大写字母。 PP 应该是 pp,或者更好的是描述性的。

标签: ruby performance iteration


【解决方案1】:

你可以使用素数分解:

require 'prime'

def pp(n)
  pd = n.prime_division
  k = pd.map(&:last).reduce(&:gcd)
  return if k < 2
  m = pd.map { |p, e| p**(e / k) }.reduce(:*)
  [m, k]
end

例如对于n = 216,你得到[[2, 3], [3, 3]],意思是216 = 23⋅33。然后找到指数的最大公约数,即最大可能指数k。如果小于2,你就输了。否则,从碎片中计算基数 m

我的电脑需要大约 4.1 秒来检查数字 2 到 200。我的电脑需要大约 0.0005 秒,而需要大约 2.9 秒来检查数字 2 到 400000。

【讨论】:

    【解决方案2】:

    想要像这样瞬间解决非常大的数字吗?使用更短的解决方案?

    pp(908485319620418693071190220095272672648773093786320077783229)
    => [29, 41]
    

    然后继续阅读:-)。这将是一个小小的旅程......


    让我们首先在不改变逻辑的情况下让您的代码更好。只需将变量重命名为 mk 并使用 each 循环:

    def pp1(n)
      ans = nil
      (2...n).each do |m|
        (2...n).each do |k|
          if m**k == n
            ans = [m, k]
            break
          end
        end
      end
      ans
    end
    

    现在检查 n 到 200 大约需要 4.1 秒(与您原来的相同):

    > require 'benchmark'
    > puts Benchmark.measure { (1..200).each { |n| pp1(n) } }
      4.219000   0.000000   4.219000 (  4.210381)
    

    第一次优化:如果我们找到解决方案,立即返回!

    def pp2(n)
      (2...n).each do |m|
        (2...n).each do |k|
          if m**k == n
            return [m, k]
          end
        end
      end
      nil
    end
    

    遗憾的是,测试仍然需要 3.9 秒。下一个优化:如果mk已经太大了,我们不要再尝试更大的k了!

    def pp3(n)
      (2...n).each do |m|
        (2...n).each do |k|
          break if m**k > n
          if m**k == n
            return [m, k]
          end
        end
      end
      nil
    end
    

    现在速度如此之快,我可以在大约同一时间内运行 1000 次测试:

    > puts Benchmark.measure { 1000.times { (1..200).each { |n| pp3(n) } } }
      4.125000   0.000000   4.125000 (  4.119359)
    

    让我们改为 n = 5000:

    > puts Benchmark.measure { (1..5000).each { |n| pp3(n) } }
      2.547000   0.000000   2.547000 (  2.568752)
    

    现在不用计算m**k 这么多,让我们使用一个可以更便宜地更新的新变量:

    def pp4(n)
      (2...n).each do |m|
        mpowk = m
        (2...n).each do |k|
          mpowk *= m
          break if mpowk > n
          if mpowk == n
            return [m, k]
          end
        end
      end
      nil
    end
    

    遗憾的是,这几乎没有让它变得更快。但是还有另一个优化:当 mk 太大时,我们不仅可以忘记用更大的 k 尝试这个 m。如果它对于 k=2 来说太大了,即 m2 已经太大了,那么我们也不需要尝试更大的 m。我们可以停止整个搜索。让我们试试吧:

    def pp5(n)
      (2...n).each do |m|
        mpowk = m
        (2...n).each do |k|
          mpowk *= m
          if mpowk > n
            return if k == 2
            break
          end
          if mpowk == n
            return [m, k]
          end
        end
      end
      nil
    end
    

    这现在可以在大约 0.07 秒内完成 5000 次测试!我们甚至可以在 6 秒内检查所有高达 100000 的数字:

    > puts Benchmark.measure { (1..100000).each { |n| pp5(n) } }
      5.891000   0.000000   5.891000 (  5.927859)
    

    无论如何,让我们看看大局。我们正在尝试m = 2..,对于每个m,我们尝试找到一个k,以便m**k == n。嘿,数学有一个解决方案!我们可以计算 k 为 k=logm(n)。让我们开始吧:

    def pp6(n)
      (2...n).each do |m|
        k = Math.log(n, m).round
        return if k < 2
        return [m, k] if m**k == n
      end
      nil
    end
    

    再次测量:

    > puts Benchmark.measure { (1..100000).each { |n| pp6(n) } }
     28.797000   0.000000  28.797000 ( 28.797254)
    

    嗯,慢点。好的,另一个想法:让外循环为k 而不是m。现在对于给定的 n 和 k,我们如何找到 m 使得 mk = n?取第k个根!

    def pp7(n)
      (2...n).each do |k|
        m = (n**(1.0 / k)).round
        return if m < 2
        return [m, k] if m**k == n
      end
      nil
    end
    

    再次测量:

    > puts Benchmark.measure { (1..100000).each { |n| pp7(n) } }
      0.828000   0.000000   0.828000 (  0.836402)
    

    很好。 400000 怎么样,我在另一个答案中的分解解决方案在 2.9 秒内解决了?

    > puts Benchmark.measure { (1..400000).each { |n| pp(n) } }
      3.891000   0.000000   3.891000 (  3.884441)
    

    好的,这有点慢。但是...有了这个解决方案,我们可以解决真正大数字:

    pp7(1000000035000000490000003430000012005000016807)
    => [1000000007, 5]
    pp7(908485319620418693071190220095272672648773093786320077783229)
    => [29, 41]
    > pp7(57248915047290578345908234051234692340579013460954153490523457)
    => nil
    

    所有这些结果都会立即出现。分解解决方案也很快解决了 2941 的情况,但是对于 10000000075 的情况和最后的随机类型的情况,它很慢,因为分解很慢。


    附:请注意,对数和平方根得到我们的浮点数。这可能会导致非常大的数字出现问题。例如:

    irb(main):122:0> pp7(10**308 + 1)
    => nil
    irb(main):123:0> pp7(10**309 + 1)
    FloatDomainError: Infinity
            from (irb):82:in `round'
            from (irb):82:in `block in pp7'
            from (irb):81:in `each'
            from (irb):81:in `pp7'
            from (irb):123
            from C:/Ruby24/bin/irb.cmd:19:in `<main>'
    

    在这种情况下,这是因为 10309 对于浮点数来说太大了:

    > (10**309).to_f
    => Infinity
    

    此外,如果数字足够大,可能会出现准确性问题。无论如何,您可以通过为对数和根编写整数版本来解决这些问题。但这是另一个问题。

    【讨论】:

      猜你喜欢
      • 2018-01-25
      • 2017-03-28
      • 2012-11-14
      • 2020-08-30
      • 2013-01-17
      • 1970-01-01
      • 2010-10-21
      • 2018-12-01
      • 2012-03-03
      相关资源
      最近更新 更多