[–] [S] 2 points 2 months ago* (last edited 2 months ago)

Python

Made a single cleaned-up solver for all 3 parts after solving them separately. Intersection logic can be optimized, but the inputs are small enough to not need it.

click to view code

# simple type to represent an arc
Arc = tuple[int, int]

# single solver for all 3 parts
class Solver:
    def __init__(self, forward_skips_visited = False, forbid_crossings = False):
        # a forward jump landing on a visited point slides right to the first free point (part 2)
        self.forward_skips_visited = forward_skips_visited
        # arcs may not cross, and a jump with no valid landing is skipped entirely (part 3)
        self.forbid_crossings = forbid_crossings

        # current position on the line
        self.pos = 0
        # set of visited points on the line
        self.visited = {0}
        # which side the arc will be drawn on next (down = 0, up = 1)
        self.side = 0
        # arc segments on each side of the line
        self.arcs: dict[int, list[Arc]] = { 0: [], 1: [] }

    # get the first arc / segment that crosses the given range
    def _get_crossing(self, start: int, end: int) -> Arc | None:
        low, high = min(start, end), max(start, end)
        for a, b in self.arcs[self.side]:
            if a < low < b < high or low < a < high < b:
                return a, b
        return None

    # try moving backwards by the given length, return None if not possible
    def _move_backward(self, length: int) -> int | None:
        next_pos = self.pos - length
        if next_pos < 0 or next_pos in self.visited:
            return None
        if self.forbid_crossings and self._get_crossing(next_pos, self.pos) is not None:
            return None
        return next_pos

    # try moving forwards by the given length, return None if not possible
    def _move_forward(self, length: int) -> int | None:
        next_pos = self.pos + length
        while True:
            # keep moving forward until an unvisited point is found
            if self.forward_skips_visited and next_pos in self.visited:
                next_pos += 1
                continue

            if not self.forbid_crossings:
                return next_pos

            crossing = self._get_crossing(self.pos, next_pos)
            if crossing is None:
                return next_pos

            # if the crossing arc ends before the next position, it becomes impossible to jump without crossing it
            _, end = crossing
            if end < next_pos:
                return None
            
            # clear the arc
            next_pos = end + 1

    # perform a jump of the given length according to the active rules
    def jump(self, length: int):
        next_pos = self._move_backward(length)
        if next_pos is None:
            next_pos = self._move_forward(length)
        if next_pos is None:
            return

        # update solver state
        self.arcs[self.side].append((min(self.pos, next_pos), max(self.pos, next_pos)))
        self.visited.add(next_pos)
        self.side = 1 - self.side
        self.pos = next_pos

def sum_final_positions(data: str, forward_skips_visited = False, forbid_crossings = False):
    total = 0
    for line in data.splitlines():
        solver = Solver(forward_skips_visited, forbid_crossings)
        for length in map(int, line.split(',')):
            solver.jump(length)
        total += solver.pos
    return total

def part1(data: str):
    """
    Rules:
    - For each jump, first try to move backwards by its specified length.
    - If the destination is negative or has been visited before, move forwards by the same distance.
    """
    return sum_final_positions(data)

def part2(data: str):
    """
    Additional rules:
    - Whenever a forward jump would land on a previously visited point,
        increase the destination by one until you reach the first unvisited point
    """
    return sum_final_positions(data, forward_skips_visited=True)

def part3(data: str):
    """
    Additional rules:
    - If a backward jump would cause a crossing, try moving forwards instead
    - If a forward jump would cause a crossing, keep increasing its length by one until there is no crossing
    - If no valid forward jump exists, skip that jump entirely and continue with the next jump length in the sequence
    """
    return sum_final_positions(data, forward_skips_visited=True, forbid_crossings=True)

  • source
  • [–] 2 points 2 months ago

    Python

    The DP solution to go column by column and compute the min flaps for each cell is correct but too slow. Fortunately this problem can be solved using MATH

    # Uses math to calculate the minimum number of flaps needed to reach the last passage at any height.
    # This solution relies on a couple of facts:
    #   1. The number of flaps needed to reach a passage at (x, y) is ceil((x + y) / 2).
    #       Proof:
    #       - Each flap increases your height by 1 and each glide decreases it by 1
    #       - Suppose you flapped f times and covered a horizontal distance of x
    #       - So your final height would be y = f - (x - f) = 2f - x
    #       - Now suppose a passage at position p_x begins at height p_y. To enter it, your height must be, y >= p_y
    #       - Substituting y, 2f - x >= p_y => 2f - p_x >= p_y
    #       - Solving for f, f >= (p_x + p_y) / 2
    #       - Since f is an integer, f = ceil((p_x + p_y) / 2)
    #   2. For each wall, the lowest opening gives the smallest lower bound on the cumulative number of flaps.
    #      The maximum of these bounds is necessary because an earlier high wall may require more flaps than the last wall.
    #   3. For this input, that maximum lower bound is attainable through all the walls, so it is the optimal answer.
    #      This is not true in general: an opening's upper edge or the distance between walls can make the lowest opening unreachable.
    def flapsMath(data: str):
        flaps = 0
    
        passages = [[int(p) for p in passage.split(',')] for passage in data.splitlines()]
        passages.sort(key=lambda p: (p[0], p[1]))  # Sort passages so that we always have the lowest opening first
        last_seen_x = 0
    
        for x, y, _ in passages:
            if x != last_seen_x:
                last_seen_x = x
                # calculate the minimum number of flaps needed to cross this passage at its lowest opening
                # (x + y + 1) // 2 is equivalent to ceil((x + y) / 2) for integers
                flaps = max(flaps, (x + y + 1) // 2)
    
        return flaps
    
  • source
  • view more: ‹ prev next ›