Skip to content

Commit

Permalink
Year 2015: Day 15
Browse files Browse the repository at this point in the history
  • Loading branch information
joshleaves committed Feb 12, 2024
1 parent 36bf5b2 commit b46d3e2
Show file tree
Hide file tree
Showing 6 changed files with 166 additions and 1 deletion.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions spec/year_2015/day_15_input
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions spec/year_2015/day_15_sample_one
Original file line number Diff line number Diff line change
@@ -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
37 changes: 37 additions & 0 deletions spec/year_2015/day_15_spec.rb
Original file line number Diff line number Diff line change
@@ -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
17 changes: 17 additions & 0 deletions year_2015.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
105 changes: 105 additions & 0 deletions year_2015/day_15.rb
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit b46d3e2

Please sign in to comment.