Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature Request: Add divisors methods #13

Open
wants to merge 4 commits into
base: master
Choose a base branch
from

Conversation

universato
Copy link

I want Integer#divisors that return the divisors of self.

6.divisors # => [1, 2, 3, 6]
7.divisors # => [1, 7]
8.divisors # => [1, 2, 4, 8]

Divisors are basic and important mathematical matters.
Enumeration of divisors can be useful or necessary when solving integer problems.

The main idea and divisors code was provided by tompng-san.
I improved the average speed and added document and tests.

The main idea and code was provided by tompng-san.
@universato
Copy link
Author

Benchmark

This PR code runs faster than the simple O(√N) code for large numbers.

Benchmark code

Details (Click here)
class Integer
  def divisors
    if prime?
      [1, self]
    elsif self == 1
      [1]
    else
      xs = prime_division.map{ |p, n| Array.new(n + 1){ |e| p**e } }
      x = xs.pop
      x.product(*xs).map{ |t| t.inject(:*) }.sort
    end
  end

  # this divisors is slow when +self+ is a prime number
  def original_divisors
    xs = prime_division.map{ |p, n| Array.new(n + 1){ |e| p**e } }
    [1].product(*xs).map{ |t| t.inject(:*) }.sort
  end

  # time complexity: O(√N)
  # this is very slow when +self+ is a large number
  def simple_divisors
    n = self
    s = Integer.sqrt(n)
    res1 = []
    res2 = []
    i = 1
    while i <= s
      if self % i == 0
        res1 << i
        res2.unshift(n / i)
      end
      i += 1
    end
    res1.pop if s * s == n
    res1.concat(res2)
  end
end

require 'bundler/inline'

gemfile do
  source 'https://rubygems.org'
  gem 'benchmark-ips'
end

require 'prime'

srand(42)
2.upto(15) do |exp|
  r = 10 ** (exp - 1) .. 10 ** exp
  samples = 100.times.map { rand(r) }

  puts "Testing 10**#{exp- 1 }..10**#{exp} (#{r})"
  Benchmark.ips do |x|
    x.config(:time => 0.05, :warmup => 0.1)
    x.report('divisors       '){ samples.each { |i| i.divisors } }
    x.report('original       '){ samples.each { |i| i.original_divisors } }
    x.report('simple_division'){ samples.each { |i| i.simple_divisors } } if exp < 14
    x.compare!
  end
end

Result

Details (Click here)
Testing 10**1..10**2 (10..100)
Warming up --------------------------------------
     divisors          282.000  i/100ms
     original          226.000  i/100ms
     simple_division     1.704k i/100ms
Calculating -------------------------------------
     divisors             2.469k (± 0.0%) i/s -    282.000  in   0.114232s
     original             2.132k (± 0.0%) i/s -    226.000  in   0.106019s
     simple_division     16.023k (± 0.0%) i/s -      1.704k in   0.106345s

Comparison:
     simple_division:    16023.3 i/s
     divisors       :     2468.7 i/s - 6.49x  (± 0.00) slower
     original       :     2131.7 i/s - 7.52x  (± 0.00) slower

Testing 10**2..10**3 (100..1000)
Warming up --------------------------------------
     divisors          176.000  i/100ms
     original          153.000  i/100ms
     simple_division   951.000  i/100ms
Calculating -------------------------------------
     divisors             1.700k (± 0.0%) i/s -    176.000  in   0.103512s
     original             1.496k (± 0.0%) i/s -    153.000  in   0.102284s
     simple_division      8.945k (± 0.0%) i/s -    951.000  in   0.106321s

Comparison:
     simple_division:     8944.6 i/s
     divisors       :     1700.3 i/s - 5.26x  (± 0.00) slower
     original       :     1495.8 i/s - 5.98x  (± 0.00) slower

Testing 10**3..10**4 (1000..10000)
Warming up --------------------------------------
     divisors          129.000  i/100ms
     original          114.000  i/100ms
     simple_division   421.000  i/100ms
Calculating -------------------------------------
     divisors             1.211k (± 0.0%) i/s -    129.000  in   0.106486s
     original             1.134k (± 0.0%) i/s -    114.000  in   0.100531s
     simple_division      3.964k (± 0.0%) i/s -    421.000  in   0.106201s

Comparison:
     simple_division:     3964.2 i/s
     divisors       :     1211.4 i/s - 3.27x  (± 0.00) slower
     original       :     1134.0 i/s - 3.50x  (± 0.00) slower

