From 2b3ac36ea45203a83b025a11e88b895d48606b12 Mon Sep 17 00:00:00 2001
From: Christopher Bohn <bohn@unl.edu>
Date: Fri, 16 Dec 2022 17:54:32 -0600
Subject: [PATCH] Completed Year 2022 Day 15

---
 2022/README.md                                |  25 ++--
 .../java/edu/unl/cse/bohn/year2022/Day15.java | 107 ++++++++++++++++--
 2 files changed, 117 insertions(+), 15 deletions(-)

diff --git a/2022/README.md b/2022/README.md
index c8dbb38..80c3a00 100644
--- a/2022/README.md
+++ b/2022/README.md
@@ -580,18 +580,29 @@ The subproblems are
 - Determine which positions cannot contain a beacon (*i.e.*, which positions are no closer to the sensor
   than the beacon)
 
-Creating many, many beacons would seem to be untenable from a memory perspective.
-I think we're going to have to dynamically build the map and then forget which sensor detected which beacon.
-This is going to be a PITA Linked List of Linked Lists.
-*Or*, I could make two passes, one to determine the matrix's dimensions, and one to populate it.
-
-Okay, the problem (unsurprisingly) wasn't all the Sensor data; it was the 6,862,736 x 6,004,906 matrix.
+Creating a 6,862,736 x 6,004,906 matrix is a bit of a memory hog.
 What? You don't have several terabytes just lying around?
 Clearly we need to go *back* to tracking the Sensor data and dynamically determine whether a given position is covered.
 
 ### Part 2
 
-...
+Hah! My CSCE 231 students who remember how to compute an address in a nested array will recognize what's happening here.
+
+Our task is to find the one location that is not covered by a sensor and then compute its linear position in a nested
+array -- er, um -- to compute its *tuning frequency*.
+
+Is there a smarter (faster) way than iterating over 16 trillion positions? Probably. What do we know?
+
+For any given sensor at (x,y) whose nearest sensor is distance d away, the sensor covers the square
+(x-d/2,y-d/2) through (x+d/2,y+d/2) plus several other locations.
+While 16 trillion locations is a bit much, we could handle 4 million rows, where each row is a set of covered ranges.
+When we look for that *one* uncovered spot, we only have to examine the rows -- or rather, *row*, that isn't fully
+covered by a range.
+
+### Refactoring opportunity
+
+The ranges could also be used to solve part 1 (no surprise there).
+I don't think I'll do that, though, since I can optimize the part 2 solution for non-negative indices.
 
 ## Day 16
 
