diff --git a/README.md b/README.md index 6ccad57..39b2082 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Consistency is hard without proper goals to set a good (let's dare say "atomic") I'm also adding notes that may be useful if you're learning Ruby. Notes for solving: -* [2015, up to day 14](year_2015.md) +* [2015, up to day 15](year_2015.md) * [2023, up to day 03](year_2023.md) # How to use diff --git a/spec/year_2015/day_15_input b/spec/year_2015/day_15_input new file mode 100644 index 0000000..66e1882 --- /dev/null +++ b/spec/year_2015/day_15_input @@ -0,0 +1,4 @@ +Sugar: capacity 3, durability 0, flavor 0, texture -3, calories 2 +Sprinkles: capacity -3, durability 3, flavor 0, texture 0, calories 9 +Candy: capacity -1, durability 0, flavor 4, texture 0, calories 1 +Chocolate: capacity 0, durability 0, flavor -2, texture 2, calories 8 \ No newline at end of file diff --git a/spec/year_2015/day_15_sample_one b/spec/year_2015/day_15_sample_one new file mode 100644 index 0000000..1a7ff25 --- /dev/null +++ b/spec/year_2015/day_15_sample_one @@ -0,0 +1,2 @@ +Butterscotch: capacity -1, durability -2, flavor 6, texture 3, calories 8 +Cinnamon: capacity 2, durability 3, flavor -2, texture -1, calories 3 diff --git a/spec/year_2015/day_15_spec.rb b/spec/year_2015/day_15_spec.rb new file mode 100644 index 0000000..266bd0e --- /dev/null +++ b/spec/year_2015/day_15_spec.rb @@ -0,0 +1,37 @@ +require 'year_2015/day_15' + +describe Year2015::Day15 do + context 'when Part 1' do + subject(:sample_one) do + described_class.new(File.read('spec/year_2015/day_15_sample_one'), true) + end + + it 'gives a final result' do + expect(sample_one.to_i).to eq(62842880) + end + end + + context 'when Part 2' do + subject(:sample_one) do + described_class.new(File.read('spec/year_2015/day_15_sample_one')) + end + + it 'gives a final result' do + expect(sample_one.to_i).to eq(57600000) + end + end + + context 'when Results' do + subject(:input_data) do + File.read('spec/year_2015/day_15_input') + end + + it 'correctly answers part 1' do + expect(described_class.new(input_data, true).to_i).to eq(222870) + end + + it 'correctly answers part 2' do + expect(described_class.new(input_data).to_i).to eq(117936) + end + end +end diff --git a/year_2015.md b/year_2015.md index e8b9b26..f5bea90 100644 --- a/year_2015.md +++ b/year_2015.md @@ -245,3 +245,20 @@ Year2015::Day14 ``` This day doesn't have complicated concepts. If you want to dig, you can think of each reindeer as a [finite-state machine](https://en.wikipedia.org/wiki/Finite-state_machine), which is [one of the concepts that are best solved thanks to Object-oriented programming](https://eev.ee/blog/2013/03/03/the-controller-pattern-is-awful-and-other-oo-heresy/). + +## Day 15 + +``` +Year2015::Day15 + when Part 1 + gives a final result + when Part 2 + gives a final result + when Results + correctly answers part 1 + correctly answers part 2 +``` + +Is there a day where you don't have to iterate through all possible solutions? + +I am (partly) joking. I am pretty sure there must be a way to solve n-sided equations, but quite frankly, there's a reason I've always thought of programming as "languages" and not "mathematics". Again, the best thing we can do to avoid an `O(n*n)` complexity is finding ways to avoid calculations we know will result in a 0 anywhere. diff --git a/year_2015/day_15.rb b/year_2015/day_15.rb new file mode 100644 index 0000000..7138685 --- /dev/null +++ b/year_2015/day_15.rb @@ -0,0 +1,105 @@ +class Year2015 + class Day15 + PROPERTIES = %i[capacity durability flavor texture].freeze + + class Ingredient + attr_reader :name, :capacity, :durability, :flavor, :texture, :calories + attr_accessor :forbidden + + INGREDIENT_RE = /^(\w+): capacity (-?\d+), durability (-?\d+), flavor (-?\d+), texture (-?\d+), calories (-?\d+)$/ + + def initialize(input_line) + input_line =~ INGREDIENT_RE + @name = $1 + @capacity = $2.to_i + @durability = $3.to_i + @flavor = $4.to_i + @texture = $5.to_i + @calories = $6.to_i + @forbidden = 100 + end + end + + def initialize(input_data, input_part_one = false) + @input_file = input_data + @version = input_part_one ? 1 : 2 + @ingredients = input_data.chomp.split("\n").map do |input_line| + Ingredient.new(input_line) + end + mark_forbidden_ranges + end + + def calculate_forbidden_range(positive, negative) + 1.upto(100).flat_map do |idx| + next idx if ((idx * negative) + ((100 - idx) * positive)).negative? + end.compact.min + end + + def mark_forbidden_ranges_for_property(prop, positive) + @ingredients.select{|ingredient| ingredient.send(prop).negative? }.each do |ingredient| + other_forbidden = calculate_forbidden_range(positive, ingredient.send(prop)) + ingredient.forbidden = [ingredient.forbidden, other_forbidden].min + end + end + + def mark_forbidden_ranges + PROPERTIES.each do |prop| + next unless @ingredients.map(&prop).any?(&:negative?) + + positive = @ingredients.map(&prop).select(&:positive?).sum + mark_forbidden_ranges_for_property(prop, positive) + end + end + + def calculate(quantities, symbol) + total = quantities.each_with_index.sum do |quantity, i| + quantity * @ingredients[i].send(symbol) + end + return 0 if total.negative? + + total + end + + def score(quantities) + PROPERTIES.map do |symbol| + calculate(quantities, symbol) + end.inject(&:*) + end + + def quantities_enumerator + Enumerator.new do |yielder| + quantities = Array.new(@ingredients.length, 1) + loop do + yielder << quantities + quantities = update_quantities(quantities, -1) + end + end + end + + def update_quantities(quantities, idx = -1) + raise StopIteration if quantities[idx].nil? + + quantities[idx] += 1 + quantities[idx] = 100 if @ingredients[idx].forbidden <= quantities[idx] + if quantities[idx] >= 100 + quantities[idx] = 1 + update_quantities(quantities, idx - 1) + end + return update_quantities(quantities, -1) unless quantities.sum == 100 + + quantities + end + + def to_i + @to_i ||= begin + @to_i = 0 + quantities_enumerator.each do |quantities| + next if @version == 2 && calculate(quantities, :calories) != 500 + + @to_i = [score(quantities), @to_i].max + end + @to_i + end + end + end +end