Testing 10**4..10**5 (10000..100000)
Warming up --------------------------------------
     divisors           91.000  i/100ms
     original           72.000  i/100ms
     simple_division   182.000  i/100ms
Calculating -------------------------------------
     divisors           836.3130.0%) i/s -     91.000  in   0.108811s
     original           729.9120.0%) i/s -     72.000  in   0.098642s
     simple_division      1.637k (± 0.0%) i/s -    182.000  in   0.111150s

Comparison:
     simple_division:     1637.4 i/s
     divisors       :      836.3 i/s - 1.96x  (± 0.00) slower
     original       :      729.9 i/s - 2.24x  (± 0.00) slower

Testing 10**5..10**6 (100000..1000000)
Warming up --------------------------------------
     divisors           55.000  i/100ms
     original           41.000  i/100ms
     simple_division    55.000  i/100ms
Calculating -------------------------------------
     divisors           557.7080.0%) i/s -     55.000  in   0.098618s
     original           396.0280.0%) i/s -     41.000  in   0.103528s
     simple_division    577.9800.0%) i/s -     55.000  in   0.095159s

Comparison:
     simple_division:      578.0 i/s
     divisors       :      557.7 i/s - 1.04x  (± 0.00) slower
     original       :      396.0 i/s - 1.46x  (± 0.00) slower

Testing 10**6..10**7 (1000000..10000000)
Warming up --------------------------------------
     divisors           26.000  i/100ms
     original           23.000  i/100ms
     simple_division    20.000  i/100ms
Calculating -------------------------------------
     divisors           267.3550.0%) i/s -     26.000  in   0.097249s
     original           219.0730.0%) i/s -     23.000  in   0.104988s
     simple_division    193.8640.0%) i/s -     20.000  in   0.103165s

Comparison:
     divisors       :      267.4 i/s
     original       :      219.1 i/s - 1.22x  (± 0.00) slower
     simple_division:      193.9 i/s - 1.38x  (± 0.00) slower

Testing 10**7..10**8 (10000000..100000000)
Warming up --------------------------------------
     divisors           10.000  i/100ms
     original            7.000  i/100ms
     simple_division     5.000  i/100ms
Calculating -------------------------------------
     divisors            88.1050.0%) i/s -     10.000  in   0.113501s
     original            66.0950.0%) i/s -      7.000  in   0.105908s
     simple_division     54.8140.0%) i/s -      5.000  in   0.091218s

Comparison:
     divisors       :       88.1 i/s
     original       :       66.1 i/s - 1.33x  (± 0.00) slower
     simple_division:       54.8 i/s - 1.61x  (± 0.00) slower

Testing 10**8..10**9 (100000000..1000000000)
Warming up --------------------------------------
     divisors            5.000  i/100ms
     original            2.000  i/100ms
     simple_division     1.000  i/100ms
Calculating -------------------------------------
     divisors            48.1690.0%) i/s -      5.000  in   0.103801s
     original            29.5620.0%) i/s -      2.000  in   0.067655s
     simple_division     18.6610.0%) i/s -      1.000  in   0.053589s

Comparison:
     divisors       :       48.2 i/s
     original       :       29.6 i/s - 1.63x  (± 0.00) slower
     simple_division:       18.7 i/s - 2.58x  (± 0.00) slower

Testing 10**9..10**10 (1000000000..10000000000)
Warming up --------------------------------------
     divisors            1.000  i/100ms
     original            1.000  i/100ms
     simple_division     1.000  i/100ms
Calculating -------------------------------------
     divisors            19.9750.0%) i/s -      1.000  in   0.050062s
     original            17.6410.0%) i/s -      1.000  in   0.056686s
     simple_division      5.5820.0%) i/s -      1.000  in   0.179143s

Comparison:
     divisors       :       20.0 i/s
     original       :       17.6 i/s - 1.13x  (± 0.00) slower
     simple_division:        5.6 i/s - 3.58x  (± 0.00) slower

Testing 10**10..10**11 (10000000000..100000000000)
Warming up --------------------------------------
     divisors            1.000  i/100ms
     original            1.000  i/100ms
     simple_division     1.000  i/100ms
Calculating -------------------------------------
     divisors             6.9620.0%) i/s -      1.000  in   0.143632s
     original             4.0420.0%) i/s -      1.000  in   0.247414s
     simple_division      1.8630.0%) i/s -      1.000  in   0.536792s

