copy pasting the rules from last year’s thread:

Rules: no spoilers.

The other rules are made up aswe go along.

Share code by link to a forge, home page, pastebin (Eric Wastl has one here) or code section in a comment.

  • Sailor Sega Saturn@awful.systems
    link
    fedilink
    English
    arrow-up
    5
    ·
    22 days ago

    I can’t sleep, so here’s 1-1 and 1-2, unfortunately I couldn’t think of any silly solutions this time, so it’s straightforward instead:

    spoiler
    #include <iostream>
    #include <vector>
    #include <numeric>
    #include <algorithm>
    #include <set>
    #include <iterator>
    
    int main() {
      std::multiset<int> l, r;
      int a, b;
      while (std::cin >> a >> b) {
        l.insert(a); r.insert(b);
      }
      std::vector<int> delta;
      std::transform(l.begin(), l.end(), r.begin(), std::back_inserter(delta),
        [](int x, int y) { return std::abs(x-y); }
      );
      std::cout << std::accumulate(delta.begin(), delta.end(), 0) << std::endl;
    }
    
    spoiler
    #include <iostream>
    #include <numeric>
    #include <set>
    
    int main() {
      std::multiset<int> l, r;
      int a, b;
      while (std::cin >> a >> b) {
        l.insert(a); r.insert(b);
      }
      std::cout << std::accumulate(l.begin(), l.end(), 0, [&r](int acc, int x) {
        return acc + x * r.count(x);
      }) << std::endl;
    }
    
    • Sailor Sega Saturn@awful.systems
      link
      fedilink
      English
      arrow-up
      6
      ·
      edit-2
      22 days ago

      2-1: I have quickly run out of hecks to give. This is the sort of problem that gives prolog programmers feelings of smug superiority.

      spoiler
      #include <string>
      #include <iostream>
      #include <sstream>
      
      int main() {
        int safe = 0;
        std::string s;
        while (std::getline(std::cin, s)) {
          std::istringstream iss(s);
          int a, b, c;
          if (!(iss >> a >> b)) {
            safe++; continue;
          }
          if (a == b || std::abs(a-b) > 3) continue;
          bool increasing = b > a;
          while (iss >> c) {
            if (b == c || std::abs(b-c) > 3) goto structuredprogrammingisfornerds;
            switch (increasing) {
              case false:
                if (c < b) { b = c; continue; }
                goto structuredprogrammingisfornerds;
              case true:
                if(c > b) { b = c; continue; }
                goto structuredprogrammingisfornerds;
            }
          }
          safe++;
          structuredprogrammingisfornerds:;
        }
        std::cout << safe << std::endl;
      }
      

      As usual the second part has punished me for my cowboy code, so I’ll have to take a different more annoying tack (maybe tomorrow). Or you know I could just double down on the haphazard approach…

      • Sailor Sega Saturn@awful.systems
        link
        fedilink
        English
        arrow-up
        6
        ·
        edit-2
        22 days ago

        I decided to double down on 2-2, since bad code is one of life’s little pleasures. Where we’re going we won’t need big-oh notation

        spoiler
        #include <string>
        #include <iostream>
        #include <sstream>
        #include <vector>
        #include <iterator>
        
        template <typename It>
        bool seemslegit(It begin, It end) {
            if (std::distance(begin, end) == 1) {
              return true;
            }
            int a = *begin++;
            int b = *begin++;
            if (a == b || std::abs(a-b) > 3) return false;;
            bool increasing = b > a;
            while (begin != end) {
              int c = *begin++;
              if (b == c || std::abs(b-c) > 3) return false;;
              switch (increasing) {
                case false:
                  if (c < b) { b = c; continue; }
                  return false;
                case true:
                  if(c > b) { b = c; continue; }
                  return false;
              }
            }
            return true;
        }
        
        template <typename It>
        void debug(It begin, It end) {
          bool legit = seemslegit(begin, end);
          while (begin != end) {
            std::cout << *begin++ << " ";
          }
          //std::cout << ": " << std::boolalpha << legit << std::endl;
        }
        
        int main() {
          int safe = 0;
          std::string s;
          while (std::getline(std::cin, s)) {
            std::istringstream iss(s);
            std::vector<int> report((std::istream_iterator<int>(iss)),
                                    std::istream_iterator<int>());
            debug(report.begin(), report.end());
            if (seemslegit(report.begin(), report.end())) {
              safe++;
              std::cout << "\n\n";
              continue;
            }
            for (int i = 0; i < report.size(); ++i) {
              auto report2 = report;
              auto it = report2.erase(report2.begin()+i);
              debug(report2.begin(), report2.end());
              if (seemslegit(report2.begin(), report2.end())) {
                safe++;
                break;
              }
            }
            std::cout << "\n\n";
         }
          std::cout << safe << std::endl;
        }
        
        Commentary

        Doing this “efficiently” should be possible. since you only need ~2-ish look-back you should be able to score reports in O(n) time. One complication is you might get the direction wrong, need to consider that erasing one of the first two elements could change the direction. But that requires thinking, and shoving all the permutations into a function with ungodly amounts of copying does not.

        • swlabr@awful.systems
          link
          fedilink
          English
          arrow-up
          4
          ·
          22 days ago
          re: 2-2

          yeah that’s what I ended up thinking. Just try the brute force and if it’s too slow, maybe I’ll try be smarter about it.

          • gerikson@awful.systems
            link
            fedilink
            English
            arrow-up
            5
            ·
            22 days ago
            re: 2-2

            I was convinced that some of the Perl gods in the subreddit would reveal some forgotten lore that solved this in one line but looks like my brute force method of removing one element at a time was the way to go.

            • Sailor Sega Saturn@awful.systems
              link
              fedilink
              English
              arrow-up
              5
              ·
              edit-2
              21 days ago

              I am now less sleep deprived so can say how to do better somewhat sensibly, albeit I cannot completely escape from C++s verbosity:

              2-2
              1. Don’t worry about the sequences changing direction. Just call the check function both assuming it is increasing and assuming it is decreasing. This is cheap enough because the wrong branch will fail after 3 elements or so.
              2. When encountering an element that fails, you only need to consider removing the previous element, or the current element. If you can get to the next element removing one of those then you can continue on without any real backtracking.

              Updated code:

              2-2
              #include <string>
              #include <iostream>
              #include <sstream>
              #include <vector>
              #include <iterator>
              
              bool valid_pair(const std::vector<int> &arr, int i, int j, bool direction) {
                if (i < 0) return true;
                if (j == arr.size()) return true;
                return    !(arr[i] == arr[j])
                       && (direction ? arr[i] < arr[j] : arr[j] < arr[i])
                       && (std::abs(arr[j]-arr[i]) <= 3);
              }
              
              bool valid(const std::vector<int> &arr, bool direction) {
                int checks = 1;
                for (int i = 1; i < arr.size(); ++i) {
                  if (valid_pair(arr, i-1, i, direction)) continue;
                  if (checks == 0) return false;
                  if (   valid_pair(arr, i-2,  i, direction)
                      && valid_pair(arr, i,  i+1, direction)) {
                    checks -= 1; i += 1;
                  } else if (valid_pair(arr, i-1, i+1, direction)) {
                    checks -= 1; i += 1;
                  } else return false;
                }
                return true;
              }
              
              int main() {
                int safe = 0;
                std::string s;
                while (std::getline(std::cin, s)) {
                  std::istringstream iss(s);
                  std::vector<int> report((std::istream_iterator<int>(iss)),
                                          std::istream_iterator<int>());
                  safe += (valid(report, true) || valid(report, false));
                }
                std::cout << safe << std::endl;
              }
              
  • gerikson@awful.systems
    link
    fedilink
    English
    arrow-up
    4
    ·
    22 days ago

    It’s that time of the year again. Last year was tough for me, i got laid off in the middle of dec and it kinda killed the vibe. I’ll see how long I keep up this year. My historical backlog is growing but I’ve made peace with it.

    • Architeuthis@awful.systemsOP
      link
      fedilink
      English
      arrow-up
      2
      ·
      edit-2
      19 days ago

      tl;dr: Day 5 was most perfectly fine code thrown out for me, because I ran face first into eliminating imaginary edge cases instead of starting out simple.

      5-1 commentary

      I went straight into a rabbit hole of doing graph traversal to find all implicit rules (i.e. 1|2, 2|3, 3|4 imply 1|3, 1|4, 2|4) so I could validate updates by making sure all consequent pairs appear in the expanded ruleset. Basically I would depth first search a tree with page numbers for nodes and rules for edges, to get all branches and recombine them to get the full ruleset.

      So ideally 1|2, 2|3, 3|4 -> 1|2|3|4 -> 1|2, 2|3, 3|4, 1|3, 1|4, 2|4

      Except I forgot the last part and just returned the branch elements pairwise in sequence, which is just the original rules, which I noticed accidentally after the fact since I was getting correct results, because apparently it was completely unnecessary and I was just overthinking myself into several corners at the same time.

      5-2 commentary and some code

      The obvious cornerstone was the comparison function to reorder the invalid updates, this is what I came up with:

      let comparerFactory (ruleset: (int*int) list) :int -> int -> int = 
          let leftIndex = 
              ruleset 
              |> List.groupBy fst 
              |> List.map (fun (key,grp)-> key, grp |> List.map snd)
              |> Map.ofList
      
          fun page1 page2 -> 
              match (leftIndex  |> Map.tryFind page1) with
              | Some afterSet when afterSet |> List.contains page2 -> -1
              | _ -> 1
      

      The memoization pattern is for caching an index of rules grouped by the before page, so I can sort according to where each part of the comparison appears. I started out with having a second index where the key was the ‘after’ page of the rule which I would check if the page didn’t appear on the left side of any rule, but it turned out I could just return the opposite case, so again unnecessary.

  • Architeuthis@awful.systemsOP
    link
    fedilink
    English
    arrow-up
    4
    ·
    edit-2
    20 days ago

    Got stuck forever on 2-2 because of an edge case that only showed up in 7/1000 reports, ended up just brute forcing it, just ran the fitness function after removing one element at a time sequentially.

    Then solved 3.x in like minutes because I could be worse at regex, posting code mostly because no-one else posted F# yet.

    edited to fix spoiler header formatting

    3-2 in F#
    "./input.actual"
    |> System.IO.File.ReadAllText
    |> fun source -> 
        System.Text.RegularExpressions.Regex.Matches(source, @"don't\(\)|do\(\)|mul\((\d+),(\d+)\)")
        |> Seq.fold
            (fun (acc, enabled) m ->
                match m.Value with
                | "don't()" -> acc, false
                | "do()" -> acc, true
                | mul when enabled && mul.StartsWith("mul") ->
                    let (x, y) = int m.Groups.[1].Value, int m.Groups.[2].Value
                    acc + x * y, enabled
                | _ -> acc, enabled ) 
            (0, true)
        |> fst
    |> printfn "The sum of all valid multiplications with respect to do() and don't() is %A"
    
    
    comments

    Not much to say, the regex grabs all relevant strings and the folding function propagates a flag that flips according to do/don’t and an accumulator that is increased when a mul() is encountered and parsed.

  • gerikson@awful.systems
    link
    fedilink
    English
    arrow-up
    4
    ·
    edit-2
    20 days ago

    Day 4 - Ceres Search

    discussion

    There’s probably a smarter way to do this…

    For part 1, I dumped everything into a matrix. Then I scanned it element by element. If I found an X, I searched in 8 directions from there and counted up if I found M A S in sequence.

    For part 2 I searched for an A, checked each diagonal corner, and counted up if the opposites were M S or S M

    https://github.com/gustafe/aoc2024/blob/main/d04-Ceres-Search.pl

    • Architeuthis@awful.systemsOP
      link
      fedilink
      English
      arrow-up
      2
      ·
      edit-2
      20 days ago
      discussion

      Same, except in 4-1 I used a recursive function to traverse each direction according to the offset decided by the selected direction (like SW is row++,col–) , due to functional programming induced brain damage.

      Would have been pretty useful too if 4-2 turned out to be about finding longer patterns, instead of smaller and in half the directions.

      4-1

      Inlined some stuff to fit everything in one function:

      let rec isXmas (row:int) (col:int) (dir:int) (depth:int) (arr: char array2d) : bool =
          let value = arr.[row,col]
          if depth = 3
          then value = 'S'
          else if  [|'X';'M';'A';'S'|].[depth] = value
          then
              let (nextRow, nextCol) =
                  match dir with
                  | 1 -> row + 1, col - 1
                  | 2 -> row + 1, col
                  | 3 -> row + 1, col + 1
                  | 4 -> row, col - 1
                  | 6 -> row, col + 1
                  | 7 -> row - 1, col - 1
                  | 8 -> row - 1, col
                  | 9 -> row - 1, col + 1
                  | _ -> failwith $"{dir} isn't a numpad direction." 
      
              let rowCount = arr |> Array2D.length1
              let colCount = arr |> Array2D.length2
             
              if nextRow >= 0 && nextRow < rowCount && nextCol >= 0 && nextCol < colCount
              then isXmas nextRow nextCol dir (depth+1) arr
              else false
          else false
      

      Then you run this for every appropriately pruned ‘X’ times every direction and count the trues.

  • gerikson@awful.systems
    link
    fedilink
    English
    arrow-up
    4
    ·
    21 days ago

    Day 3

    3-2

    I expect much wailing and gnashing of teeth regarding the parsing, which of course is utterly trivial if you know a bit if regex.

    I got bit by the input being more than one line. Embarrasing.

    I wonder if any input starts with a “don’t()” or if it’s too early for Erik to pull such trickery.

      • Architeuthis@awful.systemsOP
        link
        fedilink
        English
        arrow-up
        1
        ·
        edit-2
        16 days ago

        I almost got done in by floating point arithmetic, I think

        8-2 commentary

        Used the coordinates of every two same type frequences to create the ilnear equation (y = ax + b) and then fed it all the matrix coordinates to see which belonged to the line. To get the correct number of antinodes I had to check for |y - ax - b| < 0.0001, otherwise I got around 20 too few.

  • zogwarg@awful.systems
    link
    fedilink
    English
    arrow-up
    3
    ·
    21 days ago

    Day 3 well suited to JQ

    Part 2
    #!/usr/bin/env jq -n -R -f
    
    reduce (
      inputs |   scan("do\\(\\)|don't\\(\\)|mul\\(\\d+,\\d+\\)")
             | [[scan("(do(n't)?)")[0]], [ scan("\\d+") | tonumber]]
    ) as [[$do], [$a,$b]] (
      { do: true, s: 0 };
        if $do == "do" then .do = true
      elif $do         then .do = false
      elif .do         then .s = .s + $a * $b end
    ) | .s
    
  • swlabr@awful.systems
    link
    fedilink
    English
    arrow-up
    3
    ·
    edit-2
    21 days ago

    For 3: I made dart one-liners for both. Pasting the juicy parts.

    3:1

    RegExp(r"mul\((\d*),(\d*)\)").allMatches(input).fold<int>( 0, (p, e) => p + e.groups([1, 2]).fold(1, (p, f) => p * int.parse(f!))));

    3:2

    My original solution found do, don’t and mul entries, then stepped through them to get the solve. I decided to try regex my way through it. What I realised was that you want to ignore strings starting with don’t() and ending at the first do(). Some amount of trial and error later, I figured out the (ecma*) regex to do it, which I am proud of:

    RegExp(r"(?:don\'t\(\)(?:.(?<!do\(\)))*do\(\))|(?:mul\((\d*),(\d*)\))") .allMatches(input) .fold<int>( 0, (p, e) => p + (e.group(0)![0] != 'd' ? e.groups([1, 2]).fold<int>(1, (p, f) => p * int.parse(f!)) : 0))

    *ecma balls

  • Mii@awful.systems
    link
    fedilink
    English
    arrow-up
    3
    ·
    21 days ago

    Advent of Code is one of these things I wanna do every year and then I end up in fucking end-of-the-year crunch time every December and work for 10-12 hours and really don’t wanna code after work anymore.

    But hey, here’s a quick solution for day 1. Let’s see how far I make it.

    Day 1
    use strict;
    use List::Util qw( min max );
    
    open(FH, '<', $ARGV[0]) or die $!;
    
    my @left;
    my @right;
    
    while (<FH>) {
    	my @nums = split /\s+/, $_;
    	push(@left, $nums[0]);
    	push(@right, $nums[1]);
    }
    
    @left = sort { $b <=> $a } @left;
    @right = sort { $b <=> $a } @right;
    
    my $dist = 0;
    my $sim = 0;
    my $i = 0;
    
    foreach my $lnum (@left) {
    	$sim += $lnum * grep { $_ == $lnum } @right;
    
    	my $rnum = $right[$i++];
    	$dist += max($lnum, $rnum) - min($lnum, $rnum);
    }
    
    print 'Part 1: ', $dist, "\n";
    print 'Part 2: ', $sim, "\n";
    
    close(FH);
    
    • Mii@awful.systems
      link
      fedilink
      English
      arrow-up
      1
      ·
      13 days ago

      Yay, day 3 with Regexp magic.

      Day 3
      open(FH, '<', $ARGV[0]) or die $!;
      my $sum = 0;
      my $sum2 = 0;
      
      my $enabled = 1;
      
      while (<FH>) {
          while ($_ =~ /(?:mul\((\d{1,3}),(\d{1,3})\)|(do)\(\)|(don\'t)\(\))/g) {
              $enabled = 1 if $3;
              $enabled = 0 if $4;
              $sum += $1 * $2 if $1 && $2;
              $sum2 += $1 * $2 if $enabled && $1 && $2;
          }
      }
      
      close(FH);
      
      print "Part 1: $sum\n";
      print "Part 2: $sum2\n";
      
    • Mii@awful.systems
      link
      fedilink
      English
      arrow-up
      1
      ·
      20 days ago
      Day 2, Part 1
      use strict;
      use List::Util qw( min max );
      
      open(FH, '<', $ARGV[0]) or die $!;
      my @lines;
      while (<FH>) {
      	my @report = split /\s/, $_;
      	push @lines, \@report;
      }
      
      close FH;
      
      sub in_range {
      	my $diff = max($_[0], $_[1]) - min($_[0], $_[1]);
      	return $diff >= 1 && $diff <= 3;
      }
      
      sub is_safe {
      	my $prev = @$_[0];
      	my $dir = 0;
      
      	for (my $i = 1; $i < scalar @$_; ++$i) {
      		my $el = @$_[$i];
      		if ($el > $prev) {
      			return 0 unless $dir >= 0;
      			$dir = 1;
      		} elsif ($el < $prev) {
      			return 0 unless $dir <= 0;
      			$dir = -1;
      		}
      
      		return 0 unless in_range $prev, $el;
      		$prev = $el;
      	}
      
      	return 1;
      }
      
      sub part1 {
      	my $safe_reports = 0;
      
      	foreach (@_) {
      		$safe_reports++ if is_safe @$_;
      	}
      
      	return $safe_reports;
      }
      
      print 'Part 1: ', part1(@lines), "\n";
      

      My part 2 solution didn’t work with the real input but worked with all the test cases I threw at it, so I couldn’t figure out what was wrong with it and I’m too lazy to debug any more right now.

  • swlabr@awful.systems
    link
    fedilink
    English
    arrow-up
    3
    ·
    14 days ago

    Day 10. I lied about doing this later, I guess.

    p1, 2 I accidentally solved 2. before 1.

    My initial code was: for every 9, mark that square with a score of 1. Then: for (I = 8, then 7 … 0) => mark the square with the sum of the scores of the squares around it with a value of i + 1.

    Except that gives you all the ways to reach 9s from a 0, which is part 2. For part 1, I changed the scores to be sets of reachable 9s, and the score of a square was the size of the set at that position.

    • Architeuthis@awful.systemsOP
      link
      fedilink
      English
      arrow-up
      2
      ·
      14 days ago
      10 commentary

      Yeah basically if you were doing DFS and forgot to check if you’d already visited the next node you were solving for pt2, since the rule about the next node always having a value of +1 compared to the current one was already preventing cyclic paths.

      10 Code

      Hardly a groundbreaking implementation but I hadn’t posted actual code in a while so

      (* F# - file reading code and other boilerplate omited *)
      
      let mapAllTrails (matrix : int array2d) =
          let rowCount = matrix |> Array2D.length1
          let colCount = matrix |> Array2D.length2
      
          let rec search (current:int*int) (visited: HashSet<int*int>) (path: (int*int) list) : (int*int) list list= 
              let (row,col) = current
              let currentValue = matrix.[row,col]
      
              // Remove to solve for 10-2
              visited.Add (row,col) |> ignore
      
              // If on a 9 return the complete path
              if currentValue = 9 then [List.append path [row,col] ]
              // Otherwise filter for eligible neihboring cells and continue search
              else                    
                  [ row-1, col;row, col-1; row, col+1; row+1,col]
                  |> List.filter (fun (r,c) -> 
                      not (visited.Contains(r,c))
                      && r >= 0 && c>=0 && r < rowCount && c < colCount
                      && matrix.[r,c]-currentValue = 1 )
                  |> List.collect (fun next ->
                      [row,col] 
                      |> List.append path  
                      |> search next visited)
      
          // Find starting cells, i.e. contain 0
          matrix
          |> Global.matrixIndices
          |> Seq.filter (fun (row,col) -> matrix.[row,col] = 0)
          // Find all trails starting from those cells and flatten the result
          |> Seq.collect (fun trailhead -> search trailhead (HashSet<int*int>()) [])
          
      
      "./input.example"
      |> Common.parse 
      |> mapAllTrails
      |> Seq.length
      |> Global.shouldBe 81
      
      "./input.actual"
      |> Common.parse 
      |> mapAllTrails
      |> Seq.length
      |> printfn "The sum total of trail rankings is %d"
      
        • zogwarg@awful.systems
          link
          fedilink
          English
          arrow-up
          1
          ·
          14 days ago
          re:10

          Mwahaha I’m just lazy and did are “unique” (single word dropped for part 2) of start/end pairs.

          #!/usr/bin/env jq -n -R -f
          
          ([
               inputs/ "" | map(tonumber? // -1) | to_entries
           ] | to_entries | map( # '.' = -1 for handling examples #
               .key as $y | .value[]
             | .key as $x | .value   | { "\([$x,$y])":[[$x,$y],.] }
          )|add) as $grid | #           Get indexed grid          #
          
          [
            ($grid[]|select(last==0)) | [.] |    #   Start from every '0' head
            recurse(                             #
              .[-1][1] as $l |                   # Get altitude of current trail
              (                                  #
                .[-1][0]                         #
                | ( .[0] = (.[0] + (1,-1)) ),    #
                  ( .[1] = (.[1] + (1,-1)) )     #
              ) as $np |                         #   Get all possible +1 steps
              if $grid["\($np)"][1] != $l + 1 then
                empty                            #     Drop path if invalid
              else                               #
              . += [ $grid["\($np)"] ]           #     Build path if valid
              end                                #
            ) | select(last[1]==9)               #   Only keep complete trails
              | . |= [first,last]                #      Only Keep start/end
          ]
          
          # Get score = sum of unique start/end pairs.
          | group_by(first) | map(unique|length) | add
          
  • Sailor Sega Saturn@awful.systems
    link
    fedilink
    English
    arrow-up
    3
    ·
    edit-2
    19 days ago

    OK nerds, I was coerced into doing day five so I’m posting it here.

    spoiler

    stable sort with the ordering critera as the sort condition and it just works, either that or I got lucky inputs

    5-1 / 5-2
    #include <iostream>
    #include <vector>
    #include <algorithm>
    #include <sstream>
    #include <string>
    #include <unordered_map>
    
    std::unordered_multimap<int, int> ordering;
    
    bool sorted_before(int a, int b) {
      auto range = ordering.equal_range(a);
      for (auto it = range.first; it != range.second; ++it) {
        if (it->second == b) return true;
      }
      return false;
    }
    
    int main() {
      int sum = 0;
      std::string line;
      while (std::getline(std::cin, line) && !line.empty()) {
        int l, r;
        char c;
        std::istringstream iss(line);
        iss >> l >> c >> r;
        ordering.insert(std::make_pair(l,r));
      }
      while (std::getline(std::cin, line)) {
        std::istringstream iss(line);
        std::vector<int> pages;
        int page;
        while (iss >> page) {
          pages.push_back(page);
          iss.get();
        }   
        std::vector<int> sorted_pages = pages;
        std::stable_sort(sorted_pages.begin(), sorted_pages.end(), sorted_before);
        if (pages == sorted_pages) {  // Change to != for 5-2
          sum += sorted_pages[sorted_pages.size()/2];
        }
      }
      std::cout << "Sum: " << sum << std::endl;
    }
    
  • zogwarg@awful.systems
    link
    fedilink
    English
    arrow-up
    3
    ·
    16 days ago

    Day 8

    Al lot of grid index shuffling these past few days! Not too difficult yet though, will this year be gentler or much harsher later?

    Part 2 code in JQ
    #!/usr/bin/env jq -n -R -f
    
    [ inputs / "" ] | [.,.[0]|length] as [$H,$W] |
    
    #----- In bound selectors -----#
    def x: select(. >= 0 and . < $W);
    def y: select(. >= 0 and . < $H);
    
    reduce (
      [
        to_entries[] | .key as $y | .value |
        to_entries[] | .key as $x | .value |
        [ [$x,$y],. ]  | select(last!=".")
      ] | group_by(last)[] # Every antenna pair #
        | combinations(2)  | select(first < last)
    ) as [[[$ax,$ay]],[[$bx,$by]]] ({};
      # Assign linear anti-nodes #
      .[ range(-$H;$H) as $i | "\(
        [($ax+$i*($ax-$bx)|x), ($ay+$i*($ay-$by)|y)] | select(length==2)
      )"] = true
    ) | length