可以用红宝石中的和为N的K数的可能方程的数量

时间:2014-05-24 10:16:41

标签: ruby algorithm logic

我必须在rails上的ruby中创建一个程序,以便花费更少的时间来解决特定的情况。现在我要获得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

3 个答案:

答案 0 :(得分:2)

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

紧凑但效率低下

这是一种使用方法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花了大约五秒钟,其他人都不到一秒钟。

说明

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

假设

n

然后

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=>[[1,1,1]]个元素,如果不能与任何其他元素组合,则总和为k=3

我们接下来执行:

3 < 7

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

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

作为enum.to_a #=> [2, 3, 4, 5] ,您可能想知道为什么此数组在n => 7处结束。这是因为没有包含三个正整数的数组,其中至少有一个是56,其元素总和为7

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

1

Enumerable#each_with_object创建一个由块变量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]=[]}) 表示的初始空哈希。此哈希的默认值为:

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 => 1i=的数组。

2

作为im = [i]*m #=> [2]*1 => [2] mxi = m*i #=> 2*1 => 2 ,我们接下来执行

(m == k) #=> (1 == 3) => 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 < i = 2与小于2的整数[即2]合并。)在1,我们不添加8 <= 7[2]。原因是,为了使其成为长度f[2]数组的一部分,它的格式为k=3,其中[2,x,y]x > 2,{{1} }}。像泥一样清楚?

接下来,

y > 2

传递值

2+x+y >= 2+3+3 = 8 > 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] 的关键字。我们在这里要做的是将一个jh)与包含整数2(即m=1)的元素数组合并到{{1因此,结果数组的元素将总和为1

1未将j大于m * i + j => 1 * 2 + j => 2 + j的值传递到其块中的原因是enum3j为空(但它的一点点) 2)时更复杂。

h[l]

l > 2

所以

i > 2

因此评估左侧的表达式。同样,条件表达式有点复杂。先考虑一下:

j => 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的数组是否可以&#34;完成&#34;使用整数n并且总和为[2] + f[j]或更少。

现在,k

我们现在将l > i = 2增加到n并考虑数组f #=> {3=>[[1, 2]]},其元素总计为j

2

[2] + h[2]

所以不执行此操作(从4j => 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 => 3 = k,这意味着我们将构造具有两个([1,1,2].reduce(:+) => 4 < 7 = n =)m&#39;的数组。在这样做之后,我们看到:

2

并且在i时没有添加其他数组,因此我们有:

2

声明

f={3=>[[1, 2]], 4=>[[2, 2]]}
如果元素的总和等于m => 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 ,而不是7=>[2,2,2]

我们现在使用Hash#update(又名Hash#merge!)将g折叠到n中:

g

现在h包含所有数组(值),其键是数组总数,由整数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组成,最多包含1个元素和最多为2,不包括那些少于3个元素的数组,当添加的整数大于2时,这些数组不能求和为7

执行的操作如下:

3

最后,

7

答案 1 :(得分:1)

@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 < 5非常有用,并且比Cary的解决方案快得多,但随着k越来越大,我发现它进行了太多的迭代,sort和{{1对计算造成了很大的影响。

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

uniq

此解决方案与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)

这个解决方案速度非常快,并且通过光年来传递Cary的大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 解决方案:

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) 高达9相当,我相信它仍然不如他的解决方案复杂。

答案 2 :(得分:0)

您希望n k的数量与P(n,k)个完全相同。这个数字有一个(计算上)有点难看的复发。

这个想法是这样的:让n成为将k分区为P(n,k) = P(n-1,k-1) + P(n-k,k)非零加数的方法的数量;然后1。证明:每个分区都包含一个P(n-1,k-1),或者它不包含1作为一个加数。第一个案例1计算总和中1的个案数量;将n-1从总和中取出,并将剩余的k-1分区为现在可用的P(n-k,k)加号。第二种情况1考虑每个加数严格大于k的情况;要做到这一点,请将所有1加数减少P(n,1) = 1并从那里递减。显然,n > 0适用于所有k

integer partitions提到可能,一般{{1}}没有封闭形式。