knapsack_solver 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,48 @@
1
+ module KnapsackSolver
2
+ # This class represents an instance of a 0/1 knapsack problem.
3
+ class Instance
4
+ # Initializes instance of a 0/1 knapsack problem.
5
+ #
6
+ # @param capacity [Integer] weight capacity of the knapsack
7
+ # @param things [Array<Thing>] things which can be put into the knapsack
8
+ def initialize(capacity, things)
9
+ @weight_capacity = capacity
10
+ @things = things
11
+ end
12
+
13
+ # Creates new instance of a 0/1 knapsack problem.
14
+ #
15
+ # @param line [String] line that describes an instance of a 0/1 knapsack problem
16
+ # @return [Instance] instance of the 0/1 knapsack problem
17
+ def self.parse(line)
18
+ thing = Struct.new(:price, :weight, :index)
19
+ # Rozdelit riadok na slova a previest na cisla
20
+ items = split_line(line)
21
+ # Inicializacia premennych
22
+ things = items.drop(1).each_slice(2).with_index.each_with_object([]) do |(s, i), o|
23
+ o << thing.new(s[0], s[1], i)
24
+ end
25
+ Instance.new(items[0], things)
26
+ end
27
+
28
+ # Splits line that describes an instance of a 0/1 knapsack problem to
29
+ # individual numbers.
30
+ #
31
+ # @param line [String] line that describes an instance of a 0/1 knapsack problem
32
+ # @return [Array<Integer>] integer numbers from the line
33
+ def self.split_line(line)
34
+ items = line.split.map! do |i|
35
+ n = Integer(i)
36
+ raise StandardError, 'dataset: instance desctiption contains negative number' if n < 0
37
+ n
38
+ end
39
+ raise StandardError, 'dataset: missing knapsack capacity' if items.empty?
40
+ raise StandardError, 'dataset: missing pairs (price, weight)' if items.size.even?
41
+ items
42
+ rescue ArgumentError
43
+ raise StandardError, 'dataset: instance desctiption does not contain only integers'
44
+ end
45
+
46
+ attr_reader :weight_capacity, :things
47
+ end
48
+ end
@@ -0,0 +1,97 @@
1
+ module KnapsackSolver
2
+ # This class provides support for printing results and statistics of a
3
+ # dataset solving either to stdout or to a text file.
4
+ class OutputPrinter
5
+ # Initializes printer for output log (results, statistics).
6
+ #
7
+ # @param dataset_filenames [Array<String>] dataset filenames
8
+ # @param suffix [String] suffix of the created files
9
+ # @param results [Hash] results of solving or statistics to print
10
+ def initialize(dataset_filenames, suffix, results)
11
+ @dataset_basenames = file_basenames(dataset_filenames)
12
+ @suffix = suffix
13
+ @results = results
14
+ end
15
+
16
+ # Prints results or statistics to stdout or to files in output directory.
17
+ #
18
+ # @param out_dir [String] path to output directory
19
+ def print(out_dir = nil)
20
+ @results.each_value.with_index do |results, index|
21
+ results.each do |method, res|
22
+ print_solving_method_results(method, res, out_dir, @dataset_basenames[index])
23
+ end
24
+ end
25
+ end
26
+
27
+ protected
28
+
29
+ # Prints results of solving or statistics.
30
+ #
31
+ # @param method [Symbol] symbol for solving method
32
+ # @param res [Hash] results of the solving method
33
+ # @param out_dir [String] path to output directory
34
+ # @param basename [String] basename of dataset input file corresponding to the results
35
+ def print_solving_method_results(method, res, out_dir, basename)
36
+ of = output_filename(out_dir, basename, method.to_s)
37
+ os = output_stream(out_dir, of)
38
+ print_header(os, of, res)
39
+ res.each do |r|
40
+ os.puts r.values.each_with_object([]) { |v, a| a << v.to_s }.join(' ')
41
+ end
42
+ os.puts if out_dir.nil?
43
+ end
44
+
45
+ # Opens output file and turns on synchronized writes (this is neede for
46
+ # testing with Rspec).
47
+ #
48
+ # @param fname [String] path to the output file
49
+ # @return [#puts] output stream
50
+ def open_output_file(fname)
51
+ f = File.new(fname, 'w')
52
+ f.sync = true
53
+ f
54
+ end
55
+
56
+ # Sets output stream to stdout or to a file if path to it was provided.
57
+ #
58
+ # @param out_dir [String] directory for output files
59
+ # @param out_file [String] output file
60
+ def output_stream(out_dir, out_file)
61
+ return $stdout if out_dir.nil?
62
+ open_output_file(out_file)
63
+ end
64
+
65
+ # Prints header of output log file.
66
+ #
67
+ # @param out_stream [#puts] stream to which output will be printed
68
+ # @param out_file [String] name of output file
69
+ # @param results [Hash] results of solving or statistics
70
+ def print_header(out_stream, out_file, results)
71
+ out_stream.puts "# #{out_file}"
72
+ out_stream.puts "# #{results.first.keys.join(' ')}"
73
+ end
74
+
75
+ # Gets basenames of supplied file paths.
76
+ #
77
+ # @param paths [Array<String>] path to files
78
+ # @return [Array<String>] basenames of the paths
79
+ def file_basenames(paths)
80
+ paths.each_with_object([]) do |path, basenames|
81
+ basenames << File.basename(path, File.extname(path))
82
+ end
83
+ end
84
+
85
+ # Construct filename for output log.
86
+ #
87
+ # @param output_dir [String] output directory
88
+ # @param basename [String] basename of the output file
89
+ # @param solving_method [String] name of solving method
90
+ # @return [String] filename for output log
91
+ def output_filename(output_dir, basename, solving_method)
92
+ filename = basename + '_' + solving_method + @suffix
93
+ return filename if output_dir.nil?
94
+ File.join(output_dir, filename)
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,118 @@
1
+ require 'benchmark'
2
+
3
+ require 'knapsack_solver/dataset'
4
+ require 'knapsack_solver/solving_methods/heuristic_price_weight'
5
+ require 'knapsack_solver/solving_methods/branch_and_bound'
6
+ require 'knapsack_solver/solving_methods/dynamic_programming'
7
+ require 'knapsack_solver/solving_methods/fptas'
8
+
9
+ module KnapsackSolver
10
+ # This class solves datasets of 0/1 knapsack problem instances using a
11
+ # requested solving methods. It measures execution time of a solving and
12
+ # computes relative error if some exact solving method is requested.
13
+ class Solver
14
+ # Initializes solver for use of user selected solving methods.
15
+ #
16
+ # @param opts [Hash] parser command-line options
17
+ # @param datasets [Hash] parsed sets of 0/1 knapsack problem instances
18
+ def initialize(opts, datasets)
19
+ @opts = opts
20
+ @datasets = datasets
21
+ @solver_objects = {}
22
+ { branch_and_bound: 'KnapsackSolver::BranchAndBound',
23
+ dynamic_programming: 'KnapsackSolver::DynamicProgramming',
24
+ heuristic: 'KnapsackSolver::HeuristicPriceToWeight',
25
+ fptas: 'KnapsackSolver::Fptas' }.each do |symbol, class_name|
26
+ @solver_objects[symbol] = Object.const_get(class_name) if opts[symbol]
27
+ end
28
+ end
29
+
30
+ # Solve datasets using all selected method of solving, measure their
31
+ # execution time and compute relative errors if some exact method was
32
+ # requested.
33
+ #
34
+ # @return [Hash] results of dataset solving
35
+ def run
36
+ results = @datasets.each_with_object({}) do |dataset, res|
37
+ res[dataset.id] = @solver_objects.each_with_object({}) do |(solver, object), r|
38
+ r[solver] = dataset.instances.each_with_object([]) do |inst, a|
39
+ o = object.new(inst) unless solver == :fptas
40
+ o = object.new(inst, @opts[:fptas_epsilon]) if solver == :fptas
41
+ a << execution_time { o.run }
42
+ end
43
+ end
44
+ end
45
+ add_relative_error(results)
46
+ end
47
+
48
+ # Creates statistics (average price, execution times, relative error) from
49
+ # results of solving.
50
+ #
51
+ # @param results [Hash] solving results of datasets and solving methods
52
+ # @return [Hash] statistics for datasets and solving methods
53
+ def stats(results)
54
+ results.each_with_object({}) do |(dataset_id, method_results), q|
55
+ q[dataset_id] = method_results.each_with_object({}) do |(met, res), p|
56
+ p[met] = averages(res)
57
+ end
58
+ end
59
+ end
60
+
61
+ protected
62
+
63
+ # Computes average values from the results.
64
+ #
65
+ # @param res [Hash] results for one dataset and one method
66
+ # @return [Array<Hash>] array of computed average values
67
+ def averages(res)
68
+ [res.first.keys.reject { |k| k == :config }.each_with_object({}) do |v, o|
69
+ values = res.map { |i| i[v] }
70
+ o[('avg_' + v.to_s).to_sym] = values.reduce(:+).to_f / values.size
71
+ end]
72
+ end
73
+
74
+ # Adds relative error to results of solving if some exact method of
75
+ # solving was requested.
76
+ #
77
+ # @param results [Hash] results of solving using requested methods
78
+ # @return [Hash] the results with relative error added
79
+ def add_relative_error(results)
80
+ return results unless @opts[:branch_and_bound] || @opts[:dynamic_programming]
81
+ exact_method = @opts[:branch_and_bound] ? :branch_and_bound : :dynamic_programming
82
+ results.each_value do |method_results|
83
+ method_results.each_value do |res|
84
+ res.each_with_index do |r, i|
85
+ r[:relative_error] = relative_error(method_results[exact_method][i][:price], r[:price])
86
+ end
87
+ end
88
+ end
89
+ results
90
+ end
91
+
92
+ # Measure execution time of provided block so that measured time is non-zero.
93
+ #
94
+ # @yieldparam block for which execution time will be measured
95
+ # @return [Hash] solving results with cpu time and wall clock time of execution
96
+ def execution_time
97
+ exec_count = 1
98
+ result = nil
99
+ cpu_time = wall_clock_time = 0.0
100
+ while cpu_time.zero? || wall_clock_time.zero?
101
+ b = Benchmark.measure { exec_count.times { result = yield } }
102
+ cpu_time += b.total
103
+ wall_clock_time += b.real
104
+ exec_count *= 2
105
+ end
106
+ result.merge(cpu_time: cpu_time, wall_clock_time: wall_clock_time)
107
+ end
108
+
109
+ # Computes relative error of approximate solution.
110
+ #
111
+ # @param opt [Numeric] Optimal price.
112
+ # @param apx [Numeric] Approximate price.
113
+ # @return [Float] Relative error.
114
+ def relative_error(opt, apx)
115
+ (opt.to_f - apx.to_f) / opt.to_f
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,111 @@
1
+ module KnapsackSolver
2
+ # This class implements methods for solving 0/1 knapsack problem using
3
+ # Branch and Bound method.
4
+ class BranchAndBound
5
+ # Initializes instance of Brand and Bound 0/1 knapsack problem solver.
6
+ #
7
+ # @param instance [Instance] 0/1 knapsack problem instance
8
+ def initialize(instance)
9
+ @instance = instance
10
+ @config = Array.new(instance.things.size)
11
+ end
12
+
13
+ # Solve the instance of 0/1 knapsack problem.
14
+ #
15
+ # @return [Hash] resulting price and thing configuration (0 = thing is not in the knapsack, 1 = thing is there)
16
+ def run
17
+ solve(0)
18
+ { price: @best_price, config: @best_config }
19
+ end
20
+
21
+ protected
22
+
23
+ # Solve the problem starting at specified thing.
24
+ #
25
+ # @param index [Integer] index of thing which will be decided (put in or out from the knapsack) the next
26
+ def solve(index)
27
+ @config[index] = 0
28
+ solve(index + 1) unless stop(index)
29
+ @config[index] = 1
30
+ solve(index + 1) unless stop(index)
31
+ end
32
+
33
+ # Determine if solving of current branch should continue.
34
+ #
35
+ # @param index [Integer] index of the last decided thing so far
36
+ # @return [true, false] weather to continue with solving current branch.
37
+ def stop(index)
38
+ # Update of the best price so far
39
+ weight = config_weight(0, index)
40
+ price = config_price(0, index)
41
+ update_best_price(price, weight, index)
42
+ # No more things to put into the knapsack
43
+ return true if index >= (@instance.things.size - 1)
44
+ # The knapsack is overloaded, do not continue this branch
45
+ return true if weight > @instance.weight_capacity
46
+ if instance_variable_defined?('@best_price') &&
47
+ ((price + get_price_of_remaining_things(index + 1)) <= @best_price)
48
+ # Adding all the ramining things does not produce better price
49
+ return true
50
+ end
51
+ false
52
+ end
53
+
54
+ # Update the best price achieved so far.
55
+ #
56
+ # @param price [Integer] price of the current configuration
57
+ # @param weight [Integer] weight of the current configuration
58
+ # @param index [Integer] index of the next thing presence of which will be decided
59
+ def update_best_price(price, weight, index)
60
+ if !instance_variable_defined?('@best_price') ||
61
+ ((weight <= @instance.weight_capacity) && (price > @best_price))
62
+ @best_price = price
63
+ valid_len = index + 1
64
+ remaining = @config.size - index - 1
65
+ # All undecided things will not be put into the knapsack
66
+ @best_config = @config.slice(0, valid_len).fill(0, valid_len, remaining)
67
+ @best_config_index = index
68
+ end
69
+ end
70
+
71
+ # Gets weight of set of things. The set is subset of the things ordered by
72
+ # their index.
73
+ #
74
+ # @param start_index [Integer] index of the first thing included in the set
75
+ # @param end_index [Integer] index of the last thing included in the set
76
+ # @return [Integer] weight of the things
77
+ def config_weight(start_index, end_index)
78
+ weight = 0
79
+ @config[start_index..end_index].each_with_index do |presence, index|
80
+ weight += presence * @instance.things[index].weight
81
+ end
82
+ weight
83
+ end
84
+
85
+ # Gets price of set of things. The set is subset of the things ordered by
86
+ # their index.
87
+ #
88
+ # @param start_index [Integer] index of the first thing included in the set
89
+ # @param end_index [Integer] index of the last thing included in the set
90
+ # @return [Integer] price of the things
91
+ def config_price(start_index, end_index)
92
+ price = 0
93
+ @config[start_index..end_index].each_with_index do |presence, index|
94
+ price += presence * @instance.things[index].price
95
+ end
96
+ price
97
+ end
98
+
99
+ # Gets sum of prices of things for which their presence in the knapsack
100
+ # was not decided yet.
101
+ #
102
+ # @param from_index [Integer] index of the first undecided thing
103
+ # @return [Integer] price of the remaining things
104
+ def get_price_of_remaining_things(from_index)
105
+ price = 0
106
+ to_index = @instance.things.size - 1
107
+ @instance.things[from_index..to_index].each { |t| price += t.price }
108
+ price
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,116 @@
1
+ module KnapsackSolver
2
+ # This class implements methods for solving 0/1 knapsack problem using
3
+ # dynamic programming with decomposition by price.
4
+ class DynamicProgramming
5
+ # Initializes instance of 0/1 knapsack problem solver based on dynamic
6
+ # programming with decomposition by price.
7
+ #
8
+ # @param instance [Instance] 0/1 knapsack problem instance
9
+ def initialize(instance)
10
+ @instance = instance
11
+ @config = Array.new(instance.things.size)
12
+ end
13
+
14
+ # Solve the instance of 0/1 knapsack problem.
15
+ #
16
+ # @return [Hash] resulting price and thing configuration (0 = thing is not in the knapsack, 1 = thing is there)
17
+ def run
18
+ solve
19
+ { price: @best_price, config: @best_config }
20
+ end
21
+
22
+ protected
23
+
24
+ # Solve the instance of 0/1 knapsack problem using dynamic programming.
25
+ def solve
26
+ # Dynamic programming table
27
+ c = all_things_price + 1 # height of array from 0 to max. price
28
+ n = @instance.things.size + 1 # width of array, from 0th thing to Nth
29
+ # Value used as infinity in the dynamic programming table
30
+ @infinity = (all_things_weight + 1).freeze
31
+ @weight_array = Array.new(n) { Array.new(c, @infinity) }
32
+ @weight_array[0][0] = 0
33
+ fill_table
34
+ find_best_price
35
+ configuration_vector
36
+ end
37
+
38
+ # Fill the dynamic programming table.
39
+ def fill_table
40
+ (1..@instance.things.size).each do |ni|
41
+ (0..all_things_price).each do |ci|
42
+ @weight_array[ni][ci] = minimum_weight(ni, ci)
43
+ end
44
+ end
45
+ end
46
+
47
+ # Find the value of cell in dynamic programming table.
48
+ #
49
+ # @param ni [Integer] X axis coordinate
50
+ # @param ci [Integer] Y axis coordinate
51
+ # @return [Integer]
52
+ def minimum_weight(ni, ci)
53
+ b = weight_of(ni - 1, ci - @instance.things[ni - 1].price)
54
+ b += @instance.things[ni - 1].weight
55
+ [weight_of(ni - 1, ci), b].min
56
+ end
57
+
58
+ # Find the best price from the filled dynamic programming table.
59
+ def find_best_price
60
+ @best_price = @weight_array.last[0]
61
+ (1..all_things_price).each do |i|
62
+ @best_price = i if @weight_array.last[i] <= @instance.weight_capacity
63
+ end
64
+ end
65
+
66
+ # Reconstructs configuration vector from dynamic programming table.
67
+ def configuration_vector
68
+ @best_config = []
69
+ ci = @best_price
70
+ @instance.things.size.downto(1) do |i|
71
+ ci = determine_config_variable(i, ci)
72
+ end
73
+ end
74
+
75
+ # Determine value of one scalar for the configuration vector.
76
+ #
77
+ # return [Integer] next Y index to the dynamic programming table
78
+ def determine_config_variable(i, ci)
79
+ if @weight_array[i][ci] == @weight_array[i - 1][ci]
80
+ @best_config[i - 1] = 0
81
+ else
82
+ @best_config[i - 1] = 1
83
+ ci -= @instance.things[i - 1].price
84
+ end
85
+ ci
86
+ end
87
+
88
+ # Gets weight from dynamic programming table.
89
+ #
90
+ # @param i [Integer] Y index of dynamic programming table
91
+ # @param c [Integer] X index of dynamic programming table
92
+ # @return [Integer] the value from the array
93
+ def weight_of(i, c)
94
+ return @infinity if (i < 0) || (c < 0)
95
+ @weight_array[i][c]
96
+ end
97
+
98
+ # Computes total price of all things of the instance.
99
+ #
100
+ # @return [Integer] total price
101
+ def all_things_price
102
+ price = 0
103
+ @instance.things.each { |t| price += t.price }
104
+ price
105
+ end
106
+
107
+ # Computes total weight of all things of the instance.
108
+ #
109
+ # @return [Integer] total weight
110
+ def all_things_weight
111
+ weight = 0
112
+ @instance.things.each { |t| weight += t.weight }
113
+ weight
114
+ end
115
+ end
116
+ end