knapsack_solver 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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