[–] 2 points 11 months ago

C++

Dynamic programming and recursion - it's the AoC classic. Nothing tricky here, which is a relief after yesterday.

No sign of Dijkstra's yet, which is a surprise. Maybe it'll be tomorrow's puzzle? 25th is usually an open goal if you've done the rest of the puzzles, but I don't know if that applies to the new shorter format.

#include <boost/log/trivial.hpp>
#include <boost/unordered/unordered_flat_map.hpp>
#include <boost/unordered/unordered_flat_set.hpp>
#include <fstream>
#include <sstream>
#include <stdexcept>
#include <vector>

namespace {

using Map =
    boost::unordered::unordered_flat_map<std::string, std::vector<std::string>>;

auto read(const std::string &name) {
  auto rval = Map{};
  auto ih = std::ifstream{name};
  if (!ih)
    throw std::runtime_error{"file?"};
  auto line = std::string{};
  while (std::getline(ih, line)) {
    auto ss = std::istringstream{line};
    auto tmp = std::string{};
    ss >> tmp;
    auto key = tmp.substr(0, tmp.size() - 1);
    auto value = std::vector<std::string>{};
    while (ss >> tmp)
      value.push_back(std::move(tmp));
    rval[key] = std::move(value);
  }
  return rval;
}

auto part1() {
  auto map = read("11.1.txt");
  auto current = std::vector<std::vector<std::string>>{};
  auto paths = boost::unordered::unordered_flat_set<std::vector<std::string>>{};

  current.push_back(std::vector<std::string>{"you"});

  while (!current.empty()) {
    auto next_up = decltype(current){};
    std::swap(current, next_up);
    for (const auto &next : next_up) {
      auto &outputs = map.at(next.back());
      for (auto &output : outputs) {
        if (output == "you")
          continue;
        auto token = next;
        token.push_back(output);
        if (output == "out") {
          paths.insert(std::move(token));
        } else {
          current.push_back(std::move(token));
        }
      }
    }
  }

  return paths.size();
}

struct Route {
  std::string current;
  bool has_fft, has_dac;
};
struct RouteHash {
  std::size_t operator()(const Route &p) const {
    std::size_t seed = 0;
    boost::hash_combine(seed, p.current);
    boost::hash_combine(seed, p.has_fft);
    boost::hash_combine(seed, p.has_dac);
    return seed;
  }
};
auto operator==(const Route &a, const Route &b) {
  return a.current == b.current && a.has_fft == b.has_fft &&
         a.has_dac == b.has_dac;
}
auto cache = boost::unordered::unordered_flat_map<Route, size_t, RouteHash>{};

auto count_target(Route start, const Map &map) -> size_t {
  auto outputs = map.at(start.current);
  if (start.current == "dac")
    start.has_dac = true;
  if (start.current == "fft")
    start.has_fft = true;

  auto rval = size_t{};
  for (auto &output : outputs) {
    if (auto f = cache.find({output, start.has_fft, start.has_dac});
        f != cache.end()) {
      rval += f->second;
      continue;
    }
    if (output == "out") {
      if (start.has_dac && start.has_fft) {
        ++rval;
      }
      continue;
    }
    rval += count_target({output, start.has_fft, start.has_dac}, map);
  }
  cache[start] = rval;
  return rval;
}

auto part2() {
  auto map = read("11.2.txt");
  return count_target({"svr", false, false}, map);
}

} // namespace