Comparison:
     divisors       :        7.0 i/s
     original       :        4.0 i/s - 1.72x  (± 0.00) slower
     simple_division:        1.9 i/s - 3.74x  (± 0.00) slower

Testing 10**11..10**12 (100000000000..1000000000000)
Warming up --------------------------------------
     divisors            1.000  i/100ms
     original            1.000  i/100ms
     simple_division     1.000  i/100ms
Calculating -------------------------------------
     divisors             2.6840.0%) i/s -      1.000  in   0.372569s
     original             1.3980.0%) i/s -      1.000  in   0.715181s
     simple_division      0.5940.0%) i/s -      1.000  in   1.682943s

Comparison:
     divisors       :        2.7 i/s
     original       :        1.4 i/s - 1.92x  (± 0.00) slower
     simple_division:        0.6 i/s - 4.52x  (± 0.00) slower

Testing 10**12..10**13 (1000000000000..10000000000000)
Warming up --------------------------------------
     divisors            1.000  i/100ms
     original            1.000  i/100ms
     simple_division     1.000  i/100ms
Calculating -------------------------------------
     divisors             0.8250.0%) i/s -      1.000  in   1.211711s
     original             0.5630.0%) i/s -      1.000  in   1.776719s
     simple_division      0.1840.0%) i/s -      1.000  in   5.430217s

Comparison:
     divisors       :        0.8 i/s
     original       :        0.6 i/s - 1.47x  (± 0.00) slower
     simple_division:        0.2 i/s - 4.48x  (± 0.00) slower

Testing 10**13..10**14 (10000000000000..100000000000000)
Warming up --------------------------------------
     divisors            1.000  i/100ms
     original            1.000  i/100ms
Calculating -------------------------------------
     divisors             0.2060.0%) i/s -      1.000  in   4.854847s
     original             0.1320.0%) i/s -      1.000  in   7.557918s

Comparison:
     divisors       :        0.2 i/s
     original       :        0.1 i/s - 1.56x  (± 0.00) slower

Testing 10**14..10**15 (100000000000000..1000000000000000)
Warming up --------------------------------------
     divisors            1.000  i/100ms
     original            1.000  i/100ms
Calculating -------------------------------------
     divisors             0.1860.0%) i/s -      1.000  in   5.388732s
     original             0.0510.0%) i/s -      1.000  in  19.516686s

Comparison:
     divisors       :        0.2 i/s
     original       :        0.1 i/s - 3.62x  (± 0.00) slower

@hsbt
Copy link
Member

hsbt commented Jun 25, 2021

I'm positive to add this feature. But I'm not sure "divisors" is the best name for this.

@mame Can you review this?

@mame
Copy link
Member

mame commented Jun 25, 2021

The maintainer of this library is @marcandre , so I leave it to him.

The following is just my opinion. I'm not negative against the proposal, but I don't know its practical use case. The proposed implementation runs faster when the number is larger than 10**6, but it runs slower when the number is small. I think we need a good reason to adopt such an algorithm. Is this method used against a larger number than 10**6 in its typical use case? If it is not sure, I personally like the simpler (√n) algorithm.

lib/prime.rb Outdated
#
# == Example
# ds = []
# 10.divisors{ |d| ds << d }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# 10.divisors{ |d| ds << d }
# 10.each_divisor{ |d| ds << d }

@marcandre
Copy link
Member

I'm positive about the feature.

Implementation needs work though. @universato are you up for that?

Thanks to @mame for benchmarking it. A straight approach as he is proposing would also make each_divisor potentially work better.

Tests for divisors are fine, but each_divisor need to check the result at least once without to_a being called.

Add some tests to check the result without to_a being called.
Improve divisors of small integer.
Change API of zero and negative integers.
@universato
Copy link
Author

Naming

In some languages, libraries and web services, the feature is named or called divisors (or each_divisor).
So I think divisors is a good general name for it.

lang or lib e.t.c. name divisors of -1 divisors of 0
MATLAB divsors [1] 0
Maple divisors [1] []
Walframe divisors [1] Error message(?)
mame/prime-faster each_divisor [] []
google/ac-library.cr each_divisor ArgumentError ArgumentError
gem divisors divisors [1, -1] nil
dCode divisors [1] Error

Performance

The implementation using prime_division may be relatively difficult to estimate the complexity, but the idea itself is simple and the code is short.
And, Indeed the fist implementation runs slow when the number is smaller than 10**6, I think the impact is not so big. However, it it possible to make it faster against small numbers, so I made it faster.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

5 participants