Skip to content

Commit

Permalink
day 18 notes
Browse files Browse the repository at this point in the history
  • Loading branch information
azizj1 committed Jan 6, 2020
1 parent 1e060c9 commit c266a6b
Show file tree
Hide file tree
Showing 6 changed files with 735 additions and 227 deletions.
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@

## 2019
Worth taking notes on:
* Day 2a and b
* Day 6 a and b
* [Day 2b - System of Equations](src/2019/2b.md)
* Day 12 a and b
* Day 14 for sure!
* Day 15 - the trick with this is to not go backwards until you have to!
* Day 15 - the trick with this is to not go backwards until you have to!
* [Day 18a - Robots Unlocking Keys and Doors](src/2019/18.md)
* Dijkstra's algorithm
* [Day 18b - Multiple Robots Unlocking Keys and Doors](src/2019/18b.md)
* Combinations with Dijkstra.

263 changes: 261 additions & 2 deletions src/2019/18.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Day 18: Many-Worlds Interpretation
# Day 18: Many-Worlds Interpretation - [Code](18.ts)
Only one **entrance** (marked `@`) is present among the **open passages** (marked `.`) and **stone walls** (`#`), but you also detect an assortment of **keys** (shown as lowercase letters) and **doors** (shown as uppercase letters). Keys of a given letter open the door of the same letter: `a` opens `A`, `b` opens `B`, and so on. You aren't sure which key you need to disable the tractor beam, so you'll need to collect all of them.

For example, suppose you have the following map:
Expand Down Expand Up @@ -102,4 +102,263 @@ In your [input](18.txt), how many steps is the shortest path that collects all o
## Solution
I learned the hard way that when trying to find the shortest distance between two points in a graph or a grid, **do not use DFS!** You have to use a special type of BFS. In BFS, we use a Queue, right? Well, in this one, we use a *Priority Queue* (or a *Heap*), which makes it a **Dijkstra's Algorithm**.

The solution is broken up into two parts
The solution is broken up into two parts
1. For each `key1` to `key2` pair, get
* **Minimum** Steps between `key1` and `key2`.
* Doors between `key1` and `key2`.
* Other keys inbetween `key1` and `key2`.
2. Starting from the `@` key, traverse any reachable keys from it, keeping track of the min steps taken. Stop when all keys have been traversed. This can be solved using DFS, BFS, or Dijkstra's. I went with Dijstra's.

Here's a pseudocode for step 2:
```
distanceToCollectKeys(currentKey, keys):
if keys is empty:
return 0
result := infinity
foreach key in reachable(keys):
d := distance(currentKey, key) + distanceToCollectKeys(key, keys - key)
result := min(result, d)
return result;
```

### Step 1: Key to Key Map
It's so important to do this via Dijkstra's instead of DFS. If you don't believe me, check out the the distance differences from '@' to all keys in the graph.

DFS
```
r: 16 l: 28 b: 42 p: 58 t: 78 q: 84 f: 100 x: 114 h: 132 g: 140 k: 158 m: 170 j: 182
u: 196 e: 208 d: 224 s: 244 c: 260 y: 270 i: 286 w: 296 v: 314 n: 328 o: 340 a: 354 z: 366
```

Dijkstra's Algorithm
```
r: 16 l: 28 b: 42 p: 54 t: 72 q: 80 f: 96 x: 110 h: 126 g: 138 k: 154 m: 170 j: 182
u: 196 e: 208 d: 222 s: 240 c: 254 y: 264 i: 280 w: 294 v: 308 n: 324 o: 336 a: 350 z: 362
```

