【问题标题】:Number of possible equations of K numbers whose sum is N in rubyruby 中总和为 N 的 K 个数的可能方程数
【发布时间】:2014-05-24 10:16:40
【问题描述】:

我必须在 ruby​​ on rails 中创建一个程序,以减少解决特定条件的时间。现在我要在 k=4 时获得更少的响应时间,但在 k>5

的情况下响应时间更长

问题:

问题是响应时间。

当 k 的值大于 5 (k>5) 时,响应时间太晚,无法满足下面的公式。

输入:K,N(其中 0

Example Input:
N=10 K=3
Example Output:
Total unique equations = 8
  1 + 1 + 8 = 10
  1 + 2 + 7 = 10
  1 + 3 + 6 = 10
  1 + 4 + 5 = 10
  2 + 2 + 6 = 10
  2 + 3 + 5 = 10
  2 + 4 + 4 = 10
  3 + 3 + 4 = 10
For reference, N=100, K=3 should have a result of 833 unique sets 

这是我的红宝石代码

module Combination
  module Pairs
    class Equation
      def initialize(params)
        @arr=[]
        @n = params[:n]
        @k = params[:k]
      end

      #To create possible equations
      def create_equations
        return "Please Enter value of n and k" if @k.blank? && @n.blank?
        begin
          Integer(@k)
        rescue
          return "Error: Please enter any +ve integer value of k"
        end
        begin
          Integer(@n)
        rescue
          return "Error: Please enter any +ve integer value of n"
        end
        return "Please enter k < n" if @n < @k
        create_equations_sum
      end

      def create_equations_sum
        aar = []
        @arr = []
        @list_elements=(1..@n).to_a
        (1..@k-1).each do |i|
          aar << [*0..@n-1]
        end
        traverse([], aar, 0)
        return @arr.uniq #return result
      end

      #To check sum
      def generate_sum(*args)
        new_elements = []
        total= 0
        args.flatten.each do |arg|
          total += @list_elements[arg]
          new_elements << @list_elements[arg]
        end
        if total < @n
          new_elements << @n - total
          @arr << new_elements.sort
        else
          return
        end
      end

      def innerloop(arrayOfCurrentValues)
        generate_sum(arrayOfCurrentValues)
      end

      #Recursive method to create dynamic nested loops.
      def traverse(accumulated,params, index) 
        if (index==params.size)
          return innerloop(accumulated) 
        end
        currentParam = params[index]
        currentParam.each do |currentElementOfCurrentParam|
          traverse(accumulated+[currentElementOfCurrentParam],params, index+1)
        end
      end
    end
  end
end

使用

运行代码
params = {:n =>100, :k =>4}
c = Combination::Pairs::Equation.new(params)
c.create_equations

【问题讨论】:

  • 你有什么问题?
  • 输入:K, N (其中 0
  • 请编辑您的原始问题并将此问题添加到其中。也可以添加一些你的想法为什么它应该正确运行,或者你的确切问题在哪里。
  • 请检查是否可以理解。
  • 投赞成票,不知道为什么投反对票;计算组合系数是一项有趣且高效的任务。

标签: ruby algorithm logic


【解决方案1】:

这里有两种计算答案的方法。第一个简单但效率不高;第二种依赖于优化技术,速度更快,但需要更多的代码。

紧凑但效率低下

这是一种简洁的计算方式,使用方法Array#repeated_combination:

代码

def combos(n,k)
    [*(1..n-k+1)].repeated_combination(3).select { |a| a.reduce(:+) == n }
end

例子

combos(10,3)
  #=> [[1, 1, 8], [1, 2, 7], [1, 3, 6], [1, 4, 5],
  #    [2, 2, 6], [2, 3, 5], [2, 4, 4], [3, 3, 4]]

combos(100,4).size
  #=> 832

combos(1000,3).size
  #=> 83333

评论

前两次计算不到一秒钟,但第三次需要几分钟。

更高效,但增加了复杂性

代码

def combos(n,k)
  return nil   if k.zero?
  return [n]   if k==1
  return [1]*k if k==n
  h = (1..k-1).each_with_object({}) { |i,h| h[i]=[[1]*i] }
  (2..n-k+1).each do |i|
    g = (1..[n/i,k].min).each_with_object(Hash.new {|h,k| h[k]=[]}) do |m,f|
      im = [i]*m
      mxi = m*i
      if m==k
        f[mxi].concat(im) if mxi==n
      else
        f[mxi] << im if mxi + (k-m)*(i+1) <= n
        (1..[(i-1)*(k-m), n-mxi].min).each do |j|
          h[j].each do |a|
            f[mxi+j].concat([a+im]) if
              ((a.size==k-m && mxi+j==n) ||
               (a.size<k-m && (mxi+j+(k-m-a.size)*(i+1))<=n))
          end
        end
      end  
    end
    g.update({ n=>[[i]*k] }) if i*k == n
    h.update(g) { |k,ov,nv| ov+nv }
  end  
  h[n]
end

例子

p combos(10,3)
  #=> [[3, 3, 4], [2, 4, 4], [2, 3, 5], [1, 4, 5],
  #    [2, 2, 6], [1, 3, 6], [1, 2, 7], [1, 1, 8]]
p combos(10,4)
  #=> [[2, 2, 3, 3], [1, 3, 3, 3], [2, 2, 2, 4], [1, 2, 3, 4], [1, 1, 4, 4], 
  #    [1, 2, 2, 5], [1, 1, 3, 5], [1, 1, 2, 6], [1, 1, 1, 7]]
puts "size=#{combos(100 ,3).size}"  #=>   833
puts "size=#{combos(100 ,5).size}"  #=> 38224
puts "size=#{combos(1000,3).size}"  #=> 83333

评论

combos(1000,3).size 的计算用了大约 5 秒,其他的都在 1 秒以内。

解释

此方法使用dynamic programming 来计算解决方案。状态变量是用于计算大小不超过k 的数组的最大正整数,其元素总和不超过n。从等于一的最大整数开始。下一步是计算 k 或更少元素的所有组合,包括数字 1 和 2,然后是 1、2 和 3,依此类推,直到我们拥有 k 或更少元素的所有组合,包括数字 1通过n。然后,我们从最后一次计算中选择总和为 n 的所有 k 元素组合。

假设

k => 3
n => 7

然后

h = (1..k-1).each_with_object({}) { |i,h| h[i]=[[1]*i] }
  #=> (1..2).each_with_object({})   { |i,h| h[i]=[[1]*i] }
  #=> { 1=>[[1]], 2=>[[1,1]] } 

这读取,使用唯一的数字1[[1]] 是所有数组和 1 的数组,[[1,1]] 是所有数组和 2 的数组。

请注意,这不包括元素3=&gt;[[1,1,1]]。那是因为,已经有 k=3 元素,如果不能与任何其他元素组合,并且总和为 3 &lt; 7

我们接下来执行:

enum = (2..n-k+1).each #=> #<Enumerator: 2..5:each>

我们可以将此枚举器转换为数组,以查看它将传递到其块中的值:

enum.to_a              #=> [2, 3, 4, 5]

作为n =&gt; 7,您可能想知道为什么这个数组以5 结尾。这是因为没有包含三个正整数的数组,其中至少有一个是67,其元素之和为7

第一个值enum传入块,由块变量i表示,是2。我们现在将计算一个哈希 g,它包括所有数组的总和为 n =&gt; 7 或更少,最多有 k =&gt; 3 元素,包括一个或多个 2 和零个或多个 1' s。 (这有点拗口,但仍然不准确,我将解释。)

enum2 = (1..[n/i,k].min).each_with_object(Hash.new {|h,k| h[k]=[]})
  #=> (1..[7/2,3].min).each_with_object(Hash.new {|h,k| h[k]=[]})
  #=> (1..3).each_with_object(Hash.new {|h,k| h[k]=[]})

Enumerable#each_with_object 创建一个初始为空的散列,由块变量f 表示。这个哈希的默认值是这样的:

f[k] << o

等价于

(f[k] |= []) << o

表示如果f没有密钥k

f[k] = []

在之前执行

f[k] << o

执行。

enum2 会将以下元素传递到其块中:

enum2.to_a #=> => [[1, {}], [2, {}], [3, {}]]

(尽管当第一个元素之后的元素被传递到块中时散列可能不为空)。传递给块的第一个元素是[1, {}],由块变量表示:

m => 1
f => Hash.new {|h,k| h[k]=[]}

m =&gt; 1 表示我们将首先构造包含一个 (i=) 2 的数组。

im = [i]*m #=> [2]*1 => [2]  
mxi = m*i  #=>   2*1 =>  2

作为(m == k) #=&gt; (1 == 3) =&gt; false,我们接下来执行

f[mxi] << im if mxi + (k-m)*(i+1) <= n
  #=> f[2] << [2] if 2 + (3-1)*(1+1) <= 7
  #=> f[2] << [2] if 8 <= 7

这会考虑是否应将[2] 添加到f[2] 而不添加任何整数j &lt; i = 2。 (我们还没有考虑将一个2 与小于2 的整数组合[即1]。)作为8 &lt;= 7,我们不会将[2] 添加到f[2]。原因是,要成为长度为k=3 的数组的一部分,它将采用[2,x,y] 的形式,其中x &gt; 2y &gt; 2,所以2+x+y &gt;= 2+3+3 = 8 &gt; n = 7。清如泥?

接下来,

enum3 = (1..[(i-1)*(k-m), n-mxi].min).each
  #=> = (1..[2,5].min).each
  #=> = (1..2).each
  #=> #<Enumerator: 1..2:each>                                                

传递值

enum3.to_a #=> [1, 2]

进入它的块,由块变量j表示,它是哈希h的键。我们将在这里做的是将一个2 (m=1) 与包含不超过1(即1)且总和为j 的整数的元素数组结合起来,因此结果数组的元素总和为m * i + j =&gt; 1 * 2 + j =&gt; 2 + j

enum3 没有将大于2j 的值传递到其块中的原因是h[l] 对于l &gt; 2 是空的(但是当i &gt; 2 时它有点复杂)。

对于j =&gt; 1

h[j]              #=> [[1]]
enum4 = h[j].each #=> #<Enumerator: [[1]]:each>
enum4.to_a        #=> [[1]]
a                 #=> [1]

所以

f[mxi+j].concat([a+im]) if
  ((a.size==k-m && mxi+j==n) || (a.size<k-m && (mxi+j+(k-m-a.size)*(i+1))<=n))
  #=> f[2+1].concat([[1]+[2]) if ((1==2 && 2+1==7) || (1<=3-1 && (2+1+(1)*(3)<=7)) 
  #=> f[3].concat([1,2])      if ((false && false) || (1<=2   && (6<=7))
  #=> f[3] = [] << [[1,2]]    if (false            || (true   && true)
  #=> f[3] = [[1,2]]          if true

所以左边的表达式被计算了。同样,条件表达式有点复杂。首先考虑:

a.size==k-m && mxi+j==n

相当于:

([2] + f[j]).size == k && ([2] + f[j]).reduce(:+) == n 

也就是说,如果数组[2] + f[j]k 元素总和为n,则包含该数组。

第二个条件考虑数组[2] + f[j]元素少于k的数组是否可以用整数l &gt; i = 2“完成”并且总和为n或更少。

现在,f #=&gt; {3=&gt;[[1, 2]]}

我们现在将j 增加到2 并考虑数组[2] + h[2],其元素总数为4

对于j =&gt; 2

h[j]              #=> [[1, 1]]
enum4 = h[j].each #=> #<Enumerator: [[1, 1]]:each>
enum4.to_a        #=> [[1, 1]]
a                 #=> [1, 1]

f[mxi+j].concat([a+im]) if
  ((a.size==k-m && mxi+j==n) || (a.size<k-m && (mxi+j+(k-m-a.size)*(i+1)<=n))
  #=> f[4].concat([1, 1, 2]) if ((2==(3-1) && 2+2 == 7) || (2+2+(3-1-2)*(3)<=7))
  #=> f[4].concat([1, 1, 2]) if (true      && false)    || (false && true))
  #=> f[4].concat([1, 1, 2]) if false

因此不执行此操作(因为[1,1,2].size =&gt; 3 = k[1,1,2].reduce(:+) =&gt; 4 &lt; 7 = n

我们现在将 m 递增到 2,这意味着我们将构造具有两个 (i=) 2 的数组。这样做之后,我们看到:

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

m =&gt; 3时没有添加其他数组,所以我们有:

g #=> {3=>[[1, 2]], 4=>[[2, 2]]}

声明

g.update({ n=>[i]*k }) if i*k == n
  #=> g.update({ 7=>[2,2,2] }) if 6 == 7

如果元素之和等于n,则将元素7=&gt;[2,2,2] 添加到哈希g,但事实并非如此。

我们现在将g 折叠成h,使用Hash#update(又名Hash#merge!):

h.update(g) { |k,ov,nv| ov+nv }
  #=> {}.update({3=>[[1, 2]], 4=>[[2, 2]]} { |k,ov,nv| ov+nv }
  #=> {1=>[[1]], 2=>[[1, 1]], 3=>[[1, 2]], 4=>[[2, 2]]}

现在h 包含所有其键为数组总计的数组(值),由整数12 组成,它们最多具有3 元素并且总和最多为7,排除那些元素少于 3 的数组,当添加大于 2 的整数时,这些数组不能和 7

执行的操作如下:

 i    m    j         f

 h #=> { 1=>[[1]], 2=>[[1,1]] } 

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

 g before g.update: {7=>[[2, 2, 3], [1, 3, 3]]}
 g after  g.update: {7=>[[2, 2, 3], [1, 3, 3]]}

 h after h.update(g): {1=>[[1]],
                       2=>[[1, 1]],
                       3=>[[1, 2]],
                       4=>[[2, 2]],
                       7=>[[2, 2, 3], [1, 3, 3]]}
 4   1     1   {}
 4   1     2   {}
 4   1     3   {7=>[[1, 2, 4]]}

 g before g.update: {7=>[[1, 2, 4]]}
 g after  g.update: {7=>[[1, 2, 4]]}

 h after h.update(g): {1=>[[1]],
                       2=>[[1, 1]],
                       3=>[[1, 2]],
                       4=>[[2, 2]],
                       7=>[[2, 2, 3], [1, 3, 3], [1, 2, 4]]}
 5   1  1   {}
 5   1  2   {7=>[[1, 1, 5]]}

 g before g.update: {7=>[[1, 1, 5]]}
 g after  g.update: {7=>[[1, 1, 5]]}

 h after h.update(g): {1=>[[1]],
                       2=>[[1, 1]],
                       3=>[[1, 2]],
                       4=>[[2, 2]],
                       7=>[[2, 2, 3], [1, 3, 3], [1, 2, 4], [1, 1, 5]]}

最后,

h[n].select { |a| a.size == k }
  #=> h[7].select { |a| a.size == 3 }
  #=> [[2, 2, 3], [1, 3, 3], [1, 2, 4], [1, 1, 5]]

【讨论】:

  • 我用过。尝试将其用作combos(100,4) 或combos(100,5)。这将需要很多响应时间。因此,为了处理这种情况,我编写了自己的代码,并且发现响应时间比 Array#repeated_combination 更快。
  • 我举报了combos(4,100),但是是的,我同意你的看法。一会儿我会有更好的东西(那是'..待续'参考)。
  • 谢谢卡里。这确实是一个很好的答案,响应时间很好。
  • 我很高兴能帮上忙,塔伦。今天晚些时候我会详细解释。
  • 这对我来说将是一个很好的学习和帮助。谢谢先生。感谢您的知识和努力。
【解决方案2】:

@Cary 的回答非常深入和令人印象深刻,但在我看来,有一个更天真的解决方案,它也被证明效率更高——良好的旧递归:

def combos(n,k)
  if k == 1
    return [n]
  end
  (1..n-1).flat_map do |i|
    combos(n-i,k-1).map { |r| [i, *r].sort }
  end.uniq
end

此解决方案通过将目标总和减少 1 和前一个目标总和之间的每个数字,同时将 k 减少 1 来简单地减少每个级别的问题。现在确保你没有重复(sortuniq) - 你有你的答案......

这对k &lt; 5 来说非常好,并且比 Cary 的解决方案快得多,但是随着 k 变大,我发现它进行了太多迭代,sortuniq 造成了很大的损失计算。

所以我通过确保我只得到排序的答案来确保不需要它 - 每次递归都应该只检查大于已经使用的数字:

def combos(n,k,min = 1)
  if n < k || n < min
    return []
  end
  if k == 1
    return [n]
  end
  (min..n-1).flat_map do |i|
    combos(n-i,k-1, i).map { |r| [i, *r] }
  end
end

此解决方案与 Cary 在 combos(100, 7) 上的解决方案相当:

              user     system      total        real
My Solution   2.570000   0.010000   2.580000 (  2.695615)
Cary's        2.590000   0.000000   2.590000 (  2.609374)

但我们可以做得更好:缓存!这种递归一次又一次地做很多计算,所以缓存我们已经做过的东西会在处理长和时为我们节省很多工作:

def combos(n,k,min = 1, cache = {})
  if n < k || n < min
    return []
  end
  cache[[n,k,min]] ||= begin
    if k == 1
      return [n]
    end
    (min..n-1).flat_map do |i|
      combos(n-i,k-1, i, cache).map { |r| [i, *r] }
    end
  end
end

这个解决方案速度非常快,比 Cary 的大型 n 解决方案要快几光年:

Benchmark.bm do |bm|
  bm.report('Uri') { combos(1000, 3) }
  bm.report('Cary') { combos_cary(1000, 3) }
end

       user     system      total        real
Uri   0.200000   0.000000   0.200000 (  0.214080)
Cary  7.210000   0.000000   7.210000 (  7.220085)

并且与k 高达 9 相提并论,我相信它仍然没有他的解决方案复杂。

【讨论】:

  • 这非常令人印象深刻,@Uri。
  • @Uri,这真的很棒
【解决方案3】:

您希望将n 中的integer partitions 的数量精确地转换为k 和数。这个数字有一个(计算上)有点难看的重现。

这个想法是这样的:让P(n,k) 成为将n 划分为k 非零和的方法的数量;然后P(n,k) = P(n-1,k-1) + P(n-k,k)。证明:每个分区要么包含 1,要么不包含 1 作为加数之一。第一个案例P(n-1,k-1) 计算总和中有1 的案例数量;将1 从总和中取出,并将剩余的n-1 划分为现在可用的k-1 求和。第二种情况P(n-k,k) 考虑每个被加数严格大于1 的情况;为此,将所有k summands 减少1 并从那里递归。显然,P(n,1) = 1 代表所有 n &gt; 0

Here's a link 可能提到,一般k 没有封闭形式。

【讨论】:

  • 如果有人知道“summand”这个更常见的词,请告诉我。
  • 您提供的“这里的代码”链接是生成具有任意数量的总和的所有分区。
  • @CarySwoveland 错了,感谢您指出。将寻找执行我们想要的代码的链接。
  • @DavidEisenstat 术语比我预期的要笼统一些。我的目标是使用“术语的加法部分”的常用表达方式,字典为我提供了“加法”和“加法”选项。是常用的吗?
  • 我对如何有效地完成它有一个想法,今晚会花一些时间。顺便说一句,我喜欢你所有的曲子,尤其是cello suite #1。等一下。 “G”?
猜你喜欢
  • 1970-01-01
  • 2017-04-22
  • 2013-01-12
  • 1970-01-01
  • 1970-01-01
  • 2015-08-14
  • 2022-01-25
  • 2022-11-13
  • 1970-01-01
相关资源
最近更新 更多