require 'terminal-table/import' class SubsetSumMatrix class << self def create_empty_for(array) matrix = [] header = [nil] + build_header_from(array) matrix << header array.each_with_index do |element,i| row = header.collect{|value| 'F'} row[0] = i matrix << row end matrix end def build_header_from(array) sorted_array = array.sort sum_negative = 0 sum_positive = 0 sorted_array.each do |element| sum_negative += element if element < 0 sum_positive += element if element > 0 end (sum_negative..sum_positive).to_a end def build_column_value_to_index_hash(matrix_header) hash = {} matrix_header.each_with_index do |element,i| next if i == 0 #skipping the first index since it has no value hash[element] = i end hash end end def initialize(array) @array = array @matrix = SubsetSumMatrix.create_empty_for(array) @column_value_to_index = SubsetSumMatrix.build_column_value_to_index_hash(@matrix[0]) end def initialize_first_row @matrix[1].each_with_index do |element,i| next if i == 0 # skipping the first one since it is the index into the array if @array[@matrix[1][0]] == @matrix[0][i] # the only sum we can have is the first number itself @matrix[1][i] = 'T' end end @matrix end def populate (2...@matrix.size).each do |row| @matrix[row].each_with_index do |element,i| next if i == 0 if @array[@matrix[row][0]] == @matrix[0][i] || @matrix[row-1][i] == 'T' || current_sum_possible(row, i) @matrix[row][i] = 'T' end end end @matrix end def current_sum_possible(row, column) column_sum = @matrix[0][column] - @array[@matrix[row][0]] column_index = @column_value_to_index[column_sum] return false unless column_index @matrix[row-1][column_index] == 'T' end def derive_subset_for(reference_value) subset = [] column_index = @column_value_to_index[reference_value] (1...@matrix.size).to_a.reverse.each do |row| if @matrix[row][column_index] == 'F' return subset elsif @matrix[row-1][column_index] == 'T' next else array_value = @array[row - 1] # the -1 is to account for the fact that our rows are 1 larger than indexes of input array due to row 0 in matrix being header subset.insert(0, array_value) column_index = @column_value_to_index[@matrix[0][column_index] - array_value] end end subset end def to_s puts "Input: #{@array.inspect}" puts "Reference value: #{@reference_value}" puts table(*@matrix) end end def subset_sum_dynamic(array, target_value) matrix = SubsetSumMatrix.new(array) #matrix.to_s matrix.initialize_first_row #matrix.to_s matrix.populate #matrix.to_s subset = matrix.derive_subset_for(target_value) puts "Subset sums to: #{ subset.reduce(0){|accumulator, value| accumulator + value} }" subset end def n_integers_randomized_between(range, n) range.to_a.shuffle[0...n] end #puts subset_sum_dynamic([1, -3, 4, 5, -8, 7, -1], 0).inspect #puts subset_sum_dynamic([1, -3, 2, 4], 0).inspect #puts subset_sum_dynamic([ 802, 421, 143, -302, 137, 316, 150, -611, -466, -42, -195, -295 ], 0).inspect puts subset_sum_dynamic(n_integers_randomized_between((-1000..1000), 50), 0).inspect if ENV["attest"] this_tests "generating subset sums using dynamic programming" do test("subset should be [1,-3,2]") do actual_subset_sum = subset_sum_dynamic([1, -3, 2, 4], 0) should_equal([1,-3,2], actual_subset_sum) end test("subset should be [1,-8, 7]") do actual_subset_sum = subset_sum_dynamic([1, -3, 4, 5, -8, 7, -1], 0) should_equal([1,-8,7], actual_subset_sum) end test("subset should be [316, 150,-466]") do actual_subset_sum = subset_sum_dynamic([ 802, 421, 143, -302, 137, 316, 150, -611, -466, -42, -195, -295 ], 0) should_equal([316,150,-466], actual_subset_sum) end end end