-1

how to split a number into several numbers randomly ?

eg:i have a number 30, i want to split it into several numbers randomly,the size of every number is between 3-10,and the size of every number are different from each other

the result maybe like :[5,7,9,6,3],[9,10,3,8],...etc

I have tried,but I can't solve it,please give me help.

bluexuemei
  • 233
  • 3
  • 12
  • 2
    "split it into several numbers randomly" is vague. You must explain precisely what you mean by "randomly", though I expect you'll have some difficulty doing that. – Cary Swoveland Sep 26 '14 at 01:17
  • @Cary Swoveland,randomly,it means that the numbers of number is random – bluexuemei Sep 26 '14 at 01:31
  • 1
    I think the ambiguity is in what it means to "split a number". Are you asking for a random number of unique summands which add up to the original number? – Chris Heald Sep 26 '14 at 01:31
  • @ Chris Heald ,please see the sentence,the result maybe like :[5,7,9,6,3],[9,10,3,8],...etc。that means the number of elements in the array is random – bluexuemei Sep 26 '14 at 01:44
  • 2
    There is a huge problem with what you are trying to achieve, depending on the number and the range it could take forever to compute – bjhaid Sep 26 '14 at 01:46
  • There are many ways to interpret "random". Here are two, but there are others. 1 Select all combinations of distinct numbers between 3 and 10 (`[3], [3,4], [3,5,6,8,9],..[9,10],[10]`), throw out all combinations that don't sum to 30 and select one of those left at random. 2. Select four distinct numbers 3-10 at random. If they sum to 30, stop; if they sum to more than 30, throw out the last one. Next choose another number randomly and repeat. Continue until the sum is 30. #1 and #2 lead to much different probabilities. Do you mean #1, #2 or something else? – Cary Swoveland Sep 26 '14 at 02:28
  • If you found at least one of the answers helpful, you should select the one you liked best. – Cary Swoveland Oct 05 '14 at 04:47

5 Answers5

4

Splitting the number is called integer partition. Here's a solution based on Marc-André Lafortune's recursive algorithm:

def expand(n, max = n)
  return [[]] if n == 0
  [max, n].min.downto(1).flat_map do |i|
    expand(n-i, i).map{|rest| [i, *rest]}
  end
end

expand(30).select { |a| a.size >= 3 && a.size <= 10 }.sample(5)
#=> [[15, 3, 3, 3, 2, 2, 1, 1],
#    [9, 5, 4, 3, 2, 2, 2, 1, 1, 1],
#    [13, 10, 4, 2, 1],
#    [8, 8, 7, 2, 2, 1, 1, 1],
#    [8, 6, 4, 3, 3, 2, 1, 1, 1, 1]]

Note that the number of possible partitions gets quite large: 30 has 5,604 partitions, 100 has 190,569,292 partitions and 1,000 has 2.4 × 1031 partitions.

Community
  • 1
  • 1
