diff --git a/2022/README.md b/2022/README.md
index 80c3a009b87455d1e5f6543fe58d11d59ee1b5d1..cf6a50a44bb447019ceb05294436558b3539d541 100644
--- a/2022/README.md
+++ b/2022/README.md
@@ -606,5 +606,22 @@ I don't think I'll do that, though, since I can optimize the part 2 solution for
 
 ## Day 16
 
+- [The problem](https://adventofcode.com/2022/day/16)
+- [The solution](src/main/java/edu/unl/cse/bohn/year2022/Day16.java)
+
+### Part 1
+
+The subproblems are 
+- Parse the input to determine how much flow is possible with each valve and to create a graph of the tunnels
+- Find the (partial) walk that maximizes the flow within the allotted time
+
+I'm feeling a dynamic programming solution is in order.
+
+### Part 2
+
+...
+
+## Day 17
+
 (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
new file mode 100644
index 0000000000000000000000000000000000000000..572eb1d0dd302684786a53bbba6047ec8274fedf
--- /dev/null
+++ b/2022/src/main/java/edu/unl/cse/bohn/year2022/Day16.java
@@ -0,0 +1,110 @@
+package edu.unl.cse.bohn.year2022;
+
+import edu.unl.cse.bohn.Puzzle;
+
+import java.util.*;
+
+@SuppressWarnings("unused")
+public class Day16 extends Puzzle {
+
+    public static final int TIME_REMAINING = 30;
+
+    public Day16(boolean isProductionReady) {
+        super(isProductionReady);
+        sampleData = """
+                Valve AA has flow rate=0; tunnels lead to valves DD, II, BB
+                Valve BB has flow rate=13; tunnels lead to valves CC, AA
+                Valve CC has flow rate=2; tunnels lead to valves DD, BB
+                Valve DD has flow rate=20; tunnels lead to valves CC, AA, EE
+                Valve EE has flow rate=3; tunnels lead to valves FF, DD
+                Valve FF has flow rate=0; tunnels lead to valves EE, GG
+                Valve GG has flow rate=0; tunnels lead to valves FF, HH
+                Valve HH has flow rate=22; tunnel leads to valve GG
+                Valve II has flow rate=0; tunnels lead to valves AA, JJ
+                Valve JJ has flow rate=21; tunnel leads to valve II""";
+    }
+
+    @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);
+    }
+
+    @Override
+    public long computePart2(List<String> data) {
+        return 0;
+    }
+
+    private static class Valve {
+        private static Map<String, Valve> valves = null;
+        private static Map<Integer, Map<String, Map<Boolean, Integer>>> optimalFlows = null;
+
+        @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);
+        }
+
+        public static void createValveNetwork(List<String> descriptions) {
+            valves = new HashMap<>();
+            optimalFlows = new HashMap<>();
+            for (String description : descriptions) {
+                String name = description.substring(6, 8);
+                String[] descriptionHalves = description.split(";");
+                int flowRate = Integer.parseInt(descriptionHalves[0].split("=")[1]);
+                String neighborsString = descriptionHalves[1].split("valve")[1].strip();
+                if (neighborsString.startsWith("s ")) {
+                    neighborsString = neighborsString.substring(2);
+                }
+                String[] adjacentValves = neighborsString.split(", ");
+                valves.put(name, 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));
+                    }
+                    depthOptima.put(valve, valveOptima);
+                }
+                optimalFlows.put(t, depthOptima);
+            }
+            return optimalFlows.get(timeRemaining).get(valveName).get(valveIsAlreadyOpen);
+        }
+    }
+}