此解决方案采用动态规划。我假设已经从数组中删除了零。如果数组中的所有数字都是正数,我们也可以删除大于目标总和的元素。
代码
def sum_to_target(arr, target)
h = arr.each_index.with_object({}) do |i,h|
v = arr[i]
h.keys.each do |n|
unless h.key?(n+v) # || (n+v > target)
h[n+v] = v
return reconstruct(h, target) if n+v == target
end
end
h[v] = v unless h.key?(v)
return reconstruct(h, target) if v == target
end
nil
end
def reconstruct(h, target)
a = []
loop do
i = h[target]
a.unshift i
target -= i
return a if target == 0
end
a
end
如果arr 仅包含正值,则可能会进一步提高效率。1
示例
#1
sum_to_target [2,4,7,2], 8
#=> [2, 4, 2]
#2
arr = [64, 18, 64, 6, 39, 51, 87, 62, 78, 62, 49, 86, 35, 57, 40, 15, 74, 10, 8, 7]
a = sum_to_target(arr, 461)
#=> [64, 18, 39, 51, 87, 62, 78, 62]
让我们检查一下。
a.reduce(:+)
#=> 461
#3
a = sum_to_target([-64, 18, 64, -6, 39, 51, -87, 62, -78, 62, 49, 86, 35, 57,
40, 15, -74, 10, -8, -7], 190)
#=> [18, 64, -6, 39, 51, -87, 62, 49]
a.reduce(:+)
#=> 190
#4
arr = 1_000.times.map { rand 1..5_000 }
#=> [3471, 1891, 4257, 2265, 832, 1060, 3961, 875, 614, 2308, 2240, 3286,
# ...
# 521, 1316, 1986, 4099, 1398, 3803, 4498, 4607, 2262, 3941, 4367]
arr 是一个包含 1,000 个元素的数组,每个元素都是 1 到 5,000 之间的随机数。
answer = arr.sample(500)
#=> [3469, 2957, 1542, 950, 4765, 3126, 3602, 755, 4132, 4281, 2374,
# ...
# 427, 4238, 4397, 2717, 912, 1690, 3626, 169, 3607, 4084, 3161]
answer 是一个包含来自arr 的 500 个元素的数组,未经替换采样。
target = answer.reduce(:+)
#=> 1_226_020
target 是answer 的元素之和。我们现在将搜索 arr 以查找总和为 1,226,020 的元素集合(answer 就是这样一个集合)。
require 'time'
t = Time.now
#=> 2016-12-12 23:00:51 -0800
a = sum_to_target(arr, target)
#=> [3471, 1891, 4257, 2265, 832, 1060, 3961, 875, 614, 2308, 2240, 3286,
# ...
# 3616, 4150, 3222, 3896, 631, 2806, 1932, 3244, 2430, 1443, 1452]
注意a != answer(这并不奇怪)。
a.reduce(:+)
#=> 1226020
(Time.now-t).to_i
#=> 60 seconds
对于最后一个示例,使用 Array#combination 的方法将不得不涉足尽可能多
(1..arr.size).reduce(0) { |t,i| t + arr.combination(i).size }.to_f
#~> 1.07+301
组合。
说明
让
arr = [2,4,7,2]
target = 8
假设我们临时重新定义reconstruct 以返回传递给它的哈希值。
def reconstruct(h, target)
h
end
然后我们得到以下内容:
h = sum_to_target(arr, target)
#=> {2=>2, 6=>4, 4=>4, 9=>7, 13=>7, 11=>7, 7=>7, 8=>2}
h 定义如下。
给定一个非零整数数组arr 和一个数字n,如果n 是h 的键,则存在一个数组a,其中包含来自arr 的元素,在相同的顺序,这样a 的元素总和为n,a 的最后一个元素等于h[n]。
诚然,这很拗口。
我们现在使用reconstruct(在“代码”部分中定义)来构造一个数组answer,该数组将包含来自arr(不包含重复元素)的元素,这些元素总和为target。
reconstruct(h, target) #=> [2, 4, 2]
最初,reconstruct 初始化数组 answer,它将构建并返回:
answer = []
h 将始终包含一个等于目标 (8) 的键。作为h[8] #=> 2,我们得出answer的最后一个元素等于2,所以我们执行
answer.unshift(2) #=> [2]
现在的问题是从arr 中找到一个元素数组,总和为8 - 2 #=> 6。作为h[6] #=> 4,我们得出结论,answer 中我们刚刚添加的2 之前的元素是4:
answer.unshift(4) #=> [4, 2]
我们现在需要更多 8-2-4 #=> 2 来总计 target。作为h[2] #=> 2,我们执行
answer.unshift(2) #=> [2, 4, 2]
由于8-2-4-2 #=> 0 我们已经完成,因此返回answer。
请注意,4 在 arr 中的最后一个 2 之前,第一个 2 在 arr 中的 4 之前。 h 的构造方式确保了answer 的元素始终以这种方式排序。
现在考虑如何构造h。首先,
h = {}
作为arr[0] #=> 2,我们得出结论,仅使用arr 的第一个元素,我们可以得出的结论是:
h[2] = 2
h #=> {2=>2}
h 没有等于target (8) 的密钥,所以我们继续。现在考虑arr[1] #=> 4。仅使用arr 的前两个元素,我们可以得出以下结论:
h[2+4] = 4
h #=> {2=>2, 6=>4}
由于h 没有密钥4,
h[4] = 4
h #=> {2=>2, 6=>4, 4=>4}
h 仍然没有与target (8) 相等的键,所以我们按下并检查arr[2] #=> 7。仅使用arr 的前三个元素,我们得出以下结论:
h[2+7] = 7
h[6+7] = 7
h[4+7] = 7
h #=> {2=>2, 6=>4, 4=>4, 9=>7, 13=>7, 11=>7}
因为h没有密钥7:
h[7] = 7
h #=> {2=>2, 6=>4, 4=>4, 9=>7, 13=>7, 11=>7, 7=>7}
我们在h 中添加了四个元素,但由于arr 仅包含正数,因此对具有键9、13 和11 的元素不感兴趣。
由于h 仍然没有等于target (8) 的键,我们检查arr 中的下一个元素:arr[3] #=> 2。仅使用arr 的前四个元素,我们得出以下结论:
h[4+2] = 2
h[6+2] = 2
我们在这里停下来,因为6+2 == target #=> true。
h #=> {2=>2, 6=>2, 4=>4, 9=>7, 13=>7, 11=>7, 7=>7, 8=>2}
请注意,我们没有计算 h[2+2] = 2,因为 h 已经有一个密钥 4。此外,如果 arr 包含其他元素,我们仍然会在此时终止哈希的构造。
如果我们修改代码以利用 arr 仅包含正值这一事实,那么最终的哈希将是:
h #=> {2=>2, 6=>2, 4=>4, 7=>7, 8=>2}
如果这仍然不清楚,运行包含puts 语句的示例代码可能会有所帮助(例如,sum_to_target 中的v = arr[i] 行之后的puts "i=#{i}, h=#{h}, v=#{v}",等等)。
1 如果已知数组不包含负元素,则可以将行 unless h.key?(n+v) 更改为 unless h.key?(n+v) || (n+v > target)。 (这样做可以将 #4 的求解时间缩短 4 秒。)还可以计算 @all_positive = arr.all?(&:positive?),然后使该行以 @all_positive 为条件。