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