Card 03/ 08
The State Search Template in Python, Java and TypeScript
One breadth-first search that takes a start, a goal test and a function that lists the next states, and returns the route by following parents back.
The state search template is a breadth-first search that knows nothing about the problem: it takes a start state, a test for the goal and a function that lists the states one move away, so a new problem needs only those two functions. A parent map records, for each state reached, the state it was reached from; it doubles as the visited set, since a state is in it exactly when it has been reached. When the goal comes out of the queue, the route is rebuilt by following parents back to the start. The highlighted lines are where the goal test and the neighbours function are called. Each listing runs on the translators: from Portuguese to Korean, the neighbours of a language being the languages one translator away.
The state search template in three languages
from collections import deque
from collections.abc import Callable, Hashable, Iterable
def shortest_route[S: Hashable](start: S, is_goal: Callable[[S], bool], next_states: Callable[[S], Iterable[S]]) -> list[S] | None:
parent: dict[S, S | None] = {start: None}
queue = deque([start])
while queue:
state = queue.popleft()
if is_goal(state):
route = [state]
while (previous := parent[route[-1]]) is not None:
route.append(previous)
return route[::-1]
for following in next_states(state):
if following not in parent:
parent[following] = state
queue.append(following)
return None
pairs = [
("Portuguese", "Spanish"), ("Portuguese", "French"), ("Spanish", "English"),
("French", "German"), ("English", "Japanese"), ("German", "Japanese"),
("Japanese", "Korean"), ("English", "Korean"),
]
neighbours: dict[str, list[str]] = {}
for first, second in pairs:
neighbours.setdefault(first, []).append(second)
neighbours.setdefault(second, []).append(first)
route = shortest_route("Portuguese", lambda language: language == "Korean", lambda language: neighbours[language])
print(" -> ".join(route), f"({len(route) - 1} translations)")Portuguese -> Spanish -> English -> Korean (3 translations)import java.util.*;
import java.util.function.Function;
import java.util.function.Predicate;
public class StateSearch {
static <S> List<S> shortestRoute(S start, Predicate<S> isGoal, Function<S, List<S>> nextStates) {
Map<S, S> parent = new HashMap<>();
parent.put(start, null);
Deque<S> queue = new ArrayDeque<>(List.of(start));
while (!queue.isEmpty()) {
S state = queue.poll();
if (isGoal.test(state)) {
List<S> route = new ArrayList<>();
for (S step = state; step != null; step = parent.get(step)) route.add(step);
Collections.reverse(route);
return route;
}
for (S following : nextStates.apply(state)) {
if (!parent.containsKey(following)) {
parent.put(following, state);
queue.add(following);
}
}
}
return null;
}
public static void main(String[] args) {
String[][] pairs = {
{"Portuguese", "Spanish"}, {"Portuguese", "French"}, {"Spanish", "English"},
{"French", "German"}, {"English", "Japanese"}, {"German", "Japanese"},
{"Japanese", "Korean"}, {"English", "Korean"},
};
Map<String, List<String>> neighbours = new HashMap<>();
for (String[] pair : pairs) {
neighbours.computeIfAbsent(pair[0], k -> new ArrayList<>()).add(pair[1]);
neighbours.computeIfAbsent(pair[1], k -> new ArrayList<>()).add(pair[0]);
}
List<String> route = shortestRoute("Portuguese", language -> language.equals("Korean"), neighbours::get);
System.out.println(String.join(" -> ", route) + " (" + (route.size() - 1) + " translations)");
}
}Portuguese -> Spanish -> English -> Korean (3 translations)function shortestRoute(start: string, isGoal: (state: string) => boolean, nextStates: (state: string) => string[]): string[] | null {
const parent = new Map<string, string | null>([[start, null]]);
const queue = [start];
let head = 0;
while (head < queue.length) {
const state = queue[head++];
if (isGoal(state)) {
const route: string[] = [];
for (let step: string | null = state; step !== null; step = parent.get(step) ?? null) route.push(step);
return route.reverse();
}
for (const following of nextStates(state)) {
if (!parent.has(following)) {
parent.set(following, state);
queue.push(following);
}
}
}
return null;
}
const pairs: [string, string][] = [
["Portuguese", "Spanish"], ["Portuguese", "French"], ["Spanish", "English"],
["French", "German"], ["English", "Japanese"], ["German", "Japanese"],
["Japanese", "Korean"], ["English", "Korean"],
];
const neighbours = new Map<string, string[]>();
for (const [first, second] of pairs) {
neighbours.set(first, [...(neighbours.get(first) ?? []), second]);
neighbours.set(second, [...(neighbours.get(second) ?? []), first]);
}
const route = shortestRoute("Portuguese", (language) => language === "Korean", (language) => neighbours.get(language) ?? []);
if (route) console.log(`${route.join(" -> ")} (${route.length - 1} translations)`);Portuguese -> Spanish -> English -> Korean (3 translations)A state must work as a key in the parent map, so two equal states must count as one. In Python a tuple such as (3, 2) does; in Java a record State(int small, int large) gets equals and hashCode written for it, from Java 16; TypeScript's Map compares objects by identity, so two equal states written as objects would both be kept, and the template takes states as strings such as "3,2" instead. Java's start state has a null parent, which HashMap allows, and containsKey rather than get tells reached from unreached. The search is O(S · m) time for S reachable states with m moves each, and O(S) space for the parent map and the queue.
← → move through sections and cards