• בלוג
  • פיתרון Advent Of Code 2024 יום 7 - לא להיבהל, אני יודע לשנות את Integer

פיתרון Advent Of Code 2024 יום 7 - לא להיבהל, אני יודע לשנות את Integer

06/04/2025

יום 7 של Advent Of Code לא היה מסובך והזכיר לי כמה כיף שאפשר ברובי לשנות את המחלקות המובנות בשפה. בואו נראה מה היה שם.

תוכן עניינים

  1. האתגר
  2. פיתרון

1. האתגר

נתון קלט שמורכב מתוצאה ואופרנדים:

190: 10 19
3267: 81 40 27
83: 17 5
156: 15 6
7290: 6 8 6 15
161011: 16 10 13
192: 17 8 14
21037: 9 7 18 13
292: 11 6 16 20

אנחנו צריכים למצוא אם אפשר להוסיף את האופרטורים פלוס וכפול בין האופרנדים כדי להגיע לתוצאה. בחלק השני של האתגר מוסיפים לנו אופרטור בשם || שמשמעותו "שרשור" שני אופרנדים כך ש:

10 || 19 = 1019

ורוצים לראות אם אפשר להגיע לתוצאה עם האופרטור החדש.

2. פיתרון

החלק המרכזי של הפיתרון לקח בסך הכל מספר שורות ברובי:

def count_options(target, values)
  results = [:+, :*].repeated_permutation(values.size - 1).to_a.map do |seq|
    values[1..].zip(seq).reduce(values[0]) { |a, (v, op)| a.send(op, v) } == target
  end
  results.count(true)
end

הפונקציה repeated_permutation מופעלת על מערך ומחזירה את כל הפרמוטציות עם חזרות על האיברים שלו. הנה דוגמה:

3.3.5 :003 > [1, 2].repeated_permutation(3).to_a
 => [[1, 1, 1], [1, 1, 2], [1, 2, 1], [1, 2, 2], [2, 1, 1], [2, 1, 2], [2, 2, 1], [2, 2, 2]]

מה עושים? לוקחים את כל הפרמוטציות של האופרטורים ושותלים אותן ברשימת האופרנדים עם zip. אחרי זה מפעילים reduce כדי לרוץ על הרשימה ולבצע את הפעולות שהגרלנו ובסוף בודקים אם הגענו לתוצאה.

בשביל לקרוא את הקלט היה צריך רק עוד ביטוי רגולארי:

puts (File.readlines('input.txt').map do |line|
  target, values = line.match(/(\d+): (.*)/).captures
  count_options(target.to_i, values.split.map(&:to_i)).positive? ? target.to_i : 0
end.sum)

ומה עם החלק השני? מאוד פשוט, צריך רק להוסיף מתודה ל Integer:

class Integer
  def op_concat(other)
    (to_s + other.to_s).to_i
  end
end

ואז אפשר להישאר עם אותה פונקציה קצרה ורק לשנות את מערך האופרטורים:

def count_options(target, values)
  results = [:+, :*, :op_concat].repeated_permutation(values.size - 1).to_a.map do |seq|
    values[1..].zip(seq).reduce(values[0]) { |a, (v, op)| a.send(op, v) } == target
  end
  results.count(true)
end

סך הכל הפיתרון המלא ברובי עם פחות מ 20 שורות היה:

class Integer
  def op_concat(other)
    (to_s + other.to_s).to_i
  end
end

def count_options(target, values)
  results = [:+, :*, :op_concat].repeated_permutation(values.size - 1).to_a.map do |seq|
    values[1..].zip(seq).reduce(values[0]) { |a, (v, op)| a.send(op, v) } == target
  end
  results.count(true)
end

puts (File.readlines('input.txt').map do |line|
  target, values = line.match(/(\d+): (.*)/).captures
  count_options(target.to_i, values.split.map(&:to_i)).positive? ? target.to_i : 0
end.sum)

ואחרי שנתתי לקלוד לשפר קיבלתי את זה:

class Integer
  # Method to concatenate two numbers as strings and convert back to integer
  def op_concat(other)
    (to_s + other.to_s).to_i
  end
end

def count_options(target, values)
  # Early return if only one value
  return (values[0] == target ? 1 : 0) if values.size == 1

  # Define available operations
  operations = [:+, :*, :op_concat]

  # Count combinations that equal the target
  operations.repeated_permutation(values.size - 1).count do |ops|
    result = values[0]
    values[1..].zip(ops).each do |value, op|
      result = result.send(op, value)
    end
    result == target
  end
end

def solve_file(filename)
  # Read the file and process each line
  File.readlines(filename).sum do |line|
    if line.strip.empty?
      0  # Handle empty lines
    else
      # Parse the target and values
      match = line.match(/(\d+):(.*)/)
      if match
        target = match[1].to_i
        values = match[2].strip.split.map(&:to_i)

        # If there's at least one way to reach the target, count that target
        count_options(target, values).positive? ? target : 0
      else
        0  # Handle invalid lines
      end
    end
  end
end

# Execute the solution if run as a script
if __FILE__ == $PROGRAM_NAME
  puts solve_file('input.txt')
end

שיפור? אני לא בטוח.