2024

Part 1 was straightforward. For part 2 I took what seems to be a long-winded approach of trying to identify which levels caused unsafe differences. Then I tried reports with those levels removed and tested if they were safe. This included checking for a majority direction of differences and replacing the differences in the minority, but on my first attempt I forgot to deal with no majority!

I’ve seen from others that it’s simpler to try removing each and every level, which is plenty feasible with the input. I preferred to use this simpler logic in my Python implementation as working with its lists and numpy arrays are new to me.

— R Day 2 Part 1 —

#' Find how many reports are safe
#'
#' Check each report is
#'   either all decreasing OR all increasing
#'   AND all absolute differences are within 1 to 3
#'
#' @param input
#' A character vector of strings containing space separated digits, where
#' each string is a report and each digit is a 'level'
#'
#' @return
#' numeric(1) The number of safe reports.
solve_day2_part1 <- function(input) {
  num_list <- strsplit(input, " ")
  num_list <- lapply(num_list, as.numeric)

  diff_list <- lapply(num_list, diff)

  sum(vapply(diff_list, is_safe, logical(1)))
}

#' Check if the differences are safe
#'
#' Check the differences for
#'   either all less than zero OR all greater than zero
#'   AND all absolute differences are within 1 to 3
#'
#' @param diffs
#' Numeric differences between levels in a report
#'
#' @return
#' logical(1) whether the report differences are safe
is_safe <- function(diffs) {
  one_direction <- all(diffs < 0) | all(diffs > 0)
  safe_size <- all(abs(diffs) %in% c(1, 2, 3))

  one_direction & safe_size
}
Run
aoc_source(day = 2, part = 1)

input = aoc_read(day = 2)

aoc_run(solve_day2_part1(input))
Elapsed: 0.015 seconds
Memory:  58 KB

— R Day 2 Part 2 —

#' Find how many reports are safe with dampeners
#'
#' Check each report is
#'   either all decreasing OR all increasing
#'   AND all absolute differences are within 1 to 3
#'
#' As well as whether the same can be achieved by removing one of the levels.
#'
#' @param input
#' A character vector, where each represents  a report, containing space
#' delimited sequence of digits, 'levels'
#'
#' @return
#' numeric(1) The number of safe reports.
solve_day2_part2 <- function(input) {
  num_list <- strsplit(input, " ")
  num_list <- lapply(num_list, as.numeric)

  safe_list <- vapply(num_list, is_safe, logical(1))

  sum(safe_list)
}

#' Check if a report is safe
#'
#' Find the majority direction in differences (increasing/decreasing) if there
#' is one. Mark the minority direction as unsafe diffs. Mark absolute diffs
#' that aren't in 1, 2, 3 as unsafe. For each unsafe diff check if the report
#' could be safe with new reports made by removing the levels either side of
#' it.
#'
#' If there is no majority direction then the report is unsafe because the
#' dampener won't be enough. (I suppose this assumes report length is not 3,
#' which could be made safe)
#'
#' @param report
#' Numeric levels to be checked
#'
#' @return
#' logical(1) whether the report is safe
is_safe <- function(report, removed = 0) {
  diffs <- diff(report)
  majority_diffs <- length(diffs) / 2 + 1

  # no majority direction
  # fails all increasing OR all decreasing rule
  # dampener won't be enough
  if (max(sum(diffs < 0), sum(diffs > 0)) < majority_diffs) {
    return(FALSE)
  }

  # logical to store location of unsafe diffs
  unsafe_diffs <- logical(length(diffs))

  # if majority decreasing, unsafe where increasing
  if (sum(diffs < 0) >= 3) {
    unsafe_diffs <- diffs > 0
  }

  # if majority increasing, unsafe where decreasing
  if (sum(diffs > 0) >= 3) {
    unsafe_diffs <- unsafe_diffs | diffs < 0
  }

  # unsafe where difference not in safe size range
  unsafe_diffs <- unsafe_diffs | !abs(diffs) %in% c(1, 2, 3)

  if (!any(unsafe_diffs)) {
    # no unsafe levels
    TRUE
  } else {
    # create new reports by removing each level either side of the unsafe diffs
    unsafe_levels <- which(unsafe_diffs)
    unsafe_levels <- c(unsafe_levels, unsafe_levels + 1)
    new_reports <- lapply(unsafe_levels, \(level) report[-level])

    # check if the shortened report is safe without further option
    # to remove levels
    any(vapply(new_reports, is_safe_no_dampener, logical(1)))
  }
}

