是否有一种算法可以从给定的大于1的加数范围中找到一个求和的所有加数组合?

问题描述

我正在尝试创建一个程序,该程序采用给定的总和和给定范围的允许加数,并输出这些加数的独特配置,这些加法器加起来为和。

用例是确定不同规模的多成员区域的可能组合,以将立法机关的成员分为几类。

一个简单的示例中,假设有15名立法者,并且每个区最少有3个席位,最多只有5个席位,则可能的组合是:

  • [3,3,3,3,3]
  • [4,4,4,3]
  • [5,4,3,3]
  • [5,5,5]

我最初的想法是从嵌套数组中可能的最大的最小尺寸分区组开始,然后通过复制和修改一个条目来添加更多条目。我不知道如何实现该方法,但是我也不确定这是否是解决此问题的正确方法,我正在寻找建议。

def multi_member_districts

  reps = 19
  min = 3
  max = 6

  quomin,modmin = reps.divmod(min)
  quomax,modmax = reps.divmod(max)

  groups = Array.new(1) {Array.new}

  (quomin - 1).times do groups[0].push(min) end
  groups[0].unshift(min + modmin)

# PSEUDOCODE
# copy groups[i],insert copy at groups[i+1]
# remove the smallest element of groups[i+1] and spread it out across the other
#   numbers in groups[i+1] in all configurations in which no element exceeds max
# check that there are no duplicate configurations
# repeat

  puts "\nThe possible groups of districts are as follows:"
  groups.each_index do |i|
    (min..max).each do |j|
      unless groups[i].count(j) == 0
        puts ">> #{groups[i].count(j)} #{j}-member districts"
      end
    end
    puts
    puts "o-o-o-o-o-o-o-o-o-o-o-o-o-o"
  end
end

multi_member_districts

EDIT_1: 一个不那么琐碎的例子:19名议员,每个地区3-6个席位-

  • [4,3,3,3,3,3]
  • [4,4,4,4,3]
  • [5、5、5、4]
  • [5,4,4,3,3]
  • [5、5、3、3、3]
  • [6,5,5,3]
  • [6、4、3、3、3]
  • [6、5、4、4]
  • [6,6,4,3]

EDIT_2:澄清了我的问题,减少了代码,希望更合适

解决方法

让我们首先计算组合,其中每个组合对应于数组arr,其中arr[i]等于分配给区i的立法者人数。例如,如果有15名议员,并且每个区必须分配3至5名议员,则[3,3,4,5][5,3]将是不同的组合。我们可以使用递归来解决该问题。

def doit(nbr,rng)
  return nil if nbr < rng.begin
  recurse(nbr,rng)
end
def recurse(nbr,rng)
  (rng.begin..[rng.end,nbr].min).each_with_object([]) do |n,arr|
    if n == nbr
      arr << [n]
    elsif nbr-n >= rng.begin
      recurse(nbr-n,rng).each { |a| arr << a.unshift(n) }
    end
  end
end
doit(15,3..5)
  #=> [[3,3],[3,5],5,4],#    [3,[4,#    [4,#    [5,[5,5]]
doit(19,3..6)          
  #=> [[3,6],6,#    ...
  #    [6,[6,3]]
doit(19,3..6).size          
  #=> 111 

但是,该问题与特定区域的分配无关。为了获得感兴趣的组合,我们可以编写以下内容。

require 'set'

def really_doit(nbr,rng)
  doit(nbr,rng).map(&:tally).uniq.map do |h|
    h.flat_map { |k,v| [k]*v }.sort.reverse
  end                    
end
really_doit(15,5]] 
really_doit(19,3..6)
  #=> [[4,#    [6,4]]

Enumerable#tally在Ruby v2.7中首次亮相。要支持早期版本,请将map(&:tally)替换为map { |a| a.each_with_object(Hash.new(0)) { |n,h| h[n] += 1 }

请注意,返回的doit(nbr,rng).map(&:tally).uniq

[{3=>5},{3=>2,4=>1,5=>1},{3=>1,4=>3},{5=>3}]

really_doit(15,3..5)

[{3=>5,4=>1},{3=>3,6=>1},5=>2},4=>2,4=>4},6=>2},5=>2,{4=>2,5=>1,{4=>1,5=>3}]

really_doit(19,3..6)中。


我们可以通过在recurse中构造哈希集(而不是数组的数组)来对此进行改进:

require 'set'

def doit(nbr,rng).map { |h| h.flat_map { |k,v| [k]*v }.sort.reverse }
end

def recurse(nbr,nbr].min).each_with_object(Set.new) do |n,st|
    if n == nbr
      st << { n=>1 }
    elsif nbr-n >= rng.begin
      recurse(nbr-n,rng).each { |h| st << h.merge(n=>h[n].to_i+1 ) }
    end
  end
end
doit(15,5]] 
doit(19,3..6)          
  #=> [[4,4]]
   

请注意,recurse(nbr,rng)中的doit返回:

#<Set: {{3=>5},{5=>1,3=>2},{4=>3,3=>1},{5=>3}}>

doit(19,3..6)中执行recurse(nbr,rng) doit时返回:

#<Set: {{4=>1,3=>5},{6=>1,3=>3},{5=>2,#       {5=>1,{4=>4,{6=>2,#       {6=>1,4=>2},{5=>3,4=>1}}>