```typescript
// will return all short AND LONG paths fromKey to toKey

const getNearestKeysMap = (tunnel: ITunnel) => {
const { keys, entrance } = tunnel;
return new Map(Array.from(keys.entries())
.map(([k, p]) => [k, getDistancesToAllKeys(p, tunnel)])
).set('@', getDistancesToAllKeys(entrance, tunnel));
};

const getDistancesToAllKeys = (from: IPoint, tunnel: IBaseTunnel) => {
const queue = new PriorityQueue<IPriorityQueueState>(p => -1 * p.distance);
const visited = new GenericSet<IPoint>(toString);
const result: IKeyToKeyInfo[] = [];
const isValid = makeIsValid(tunnel)(visited);
const addPointInfo = makeAddPointInfo(tunnel);

visited.add(from);
queue.enqueue({
point: from,
distance: 0,
keysObtained: new GenericSet(s => s),
doorsInWay: new GenericSet(s => s)
});

while (!queue.isEmpty()) {
const {point, distance, keysObtained, doorsInWay} = queue.dequeue()!;
const neighbors = getNeighbors(point)
.filter(isValid)
.map(addPointInfo);

for (let i = 0; i < neighbors.length; i++) {
const neighbor = neighbors[i];
const newKeysObtained = i === neighbors.length - 1 ? keysObtained : new GenericSet(keysObtained);
const newDoorsInWay = i === neighbors.length - 1 ? doorsInWay : new GenericSet(doorsInWay);
if (neighbor.door != null)
newDoorsInWay.add(neighbor.door);
if (neighbor.key != null)
newKeysObtained.add(neighbor.key);

visited.add(neighbor.point);
queue.enqueue({
point: neighbor.point,
distance: distance + 1,
keysObtained: newKeysObtained,
doorsInWay: newDoorsInWay
});

if (neighbor.key != null) {
const keysInWay = new GenericSet(keysObtained);
keysInWay.delete(neighbor.key); // don't include toKey in keysInWay
result.push({
toKey: neighbor.key,
steps: distance + 1,
doorsInWay: new GenericSet(doorsInWay),
keysInWay
});
}
}
}
return result;
};
```

Utility functions for the method above:
```typescript
interface IPriorityQueueState {
point: IPoint;
distance: number;
keysObtained: GenericSet<string>;
doorsInWay: GenericSet<string>;
}

interface IKeyToKeyInfo {
toKey: string;
steps: number;
doorsInWay: GenericSet<string>;
keysInWay: GenericSet<string>;
}

const getNeighbors = ({row, col}: IPoint, exclude?: IPoint | null): IPoint[] =>
exclude == null ?
[{row: row - 1, col}, {row: row + 1, col}, {row, col: col + 1}, {row, col: col - 1}] :
getNeighbors({row, col}, null).filter(p => p.row !== exclude.row || p.col !== exclude.col);

const makeIsValid = ({grid, keys, doors}: IBaseTunnel) => (visited: GenericSet<IPoint>) => (p: IPoint) => {
const cell = grid[p.row][p.col];
if (cell == null ||
visited.has(p) ||
cell !== '.' &&
cell !== '@' &&
!equals(keys.get(cell), p) && // stepping on a key is valid
!equals(doors.get(cell), p) // stepping on a door is valid. We don't care about doors right now
)
return false;
return true;
};

const makeAddPointInfo = ({grid, keys, doors}: IBaseTunnel) => (p: IPoint) => {
const cell = grid[p.row][p.col];
return {
point: p,
key: equals(p, keys.get(cell)) ? cell : null,
door: equals(p, doors.get(cell)) ? cell : null
};
};
```

There are cases where the above code won't do so well. E.g., imagine the grid

```
[2, 20 steps]
##########
#.a###.Ab#
#.B..@.###
#...######
##########
```
It'd say `@ -> a` equals `4`, and `@ -> b` equals `4`. However, each key requires the other key, resulting in failure.

To avoid this, the `getDistancesToAllKeys` would have to return the following:
* `@ -> a` equals `4`
* `@ -> a` equals `6`
* `@ -> a` equals `8`
* `@ -> b` equals `4`

We can do this by removing the `visited` Set from the equation, allowing us to find all routes. However, since it passes the test case without this issue, we won't add this constraint.

