TypeScript · Course navigation

TypeScript: a generic breadth-first search solves the water-jug puzzle

Write the search once for any state type and apply it to two jugs.

On this page
Before you start

TypeScript generics and arrays.

  • Write the search once for any state type and apply it to two jugs.
  • Predict the sample output and explain the state changes.
  • Complete the practice task and check the boundary cases.

The concept

State-space search separates the problem from the algorithm. The problem supplies a start state, a goal test, and a function that lists the next states. The search supplies the order of exploration. With generics, one breadth-first search works for any state type S, as long as the caller gives a key function that turns a state into a string so visited states can be remembered.

The puzzle has a 3-litre jug and a 5-litre jug and no markings. The goal is to have exactly 4 litres in one of them. A move fills a jug, empties a jug, or pours one into the other until the target is full or the source is empty. Because BFS expands states by number of moves, the first solution it finds uses the fewest moves.

Code example

TypeScript
function bfs<S>(start: S, isGoal: (s: S) => boolean, next: (s: S) => S[], key: (s: S) => string): S[] | null {
  const queue: S[][] = [[start]];
  const seen = new Set<string>([key(start)]);
  while (queue.length > 0) {
    const path = queue.shift()!;
    const state = path[path.length - 1];
    if (isGoal(state)) {
      return path;
    }
    for (const candidate of next(state)) {
      if (!seen.has(key(candidate))) {
        seen.add(key(candidate));
        queue.push([...path, candidate]);
      }
    }
  }
  return null;
}

type Jugs = [number, number];
const capacity: Jugs = [3, 5];

function moves([a, b]: Jugs): Jugs[] {
  const pourAB = Math.min(a, capacity[1] - b);
  const pourBA = Math.min(b, capacity[0] - a);
  return [
    [capacity[0], b],
    [a, capacity[1]],
    [0, b],
    [a, 0],
    [a - pourAB, b + pourAB],
    [a + pourBA, b - pourBA],
  ];
}

const path = bfs<Jugs>([0, 0], ([a, b]) => a === 4 || b === 4, moves, ([a, b]) => a + "," + b);
console.log(path!.map(([a, b]) => "(" + a + "," + b + ")").join(" "));
console.log("moves: " + (path!.length - 1));
Output
(0,0) (0,5) (3,2) (0,2) (2,0) (2,5) (3,4)
moves: 6

Follow the code

Open the walkthrough to inspect the example one step at a time.

Show guided walkthrough
Guided walkthroughReady
1function bfs<S>(start: S, isGoal: (s: S) => boolean, next: (s: S) => S[], key: (s: S) => string): S[] | null {
2 const queue: S[][] = [[start]];
3 const seen = new Set<string>([key(start)]);
4 while (queue.length > 0) {
5 const path = queue.shift()!;
6 const state = path[path.length - 1];
7 if (isGoal(state)) {
8 return path;
9 }
10 for (const candidate of next(state)) {
11 if (!seen.has(key(candidate))) {
12 seen.add(key(candidate));
13 queue.push([...path, candidate]);
14 }
15 }
16 }
17 return null;
18}
19
20type Jugs = [number, number];
21const capacity: Jugs = [3, 5];
22
23function moves([a, b]: Jugs): Jugs[] {
24 const pourAB = Math.min(a, capacity[1] - b);
25 const pourBA = Math.min(b, capacity[0] - a);
26 return [
27 [capacity[0], b],
28 [a, capacity[1]],
29 [0, b],
30 [a, 0],
31 [a - pourAB, b + pourAB],
32 [a + pourBA, b - pourBA],
33 ];
34}
35
36const path = bfs<Jugs>([0, 0], ([a, b]) => a === 4 || b === 4, moves, ([a, b]) => a + "," + b);
37console.log(path!.map(([a, b]) => "(" + a + "," + b + ")").join(" "));
38console.log("moves: " + (path!.length - 1));

Follow the example

Start to inspect the source, values, and output. These prepared steps explain the published sample; grouped loop steps describe multiple iterations.

View the complete dry-run table
Prepared trace of the published program. Some rows group repeated iterations.
Step / lineWhat happensState after stepOutput so far
The search knows nothing about jugs. It works for any state type S.—
—
Pouring stops when the source is empty or the target is full.—
—
A state reached before is not queued again.—
—
The sequence of states from empty jugs to 4 litres.—
(0,0) (0,5) (3,2) (0,2) (2,0) (2,5) (3,4)
Six moves is the minimum for this puzzle.—
(0,0) (0,5) (3,2) (0,2) (2,0) (2,5) (3,4)
moves: 6

Behind the scenes

How TypeScript processes this code

TypeScript checks types and emits JavaScript. Type annotations are removed before execution.

Cost and common mistakes

O(S) states visited; there are at most 4 × 6 = 24 states here.

  • Without the visited set, the search would loop forever, refilling and emptying the same jugs.
  • The puzzle has a solution only when the target is a multiple of the greatest common divisor of the two capacities and fits in the larger jug.
  • queue.shift() is O(n) for long arrays. A real queue (or an index pointer) is better for large state spaces.

Try it yourself

Is 4 litres reachable with jugs of 2 and 6 litres?

Reveal answer and reasoning

Yes. The greatest common divisor of 2 and 6 is 2, 4 is a multiple of 2, and 4 fits in the 6-litre jug: fill the 6-litre jug and pour 2 litres into the 2-litre jug, which leaves 4.

Further reading