#' Check if the rest of a report is safe
#'
#' Check the differences for
#'   either all less than zero OR all greater than zero
#'   AND all absolute differences are within 1 to 3
#'
#' @param report
#' Numeric levels to be checked
#'
#' @return
#' logical(1) whether the report is safe
is_safe_no_dampener <- function(report) {
  diffs <- diff(report)

  one_direction <- all(diffs < 0) | all(diffs > 0)
  safe_size <- all(abs(diffs) %in% c(1, 2, 3))

  one_direction & safe_size
}
Run
aoc_source(day = 2, part = 2)

input = aoc_read(day = 2)

aoc_run(solve_day2_part2(input))
Elapsed: 0.049 seconds
Memory:  193 KB

— Python Day 2 Part 1 —

import numpy as np

def solve_day2_part1(text):
  """
  Find how many reports are safe
  
  Check each report is
     either all decreasing OR all increasing
     AND all absolute differences are within 1 to 3
  
  Parameters
  ----------
  text : list of str
      Where each string is a report and each digit is a 'level'

  Returns
  -------
  int
    The number of safe reports
  
  """
  reports_list = [line.split() for line in text]
  reports_list = [np.array(report, dtype = np.int32) for report in reports_list]
    
  diffs_list = [np.diff(report) for report in reports_list]
  
  return(sum([is_safe(diffs) for diffs in diffs_list]))
  
def is_safe(diffs):
  """
  Check if the differences are safe
  
  Check the differences for
     either all decreasing OR all increasing
     AND all absolute differences are within 1 to 3
  
  Parameters
  ----------
  diffs : numpy array of int
      Where each int is a difference between consecutive levels in a report

  Returns
  -------
  bool
      Whether the differences are safe
  """
  one_direction = all(diffs < 0) or all(diffs > 0)
  safe_size = all(abs(diffs) <= 3)
  
  return(one_direction and safe_size)
Run
aoc_source(day = 2, part = 1)

input = aoc_read(day = 2)

result = aoc_run("solve_day2_part1(input)")
Elapsed: 0.037 seconds
Memory:  610 KB

— Python Day 2 Part 2 —

import numpy as np

def solve_day2_part2(text):
  """
  Find how many reports are safe
  
  Check each report is
     either all decreasing OR all increasing
     AND all absolute differences are within 1 to 3
  
    Parameters
  ----------
  text : list of str
      Where each string is a report and each digit is a 'level'

  Returns
  -------
  int
    The number of safe reports
  
  """
  reports_list = [line.split() for line in text]
  reports_list = [np.array(report, dtype = np.int32) for report in reports_list]
    
  
  return(sum([is_safe(report) for report in reports_list]))

def is_safe(report):
  """
  Check if a report is safe with a dampener
  
  If a report is not ordinarily safe, try removing each single level to
  then check if is safe.
  
  Parameters
  ----------
  report : numpy array of int
      Where each int is a level in the report

  Returns
  -------
  bool
      Whether the differences are safe
  """
  if (is_safe_no_dampener(report)):
    return(True)
  
  for i in range(len(report)):
    new_report = np.delete(report, i)
    
    if (is_safe_no_dampener(new_report)):
      return(True)
    
  return(False)
    

def is_safe_no_dampener(report):
  """
  Check if a report is safe
  
  Check the report is safe by finding the differences and confirming:
     either all decreasing OR all increasing
     AND all absolute differences are within 1 to 3
  
  Parameters
  ----------
  report : numpy array of int
      Where each int is a level in the report

  Returns
  -------
  bool
      Whether the differences are safe
  """
  diffs = np.diff(report)
  
  one_direction = all(diffs < 0) or all(diffs > 0)
  safe_size = all(abs(diffs) <= 3)
  
  return(one_direction and safe_size)
Run
aoc_source(day = 2, part = 2)

input = aoc_read(day = 2)

result = aoc_run("solve_day2_part2(input)")
Elapsed: 0.352 seconds
Memory:  610 KB
Back to top