פיתרון Advent Of Code 2024 יום 2 ב Ruby
כן אני אוהב לפתור חידות תכנות והחידות של Advent Of Code, מצליחות הרבה פעמים להפתיע, למרות המלל הרב. בפוסט זה נראה פיתרון לא מלהיב מדי של יום 2 מהסט האחרון ברובי ואנסה לדמיין איך היה נראה פיתרון יעיל יותר.
1. האתגר
נתון קלט שבנוי מרשימות של מספרים:
7 6 4 2 1
1 2 7 8 9
9 7 6 2 1
1 3 2 4 5
8 6 4 4 1
1 3 6 7 9
והאתגר מחולק לשני חלקים. בחלק הראשון צריך למצוא כמה מהרשימות האלה מתאימות לסט התנאים הבא:
או שהרשימה רק עולה או רק יורדת.
קצב העלייה או הירידה לא גבוה מ-3.
בחלק השני עלינו לדמיין שאפשר למחוק מספר אחד מהרשימה ולראות אם עכשיו הרשימה תתאים לתנאי.
2. פיתרון חלק 1
החלק הראשון היה די פשוט והמפתח לפיתרון הוא השורה הבאה:
line.split.map(&:to_i).each_cons(2).map { _2 - _1 }
אני לוקח שורה, חותך אותה למספרים, הופך כל מספר ל integer ואז רץ על כל הזוגות ברשימה ומחשב את ההפרש ביניהם. סך הכל אם הרשימה התחילה כך:
line = '7 6 4 2 1'
אז שורת הקריאה תחזיר לי:
3.3.5 :021 > line.split.map(&:to_i).each_cons(2).map { _2 - _1 }
=> [-1, -2, -2, -1]
אחרי שיש את זה קל לרוץ על ההפרשים ולבדוק שהכל מתאים לתנאים, וסך הכל הפיתרון:
filename = ARGV[0]
parsed_input = File.foreach(filename).map do |line|
line.split.map(&:to_i).each_cons(2).map { _2 - _1 }
end
safe = parsed_input.filter do |seq|
(seq.all?(&:positive?) || seq.all?(&:negative?)) && (seq.all? { it.abs <= 3 })
end
pp safe.count
3. פיתרון חלק 2
בחלק השני היה צריך לזהות אם אפשר למחוק איבר אחד מהרשימה ולקבל רשימה תקינה. הדרך הקלה לגשת לזה היתה פשוט לנסות למחוק איברים ולראות אם יש מישהו שבלעדיו הרשימה תקינה. זה הקוד:
filename = ARGV[0]
def check_seq(seq)
diff_seq = seq.each_cons(2).map { _2 - _1 }
(diff_seq.all?(&:positive?) || diff_seq.all?(&:negative?)) && (diff_seq.all? { it.abs <= 3 })
end
parsed_input = File.foreach(filename).map do |line|
line.split.map(&:to_i)
end
safe = parsed_input.filter do |seq|
check_seq(seq) ||
seq.each_index.any? do |i|
check_seq(seq.reject.with_index { _2 == i })
end
end
pp safe.count
בשביל למחוק אינדקס מרשימה ברובי השתמשתי בשורה:
[2, 3, 5].reject.with_index { _2 == 1 }
וכן זה עובד אבל אני מודה שלא שימח אותי במיוחד בגלל שהוא עושה הרבה עבודה כפולה. אם נדמיין את הרשימה הבאה:
line = '2 3 4 5 6 7 8 9 10 11 12 100'
אז ברור שתחילת הרשימה תקינה והמספר 100 בסוף הוא הבעייתי. אין טעם לנסות למחוק את כל האינדקסים בהתחלה כי אנחנו כבר יודעים שגם איתם הכל בסדר, ואפשר מראש למחוק רק את האינדקס הראשון שמכיל הפרה. אני חושב שזה יהיה שיפור טוב לתוכנית אבל מודה שלא הספקתי לכתוב אותו כי הוא קצת יאריך את הקוד.
4. פיתרון של Chat GPT
המטרה של Advent Of Code אף פעם לא היתה לפתור את התרגיל בשביל שיהיה לך את קוד הפיתרון (אנשים תמיד פרסמו את התשובות שלהם ברשת ואין בעיה להעתיק), אלא בשביל האתגר האישי והחוויה הלימודית. מסיבה זו אני לא חושב שזה משנה ש Chat GPT יכול לפתור את התרגילים בקלות. ובכל זאת יש משהו קסום ביכולת להדביק דף שאלה ל Chat GPT ולקבל קוד שעובד. זה הקוד שקיבלתי ממנו:
#!/usr/bin/env ruby
# --- Helpers ---
# Returns true if the array of levels is strictly increasing or strictly decreasing
# with each adjacent difference in the range [1..3].
def is_safe?(levels)
return true if levels.size < 2 # Trivially safe if 0 or 1 level
# We check both possibilities (strictly increasing or strictly decreasing).
is_increasing = true
is_decreasing = true
(0...levels.size-1).each do |i|
diff = levels[i+1] - levels[i]
# Must be in [-3..-1] or [1..3]; if it's 0 or outside ±3 => not safe
return false if diff.abs < 1 || diff.abs > 3
# If it's not strictly positive, can't be strictly increasing
is_increasing = false if diff <= 0
# If it's not strictly negative, can't be strictly decreasing
is_decreasing = false if diff >= 0
end
# Safe if either strictly increasing or strictly decreasing
is_increasing || is_decreasing
end
# Returns true if the array of levels is safe as-is OR
# can become safe by removing exactly one level.
def is_safe_with_dampener?(levels)
# If already safe, no need to remove anything.
return true if is_safe?(levels)
# Otherwise, try removing each level in turn.
(0...levels.size).each do |idx|
new_levels = levels[0...idx] + levels[idx+1..-1]
return true if is_safe?(new_levels)
end
false
end
# --- Read the input ---
# Typically you'd read from a file or from standard input.
# For Advent of Code, you might do something like:
# lines = File.readlines("input.txt").map(&:strip)
# Here we'll read from standard input (ARGF) to keep it general.
lines = ARGF.each_line.map(&:strip)
# Convert each line of space-separated numbers into an integer array.
reports = lines.map { |line| line.split.map(&:to_i) }
# --- Part 1: Count how many reports are safe under the original rules ---
part1_safe_count = reports.count { |levels| is_safe?(levels) }
# --- Part 2: Count how many reports are safe with the "Problem Dampener" ---
part2_safe_count = reports.count { |levels| is_safe_with_dampener?(levels) }
# Output the answers
puts "Part 1 (original rules) safe reports: #{part1_safe_count}"
puts "Part 2 (with Problem Dampener) safe reports: #{part2_safe_count}"
מבחינת הקוד אין פה הברקות. אישית אני לא משתמש ב return בתוך בלוקים שעוברים לפונקציות, וגם אני לא חושב שיש צורך בכל כך הרבה הערות, אבל עדיין צריך להוריד את הכובע. העובדה שאפשר להמשיך את השיחה כדי לגלות בעיות ולשפר את הקוד יכולה לשפר משמעותית גם את חווית הלימוד.