פתרון Advent Of Code 2025 יום 7

03/01/2026

אני רוצה להמשיך בסדרת הפתרונות של Advent Of Code ולדבר על יום 7, יום שלימד אותי שתכנות דינמי זה דווקא די כיף ולא מסובך. כמו אתמול גם פה החלק הראשון פשוט והחלק השני דורש את המאמץ.

1. האתגר

נתון קלט שמתאר סוג של מסלול מלמעלה למטה:

.......S.......
...............
.......^.......
...............
......^.^......
...............
.....^.^.^.....
...............
....^.^...^....
...............
...^.^...^.^...
...............
..^...^.....^..
...............
.^.^.^.^.^...^.
...............

במסלול יש לנו קרן שמתחילה מהאות S בשורה הראשונה ויורדת כלפי מטה. כל פעם שהיא פוגעת ב ^ היא מתפצלת ל-2 אחת בכל צד של ה ^ לדוגמה אחרי שתי שורות נקבל:

.......S.......
.......|.......
......|^|......
...............
......^.^......
...............
.....^.^.^.....
...............
....^.^...^....
...............
...^.^...^.^...
...............
..^...^.....^..
...............
.^.^.^.^.^...^.
...............

ובסוף נגיע ל:

.......S.......
.......|.......
......|^|......
......|.|......
.....|^|^|.....
.....|.|.|.....
....|^|^|^|....
....|.|.|.|....
...|^|^|||^|...
...|.|.|||.|...
..|^|^|||^|^|..
..|.|.|||.|.|..
.|^|||^||.||^|.
.|.|||.||.||.|.
|^|^|^|^|^|||^|
|.|.|.|.|.|||.|

המשימה היא לחשב כמה פעמים הקרן מתפצלת.

2. הפתרון

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

class String
  def find_all_indexes(ch)
    chars.each_with_index.filter {|c, i| c == ch }.map {|c, i| i }
  end
end

בגלל שזה רובי אני יכול להגדיר את הפונקציה בתור הרחבה ל Stirng ועכשיו אפשר לכתוב:

"abcabcabc".find_all_indexes("a")

כדי להפעיל את הפונקציה על המחרוזת שמשמאל לנקודה. הקוד עצמו בתוספת הערות של AI הוא:

  def part1
    beam_indexes = Set.new
    count = 0

    File.read(@filename).lines.each do |line|
      # Initialize beam positions when we encounter the start marker
      if beam_index = line =~ /S/
        puts "--- START ---"
        beam_indexes = Set.new([beam_index])
      end

      # Find all splitter positions in current line
      splitter_indexes = Set.new(line.find_all_indexes('^'))
      # When a beam hits a splitter, it splits into two beams (left and right)
      splitted_beams = Set.new((splitter_indexes & beam_indexes).flat_map {|i| [i-1, i+1] })
      # Count interactions with splitters
      count += (splitter_indexes & beam_indexes).size
      # Beams that don't hit splitters continue straight
      continuing_beams = beam_indexes - splitter_indexes
      # Update beam positions: continuing beams + newly split beams
      beam_indexes = continuing_beams + splitted_beams
    end

    count
  end

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

count += (splitter_indexes & beam_indexes).size

כששלחתי את הקוד לקלוד עבור Code Review הוא טען שפעולת חיבור של שתי קבוצות ברובי מחזירה מערך אבל בדיקה בקונסול מראה לי שזו טעות וחיבור שתי קבוצות מחזיר קבוצה:

3.3.5 :005 > (Set.new + Set.new).class
 => Set

הוא גם טען ששכחתי לעשות require ל set וזו גם טעות שלו. שני הדברים מלמדים אותי שקלוד כנראה חושב שאני בגרסה ישנה יותר של רובי. סך הכל הקוד עבד בסדר גמור והחזיר תוצאה נכונה.

3. הטוויסט - תכנות דינמי

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

.......S.......
.......|.......
......|^.......
......|........
.....|^.^......
.....|.........
....|^.^.^.....
....|..........
...|^.^...^....
...|...........
..|^.^...^.^...
..|............
.|^...^.....^..
.|.............
|^.^.^.^.^...^.
|..............

וזו דוגמה לעוד עולם אפשרי:

.......S.......
.......|.......
......|^.......
......|........
......^|^......
.......|.......
.....^|^.^.....
......|........
....^.^|..^....
.......|.......
...^.^.|.^.^...
.......|.......
..^...^|....^..
.......|.......
.^.^.^|^.^...^.
......|........

והשאלה היא כמה עולמות אפשריים כאלה קיימים.

אז ברור שאנחנו לא יכולים "לנסות" את כל העולמות כלומר לחשב ולשמור כל מסלול אפשרי. זה פשוט ייקח הרבה יותר מדי פעולות בגלל החישובים הכפולים. וברגע שחשבתם את המילה "חישובים כפולים" מיד אתם יודעים שנכנסתם לארץ התכנות הדינמי.

תכנות דינמי בסך הכל אומר שאנחנו שומרים תוצאות של חישובי ביניים כדי לא לחזור עליהם. בסיפור של הקרן אני רוצה לסמן Slitter מסוים ולהסתכל עליו שימו לב לכוכבית בציור הבא:

.......S.......
.......|.......
......|^.......
......|........
......^|^......
.......|.......
.....^|^.^.....
......|........
....^.*|..^....
.......|.......
...^.^.|.^.^...
.......|.......
..^...^|....^..
.......|.......
.^.^.^|^.^...^.
......|........

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

.......S.......
.......|.......
......|^.......
......|........
......^|^......
.......|.......
.....@|@.^.....
......|........
....^.*|..^....
.......|.......
...^.^.|.^.^...
.......|.......
..^...^|....^..
.......|.......
.^.^.^|^.^...^.
......|........

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

מה עושים במקום? תכנות דינמי אומר שכדאי לנו לשמור בכל משבצת כמה אפשרויות יש להגיע אליה. בכל שורה נוכל להסתכל על השורה הקודמת כדי לעדכן את המונה: אם מעלינו היה Splitter הערך החדש הוא 0, אחרת סוכמים את ערך המשבצת שמעלינו ואת האלכסונים שלמעלה אם הם החזיקו Splitter. בסוף נוכל לסכום את כל האפשרויות של כל המשבצות בשורה האחרונה וכך נקבל את מספר העולמות.

הנה זה ברובי לא מסובך בכלל:

  def part2
    beams_through = []

    File.read(@filename).lines.each do |line|
      if beam_index = line =~ /S/
        puts "--- START ---"
        beams_through = line.chars.map {|c| c == "S" ? 1 : 0 }
      end

      splitter_indexes = Set.new(line.find_all_indexes('^'))
      beams_through = beams_through.each_with_index.map do |previous_count, index|
        if splitter_indexes.include?(index)
          0
        else
          value = beams_through[index]
          value += beams_through[index - 1] if splitter_indexes.include?(index - 1)
          value += beams_through[index + 1] if splitter_indexes.include?(index + 1)

          value
        end
      end

      pp beams_through
    end
    beams_through.sum
  end