Stefan
  • 109,145
  • 14
  • 143
  • 218
  • The authors of the other answers--me include--interpreted the problem as one of randomly choosing a set of distinct numbers between `3` and `10` that sum to `30`. (You could replace `a.size >= 3..` with `a.min >= 3 && a.max <= 10 & a.reduce(:+) == 30`, but that's not very efficient, as the range of possible set sizes could determined in advance, reducing the number of combinations you generate with `expand`.) Perhaps the OP will clarify. – Cary Swoveland Sep 27 '14 at 21:24
1

Very nice puzzle. I would go with:

class Fixnum
  def random_split(set = nil, repeats = false)
    set ||= 1..self
    set = [*set]
    return if set.empty? || set.min > self || set.inject(0, :+) < self
    tried_numbers = []
    while (not_tried = (set - tried_numbers).select {|n| n <= self }).any?
      tried_numbers << number = not_tried.sample
      return [number] if number == self
      new_set = set.dup
      new_set.delete_at(new_set.index(number)) unless repeats
      randomized_rest = (self-number).random_split(new_set, repeats)
      return [number] + randomized_rest if randomized_rest
    end   
  end
end

30.random_split(3..10)

In general the code above covers a lot of cases. You can execute it without any params, it will then assume it is to pick numbers from 1 up to given number and resulting set should not contain any repetitions. You can optionally pass the set given number are to be taken from. If you pass [1,2,3,4,4,4], it will take care that 4 is not repeated more than 3 times. If the second param is set to true, it will allow set elements to appear twice or more in the results.

BroiSatse
  • 44,031
  • 8
  • 61
  • 86
  • 1
    did you try `100.random_split(1..10)` – bjhaid Sep 26 '14 at 01:44
  • @bjhaid - Thanks, this would return nil in the end, however should be more clever than checking all the possible cases. Fixed now. – BroiSatse Sep 26 '14 at 01:47
  • I think the question is ambiguous which makes it difficult to get a *correct* answer – bjhaid Sep 26 '14 at 01:48
  • Good, though a bit complicated, this is what I want – bluexuemei Sep 26 '14 at 01:52
  • @user3673267 - Unfortunately it is quite complicated, but the task is not so simple, there are a lot of cases and performance issues. I am not sure though if the algorithm before is just, would need some more time to come up with 100% just one. – BroiSatse Sep 26 '14 at 01:59
0

Not the best algo in the world (it does some trial and error) but hey, CPU cycles are cheap nowadays...This should work on every number:

def split_this_number_into_several_numbers_randomly(a_number, min_number_to_start_from)
  random_numbers = [0]

  until (random_numbers.inject(&:+) == a_number)
    random_numbers << rand(min_number_to_start_from..a_number/3) # replace 30 here, I assumed you wanted up to 1/3 of the original number
    if (r = random_numbers.detect { |x| random_numbers.count(x) > 1}) then random_numbers.delete(r) end # so we have all unique numbers
    random_numbers.pop if random_numbers.inject(&:+) >= a_number - min_number_to_start_from && random_numbers.inject(&:+) != a_number
  end
  random_numbers.delete_if{ |x| x == 0 }
end

and of course, some code to test it:

all_true = true
1000.times do
  arr = split_this_number_into_several_numbers_randomly(30, 3)
  all_true == false unless arr.inject(&:+) == 30
  all_true == false unless arr.size == arr.uniq.size
end

p all_true #=> true
daremkd
  • 8,244
  • 6
  • 40
  • 66
0

The OP has confirmed in a comment on the question that random selections are to reflect the following procedure: "Select all combinations of distinct numbers between 3 and 10 ([3], [3,4], [3,5,6,8,9],..[9,10],[10]), throw out all combinations that don't sum to 30 and select one of those left at random". The following is a straightforward way of implementing that. Efficiency improvements could be made, but it would be a lot of work.

Code

def arrays(sum, range)
  largest_sum = (range.first+range.last)*(range.last-range.first+1)/2
  (raise ArgumentError,
    "largest sum for range = #{largest_sum}") if sum > largest_sum 
  avail = [*range]
  b = -(2*range.last + 1.0)
  c = 8.0*sum
  min_nbr = ((-b - (b*b - c)**0.5)/2).ceil.to_i
  max_nbr = ((-1.0 + (1.0 + c)**0.5)/2).to_i
  (min_nbr..max_nbr).each_with_object([]) { |n, a|
    a.concat(avail.combination(n).select { |c| c.inject(:+) == sum }) }
end

Note min_nbr and max_nbr employ the quadratic formula to determine the range of the number of distinct numbers which may sum to sum.

Examples

sum = 30
range = (3..10)
arr = arrays(sum, range) # all combinations that sum to 30
  #=> [[3, 8, 9, 10], [4, 7, 9, 10], [5, 6, 9, 10], [5, 7, 8, 10],
  #    [6, 7, 8, 9],
  #    [3, 4, 5, 8, 10], [3, 4, 6, 7, 10], [3, 4, 6, 8, 9],
  #    [3, 5, 6, 7, 9], [4, 5, 6, 7, 8]]

(Solution time: well under 1 sec.)

10.times { p arr[rand(arr.size)] } # 10 random selections
  #=> [3, 4, 6,  8,  9]
  #   [3, 4, 6,  8,  9]
  #   [5, 7, 8, 10]
  #   [4, 5, 6,  7,  8]
  #   [3, 4, 5,  8, 10]
  #   [6, 7, 8,  9]
  #   [3, 4, 5,  8, 10]
  #   [4, 5, 6,  7,  8]
  #   [6, 7, 8,  9]
  #   [3, 4, 6,  7, 10]

sum = 60
range = (3..10)
arr = arrays(sum, range)
  #=> in `arrays': largest sum for range = 52 (ArgumentError)

Two more...

sum = 60
range = (3..20)
arr = arrays(sum, range) # all combinations that sum to 60
arr.size
  #=> 1092
(Solution time: about 1 sec.)  
10.times { p arr[rand(arr_size)] } # 10 random selections
  #=> [12, 14, 15, 19]
  #    [3,  4,  6,  7, 11, 13, 16]
  #    [3,  6,  7,  9, 15, 20]
  #    [3,  8, 14, 17, 18]
  #    [3,  4,  5,  7, 10, 13, 18]
  #    [3,  5,  6,  7, 11, 13, 15]
  #    [5,  6,  7,  8, 14, 20]
  #    [4,  5,  9, 11, 15, 16]
  #    [4,  5,  8, 13, 14, 16]
  #    [3,  4,  5, 12, 16, 20]

sum = 100
range = (3..30)
arr = arrays(sum, range) # all combinations that sum to 100
arr.size
  #=> 54380
(Solution time: 3 or 4 minutes)
10.times { p arr[rand(arr_size)] } # 10 random selections
  #=> [3,  4,  6,  9, 11, 12, 15, 17, 23]
  #   [4,  5,  6,  7,  9, 13, 14, 17, 25]
  #   [4,  5,  6,  7, 11, 17, 21, 29]
  #   [9, 10, 12, 13, 17, 19, 20]
  #   [6,  9, 10, 23, 25, 27]
  #   [3,  4,  5,  6,  7,  8,  9, 14, 15, 29]
  #   [3,  4,  5,  6,  7,  8,  9, 15, 17, 26]
  #   [3,  4,  5,  6,  7,  8, 17, 22, 28]
  #   [3,  5,  6,  7,  9, 12, 13, 15, 30]
  #   [6,  8,  9, 10, 13, 15, 18, 21]
Cary Swoveland
  • 106,649
  • 6
  • 63
  • 100
0

My version using recursion

def split_number(n, acc = [])
  max = n.to_i - 1
  return n, acc if max.zero?

  r = rand(max) + 1
  remainder = n - acc.inject(0, :+) - r
  acc << split_number(remainder, acc).first if remainder > 0
  [r, acc].flatten
end

split_number(100)
# => [34, 1, 13, 1, 9, 4, 12, 21, 5]  
Jimmy Ngu
  • 73
  • 5