auto main() -> int {
  BOOST_LOG_TRIVIAL(info) << "Day 11";
  BOOST_LOG_TRIVIAL(info) << "1: " << part1();
  BOOST_LOG_TRIVIAL(info) << "2: " << part2();
}
  • source
  • [–] 1 point 11 months ago

    Uiua

    Part 1 only for now, probably not the most optimal solution (calculating all possible rectangles) but at least it's short ^^

    Run with example input

    ::spoiler Code

    $ 7,1
    $ 11,1
    $ 11,7
    $ 9,7
    $ 9,5
    $ 2,5
    $ 2,3
    $ 7,3
    # &fras "input-9.txt" β—Œ
    ⊜(βŠœβ‹•βŠΈβ‰ @,)βŠΈβ‰ @\n
    β§…<2
    ≑(/Γ—+₁-βŠƒβ†§β†₯°⊟)
    /β†₯
    

    :::

  • source
  • [–] 3 points 11 months ago*

    Python

    Part 1 can be done with simple dfs / backtracking, even without memoization.

    Part 2 needed to have dp / memoization to finish in a reasonable time. Initially, I also checked for cycles in the path, but I removed the check once I realized there were none. Also, instead of adding booleans / int to my memoization function, I chose to compute both path arrangements (svr -> fft -> dac -> out AND svr -> dac -> fft -> out) and add them up.

    click to view code

    from collections import defaultdict
    
    # build the adjacency graph from the input data
    def build_graph(data: str):
        rules_data = data.splitlines()
        graph = defaultdict(list)
        for rule_data in rules_data:
            from_node, to_nodes = rule_data.split(": ")
            graph[from_node].extend(to_nodes.split(' '))
        return graph
    
    # simply dfs every path from 'you' until we reach 'out'
    # this is what I initially used for part 1
    def count_paths_dfs(data: str, start = "you", end = "out"):
        graph = build_graph(data)
    
        paths_to_end = 0
        stack = [start]     # currently active paths
        while stack:
            node = stack.pop()
            if node == end:
                # we reached end, no need to check further
                paths_to_end += 1
                continue
            # every node connected to current node is a potential path
            stack.extend(graph[node])
        
        return paths_to_end
    
    # memoized version of counting paths from start to end
    def count_paths_memo(graph: defaultdict[list], start = "you", end = "out"):
        # I used to have some code to check for cycles, but thankfully it wasn't needed
    
        # its pretty much same logic as count_paths_dfs, except recursive and with memoization
        memo = {}
        def backtrack(curr: str):
            if curr in memo:
                return memo[curr]        
            if curr == end:
                ans = 1
            else:
                ans = sum(backtrack(conn) for conn in graph[curr])
            memo[curr] = ans
            return ans
        
        return backtrack(start)
    
    # Optimized part 1 using memoization (not necessary, but faster)
    def part1_with_memoiz(data: str, start = "you", end = "out"):
        graph = build_graph(data)
        return count_paths_memo(graph, start, end)
    
    # Part 2 requires counting paths from "svr" to "out", 
    #   including only those paths that go through both "fft" and "dac"
    # Instead of adding booleans / int to my memoization function, 
    #   I chose to compute all path arrangements and add them up
    def part2(data: str):
        graph = build_graph(data)
    
        # getting from start to one requirement
        svr_to_fft = count_paths_memo(graph, "svr", "fft")
        svr_to_dac = count_paths_memo(graph, "svr", "dac")
        # getting from one requirement to the other
        fft_to_dac = count_paths_memo(graph, "fft", "dac")
        dac_to_fft = count_paths_memo(graph, "dac", "fft")
        # the final push to out
        fft_to_out = count_paths_memo(graph, "fft", "out")
        dac_to_out = count_paths_memo(graph, "dac", "out")
    
        # total paths = (start -> fft -> dac -> out) + (start -> dac -> fft -> out)
        svr_to_out = (
            svr_to_dac * dac_to_fft * fft_to_out +
            svr_to_fft * fft_to_dac * dac_to_out
        )
        return svr_to_out
    
    sample_p1 = """aaa: you hhh
    you: bbb ccc
    bbb: ddd eee
    ccc: ddd eee fff
    ddd: ggg
    eee: out
    fff: out
    ggg: out
    hhh: ccc fff iii
    iii: out"""
    assert (t := count_paths_dfs(sample_p1)) == 5, f"Expected 5, got {t}"
    assert (t := part1_with_memoiz(sample_p1)) == 5, f"Expected 5, got {t}"
    
    sample_p2 = """svr: aaa bbb
    aaa: fft
    fft: ccc
    bbb: tty
    tty: ccc
    ccc: ddd eee
    ddd: hub
    hub: fff
    eee: dac
    dac: fff
    fff: ggg hhh
    ggg: out
    hhh: out"""
    assert (t := part2(sample_p2)) == 2, f"Expected 2, got {t}"
    

  • source
  • [–] 2 points 11 months ago* (last edited 11 months ago) (2 children)

    Haskell

    Oh, this one was easy (dynamic programming at last!). Still haven't figured out the right way to approach yesterday's part two, though.

    import Data.List  
    import Data.Map (Map)  
    import Data.Map qualified as Map  
    
    readInput =  
      Map.fromList  
        . map ((\(name : outs) -> (init name, outs)) . words)  
        . lines  
    
    part1 input = go "you"  
      where  
        go "out" = 1  
        go name = maybe 0 (sum . map go) $ input Map.!? name  
    
    part2 input = let (both, _, _, _) = pathsFrom "svr" in both  
      where  
        pathsFrom =  
          (Map.!)  
            . Map.insert "out" (0, 0, 0, 1)  
            . Map.fromList  
            . (zip <*> map findPaths)  
            $ Map.keys input ++ concat (Map.elems input)  
        findPaths n =  
          let (both, dac, fft, none) =  
                unzip4 $ maybe [] (map pathsFrom) (input Map.!? n)  
           in case n of  
                "dac" -> (sum both + sum fft, sum dac + sum none, 0, 0)  
                "fft" -> (sum both + sum dac, 0, sum fft + sum none, 0)  
                _ -> (sum both, sum dac, sum fft, sum none)  
    
    main = do  
      input <- readInput <$> readFile "input11"  
      print $ part1 input  
      print $ part2 input  
    
  • source
  • [–] 3 points 11 months ago* (last edited 11 months ago)

    In this case, I formulated both questions as a linear algebra question. The first one is over the finite field F2, which I solved by using the library galois and some manual row reduction. The second was over positive integers which is not a field, so I solved it over Q using sympy and then looked for positive integer minimal solutions. In some cases these are under determined, and in some cases exactly solvable systems of the form Ax = b for x where A is the vector made from button switch vectors, b is the light switch pattern vector in the first case or jolts in the second. For under determined ones, your solutions are of the form particular + linear combinations of null space (mod 2 in the first case) so you only search for the minimal one there and in the second you have to search both minimal and positive integer one (because your solution will be over Q and not Z+) in the second case. Wonders of vectorization makes a quick work of these last parts (0.2 second in the first problem about 20s in the second). Also nullspace seems to generally have less than or equal to two dimensions so search space is much smaller than using all the button press vectors.

    import itertools as it
    from pathlib import Path
    
    import numpy as np
    import galois
    from sympy import Matrix, symbols, linsolve
    
    cwd = Path(__file__).parent
    GF2 = galois.GF(2)
    
    def convert_line(line):
    
      target = line.split('] ')[0][1:]
      vectors = line.split('] ')[1].split(' ')[:-1]
      jolts = line.split('] ')[1].split(' ')[-1].strip()
    
      ndims = len(target)
    
      target = np.array([0 if l=='.' else 1 for l in target], dtype=int)
      jolts = np.array(list(map(int,jolts[1:-1].split(','))))
    
      M = []
    
      for v in vectors:
        coords = [int(x) for x in v if x.isnumeric()]
        vec = np.zeros(ndims, dtype=int)
        vec[coords] = 1
        M.append(vec)
    
      return np.array(M).T,target,jolts
    
    
    def parse_input(file_path):
      with file_path.open("r") as fp:
        manual = list(map(convert_line, fp.readlines()))
    
      return manual
    
    
    def find_pivots(R):
        pivots = []
        m, n = R.shape
        row = 0
    
        for col in range(n):
          if row < m and R[row, col] == 1:
            pivots.append(col)
            row += 1
    
        return pivots
    
    
    def solve_GF2(A, x):
    
      nullspace = A.null_space()
    
      M = GF2(np.hstack([np.array(A), np.array(x)[:,None]]))
      R = M.row_reduce()
    
      pivots = find_pivots(R)
    
      m, n = R.shape
      n -= 1
    
      particular = GF2.Zeros(n)
    
      for r, c in enumerate(pivots):
          particular[c] = R[r, n]
    
      return np.array(particular), nullspace
    
    
    def solve_Q(M, x):
      b = symbols(" ".join([f"b{i}" for i in range(M.shape[1])]))
      solution = list(linsolve((M, x), b))[0]
      syms = list(solution.free_symbols)
      func = Matrix(solution)
    
      particular = np.array(func.subs({s:0 for s in syms}).tolist()).flatten().astype(float)
      nullspace = np.array([np.array(x.tolist()).flatten() for x in M.nullspace()]).astype(float)
    
      return particular, nullspace
    
    
    def minimize(nullspace, particular, jolt):
    
      nvecs = nullspace.shape[0]
    
      if not jolt:
        coef = np.array(list(it.product(np.arange(0, 2), repeat=nvecs)))
        A = np.sum(np.mod(coef@np.array(nullspace) + particular[None,:],2),axis=-1)
        I = np.argmin(A)
        res = np.mod(coef[I]@np.array(nullspace) + particular[None,:],2)
    
        return np.sum(res)
      else:
        N = 100
        I = []
    
        while len(I)==0: # look for a positive integer solution, if does not exist increase N
    
          coef = np.array(list(it.product(np.arange(-N, N), repeat=nvecs)))
          A = coef@np.array(nullspace) + particular[None,:]
          mask = (A >= 0) & np.isclose(A, A.astype(int))
          I = np.where(mask.all(axis=1))[0]
          N += 500
    
        return np.min(np.sum(A[I,:],axis=-1))
    
    
    def solve_problem(file_name, jolt=False):
    
      manual = parse_input(Path(cwd, file_name))
      sum_press = 0
    
      for ind,(M, light_target, jolt_target) in enumerate(manual):
    
        if not jolt:
          #part1 solve over GF2, looks for minimal solution of the form particular + null
          M = GF2(M)
          target = GF2(light_target)
          particular, nullspace = solve_GF2(M, target)
    
        else:
          #part2 solve over Q, look for minimal integer, positive solution of the form particular + null
          target = Matrix(jolt_target.astype(int))
          M = Matrix(M.astype(int))
          particular, nullspace = solve_Q(M, target)
    
        sum_press += minimize(nullspace, particular, jolt)
    
      return sum_press
    
    
    if __name__ == "__main__":
    
      assert solve_problem("test_input") == 7
      assert solve_problem("input") == 475
      assert solve_problem("test_input", True) == 33
      assert solve_problem("input", True) == 18273
    
  • source
  • [–] 4 points 11 months ago

    C++

    BFS for part 1, "Z3 of shame" for part 2. On the plus side, it's not that goddamned hailstones question from 2023 day 24 again. On the minus side, if I wanted to be reminded how bad I am at maths, then I could head on over to Project Euler and struggle with the questions there.

    Z3 is incredibly fast. Well done, Microsoft research.

    #include <boost/log/trivial.hpp>
    #include <boost/unordered/unordered_flat_map.hpp>
    #include <boost/unordered/unordered_flat_set.hpp>
    #include <fstream>
    #include <sstream>
    #include <stdexcept>
    #include <vector>
    #include <z3++.h>
    
    namespace {
    
    using Button = boost::unordered::unordered_flat_set<size_t>;
    using Joltage = std::vector<int>;
    
    struct Machine {
      std::string pattern;
      std::vector<Button> buttons;
      Joltage joltage;
    };
    
    using Machines = std::vector<Machine>;
    using PatternCount = boost::unordered::unordered_flat_map<std::string, size_t>;
    
    auto parse_button(const std::string &in) {
      auto rval = Button{};
      auto start = size_t{};
      while (start < in.size()) {
        auto comma = in.find(',', start);
        if (comma == std::string::npos)
          comma = in.size();
        rval.insert(std::stoull(in.substr(start, comma - start)));
        start = comma + 1;
      }
      return rval;
    }
    
    auto parse_joltage(const std::string &in) {
      auto rval = Joltage{};
      auto start = size_t{};
      while (start < in.size()) {
        auto comma = in.find(',', start);
        if (comma == std::string::npos)
          comma = in.size();
        rval.push_back(std::stoi(in.substr(start, comma - start)));
        start = comma + 1;
      }
      return rval;
    }
    
    auto read() {
      auto rval = Machines{};
      auto ih = std::ifstream{"10.txt"};
      if (!ih)
        throw std::runtime_error{"file?"};
      auto line = std::string{};
      while (std::getline(ih, line)) {
        auto m = Machine{};
        auto ss = std::istringstream{line};
        auto str = std::string{};
        ss >> m.pattern;
        m.pattern = m.pattern.substr(1, m.pattern.size() - 2);
        while (ss >> str) {
          if (str.at(0) == '(')
            m.buttons.push_back(parse_button(str.substr(1, str.size() - 2)));
          else
            m.joltage = parse_joltage(str.substr(1, str.size() - 2));
        }
        rval.push_back(std::move(m));
      }
      return rval;
    }
    
    auto minimum_presses(const Machine &m) {
      auto presses = size_t{};
      auto pattern = PatternCount{};
      auto first = std::string(m.pattern.size(), '.');
      auto next_iter = std::vector<std::string>{};
    
      pattern[first] = 0;
      next_iter.push_back(first);
    
      while (true) {
        auto this_iter = next_iter;
        next_iter.clear();
        ++presses;
    
        for (auto &iter : this_iter) {
          for (auto &button : m.buttons) {
            auto copy = iter;
            for (auto toggle : button)
              copy[toggle] = copy[toggle] == '.' ? '#' : '.';
            if (copy == m.pattern)
              return presses;
            if (auto f = pattern.find(copy); f != pattern.end())
              continue;
            pattern[copy] = presses;
            next_iter.push_back(std::move(copy));
          }
        }
      }
    }
    
    auto part1(const Machines &machines) {
      auto rval = size_t{};
      for (const auto &machine : machines)
        rval += minimum_presses(machine);
      return rval;
    }
    
    auto solve(const Machine &m) {
      z3::context context;
      z3::optimize opt(context);
    
      auto array = std::vector<z3::expr>{};
      auto sum = z3::expr{context.int_val(0)};
      for (auto i = size_t{}; i < m.buttons.size(); ++i) {
        auto name = std::string{"a"};
        name[0] += i;
        auto x = context.int_const(name.c_str());
        opt.add(x >= 0);
        sum = sum + x;
        array.push_back(std::move(x));
      }
      auto z = context.int_const("z");
      opt.add(sum == z);
    
      for (auto joltage = size_t{}; joltage < m.joltage.size(); ++joltage) {
        auto ex = z3::expr{context.int_val(0)};
        for (auto button = size_t{}; button < m.buttons.size(); ++button) {
          if (m.buttons.at(button).contains(joltage))
            ex = ex + array.at(button);
        }
        opt.add(ex == m.joltage.at(joltage));
      }
    
      z3::optimize::handle handle = opt.minimize(z);
      opt.check();
      return opt.lower(handle).as_uint64();
    }
    
    auto part2(const Machines &machines) {
      auto total = size_t{};
      for (auto &machine : machines)
        total += solve(machine);
      return total;
    }
    
    } // namespace
    
    auto main() -> int {
      auto machine = read();
      BOOST_LOG_TRIVIAL(info) << "Day 10: read " << machine.size();
      BOOST_LOG_TRIVIAL(info) << "1: " << part1(machine);
      BOOST_LOG_TRIVIAL(info) << "2: " << part2(machine);
    }
    
  • source
  • [–] 2 points 11 months ago

    Applied topology! Instead of using the standard even-odd raycasting trick for winding number (which I didn't remember), I constructed an explicit inward-pointing normal at each edge of the large polygon. This way you can test directly whether edges that lie on the boundary of your candidate rectangle have the right orientation. The code isn't pretty (edge-exterior-to-rect? is particularly awful), and I'm sure it could be made faster, but this worked almost the first time.

    (ql:quickload :str)
    
    (defun parse-line (line)
      (map 'vector #'parse-integer (str:split "," line)))
    
    (defun read-inputs (filename)
      (let ((lines (uiop:read-file-lines filename)))
        (make-array (list (length lines) 2)
                    :initial-contents (mapcar #'parse-line lines))))
    
    (defun area (corners v w)
      "Yield the area of the rectangle between two diagonally opposite corners identified by index."
      (* (1+ (abs (- (aref corners v 0) (aref corners w 0))))
         (1+ (abs (- (aref corners v 1) (aref corners w 1))))))
    
    (defun main-1 (filename)
      (let* ((red-tiles (read-inputs filename))
             (ntiles (car (array-dimensions red-tiles))))
        (loop for v from 0 to (1- ntiles)
              maximize (loop for w from 0 to (1- v)
                             maximize (area red-tiles v w)))))
    
    ;;; A rectangle is enclosed by the region if and only if both of the following are true:
    ;;; 1. No edge of the region passes through the interior of the rectangle;
    ;;; 2. When an edge of the region runs along an edge of the rectangle, the interior of
    ;;;    the region and the interior of the rectangle lie on the same side.
    ;;; So our first problem is to find the interior side of each edge of the region.
    
    (defun edges-with-interior (corners)
      ;; Anchor: a vertical edge with minimum x coordinate has the interior on the +x side
      (let* ((n (car (array-dimensions corners)))
             (min-x (loop for i from 0 to (1- n) minimize (aref corners i 0)))
             (arg-min-x (loop for i from 0 to (1- n)
                              when (= (aref corners i 0) min-x)
                                return i))
             ;; Mapping from edge direction unit vector to inward-pointing normal to edge
             ;; Initial inward normal is known to be #(1 0); sense of the rotation from edge
             ;; direction depends on direction of initial edge
             (edge-interior-dir
               (if (> (aref corners (1+ arg-min-x) 1) (aref corners arg-min-x 1)) ; +y
                   '((#(0 1) . #(1 0)) (#(1 0) . #(0 -1)) (#(0 -1) . #(-1 0)) (#(-1 0) . #(0 1)))
                   '((#(0 -1) . #(1 0)) (#(1 0) . #(0 1)) (#(0 1) . #(-1 0)) (#(-1 0) . #(0 -1)))))
             (interior-labeled-edges
               (loop for i from arg-min-x to (+ arg-min-x n -1)
                     collect (let* ((j (mod i n))
                                    (jj (mod (1+ i) n))
                                    (unit-edge
                                      (vector (signum (- (aref corners jj 0) (aref corners j 0)))
                                              (signum (- (aref corners jj 1) (aref corners j 1)))))
                                    (unit-interior-normal
                                      (cdr (assoc unit-edge edge-interior-dir :test #'equalp))))
                               (vector (aref corners j 0) (aref corners j 1)
                                       (aref corners jj 0) (aref corners jj 1)
                                       (aref unit-interior-normal 0) (aref unit-interior-normal 1))))))
        (make-array (list n 6) :initial-contents interior-labeled-edges)))
    
    (defun edge-exterior-to-rect? (corners edges-with-interior i v w)
      (let ((rxmin (min (aref corners v 0) (aref corners w 0)))
            (rxmax (max (aref corners v 0) (aref corners w 0)))
            (rymin (min (aref corners v 1) (aref corners w 1)))
            (rymax (max (aref corners v 1) (aref corners w 1)))
            (exmin (min (aref edges-with-interior i 0) (aref edges-with-interior i 2)))
            (exmax (max (aref edges-with-interior i 0) (aref edges-with-interior i 2)))
            (eymin (min (aref edges-with-interior i 1) (aref edges-with-interior i 3)))
            (eymax (max (aref edges-with-interior i 1) (aref edges-with-interior i 3)))
            (ixdir (aref edges-with-interior i 4))
            (iydir (aref edges-with-interior i 5)))
        (or (and (< exmin rxmin) (<= exmax rxmin)) ; edge is left of rectangle
            (and (>= exmin rxmax) (> exmax rxmax)) ; edge is right of rectangle
            (and (< eymin rymin) (<= eymax rymin)) ; edge is above rectangle
            (and (>= eymin rymax) (> eymax rymax)) ; edge is below rectangle
            (and (= exmin exmax rxmin) (= ixdir 1) (= iydir 0)) ; edge lies on left side pointing in
            (and (= exmin exmax rxmax) (= ixdir -1) (= iydir 0)) ; edge lies on right side pointing in
            (and (= eymin eymax rymin) (= ixdir 0) (= iydir 1)) ; edge lies on top side pointing in
            (and (= eymin eymax rymax) (= ixdir 0) (= iydir -1))))) ; edge lies on bottom side pointing in
    
    (defun enclosed? (corners edges-with-interior v w)
      (loop for i from 0 to (1- (car (array-dimensions edges-with-interior)))
            unless (edge-exterior-to-rect? corners edges-with-interior i v w)
              return nil
            finally (return t)))
    
    (defun main-2 (filename)
      (let* ((red-tiles (read-inputs filename))
             (ntiles (car (array-dimensions red-tiles)))
             (edges-with-interior (edges-with-interior red-tiles)))
        (loop for v from 0 to (1- ntiles)
              maximize (loop for w from 0 to (1- v)
                             maximize (if (enclosed? red-tiles edges-with-interior v w)
                                          (area red-tiles v w)
                                          0)))))
    
  • source
  • [–] 5 points 11 months ago

    Go

    Linear programming all over again! I solved part 1 with a BFS like many. But for part 2 a proper solver was needed. I'm terrible at LP so I tried to use a simplex implementation, but it was not enough to find a solution, Z3 was needed but I couldn't bother to install anything so... screw it.

    Here is my proposal part 2 doesn't work but the modelisation is interesting enough that it is worth posting:

    day10.go

    package main
    
    import (
    	"aoc/utils"
    	"fmt"
    	"math"
    	"regexp"
    	"slices"
    	"strconv"
    	"strings"
    	"sync"
    
    	"gonum.org/v1/gonum/mat"
    	"gonum.org/v1/gonum/optimize/convex/lp"
    )
    
    type button []int8
    
    type problem struct {
    	want                int
    	buttons             []button
    	joltageRequirements []int8
    }
    
    func parseLights(lights string) (want int) {
    	want = 0
    	for idx, ch := range lights {
    		if ch == '#' {
    			want += (1 << idx)
    		}
    	}
    	return want
    }
    
    func parseProblem(line string) problem {
    	reg := regexp.MustCompile(`\[([.#]+)\] ((?:(?:\([^)]+\))\s*)+) {([^}]+)}`)
    	matches := reg.FindStringSubmatch(line)
    
    	lights := matches[1]
    	buttonString := matches[2]
    	joltage := matches[3]
    
    	buttonString = string(slices.DeleteFunc(
    		[]rune(buttonString),
    		func(ch rune) bool {
    			return ch == '(' || ch == ')'
    		}))
    
    	buttons := []button{}
    	for switchString := range strings.SplitSeq(buttonString, " ") {
    		switches := []int8{}
    		for sw := range strings.SplitSeq(switchString, ",") {
    			val, _ := strconv.Atoi(sw)
    			switches = append(switches, int8(val))
    		}
    		buttons = append(buttons, switches)
    	}
    
    	joltageRequirements := []int8{}
    	for req := range strings.SplitSeq(joltage, ",") {
    		val, _ := strconv.Atoi(req)
    		joltageRequirements = append(joltageRequirements, int8(val))
    	}
    
    	return problem{
    		want:                parseLights(lights),
    		buttons:             buttons,
    		joltageRequirements: joltageRequirements,
    	}
    }
    
    func getProblemChannel(input chan string) chan problem {
    	ch := make(chan problem, cap(input))
    	go func() {
    		for line := range input {
    			ch <- parseProblem(line)
    		}
    		close(ch)
    	}()
    	return ch
    }
    
    func (b button) value() (val int) {
    	val = 0
    	for _, sw := range b {
    		val += 1 << sw
    	}
    	return val
    }
    
    func bfs(goal int, buttons []button) int {
    	type bfsState struct {
    		epoch int8
    		value int
    	}
    
    	visited := []int{0}
    	queue := []bfsState{}
    
    	for _, bt := range buttons {
    		value := bt.value()
    		queue = append(queue, bfsState{epoch: 1, value: value})
    		visited = append(visited, value)
    	}
    
    	for len(queue) > 0 {
    		state := queue[0]
    		queue = queue[1:]
    
    		if state.value == goal {
    			return int(state.epoch)
    		}
    
    		for _, bt := range buttons {
    			nextValue := state.value ^ bt.value()
    			if !slices.Contains(visited, nextValue) {
    				visited = append(visited, nextValue)
    				queue = append(queue, bfsState{
    					epoch: state.epoch + 1,
    					value: nextValue,
    				})
    			}
    		}
    	}
    
    	return math.MaxInt
    }
    
    func (p problem) solve() int {
    	depth := bfs(p.want, p.buttons)
    	return int(depth)
    }
    
    func parallel(threadCount int, f func(thrId int)) {
    	var wg sync.WaitGroup
    	for id := range threadCount {
    		wg.Go(func() {
    			f(id)
    		})
    	}
    	wg.Wait()
    }
    
    func stepOne(input chan string) (int, error) {
    	sum := 0
    	pbChan := getProblemChannel(input)
    	for pb := range pbChan {
    		depth := pb.solve()
    		fmt.Println(depth)
    		sum += depth
    	}
    	return sum, nil
    }
    
    type linearEquation struct {
    	solution int
    	coeffs   []int
    }
    
    func (p problem) toLinearEquations() linearProblem {
    	equations := make([]linearEquation, len(p.joltageRequirements))
    	varCount := len(p.buttons)
    
    	for equIdx, solution := range p.joltageRequirements {
    		equ := linearEquation{
    			solution: int(solution),
    			coeffs:   make([]int, varCount),
    		}
    
    		for btIdx, bt := range p.buttons {
    			if slices.Contains(bt, int8(equIdx)) {
    				equ.coeffs[btIdx] = 1
    			}
    		}
    		equations[equIdx] = equ
    	}
    	return equations
    }
    
    type linearProblem []linearEquation
    
    func (lp linearProblem) A() mat.Matrix {
    	rows := len(lp)
    	cols := len(lp[0].coeffs)
    	data := make([]float64, rows*cols)
    
    	for r := range rows {
    		for c := range cols {
    			data[r*cols+c] = float64(lp[r].coeffs[c])
    		}
    		fmt.Println(data[r*cols : (r+1)*cols])
    	}
    
    	return mat.NewDense(rows, cols, data)
    }
    
    func (lp linearProblem) c() []float64 {
    	count := len(lp[0].coeffs)
    	consts := make([]float64, count)
    	for idx := range count {
    		consts[idx] = 1.0
    	}
    	return consts
    }
    
    func (lp linearProblem) b() []float64 {
    	bees := make([]float64, len(lp))
    	for idx := range bees {
    		bees[idx] = float64(lp[idx].solution)
    	}
    	return bees
    }
    
    func stepTwo(input chan string) (int, error) {
    	sum := 0
    	pbChan := getProblemChannel(input)
    	for pb := range pbChan {
    		lpb := pb.toLinearEquations()
    		c := lpb.c()
    		A := lpb.A()
    		b := lpb.b()
    
    		optF, _, err := lp.Simplex(c, A, b, 1.0, nil)
    		if err != nil {
    			fmt.Errorf("simplex error: %v\n", err)
    		}
    		fmt.Println(optF)
    		sum += int(optF)
    	}
    
    	return sum, nil
    }
    
    func main() {
    	input := utils.FilePath("day10.txt")
    	utils.RunStep(utils.ONE, input, stepOne)
    	utils.RunStep(utils.TWO, input, stepTwo)
    }
    

  • source
  • [–] 2 points 11 months ago

    Python

    Send in the object orientation!
    Honestly though, it was just a convenient way to keep things contained.

    from pathlib import Path
    
    
    class Grid:
        def __init__(self, input: str) -> None:
            self.grid = list(map(lambda l: list(map(lambda c: 1 if c == "@" else 0, l)), input.splitlines()))
            self.dims = (len(self.grid[0]), len(self.grid))
    
        def get(self, x: int, y: int) -> int:
            if x < 0 or x >= self.dims[0] or y < 0 or y >= self.dims[1]:
                return 0
            return self.grid[x][y]
    
        def set(self, x: int, y: int, value: int):
            self.grid[x][y] = value
    
        def is_accessible(self, x: int, y: int) -> bool:
            if self.get(x, y):
                return sum(map(lambda t: self.get(*t), [(ix, iy) for ix in range(x - 1, x + 2) for iy in range(y - 1, y + 2)])) - 1 < 4
            return False
    
    
    def part_one(input: str) -> int:
        grid = Grid(input)
        return len(list(filter(None, map(lambda t: grid.is_accessible(*t), [(x, y) for x in range(grid.dims[0]) for y in range(grid.dims[1])]))))
    
    
    def part_two(input: str) -> int:
        grid = Grid(input)
        total = 0
        while True:
            remove = list(filter(None, map(lambda t: t if grid.is_accessible(*t) else None, [(x, y) for x in range(grid.dims[0]) for y in range(grid.dims[1])])))
            total += len(remove)
            if remove:
                [grid.set(*t, 0) for t in remove]
            else:
                break
        return total
    
    
    if __name__ == "__main__":
        input = Path("_2025/_4/input").read_text("utf-8")
        print(part_one(input))
        print(part_two(input))
    
  • source
  • [–] 3 points 11 months ago

    Uiua

    Not really proud of this one. Part 1's still ok, just calculated all distances between boxes after realizing it's not that many (499500, a reasonable amount I think, relatively speaking).
    The dumbest thing I did this time was manually implementing the function to take the first n rows of an array by using a loop. Only when I was working on part 2 did I realize that I can just use the "take" function Uiua provides. Additionally, I even had some mistake in my reimplementation of it that only caused issues in part 2 somehow.
    For anyone interested, it's the commented out line in P₁ below.
    Part 2 is just a brute force. For the actual input, I searched manually until I got to the pair number just before the actual solution because I didn't want it to run that long and it takes long enough as is.

    Run with example input

    Code

    $ 162,817,812
    $ 57,618,57
    $ 906,360,560
    $ 592,479,940
    $ 352,342,300
    $ 466,668,158
    $ 542,29,236
    $ 431,825,988
    $ 739,650,466
    $ 52,470,668
    $ 216,146,977
    $ 819,987,18
    $ 117,168,530
    $ 805,96,715
    $ 346,949,466
    $ 970,615,88
    $ 941,993,340
    $ 862,61,35
    $ 984,92,344
    $ 425,690,689
    
    Dist ← ⍜(β‰‘Β°βˆš)/+⌡-°⊟
    Prep ← (
      ⊜(βŠœβ‹•βŠΈβ‰ @,)βŠΈβ‰ @\n
      β§…<2
      βŠββŠΈβ‰‘Dist
    )
    
    Merge ← (
      βœβ™­β‚‚βŠ›
      {}
      β₯(⍣(βŠ™(Β°βŠ‚
            β–‘β₯(
              βŸœβŠΈΛœβ‰‘βŒŸ(>/+βŠƒβ¨‚(Γ—β‚‚β‹…β§»))
              β—΄βŠ‚βŠ™(β™­βˆ©βŒŸβ–½βŸœΒ¬)
            )∞
          )
          βŠ‚
        | ∘)
      )∞
      βŠ™β—Œ
    )
    
    P₁ ← (
      Prep
      # β₯₁₀(βŠ™β€™βŠ‘Β°ΛœβŠ‚)
      ↙₁₀
      Merge
      ⍆≑◇⧻
      /×↙₋₃
    )
    
    Pβ‚‚ ← (
      Prep
      1_0
      ⊸⍒(
        ⍜°˜⊟(
          βŠ™+₁
          β‹…βŸœβ†™
          βŠ™(Merge
            β—‡β§»βŠ’)
        )
      | <20⊣)
      -β‚βŠ’
      ⊑
      Γ—Β°βŠŸβŠ‘β‚€β‰
    )
    
    [βŠƒP₁Pβ‚‚]
    ≑(&p $"Part _: _") 1_2
    

  • source
  • [–] 5 points 11 months ago* (last edited 11 months ago) (4 children)

    Uiua

    (added language tag)

    Quiet here, isn't it?

    Here's part1 to be going on with.

    # AOC 2025 Day 10 - Wiring maze
    D ← "[.##.] (3) (1,3) (2) (2,3) (0,2) (0,1) {3,5,4,7}\n[...#.] (0,2,3,4) (2,3) (0,4) (0,1,2) (1,2,3,4) {7,5,12,7,2}\n[.###.#] (0,1,2,3,4) (0,3,4) (0,1,2,4,5) (1,2) {10,11,11,5,10,5}"
    # D      ← &fras"randomAOC/AOC2025day10.txt"
    Digits ← βŠœβ‹•βŠΈβˆŠ+@0⇑10
    Parse  ← ⊜(β–‘βŠœβ–‘βŠΈβ‰ @\s)βŠΈβ‰ @\n
    Part₁ ← (
      ≑◇(
        =@#β†˜β‚β†˜β‚‹β‚Β°β–‘Β°βŠ‚β†˜Β―1  # target
        βŠ™β¬š0≑◇(°⊚Digits)  # presses
        ⧻⊒path(β‰‘βŒžβ‰ |=0/+) # find shortest path
      )
      /+-1
    )
    Part₁ Parse D
    

    I've given up on Part 2. I knew what I needed to do but didn't have the understanding of how to use the matrix elimination method to get beyond the first stages. But I did find this:

    How to solve part 2 without librariesThis is a solver written totally from scratch in Dart, so easily readable unlike some other languages :-): [https://github.com/ayoubzulfiqar/advent-of-code/blob/main/2025/Dart/Day10/part_2.dart](GitHub link)

    There's lots of parallelism (that's over the top for this problem), but the core solveSystem method is very clearly written, just long...

  • source
  • [–] 1 point 11 months ago* (last edited 11 months ago)

    Uiua

    Part 1 was fun, though I had quite some frustration with getting the loop work the way I wanted it to.
    For part 2 I didn't actually need to modify that much, just pass another function to deal with the list of tachyons: part 1, remove duplicates, part 2, don't remove and count at the end. Easy. Or so I thought.
    I realized something was wrong when it just wouldn't stop running and the fan got louder.

    Today I started over from scratch and decided to solve both parts at once since I basically got the counts for each row anyways, so I just had to change the way the splitting was handled.
    There was one occasion where I got an error that the resulting array would be too big (~4GB) but at least I got a warning instead of seeing RAM usage spike suddenly :P

    Run with example input

    Code

    $ .......S.......
    $ ...............
    $ .......^.......
    $ ...............
    $ ......^.^......
    $ ...............
    $ .....^.^.^.....
    $ ...............
    $ ....^.^...^....
    $ ...............
    $ ...^.^...^.^...
    $ ...............
    $ ..^...^.....^..
    $ ...............
    $ .^.^.^.^.^...^.
    $ ...............
    # &fras "input-7.txt" β—Œ
    βŠœβˆ˜βŠΈβ‰ @\n
    βŠƒ(=@SβŠ‘β‚€)(⊏⊚>0βŠΈβ‰‘/+=@^β†˜β‚‚)
    ShootSplit ← (
      ‚(=2+>0)
      ⧻⊸⊚
      βŠ™(βŸœβ‚‚(
          βŸœβŠβŠšβŠ™βŸœ[β§»]
          ⍚(βŠ‚βŠƒ-₁+₁)
          /+β‰‘βŒŸ(⬚0Λœβ†―Γ—β—‡Β°βŠš)
        )
        +⍜⊏(Λœβ†―0β§»)⊚
      )
    )
    
    Solve ← (
      0
      β₯(βŠ™(+|ShootSplit|βŠƒβŠ‘β‚€β†˜β‚))β—‘β‹…β‹…β§»
      βŠŸβŠ™βŠ™β—ŒβŠ™/+
    )
    
    Solve
    ≑(&p $"Part _: _") 1_2
    

    And for completeness sake:

    Previous attemptEdit: Tried to scrape the old code together but seems like it doesn't actually work like this. Probably just some small thing I missed but I don't have the mental energy to take a look at this mess again :P

    Prep ← βŠƒ(⊚=@S)βŠœβˆ˜βŠΈβ‰ @\n
    
    Splits! ← (
      ˙⍀>0β—‘β‹…β§»
      ‚(=β§»β€™Λœβ¨‚)
      βˆ©βŒŸβ–½βŠΈΒ¬
      ⊸⧻
      βŠ™(♭≑[βŠƒ-₁+₁]
        ^βŠ‚)
    )
    
    Shoot! ← (
      0
      ⍒(βŠ™(+
        | ⍣Splits!^βŠΈβ‹…0
        | ⊚=@^Β°βŠ‚
        )
      | >0β‹…β‹…β§»)
    )
    
    PartOne ← (
      &fras "input-7.txt"
      Prep
      βŠ™β‹…β—ŒShoot!β—΄
    )
    
    PartTwo ← (
      &fras "input-7.txt"
      Prep
      β§»β‹…βŠ™β—ŒShoot!∘
    )
    

  • source
  • [–] 2 points 11 months ago

    Uiua

    My silly part one solution, because I have no idea how to do part two (this is usually the difficulty at which I bow out, but I want to try actually expanding my knowledge this year!).

    Parse         ← ⊜(βŠœβ‹•βŠΈβ‰ @,)βŠΈβ‰ @\n
    CornerClosest ← βŠβŠ’ββŠΈβ‰‘βŒž(/+ⁿ2-)
    Big           ← 9999999
    RectSize      ← /Γ—+1⌡-
    Part₁ ← (
      ∩⌟∩⌟CornerClosest 0_0 1000000_1000000 Big_0 0_Big
      β†₯∩RectSize
    )
    
    $ 7,1
    $ 11,1
    $ 11,7
    $ 9,7
    $ 9,5
    $ 2,5
    $ 2,3
    $ 7,3
    
    &fras "9.txt"
    Part₁ Parse
    
  • source
  • [–] 2 points 11 months ago

    Rust

    This one broke me, I couldn't do it without looking at some of the solutions in this thread. In the end, basically all that was necessary for part 2 is to check for every candidate rectangle if:

    1. No corners (red tiles) are inside the rectangle, and
    2. No lines intersect the rectangle.

    Plus a bunch shifting numbers around by 1. The code is not pretty at all, but at least it turned very efficient, solving part 2 in just 4.3ms. I have no idea how generalized the solution is. It definitely assumes that any larger rectangle is spanned inside the area and that the path doesn't cross over itself. Also of course that there are no two corners next to each other forming a 180 degree turn.

    View on github

    Code

    use std::ops::{Range, RangeInclusive};
    
    fn parse_input(input: &str) -> Vec<(u32, u32)> {
        input
            .lines()
            .map(|l| {
                let (a, b) = l.split_once(',').unwrap();
                (a.parse::<u32>().unwrap(), b.parse::<u32>().unwrap())
            })
            .collect()
    }
    
    #[inline]
    fn area(a: (u32, u32), b: (u32, u32)) -> u64 {
        (a.0.abs_diff(b.0) as u64 + 1) * (a.1.abs_diff(b.1) as u64 + 1)
    }
    
    fn part1(input: String) {
        let tiles = parse_input(&input);
        let mut largest = 0;
        for t1 in &tiles {
            for t2 in &tiles {
                let a = area(*t1, *t2);
                if a > largest {
                    largest = a;
                }
            }
        }
        println!("{largest}");
    }
    
    // Returns true only if t is not inside of the rectangle
    #[inline]
    fn red_allowed(t: (u32, u32), x_range: Range<u32>, y_range: Range<u32>) -> bool {
        !(t.0 > x_range.start && t.0 + 1 < x_range.end && t.1 > y_range.start && t.1 + 1 < y_range.end)
    }
    
    fn is_contained(
        a: (u32, u32),
        b: (u32, u32),
        tiles_x: &[(u32, u32)],
        tiles_y: &[(u32, u32)],
        vert_lines: &[(u32, RangeInclusive<u32>)],
        hori_lines: &[(u32, RangeInclusive<u32>)],
    ) -> bool {
        let x_range = a.0.min(b.0)..(a.0.max(b.0) + 1);
        let y_range = a.1.min(b.1)..(a.1.max(b.1) + 1);
    
        // Check that no corners (red tiles) are inside the rectangle
        let corners_ok = if x_range.end - x_range.start <= y_range.end - y_range.start {
            // Use tiles_x
            let start = match tiles_x.binary_search(&(x_range.start, 0)) {
                Ok(e) => e,
                Err(e) => e,
            };
            tiles_x
                .iter()
                .skip(start)
                .take_while(|t| t.0 < x_range.end)
                .filter(|&&t| t != a && t != b)
                .all(|t| red_allowed(*t, x_range.clone(), y_range.clone()))
        } else {
            // Use tiles_y
            let start = match tiles_y.binary_search_by_key(&(y_range.start, 0), |(x, y)| (*y, *x)) {
                Ok(e) => e,
                Err(e) => e,
            };
            tiles_y
                .iter()
                .skip(start)
                .take_while(|t| t.1 < y_range.end)
                .filter(|&&t| t != a && t != b)
                .all(|t| red_allowed(*t, x_range.clone(), y_range.clone()))
        };
        if !corners_ok {
            return false;
        }
    
        // Check that no line intersects the inside of the rectangle
        let start = match vert_lines.binary_search_by_key(&x_range.start, |(x, _)| *x) {
            Ok(e) => e,
            Err(e) => e,
        };
        for (x, line) in vert_lines
            .iter()
            .skip(start)
            .take_while(|(x, _)| *x < x_range.end)
        {
            if x_range.start < *x
                && x_range.end > *x + 1
                && (y_range.start + 1).max(*line.start()) < (y_range.end - 1).min(line.end() + 1)
            {
                return false;
            }
        }
        let start = match hori_lines.binary_search_by_key(&y_range.start, |(y, _)| *y) {
            Ok(e) => e,
            Err(e) => e,
        };
        for (y, line) in hori_lines
            .iter()
            .skip(start)
            .take_while(|(y, _)| *y < y_range.end)
        {
            if y_range.start < *y
                && y_range.end > *y + 1
                && (x_range.start + 1).max(*line.start()) < (x_range.end - 1).min(line.end() + 1)
            {
                return false;
            }
        }
        true
    }
    
    fn part2(input: String) {
        let tiles = parse_input(&input);
    
        let mut vert_lines = Vec::new();
        let mut hori_lines = Vec::new();
        let mut prev = *tiles.last().unwrap();
        for &t in &tiles {
            if t.0 == prev.0 {
                vert_lines.push((t.0, t.1.min(prev.1)..=t.1.max(prev.1)));
            } else {
                debug_assert_eq!(t.1, prev.1);
                hori_lines.push((t.1, t.0.min(prev.0)..=t.0.max(prev.0)));
            }
            prev = t;
        }
        vert_lines.sort_by_key(|(x, _)| *x);
        hori_lines.sort_by_key(|(y, _)| *y);
    
        let mut tiles_x = tiles.clone();
        tiles_x.sort();
        let mut tiles_y = tiles.clone();
        tiles_y.sort_by_key(|(x, y)| (*y, *x));
        let mut largest = 0;
        for (idx, t1) in tiles.iter().enumerate() {
            for t2 in tiles.iter().take(idx) {
                let a = area(*t1, *t2);
                if a > largest && is_contained(*t1, *t2, &tiles_x, &tiles_y, &vert_lines, &hori_lines) {
                    largest = a;
                }
            }
        }
        println!("{largest}");
    }
    
    util::aoc_main!();
    

  • source
  • [–] [S] 1 point 11 months ago* (last edited 11 months ago)

    Rust

    pt2: 71ms.

    Wow. What a step up in difficulty. Using the geo library got me the right answer, and quickly in terms of code, but run time was terrible (2m+). Switching back to simply checking if a wall is entirely outside the rectangle got me a much faster win, and the code isn't too bad either.

    Code and algorithm description(A wall is outside if its entirely to the left OR entirely to the right of the rect) OR (entirely above OR entirely below).

       fn check_wall_outside(
            c: &(&Coord, &Coord),
            top: usize,
            bottom: usize,
            left: usize,
            right: usize,
        ) -> bool {
            if (c.0.x <= left && c.1.x <= left) || (c.0.x >= right && c.1.x >= right) {
                return true;
            }
            if (c.0.y <= top && c.1.y <= top) || (c.0.y >= bottom && c.1.y >= bottom) {
                return true;
            }
            false
        }
    
       #[test]
        fn test_y2025_day9_part2_mine1() {
            let input = include_str!("../../input/2025/day_9.txt");
            let coords = input
                .lines()
                .map(|l| {
                    let halfs = l.split_once(',').unwrap();
                    Coord {
                        x: halfs.0.parse::<usize>().unwrap(),
                        y: halfs.1.parse::<usize>().unwrap(),
                    }
                })
                .collect::<Vec<Coord>>();
    
            let mut walls = vec![];
    
            for i in 0..coords.len() {
                let first = &coords[i];
                let second = coords.get(i + 1).unwrap_or(coords.first().unwrap());
    
                walls.push((first, second));
            }
    
            let mut max_area = 0;
            for i in 0..coords.len() {
                let first = &coords[i];
                'next_rect: for j in i..coords.len() {
                    let second = &coords[j];
                    if first == second {
                        continue 'next_rect;
                    }
                    let area = (first.x.abs_diff(second.x) + 1) * (first.y.abs_diff(second.y) + 1);
                    if area < max_area {
                        continue 'next_rect;
                    }
    
                    let (top, bottom) = if first.y > second.y {
                        (second.y, first.y)
                    } else {
                        (first.y, second.y)
                    };
    
                    let (left, right) = if first.x > second.x {
                        (second.x, first.x)
                    } else {
                        (first.x, second.x)
                    };
    
                    for wall in &walls {
                        if !check_wall_outside(wall, top, bottom, left, right) {
                            continue 'next_rect;
                        }
                    }
    
                    max_area = area;
                }
            }
            assert_eq!(max_area, 1542119040);
            println!("Part 2: {}", max_area);
        }
    

  • source
  • [–] 2 points 11 months ago*

    Python

    Part 2 is only partially optimized (runs in ~7.5s).

    The optimization I did do is to compress the grid coords by recording all red tiles and their adjacent cells only (thanks Everybody,Codes #15)
    The part which needs further optimization is my approach to getting the largest rectangle in the polygon. I did it by flood-filling the area outside the polygon, then walking through all cells of every pair of red tiles to make sure their rectangle is within the polygon.

    click to view code

    # combinations helps us get all pairs of red tiles
    # pairwise helps us get all adjacent pairs of red tiles
    from itertools import combinations, pairwise
    from collections import deque
    
    def get_area(p1, p2):
        x1, y1 = p1
        x2, y2 = p2
        return (abs(x2 - x1) + 1) * (abs(y2 - y1) + 1)
    
    @timer()
    def part1(data: str):
        red_tiles = [tuple(map(int, line.split(','))) for line in data.splitlines()]
    
        max_area = 0
        for r1, r2 in combinations(red_tiles, 2):
            max_area = max(max_area, get_area(r1, r2))
        return max_area
    
    def compress_coordinates(coords: list[tuple[int, int]]):
        i_of_interest_set = set()
        j_of_interest_set = set()
    
        for i, j in coords:
            i_of_interest_set.update([i-1, i, i+1])
            j_of_interest_set.update([j-1, j, j+1])
        
        sorted_is = sorted(list(i_of_interest_set))
        sorted_js = sorted(list(j_of_interest_set))
        m = len(sorted_is)
        n = len(sorted_js)
    
        compress_i_map = {val: id for id, val in enumerate(sorted_is)}
        compress_j_map = {val: id for id, val in enumerate(sorted_js)}
        return m, n, compress_i_map, compress_j_map
    
    
    @timer()
    def part2(data: str):
        red_tiles = [tuple(map(int, line.split(','))) for line in data.splitlines()]
        m, n, compress_i_map, compress_j_map = compress_coordinates(red_tiles)
    
        def get_compressed_coords(og_coords: tuple[int, int]):
            return compress_i_map[og_coords[0]], compress_j_map[og_coords[1]]
    
        # make the grid
        grid = [['.']*n for _ in range(m)]
        for i, j in red_tiles:
            ci, cj = get_compressed_coords((i, j))
            grid[ci][cj] = '#'
    
        # make the walls of the polygon enclosed by the red tiles
        def make_line(r1, r2):
            r1_i, r1_j = get_compressed_coords(r1)
            r2_i, r2_j = get_compressed_coords(r2)
    
            wall_char = '#'
            if r1_i == r2_i:
                # same row
                row = r1_i
                for col in range(min(r1_j, r2_j) + 1, max(r1_j, r2_j)):
                    grid[row][col] = wall_char
            elif r1_j == r2_j:
                # same col
                col = r1_j
                for row in range(min(r1_i, r2_i) + 1, max(r1_i, r2_i)):
                    grid[row][col] = wall_char
    
        for r1, r2 in pairwise(red_tiles):
            make_line(r1, r2)
        make_line(red_tiles[-1], red_tiles[0])
    
        # whiteout the exterior
        DIRECTIONS = [(0, 1), (1, 0), (0, -1), (-1, 0)]
        queue = deque([(0, 0)])
        while queue:
            i, j = queue.popleft()
            if grid[i][j] == ' ':
                continue
            grid[i][j] = ' '
    
            for di, dj in DIRECTIONS:
                ni, nj = i+di, j+dj
                if not (0 <= ni < m) or not (0 <= nj < n):
                    continue
                if grid[ni][nj] != '.':
                    continue
                queue.append((ni, nj))
        
        # DEBUG: print the grid
        # print()
        # for row in grid:
        #     print(''.join(row))
        
        # check if the rectangle formed by the corners r1, r2 is contained within the red-green polygon
        def check_contained(r1, r2):
            r1_i, r1_j = get_compressed_coords(r1)
            r2_i, r2_j = get_compressed_coords(r2)
            
            min_i, max_i = min(r1_i, r2_i), max(r1_i, r2_i)
            min_j, max_j = min(r1_j, r2_j), max(r1_j, r2_j)
    
            for i in range(min_i, max_i+1):
                for j in range(min_j, max_j+1):
                    if grid[i][j] == ' ':
                        return False
            return True
    
        # get the max area of a rectangle that is contained within the red-green polygon
        #   and has red tiles at two of its corners
        max_area = 0
        for r1, r2 in combinations(red_tiles, 2):
            area = get_area(r1, r2)
            if area <= max_area:
                # dont bother
                continue
            if not check_contained(r1, r2):
                continue
            max_area = area
    
        return max_area
    
    sample = """7,1
    11,1
    11,7
    9,7
    9,5
    2,5
    2,3
    7,3"""
    assert part1(sample) == 50
    assert part2(sample) == 24
    

    ___

  • source
  • [–] 3 points 11 months ago* (last edited 11 months ago)

    Rust

    The problem today just flustered me. Sometimes I'm not convinced I've gotten any better at programming since 2022 (yes, I still used that exact hack of a combine_ranges for this problem) Runs on my Thinkpad P1 Gen2 in 180.9 +- 0.9 ms on external power (or 726.8 +- 2.1 ms on battery; can't remember how I configured performance/battery saver honestly lol)

    solution

    pub fn day09(input: &str) -> (u64, u64) {
        let mut part1 = 0;
        let mut part2 = 0;
        let mut squares = Vec::new();
        for line in input.trim().lines() {
            let (x, y) = line.split_once(',').unwrap();
            squares.push([x.parse::<u64>().unwrap(), y.parse::<u64>().unwrap()]);
        }
    
        let mut chunks = squares.chunks_exact(2).peekable();
        let mut lines = Vec::new();
        let peek = chunks.peek().unwrap();
        // dir is true if scanning vertically, false if horizontally
        let dir = peek[0][1] == peek[1][1];
        if !dir {
            debug_assert_eq!(peek[0][0], peek[1][0]);
        }
    
        // Each line is a tuple where .0 is the index in the scan direction and .1 is the pair of end
        // points for a line perpendicular to the scan direction
        for line in chunks {
            lines.push((
                line[0][dir as usize],
                [line[0][!dir as usize], line[1][!dir as usize]],
            ));
        }
        lines.sort();
    
        // rot is true if field 0 being less than field 1 enters the shape, false for exiting
        let rot = lines[0].1[0] < lines[0].1[1];
    
        for idx_1 in 0..squares.len() - 1 {
            'o: for idx_2 in (idx_1 + 1)..squares.len() {
                let s1 = squares[idx_1];
                let s2 = squares[idx_2];
                let area = (s1[0].abs_diff(s2[0]) + 1) * (s1[1].abs_diff(s2[1]) + 1);
                part1 = std::cmp::max(part1, area);
    
                if area <= part2 {
                    continue;
                }
                // Think of "up" as the direction the scan line travels
                let floor = std::cmp::max(s1[dir as usize], s2[dir as usize]);
                let ceil = std::cmp::min(s1[dir as usize], s2[dir as usize]);
                let left_wall = std::cmp::min(s1[!dir as usize], s2[!dir as usize]);
                let right_wall = std::cmp::max(s1[!dir as usize], s2[!dir as usize]);
                let floor_idx = lines.binary_search_by_key(&floor, |(l, _)| *l).unwrap();
                let ceil_idx = lines.binary_search_by_key(&ceil, |(l, _)| *l).unwrap();
      
                // If a line contracts the valid range, check if that contraction intersects the walls,
                // since that necessitates uncolored tiles in the bounding box
                let skip = |line: [u64; 2]| {
                    if rot != (line[0] < line[1]) {
                        return (left_wall..right_wall).contains(&line[0])
                            || (left_wall..right_wall).contains(&line[1]);
                    }
                    false
                };
    
                for line in &lines[ceil_idx..floor_idx] {
                    if skip(line.1) {
                        continue 'o;
                    }
                }
                let mut combo_ranges = Vec::new();
                // This `rev` is needed for correctness, although the full input does not seem to care
                // It actually hurts performance a lot :(
                for line in lines[0..=ceil_idx].iter().rev() {
                    if skip(line.1) {
                        continue 'o;
                    }
                    if rot {
                        combo_ranges.push(line.1);
                    } else {
                        combo_ranges.push([line.1[1], line.1[0]]);
                    }
                    combo_ranges = combine_ranges(combo_ranges);
                    // If overlapping the target range results in no change of range, then the target
                    // range is fully covered and this rectangle is valid
                    let mut test_range = combo_ranges.clone();
                    test_range.push([left_wall, right_wall]);
                    if combo_ranges == combine_ranges(test_range) {
                        part2 = area;
                        continue 'o;
                    }
                }
            }
        }
        (part1, part2)
    }
    

  • source
  • view more: β€Ή prev next β€Ί