diff --git a/2022/README.md b/2022/README.md index cf6a50a44bb447019ceb05294436558b3539d541..8ca785acb2b8f5c76d679b80545213234ce68f34 100644 --- a/2022/README.md +++ b/2022/README.md @@ -617,11 +617,46 @@ The subproblems are I'm feeling a dynamic programming solution is in order. +Ugh. The search space is big. My initial approach allows valves to be re-opened. +Instead of keying to boolean, maybe use a bit vector. +This will require remembering which bit position corresponds to which valve. +Unfortunately, that'll require too much space/time to calculate the full solution space. + +Perhaps instead we focus on getting all the valves (with non-zero flow) open as soon as possible. +The particular order is affected by how much flow each contributes. +Once they're all open, we can kill the remaining time doing whatever. + +(not yet complete) + ### Part 2 ... ## Day 17 +- [The problem](https://adventofcode.com/2022/day/17) +- [The solution](src/main/java/edu/unl/cse/bohn/year2022/Day17.java) + +### Part 1 + +The subproblems are: +- Simulate the movements of the rock +- Detect collisions + +### Part 2 + +Processing a trillion rocks will require some changes. +Besides changing a few `int`s to `long`s, we'll need to keep our memory use under control. + +We can do this by "throwing away" a few rows from the bottom of the tunnel whenever the tunnel is blocked. + +No. That solves the memory problem but not the time problem. +Even if we could process 4 billion rocks per second (which we can't) then it would take a little over 4 minutes to +process 1 trillion rocks, and AOC solutions shouldn't take that long. + +... + +## Day 18 + (coming soon) diff --git a/2022/src/main/java/edu/unl/cse/bohn/year2022/Day16.java b/2022/src/main/java/edu/unl/cse/bohn/year2022/Day16.java index 572eb1d0dd302684786a53bbba6047ec8274fedf..033ace36d54023a140ea6bd408b7a8e924a0ba91 100644 --- a/2022/src/main/java/edu/unl/cse/bohn/year2022/Day16.java +++ b/2022/src/main/java/edu/unl/cse/bohn/year2022/Day16.java @@ -3,11 +3,13 @@ package edu.unl.cse.bohn.year2022; import edu.unl.cse.bohn.Puzzle; import java.util.*; +import java.util.stream.Collectors; -@SuppressWarnings("unused") +@SuppressWarnings({"unused"}) public class Day16 extends Puzzle { - public static final int TIME_REMAINING = 30; + public static final boolean verbose = true; + public static final int TIME_AVAILABLE = 30; public Day16(boolean isProductionReady) { super(isProductionReady); @@ -26,9 +28,15 @@ public class Day16 extends Puzzle { @Override public long computePart1(List<String> data) { - Valve.createValveNetwork(data); - String startingLocation = data.get(0).substring(6, 8); - return Valve.getOptimalFlow(TIME_REMAINING, "AA", false); + Map<ValvePair, Long> shortestDistances = Valve.createValveNetwork(data); + Valve startingLocation = Valve.getValve(data.get(0).substring(6, 8)); + Set<Valve> openValves = Valve.getValves().stream() + .filter(valve -> valve.getFlowRate() == 0) + .collect(Collectors.toSet()); + Set<Valve> closedValves = Valve.getValves().stream() + .filter(valve -> valve.getFlowRate() > 0) + .collect(Collectors.toSet()); + return findOptimalFlow(startingLocation, shortestDistances, TIME_AVAILABLE, closedValves, openValves); } @Override @@ -36,24 +44,53 @@ public class Day16 extends Puzzle { return 0; } + private long findOptimalFlow(Valve startingLocation, Map<ValvePair, Long> shortestDistances, long timeRemaining, + Set<Valve> closedValves, Set<Valve> openValves) { + if (verbose) { + System.out.println("At " + startingLocation + " with " + timeRemaining + " minutes left. Open: " + + openValves + " Closed: " + closedValves); + } + if (timeRemaining < 1) { + return 0L; + } + // with one minute remaining, even opening a valve would relieve no pressure. + // also, if all valves are open, might as well just kill time + long currentFlowRate = openValves.stream().mapToLong(Valve::getFlowRate).sum(); + if (timeRemaining == 1 || closedValves.isEmpty()) { + return timeRemaining * currentFlowRate; + } + long maximumFlow = Long.MIN_VALUE; + // we know how long it takes to get from here to anywhere else, and + // the only locations worth visiting are those with open valves -- + // which valve should we open next? + for (Valve valve : closedValves) { + Set<Valve> newClosedValves = new HashSet<>(closedValves); + newClosedValves.remove(valve); + Set<Valve> newOpenValves = new HashSet<>(openValves); + newOpenValves.add(valve); + // lose one minute to turn valve, and more time to travel to next location + long newTimeRemaining = timeRemaining - shortestDistances.get(new ValvePair(startingLocation, valve)) - 1; + if (newTimeRemaining > 0) { + long flowDuringTravel = (timeRemaining - newTimeRemaining) * currentFlowRate; + maximumFlow = Math.max(maximumFlow, + findOptimalFlow(valve, shortestDistances, newTimeRemaining, newClosedValves, newOpenValves)); + } + } + // would we have been better off walking in circles? + maximumFlow = Math.max(maximumFlow, timeRemaining * currentFlowRate); + return maximumFlow; + } + private static class Valve { - private static Map<String, Valve> valves = null; - private static Map<Integer, Map<String, Map<Boolean, Integer>>> optimalFlows = null; + private static final Set<Valve> valves = new HashSet<>(); + private static final Map<ValvePair, Long> shortestDistances = new HashMap<>(); - @SuppressWarnings("FieldCanBeLocal") private final String name; private final int flowRate; - private final Set<String> adjacentValves; - - private Valve(String name, int flowRate, String[] adjacentValves) { - this.name = name; - this.flowRate = flowRate; - this.adjacentValves = Set.of(adjacentValves); - } + private final Set<String> adjacentValveNames; + private Set<Valve> adjacentValves = null; - public static void createValveNetwork(List<String> descriptions) { - valves = new HashMap<>(); - optimalFlows = new HashMap<>(); + public static Map<ValvePair, Long> createValveNetwork(List<String> descriptions) { for (String description : descriptions) { String name = description.substring(6, 8); String[] descriptionHalves = description.split(";"); @@ -63,48 +100,75 @@ public class Day16 extends Puzzle { neighborsString = neighborsString.substring(2); } String[] adjacentValves = neighborsString.split(", "); - valves.put(name, new Valve(name, flowRate, adjacentValves)); + valves.add(new Valve(name, flowRate, adjacentValves)); } - } - - public static int getOptimalFlow(int timeRemaining, String valveName, boolean valveIsAlreadyOpen) { - if (optimalFlows == null) { - throw new IllegalStateException("Cannot calculate optimal flow without creating valve network first."); - } - int depthPreviouslyCalculated = optimalFlows.size(); - if (timeRemaining < depthPreviouslyCalculated) { - return optimalFlows.get(timeRemaining).get(valveName).get(valveIsAlreadyOpen); - } - /* TODO: incorporate time to determine "eventual total pressure release" */ - for (int t = depthPreviouslyCalculated; t <= timeRemaining; t++) { - HashMap<String, Map<Boolean, Integer>> depthOptima = new HashMap<>(); - for (String valve : valves.keySet()) { - HashMap<Boolean, Integer> valveOptima = new HashMap<>(); - if (t == 0) { - valveOptima.put(true, 0); - valveOptima.put(false, 0); - } else { - int depth = t - 1; - // if the valve is already open then we must move - int optimalFlow = valves.get(valve).adjacentValves.stream() - .map(name -> optimalFlows.get(depth).get(name).get(true)) - .max(Integer::compareTo).orElseThrow(); - valveOptima.put(true, optimalFlow); - // if the valve is still closed then... - // option 1: move to an adjacent valve - int optimalFlowByMoving = valves.get(valve).adjacentValves.stream() - .map(name -> optimalFlows.get(depth).get(name).get(false)) - .max(Integer::compareTo).orElseThrow(); - // option 2: turn the current valve - int optimalFlowByOpeningValve = valves.get(valve).flowRate - + optimalFlows.get(depth).get(valve).get(true); - valveOptima.put(false, Math.max(optimalFlowByMoving, optimalFlowByOpeningValve)); + // initially assume it takes forever to get from anywhere to anywhere +// Valve.valves.forEach(from -> +// valves.forEach(to -> +// shortestDistances.put(new ValvePair(from, to), Long.MAX_VALUE))); + // oops, that had an overflow problem during addition + Valve.valves.forEach(from -> + valves.forEach(to -> + shortestDistances.put(new ValvePair(from, to), 2L * valves.size()))); + // but we know some locations are just one minute apart + valves.forEach(from -> + from.getAdjacentValves().forEach(to -> + shortestDistances.put(new ValvePair(from, to), 1L))); + // just in case we want to go from where we are to the same location, why take a tunnel? + valves.forEach(valve -> shortestDistances.put(new ValvePair(valve, valve), 0L)); + // find the shortest travel time from anywhere to anywhere + for (Valve valve : valves) { + for (Valve from : valves) { + for (Valve to : valves) { + ValvePair valvePair = new ValvePair(from, to); + shortestDistances.put(valvePair, Math.min(shortestDistances.get(valvePair), + shortestDistances.get(new ValvePair(from, valve)) + + shortestDistances.get(new ValvePair(valve, to))) + ); } - depthOptima.put(valve, valveOptima); } - optimalFlows.put(t, depthOptima); } - return optimalFlows.get(timeRemaining).get(valveName).get(valveIsAlreadyOpen); + return shortestDistances; + } + + public static Set<Valve> getValves() { + return Collections.unmodifiableSet(valves); + } + + public static Valve getValve(String name) { + return valves.stream().filter(valve -> valve.name.equals(name)).findAny().orElse(null); + } + + private Valve(String name, int flowRate, String[] adjacentValves) { + this.name = name; + this.flowRate = flowRate; + this.adjacentValveNames = Set.of(adjacentValves); + } + + public String getName() { + return name; + } + + public int getFlowRate() { + return flowRate; + } + + public Set<Valve> getAdjacentValves() { + if (adjacentValves == null) { + adjacentValves = adjacentValveNames.stream() + .map(valveName -> + valves.stream().filter(v -> v.name.equals(valveName)).findFirst().orElseThrow()) + .collect(Collectors.toSet()); + } + return adjacentValves; } + + @Override + public String toString() { + return name + "(" + flowRate + ")"; + } + } + + private record ValvePair(Valve from, Valve to) { } } diff --git a/2022/src/main/java/edu/unl/cse/bohn/year2022/Day17.java b/2022/src/main/java/edu/unl/cse/bohn/year2022/Day17.java new file mode 100644 index 0000000000000000000000000000000000000000..02e89dffc54bb674b709b12b8e5cb8647b71c51a --- /dev/null +++ b/2022/src/main/java/edu/unl/cse/bohn/year2022/Day17.java @@ -0,0 +1,166 @@ +package edu.unl.cse.bohn.year2022; + +import edu.unl.cse.bohn.Puzzle; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.stream.IntStream; + +@SuppressWarnings("unused") +public class Day17 extends Puzzle { + public static final boolean veryVerbose = false; + public static final boolean verbose = false; + + public Day17(boolean isProductionReady) { + super(isProductionReady); + sampleData = ">>><<><>><<<>><>>><<<>>><<<><<<>><>><<>>"; + } + + public static final List<Sprite> sprites = List.of( + new Sprite(List.of("####")), + new Sprite(List.of(".#.", "###", ".#.")), + new Sprite(List.of("###", "..#", "..#")), + new Sprite(List.of("#", "#", "#", "#")), + new Sprite(List.of("##", "##")) + ); + + public static final int NUMBER_OF_ROCKS_TO_DROP = 2022; + + @Override + public long computePart1(List<String> data) { + List<Character> jets = data.get(0).chars().mapToObj(c -> (char) c).toList(); + return dropRocks(jets, NUMBER_OF_ROCKS_TO_DROP); + } + + @Override + public long computePart2(List<String> data) { + return 0; + } + + @SuppressWarnings({"OverlyLongMethod", "SameParameterValue"}) + private int dropRocks(List<Character> jets, int numberOfRocksToDrop) { + Iterator<Character> jetIterator = jets.iterator(); + Iterator<Sprite> spriteIterator = sprites.iterator(); + List<int[]> tunnel = new ArrayList<>(numberOfRocksToDrop * 3); + int[] floor = {1, 1, 1, 1, 1, 1, 1, 1, 1}; + int[] walls = {1, 0, 0, 0, 0, 0, 0, 0, 1}; + int height = 0; + final int horizontalOffset = 3; + final int verticalOffset = 4; + int x = 0, y = 0; + int[][] rock = null; + boolean rockIsAtRest = true; + int numberOfRocksAtRest = 0; + tunnel.add(floor); + while (numberOfRocksAtRest < numberOfRocksToDrop) { + if (rockIsAtRest) { + rockIsAtRest = false; + // get next rock + if (!spriteIterator.hasNext()) { + spriteIterator = sprites.iterator(); + } + rock = spriteIterator.next().rockOrNot; + // expand the tunnel as necessary + for (int i = tunnel.size(); i < height + rock.length + verticalOffset; i++) { + tunnel.add(walls.clone()); + } + // place the rock + x = horizontalOffset; + y = height + verticalOffset; + for (int i = 0; i < rock.length; i++) { + System.arraycopy(rock[i], 0, tunnel.get(y + i), x, rock[i].length); + } + if (veryVerbose || verbose) { + System.out.println(System.lineSeparator() + + "-- " + numberOfRocksAtRest + " rocks at rest -- new rock dropped --"); + IntStream.iterate(tunnel.size() - 1, i -> i >= 0, i -> i - 1) + .mapToObj(i -> Arrays.toString(tunnel.get(i))).forEach(System.out::println); + } + } else { + if (!jetIterator.hasNext()) { + jetIterator = jets.iterator(); + } + char jet = jetIterator.next(); + // horizontal motion + eraseRock(tunnel, rock, x, y); + x += jet == '>' ? 1 : -1; + drawRock(tunnel, rock, x, y); + boolean foundCollision = detectCollision(tunnel, rock, y); + if (foundCollision) { + eraseRock(tunnel, rock, x, y); + x += jet == '>' ? -1 : 1; + drawRock(tunnel, rock, x, y); + } + // vertical motion + eraseRock(tunnel, rock, x, y); + y--; + drawRock(tunnel, rock, x, y); + foundCollision = detectCollision(tunnel, rock, y); + if (foundCollision) { + eraseRock(tunnel, rock, x, y); + y++; + drawRock(tunnel, rock, x, y); + rockIsAtRest = true; + numberOfRocksAtRest++; + height = Math.max(height, y + rock.length - 1); + } + if (veryVerbose) { + System.out.println(System.lineSeparator() + jet); + IntStream.iterate(tunnel.size() - 1, i -> i >= 0, i -> i - 1) + .mapToObj(i -> Arrays.toString(tunnel.get(i))).forEach(System.out::println); + } + } + } + if (veryVerbose || verbose) { + System.out.println(System.lineSeparator() + + "-- " + numberOfRocksAtRest + " rocks at rest -- complete --"); + IntStream.iterate(tunnel.size() - 1, i -> i >= 0, i -> i - 1) + .mapToObj(i -> Arrays.toString(tunnel.get(i))).forEach(System.out::println); + } + return height; + } + + private static boolean detectCollision(List<int[]> tunnel, int[][] rock, int y) { + boolean foundCollision = false; + for (int i = 0; i < rock.length; i++) { + int[] row = tunnel.get(y + i); + for (int positionValue : row) { + foundCollision = foundCollision || (positionValue == 2); + } + } + return foundCollision; + } + + private static void drawRock(List<int[]> tunnel, int[][] rock, int x, int y) { + for (int i = 0; i < rock.length; i++) { + int[] row = tunnel.get(y + i); + for (int j = 0; j < rock[i].length; j++) { + row[x + j] += rock[i][j]; + } + } + } + + private static void eraseRock(List<int[]> tunnel, int[][] rock, int x, int y) { + for (int i = 0; i < rock.length; i++) { + int[] row = tunnel.get(y + i); + for (int j = 0; j < rock[i].length; j++) { + row[x + j] -= rock[i][j]; + } + } + } + + private static class Sprite { + public final int[][] rockOrNot; + + public Sprite(List<String> description) { + rockOrNot = new int[description.size()][description.get(0).length()]; + for (int i = 0; i < description.size(); i++) { + for (int j = 0; j < description.get(i).length(); j++) { + rockOrNot[i][j] = description.get(i).charAt(j) == '#' ? 1 : 0; + } + } + } + } +}