diff --git a/2022/src/main/java/edu/unl/cse/bohn/year2022/Day15.java b/2022/src/main/java/edu/unl/cse/bohn/year2022/Day15.java
index dfbed89..d371996 100644
--- a/2022/src/main/java/edu/unl/cse/bohn/year2022/Day15.java
+++ b/2022/src/main/java/edu/unl/cse/bohn/year2022/Day15.java
@@ -2,13 +2,15 @@ package edu.unl.cse.bohn.year2022;
 
 import edu.unl.cse.bohn.Puzzle;
 
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
+import java.util.*;
 import java.util.stream.IntStream;
 
 @SuppressWarnings({"unused"})
 public class Day15 extends Puzzle {
+
+    public static final int FULL_SIZE_PROBLEM = 4000000;
+    public static final int SAMPLE_SIZE_PROBLEM = 20;
+
     public Day15(boolean isProductionReady) {
         super(isProductionReady);
         sampleData = """
@@ -33,13 +35,63 @@ public class Day15 extends Puzzle {
         for (String datum : data) {
             new Sensor(datum);
         }
-        //noinspection MagicNumber
-        return Sensor.countLocationsThatCannotHaveBeacon(isProductionReady ? 2000000 : 10);
+        return Sensor.countLocationsThatCannotHaveBeacon(isProductionReady
+                ? FULL_SIZE_PROBLEM / 2
+                : SAMPLE_SIZE_PROBLEM / 2);
     }
 
+    @SuppressWarnings("OverlyLongMethod")
     @Override
     public long computePart2(List<String> data) {
-        return 0;
+        // I'm not going to foolproof this. I'll just assume (correctly) that part 1 is executed before part 2
+        long upperLimit = isProductionReady ? FULL_SIZE_PROBLEM : SAMPLE_SIZE_PROBLEM;
+        List<Set<Range>> rows = new ArrayList<>(FULL_SIZE_PROBLEM + 1);
+        for (int i = 0; i <= FULL_SIZE_PROBLEM; i++) {
+            rows.add(new HashSet<>());
+        }
+        for (Sensor sensor : Sensor.sensors) {  // breaking encapsulation a little
+            for (int y = sensor.getLeastY(); y <= sensor.getGreatestY(); y++) {
+                if (y >= 0 && y <= FULL_SIZE_PROBLEM) {
+                    Set<Range> row = rows.get(y);
+                    Range newRange = new Range(Math.max(0, sensor.getMinimumX(y)),
+                            Math.min(FULL_SIZE_PROBLEM, sensor.getMaximumX(y)));
+                    if (row.stream().noneMatch(newRange::isFullyEnclosedBy)) {
+                        row.stream().filter(range -> range.isFullyEnclosedBy(newRange))
+                                .findAny().ifPresent(row::remove);
+                        row.add(newRange);
+                    }
+                }
+            }
+        }
+        // Each row is now a set of overlapping ranges (with none fully enclosed by another)
+        // Let's merge!
+        Integer incompleteRowNumber = null;
+        Range nonOverlappingRange = null;
+        for (int rowNumber = 0; rowNumber <= upperLimit; rowNumber++) {
+            Set<Range> row = rows.get(rowNumber);
+            while (row.size() > 1 && incompleteRowNumber == null) {
+                Range range = row.stream().findAny().orElseThrow();
+                row.remove(range);
+                Range overlappingRange = row.stream().filter(range::overlaps).findAny().orElse(null);
+                if (overlappingRange == null) {
+                    nonOverlappingRange = range;
+                    incompleteRowNumber = rowNumber;
+                } else {
+                    row.remove(overlappingRange);
+                    row.add(Range.merge(range, overlappingRange));
+                }
+            }
+        }
+        assert incompleteRowNumber != null;
+        Range range = rows.get(incompleteRowNumber).stream().findAny().orElseThrow();
+        long y = (long)incompleteRowNumber;
+        long x;
+        if (range.maximumX < nonOverlappingRange.minimumX) {
+            x = range.maximumX + 1;
+        } else {
+            x = nonOverlappingRange.maximumX + 1;
+        }
+        return FULL_SIZE_PROBLEM * x + y;
     }
 
     private static class Sensor {
@@ -48,7 +100,6 @@ public class Day15 extends Puzzle {
 
         private static final Set<Sensor> sensors = new HashSet<>();
 
-        //        private static boolean[][] mightContainAnUnknownBeacon = null;
         private static int minimumX = Integer.MAX_VALUE;
         private static int maximumX = Integer.MIN_VALUE;
         private static int minimumY = Integer.MAX_VALUE;
@@ -84,6 +135,28 @@ public class Day15 extends Puzzle {
             return getDistanceToLocation(x, y) <= getDistanceToBeacon();
         }
 
+        public int getMinimumX(int y) {
+            int beaconDistance = getDistanceToBeacon();
+            int rowDistance = Math.abs(this.y - y);
+            int leftoverDistance = beaconDistance - rowDistance;
+            return this.x - leftoverDistance;
+        }
+
+        public int getMaximumX(int y) {
+            int beaconDistance = getDistanceToBeacon();
+            int rowDistance = Math.abs(this.y - y);
+            int leftoverDistance = beaconDistance - rowDistance;
+            return this.x + leftoverDistance;
+        }
+
+        public int getLeastY() {
+            return this.y - getDistanceToBeacon();
+        }
+
+        public int getGreatestY() {
+            return this.y + getDistanceToBeacon();
+        }
+
         @Override
         public String toString() {
             return "Sensor at x=" + x + ", y=" + y + ": closest beacon is at x=" + nearestBeaconX
@@ -112,7 +185,7 @@ public class Day15 extends Puzzle {
         }
 
         public static boolean isCoveredBySensor(int x, int y) {
-            return sensors.stream().anyMatch(sensor -> sensor.isNoFartherThanBeacon(x,y));
+            return sensors.stream().anyMatch(sensor -> sensor.isNoFartherThanBeacon(x, y));
         }
 
         public static int countLocationsThatCannotHaveBeacon(int y) {
@@ -123,4 +196,22 @@ public class Day15 extends Puzzle {
             return locationsInRangeOfSensor - locationsWithKnownBeacon;
         }
     }
+
+    private record Range(int minimumX, int maximumX) {
+        public boolean overlaps(Range other) {
+            return ((other.minimumX <= this.maximumX) && (this.maximumX <= other.maximumX))
+                    || ((this.minimumX <= other.maximumX) && (other.maximumX <= this.maximumX));
+        }
+
+        public boolean isFullyEnclosedBy(Range other) {
+            return (this.minimumX >= other.minimumX) && (this.maximumX <= other.maximumX);
+        }
+
+        public static Range merge(Range range1, Range range2) {
+            return range1.overlaps(range2)
+                    ? new Range(Integer.min(range1.minimumX, range2.minimumX),
+                                Integer.max(range1.maximumX, range2.maximumX))
+                    : null;
+        }
+    }
 }
-- 
GitLab