### Step 2: Traversing the Transformed Map
Now that we have transformed the plane from a grid to a graph of keys to keys, we can solve this using any graph traversal we want. I implemented all three solutions. Here's what I learned
* Dijkstra was the fastest, then DFS, then BFS.
* BFS and Dijkstra's are almost identical. The key differences are:
* BFS uses a normal queue, Dijkstra's uses a PriorityQueue, dequeuing the element with the fewest steps first.
* Because of this, Dijkstra can terminate as soon as it finds a solution, BFS has to traverse the entire map.
* DFS also has the traverse the entire graph.

Here is the Dijkstra and BFS solution (see comments to see how to convert it to BFS):

```typescript
const solve = (keytoKeyMap: Map<string, IKeyToKeyInfo[]>) => {
type QueueState = {totalSteps: number; keysObtained: string; atKey: string};
// const queue = new Queue<QueueState>(); FOR BFS
const queue = new PriorityQueue<QueueState>(p => -1 * p.totalSteps);
const visited = new Map<string, number>();
const getReachableKeys = makeGetReachableKeys(keytoKeyMap);
let minSteps = Infinity;

queue.enqueue({totalSteps: 0, keysObtained: '', atKey: '@'});
while (!queue.isEmpty()) {
const { totalSteps, keysObtained, atKey } = queue.dequeue()!;
if (keysObtained.length === keytoKeyMap.size - 1) {
minSteps = Math.min(totalSteps, minSteps);
return minSteps; // for BFS, remove this line.
}

const reachableKeys = getReachableKeys(atKey, new GenericSet(k => k, [...keysObtained]));
for (const key of reachableKeys) {
const newKeysObtained = keysObtained + key.toKey;
const newCacheKey = getCacheKey(key.toKey, newKeysObtained);
const newTotalSteps = totalSteps + key.steps;
// the visited.get(newCacheKey)! > newTotalSteps is crucial, because you may find a later route that has
// fewer steps
if (!visited.has(newCacheKey) || visited.get(newCacheKey)! > newTotalSteps) {
queue.enqueue({
totalSteps: newTotalSteps,
atKey: key.toKey,
keysObtained: newKeysObtained
});
visited.set(newCacheKey, newTotalSteps);
}
}
}
return minSteps;
};
```

And lastly, the DFS solution is as follows:

```typescript
const solve = (keyToKeyMap: Map<string, IKeyToKeyInfo[]>) => {
const getReachableKeys = makeGetReachableKeys(keyToKeyMap);
const cache: {[key: string]: number} = {};

const helper = (fromKey: string, keysObtained: string): number => {
if (keysObtained.length === keyToKeyMap.size - 1) {
return 0;
} // minus 1 because @ is in keysToKeysSteps
const cacheKey = getCacheKey(fromKey, keysObtained);
if (cache[cacheKey] != null)
return cache[cacheKey];
// has to be uppercased so it can be compared against doors
const reachableKeys = getReachableKeys(fromKey, new GenericSet(k => k, [...keysObtained]));
let totalSteps = Infinity;
for (const reachableKey of reachableKeys) {
const steps =
reachableKey.steps +
Math.min(totalSteps, helper(reachableKey.toKey, keysObtained + reachableKey.toKey));
totalSteps = Math.min(totalSteps, steps);
}
cache[cacheKey] = totalSteps;
return totalSteps;
};

return helper('@', '');
};
```

Helper functions used by both:

```typescript
export const makeGetReachableKeys =
(keyToKeyMap: Map<string, IKeyToKeyInfo[]>) =>
(fromKey: string, keysObtained: GenericSet<string>) =>
keyToKeyMap.get(fromKey)
?.filter(k => !keysObtained.has(k.toKey))
?.filter(k => k.doorsInWay.subsetOf(keysObtained, k => k.toString().toLowerCase()))
?.filter(k => k.keysInWay.subsetOf(keysObtained)) ?? [];
// if you skip over keysInWay, you'e looking at a suboptiaml route because you'll examine routes
// like a -> c, when there is key b between a -> c

export const getCacheKey = (fromKey: string, keysObtained: string) =>
`${fromKey},${[...keysObtained].sort().toString()}`;
```
Loading

0 comments on commit c266a6b

Please sign in to comment.