Here's mine.
I started out with very straightforward programming, but ended up
making it muddier as I optimized. I also implemented a bunch of
functions that I figured I could probably find alread written if I
looked, but I figured they were good exercises : Integer#factors,
Integer#divisors, Array#all_combinations....
I thought it was pretty fast, especially compared to my initial
implementation. Then I timed Ryan's on my machine... Oh well. I
enjoyed the challenge.
-Adam
---
class Integer
def weird?
(d = divisors).pop #remove self (which is always last)
d.sum > self &&
!d.quick_find_subset_with_sum(self) && #weed out most
!d.find_subset_with_sum(self) #confirm the rest
end
def divisors
factors.all_combinations.uniq.inject([]){|result,combo|
result << combo.product
}.sort.uniq
end
def factors
value, candidate = self, 3
factors = [1]
while value % 2 == 0
factors << 2
value /= 2
end
while candidate <= Math.sqrt(value)
while value % candidate == 0
factors << candidate
value /= candidate
end
candidate += 2
end
factors << value if value != 1
factors
end
end
class Array
def product
inject(1){|p,v| p*v}
end
def sum
inject(0){|s,v| s+v}
end
def all_combinations
ComboIndexGenerator.new(self.size).inject([]) {|result, index_set|
result << values_at(*index_set)
}
end
#this was my first attempt, which was straightforward,
# but slow as heck for large sets
def slow_find_subset_with_sum n
return nil if sum < n
all_combinations.each {|set|
return set if set.sum == n
}
nil
end
#this is my second attempt which is fast but misses some subsets.
#but it is useful for quickly rejecting many non-weird numbers.
def quick_find_subset_with_sum n
a = self.sort.reverse
sum,set = 0,[]
a.each {|e|
if (sum+e <= n)
sum+=e
set<<e
end
return set if sum == n
}
nil
end
#this one works pretty quickly...
#it never tests subsets which are less than the sum,
#and keeps track of sets it has already calculated
def find_subset_with_sum n
possibilities, seen = [self],{}
until possibilities.empty?
candidate = possibilities.pop
diff = candidate.sum - n
return candidate if diff == 0
break if diff < 0
candidate.each_with_index{|e,i|
break if e > diff
new_cand = (candidate.dup)
new_cand.delete_at(i)
return new_cand if e == diff
possibilities << new_cand if !seen[new_cand]
seen[new_cand]=true
}
end
nil
end
end
#this class generates an all the possible combinations of n items
#it returns an array with the next combination every time you call #next
class ComboIndexGenerator
include Enumerable
def initialize nitems
@n = nitems
@max = 2**@n
@val=0
end
def to_a
return nil if @val==@max
(0..@n).inject([]){|a,bit| a<<bit if @val[bit]==1; a}
end
def next
@val+=1 if @val<@max
to_a
end
def each &b
yield to_a
while (n=self.next)
yield n
end
end
end
if $0 == __FILE__
if ARGV.length < 1
puts "Usage: #$0 <upper limit>"
exit(1)
end
puts "Weird numbers up to and including #{ARGV[0]}:"
70.upto(ARGV[0].to_i) do |i|
puts i if i.weird?
end
end