Skip to main content

Tries for Searching Many Words

Editorial

Every word is its own search

Word Search asks whether one word lies on a board of letters, along a path of side-by-side cells that uses no cell twice. Word Search II hands you a list of words and asks which of them are on the board. The first solution almost everyone writes runs the one-word search once per word. Here it is against one search that carries a trie of all the words, on a 32 × 32 board and some thousands of words, the same in every language:

import time

x = 11                                        # the same board and words in every language


def next_number():
    global x
    x = x * 48271 % 2147483647
    return x


def letter():
    return "abcdefgh"[next_number() % 8]


N = 32
board = [[letter() for _ in range(N)] for _ in range(N)]
words = list(dict.fromkeys("".join(letter() for _ in range(4 + next_number() % 5)) for _ in range(5000)))
print(len(words), "words on a", N, "x", N, "board")

steps_one = 0


def search_one(r, c, word, k):                # Word Search: does word[k:] lie on a path from (r, c)?
    global steps_one
    if r < 0 or c < 0 or r >= N or c >= N or board[r][c] != word[k]:
        return False
    steps_one += 1
    if k == len(word) - 1:
        return True
    board[r][c] = "#"                         # used by this path
    found = (search_one(r + 1, c, word, k + 1) or search_one(r - 1, c, word, k + 1)
             or search_one(r, c + 1, word, k + 1) or search_one(r, c - 1, word, k + 1))
    board[r][c] = word[k]
    return found


def on_board(word):
    for r in range(N):
        for c in range(N):
            if search_one(r, c, word, 0):
                return True
    return False


start = time.perf_counter()
found_one = [w for w in words if on_board(w)]  # one search per word
one_ms = round((time.perf_counter() - start) * 1000)


class Node:
    def __init__(self):
        self.children = [None] * 26
        self.word = None                      # the word that ends here, until it's found


steps_trie = 0
found_trie = []


def walk(r, c, node):                         # one search for every word: carry the trie node along
    global steps_trie
    if r < 0 or c < 0 or r >= N or c >= N or board[r][c] == "#":
        return
    here = board[r][c]
    child = node.children[ord(here) - ord("a")]
    if child is None:                         # no word goes on with this letter: turn back
        return
    steps_trie += 1
    if child.word is not None:
        found_trie.append(child.word)
        child.word = None                     # found once is enough
    board[r][c] = "#"
    walk(r + 1, c, child)
    walk(r - 1, c, child)
    walk(r, c + 1, child)
    walk(r, c - 1, child)
    board[r][c] = here


start = time.perf_counter()
root = Node()
for w in words:
    node = root
    for ch in w:
        i = ord(ch) - ord("a")
        if node.children[i] is None:
            node.children[i] = Node()
        node = node.children[i]
    node.word = w
for r in range(N):
    for c in range(N):
        walk(r, c, root)
trie_ms = round((time.perf_counter() - start) * 1000)

same = "true" if sorted(found_one) == sorted(found_trie) else "false"
print(f"found: {len(found_one)} one word at a time, {len(found_trie)} with the trie, the same words: {same}")
print(f"cells stepped on: {steps_one} one word at a time, {steps_trie} with the trie")
print(f"time: {one_ms} ms one word at a time, {trie_ms} ms with the trie")

# 4891 words on a 32 x 32 board
# found: 2454 one word at a time, 2454 with the trie, the same words: true
# cells stepped on: 718589 one word at a time, 55040 with the trie
# time: 932 ms one word at a time, 75 ms with the trie
import java.util.*;

public class Main {
    static long x = 11;                              // the same board and words in every language
    static final int N = 32;
    static final char[][] board = new char[N][N];
    static long stepsOne = 0, stepsTrie = 0;
    static final List<String> foundTrie = new ArrayList<>();

    static int nextNumber() {
        x = x * 48271 % 2147483647;
        return (int) x;
    }

    static char letter() {
        return "abcdefgh".charAt(nextNumber() % 8);
    }

    static boolean searchOne(int r, int c, String word, int k) {   // Word Search: does word[k..] lie on a path from (r, c)?
        if (r < 0 || c < 0 || r >= N || c >= N || board[r][c] != word.charAt(k)) return false;
        stepsOne++;
        if (k == word.length() - 1) return true;
        board[r][c] = '#';                           // used by this path
        boolean found = searchOne(r + 1, c, word, k + 1) || searchOne(r - 1, c, word, k + 1)
            || searchOne(r, c + 1, word, k + 1) || searchOne(r, c - 1, word, k + 1);
        board[r][c] = word.charAt(k);
        return found;
    }

    static boolean onBoard(String word) {
        for (int r = 0; r < N; r++)
            for (int c = 0; c < N; c++)
                if (searchOne(r, c, word, 0)) return true;
        return false;
    }

    static class Node {
        final Node[] children = new Node[26];
        String word;                                 // the word that ends here, until it's found
    }

    static void walk(int r, int c, Node node) {      // one search for every word: carry the trie node along
        if (r < 0 || c < 0 || r >= N || c >= N || board[r][c] == '#') return;
        char here = board[r][c];
        Node child = node.children[here - 'a'];
        if (child == null) return;                   // no word goes on with this letter: turn back
        stepsTrie++;
        if (child.word != null) {
            foundTrie.add(child.word);
            child.word = null;                       // found once is enough
        }
        board[r][c] = '#';
        walk(r + 1, c, child);
        walk(r - 1, c, child);
        walk(r, c + 1, child);
        walk(r, c - 1, child);
        board[r][c] = here;
    }

    public static void main(String[] args) {
        for (int r = 0; r < N; r++)
            for (int c = 0; c < N; c++) board[r][c] = letter();
        LinkedHashSet<String> unique = new LinkedHashSet<>();
        for (int k = 0; k < 5000; k++) {
            int length = 4 + nextNumber() % 5;
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < length; i++) sb.append(letter());
            unique.add(sb.toString());
        }
        List<String> words = new ArrayList<>(unique);
        System.out.println(words.size() + " words on a " + N + " x " + N + " board");

        long start = System.nanoTime();
        List<String> foundOne = new ArrayList<>();
        for (String w : words) if (onBoard(w)) foundOne.add(w);   // one search per word
        long oneMs = (System.nanoTime() - start) / 1000000;

        start = System.nanoTime();
        Node root = new Node();
        for (String w : words) {
            Node node = root;
            for (char ch : w.toCharArray()) {
                if (node.children[ch - 'a'] == null) node.children[ch - 'a'] = new Node();
                node = node.children[ch - 'a'];
            }
            node.word = w;
        }
        for (int r = 0; r < N; r++)
            for (int c = 0; c < N; c++) walk(r, c, root);
        long trieMs = (System.nanoTime() - start) / 1000000;

        List<String> a = new ArrayList<>(foundOne), b = new ArrayList<>(foundTrie);
        Collections.sort(a);
        Collections.sort(b);
        System.out.println("found: " + foundOne.size() + " one word at a time, " + foundTrie.size()
            + " with the trie, the same words: " + a.equals(b));
        System.out.println("cells stepped on: " + stepsOne + " one word at a time, " + stepsTrie + " with the trie");
        System.out.println("time: " + oneMs + " ms one word at a time, " + trieMs + " ms with the trie");
    }
}

// 4891 words on a 32 x 32 board
// found: 2454 one word at a time, 2454 with the trie, the same words: true
// cells stepped on: 718589 one word at a time, 55040 with the trie
// time: 50 ms one word at a time, 16 ms with the trie
#include <bits/stdc++.h>
using namespace std;

long long x = 11;                             // the same board and words in every language
const int N = 32;
vector<string> board(N, string(N, ' '));
long long stepsOne = 0, stepsTrie = 0;
vector<string> foundTrie;

int next_number() {
    x = x * 48271 % 2147483647;
    return (int) x;
}

char letter() {
    return "abcdefgh"[next_number() % 8];
}

bool searchOne(int r, int c, const string& word, size_t k) {   // Word Search: does word[k..] lie on a path from (r, c)?
    if (r < 0 || c < 0 || r >= N || c >= N || board[r][c] != word[k]) return false;
    stepsOne++;
    if (k == word.size() - 1) return true;
    board[r][c] = '#';                        // used by this path
    bool found = searchOne(r + 1, c, word, k + 1) || searchOne(r - 1, c, word, k + 1)
        || searchOne(r, c + 1, word, k + 1) || searchOne(r, c - 1, word, k + 1);
    board[r][c] = word[k];
    return found;
}

bool onBoard(const string& word) {
    for (int r = 0; r < N; r++)
        for (int c = 0; c < N; c++)
            if (searchOne(r, c, word, 0)) return true;
    return false;
}

struct Node {
    Node* children[26] = {};
    string word;                              // the word that ends here, until it's found
};

void walk(int r, int c, Node* node) {         // one search for every word: carry the trie node along
    if (r < 0 || c < 0 || r >= N || c >= N || board[r][c] == '#') return;
    char here = board[r][c];
    Node* child = node->children[here - 'a'];
    if (!child) return;                       // no word goes on with this letter: turn back
    stepsTrie++;
    if (!child->word.empty()) {
        foundTrie.push_back(child->word);
        child->word.clear();                  // found once is enough
    }
    board[r][c] = '#';
    walk(r + 1, c, child);
    walk(r - 1, c, child);
    walk(r, c + 1, child);
    walk(r, c - 1, child);
    board[r][c] = here;
}

int main() {
    for (int r = 0; r < N; r++)
        for (int c = 0; c < N; c++) board[r][c] = letter();
    vector<string> words;
    unordered_set<string> seen;
    for (int k = 0; k < 5000; k++) {
        int length = 4 + next_number() % 5;
        string w;
        for (int i = 0; i < length; i++) w += letter();
        if (seen.insert(w).second) words.push_back(w);
    }
    cout << words.size() << " words on a " << N << " x " << N << " board\n";

    auto start = chrono::steady_clock::now();
    vector<string> foundOne;
    for (const string& w : words) if (onBoard(w)) foundOne.push_back(w);   // one search per word
    long long oneMs = chrono::duration_cast<chrono::milliseconds>(chrono::steady_clock::now() - start).count();

    start = chrono::steady_clock::now();
    Node* root = new Node();
    for (const string& w : words) {
        Node* node = root;
        for (char ch : w) {
            if (!node->children[ch - 'a']) node->children[ch - 'a'] = new Node();
            node = node->children[ch - 'a'];
        }
        node->word = w;
    }
    for (int r = 0; r < N; r++)
        for (int c = 0; c < N; c++) walk(r, c, root);
    long long trieMs = chrono::duration_cast<chrono::milliseconds>(chrono::steady_clock::now() - start).count();

    vector<string> a = foundOne, b = foundTrie;
    sort(a.begin(), a.end());
    sort(b.begin(), b.end());
    cout << "found: " << foundOne.size() << " one word at a time, " << foundTrie.size()
         << " with the trie, the same words: " << (a == b ? "true" : "false") << "\n";
    cout << "cells stepped on: " << stepsOne << " one word at a time, " << stepsTrie << " with the trie\n";
    cout << "time: " << oneMs << " ms one word at a time, " << trieMs << " ms with the trie\n";
}

// 4891 words on a 32 x 32 board
// found: 2454 one word at a time, 2454 with the trie, the same words: true
// cells stepped on: 718589 one word at a time, 55040 with the trie
// time: 38 ms one word at a time, 4 ms with the trie
let x = 11;                                   // the same board and words in every language

function nextNumber() {
    x = x * 48271 % 2147483647;
    return x;
}

function letter() {
    return "abcdefgh"[nextNumber() % 8];
}

const N = 32;
const board = [];
for (let r = 0; r < N; r++) {
    board.push([]);
    for (let c = 0; c < N; c++) board[r].push(letter());
}
const unique = new Set();
for (let k = 0; k < 5000; k++) {
    const length = 4 + nextNumber() % 5;
    let w = "";
    for (let i = 0; i < length; i++) w += letter();
    unique.add(w);
}
const words = [...unique];
console.log(`${words.length} words on a ${N} x ${N} board`);

let stepsOne = 0;

function searchOne(r, c, word, k) {           // Word Search: does word[k..] lie on a path from (r, c)?
    if (r < 0 || c < 0 || r >= N || c >= N || board[r][c] !== word[k]) return false;
    stepsOne++;
    if (k === word.length - 1) return true;
    board[r][c] = "#";                        // used by this path
    const found = searchOne(r + 1, c, word, k + 1) || searchOne(r - 1, c, word, k + 1)
        || searchOne(r, c + 1, word, k + 1) || searchOne(r, c - 1, word, k + 1);
    board[r][c] = word[k];
    return found;
}

function onBoard(word) {
    for (let r = 0; r < N; r++)
        for (let c = 0; c < N; c++)
            if (searchOne(r, c, word, 0)) return true;
    return false;
}

let start = performance.now();
const foundOne = words.filter(onBoard);       // one search per word
const oneMs = Math.round(performance.now() - start);

class Node {
    constructor() {
        this.children = new Array(26).fill(null);
        this.word = null;                     // the word that ends here, until it's found
    }
}

let stepsTrie = 0;
const foundTrie = [];

function walk(r, c, node) {                   // one search for every word: carry the trie node along
    if (r < 0 || c < 0 || r >= N || c >= N || board[r][c] === "#") return;
    const here = board[r][c];
    const child = node.children[here.charCodeAt(0) - 97];
    if (child === null) return;               // no word goes on with this letter: turn back
    stepsTrie++;
    if (child.word !== null) {
        foundTrie.push(child.word);
        child.word = null;                    // found once is enough
    }
    board[r][c] = "#";
    walk(r + 1, c, child);
    walk(r - 1, c, child);
    walk(r, c + 1, child);
    walk(r, c - 1, child);
    board[r][c] = here;
}

start = performance.now();
const root = new Node();
for (const w of words) {
    let node = root;
    for (const ch of w) {
        const i = ch.charCodeAt(0) - 97;
        if (node.children[i] === null) node.children[i] = new Node();
        node = node.children[i];
    }
    node.word = w;
}
for (let r = 0; r < N; r++)
    for (let c = 0; c < N; c++) walk(r, c, root);
const trieMs = Math.round(performance.now() - start);

const same = [...foundOne].sort().join(" ") === [...foundTrie].sort().join(" ");
console.log(`found: ${foundOne.length} one word at a time, ${foundTrie.length} with the trie, the same words: ${same}`);
console.log(`cells stepped on: ${stepsOne} one word at a time, ${stepsTrie} with the trie`);
console.log(`time: ${oneMs} ms one word at a time, ${trieMs} ms with the trie`);

// 4891 words on a 32 x 32 board
// found: 2454 one word at a time, 2454 with the trie, the same words: true
// cells stepped on: 718589 one word at a time, 55040 with the trie
// time: 73 ms one word at a time, 26 ms with the trie
let x = 11;                                   // the same board and words in every language

function nextNumber(): number {
    x = x * 48271 % 2147483647;
    return x;
}

function letter(): string {
    return "abcdefgh"[nextNumber() % 8];
}

const N = 32;
const board: string[][] = [];
for (let r = 0; r < N; r++) {
    board.push([]);
    for (let c = 0; c < N; c++) board[r].push(letter());
}
const unique = new Set<string>();
for (let k = 0; k < 5000; k++) {
    const length = 4 + nextNumber() % 5;
    let w = "";
    for (let i = 0; i < length; i++) w += letter();
    unique.add(w);
}
const words = [...unique];
console.log(`${words.length} words on a ${N} x ${N} board`);

let stepsOne = 0;

function searchOne(r: number, c: number, word: string, k: number): boolean {   // Word Search: does word[k..] lie on a path from (r, c)?
    if (r < 0 || c < 0 || r >= N || c >= N || board[r][c] !== word[k]) return false;
    stepsOne++;
    if (k === word.length - 1) return true;
    board[r][c] = "#";                        // used by this path
    const found = searchOne(r + 1, c, word, k + 1) || searchOne(r - 1, c, word, k + 1)
        || searchOne(r, c + 1, word, k + 1) || searchOne(r, c - 1, word, k + 1);
    board[r][c] = word[k];
    return found;
}

function onBoard(word: string): boolean {
    for (let r = 0; r < N; r++)
        for (let c = 0; c < N; c++)
            if (searchOne(r, c, word, 0)) return true;
    return false;
}

let start = performance.now();
const foundOne = words.filter(onBoard);       // one search per word
const oneMs = Math.round(performance.now() - start);

class TrieNode {
    children: (TrieNode | null)[] = new Array(26).fill(null);
    word: string | null = null;               // the word that ends here, until it's found
}

let stepsTrie = 0;
const foundTrie: string[] = [];

function walk(r: number, c: number, node: TrieNode): void {   // one search for every word: carry the trie node along
    if (r < 0 || c < 0 || r >= N || c >= N || board[r][c] === "#") return;
    const here = board[r][c];
    const child = node.children[here.charCodeAt(0) - 97];
    if (child === null) return;               // no word goes on with this letter: turn back
    stepsTrie++;
    if (child.word !== null) {
        foundTrie.push(child.word);
        child.word = null;                    // found once is enough
    }
    board[r][c] = "#";
    walk(r + 1, c, child);
    walk(r - 1, c, child);
    walk(r, c + 1, child);
    walk(r, c - 1, child);
    board[r][c] = here;
}

start = performance.now();
const root = new TrieNode();
for (const w of words) {
    let node = root;
    for (const ch of w) {
        const i = ch.charCodeAt(0) - 97;
        if (node.children[i] === null) node.children[i] = new TrieNode();
        node = node.children[i]!;
    }
    node.word = w;
}
for (let r = 0; r < N; r++)
    for (let c = 0; c < N; c++) walk(r, c, root);
const trieMs = Math.round(performance.now() - start);

const same = [...foundOne].sort().join(" ") === [...foundTrie].sort().join(" ");
console.log(`found: ${foundOne.length} one word at a time, ${foundTrie.length} with the trie, the same words: ${same}`);
console.log(`cells stepped on: ${stepsOne} one word at a time, ${stepsTrie} with the trie`);
console.log(`time: ${oneMs} ms one word at a time, ${trieMs} ms with the trie`);

// 4891 words on a 32 x 32 board
// found: 2454 one word at a time, 2454 with the trie, the same words: true
// cells stepped on: 718589 one word at a time, 55040 with the trie
// time: 72 ms one word at a time, 26 ms with the trie
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;

class Node {
    public readonly Node[] Children = new Node[26];
    public string Word;                       // the word that ends here, until it's found
}

class Program {
    static long x = 11;                       // the same board and words in every language
    const int N = 32;
    static readonly char[,] board = new char[N, N];
    static long stepsOne = 0, stepsTrie = 0;
    static readonly List<string> foundTrie = new List<string>();

    static int NextNumber() {
        x = x * 48271 % 2147483647;
        return (int) x;
    }

    static char Letter() => "abcdefgh"[NextNumber() % 8];

    static bool SearchOne(int r, int c, string word, int k) {   // Word Search: does word[k..] lie on a path from (r, c)?
        if (r < 0 || c < 0 || r >= N || c >= N || board[r, c] != word[k]) return false;
        stepsOne++;
        if (k == word.Length - 1) return true;
        board[r, c] = '#';                    // used by this path
        bool found = SearchOne(r + 1, c, word, k + 1) || SearchOne(r - 1, c, word, k + 1)
            || SearchOne(r, c + 1, word, k + 1) || SearchOne(r, c - 1, word, k + 1);
        board[r, c] = word[k];
        return found;
    }

    static bool OnBoard(string word) {
        for (int r = 0; r < N; r++)
            for (int c = 0; c < N; c++)
                if (SearchOne(r, c, word, 0)) return true;
        return false;
    }

    static void Walk(int r, int c, Node node) {   // one search for every word: carry the trie node along
        if (r < 0 || c < 0 || r >= N || c >= N || board[r, c] == '#') return;
        char here = board[r, c];
        Node child = node.Children[here - 'a'];
        if (child == null) return;            // no word goes on with this letter: turn back
        stepsTrie++;
        if (child.Word != null) {
            foundTrie.Add(child.Word);
            child.Word = null;                // found once is enough
        }
        board[r, c] = '#';
        Walk(r + 1, c, child);
        Walk(r - 1, c, child);
        Walk(r, c + 1, child);
        Walk(r, c - 1, child);
        board[r, c] = here;
    }

    static void Main() {
        for (int r = 0; r < N; r++)
            for (int c = 0; c < N; c++) board[r, c] = Letter();
        var words = new List<string>();
        var seen = new HashSet<string>();
        for (int k = 0; k < 5000; k++) {
            int length = 4 + NextNumber() % 5;
            var chars = new char[length];
            for (int i = 0; i < length; i++) chars[i] = Letter();
            string w = new string(chars);
            if (seen.Add(w)) words.Add(w);
        }
        Console.WriteLine($"{words.Count} words on a {N} x {N} board");

        var watch = Stopwatch.StartNew();
        var foundOne = words.Where(OnBoard).ToList();   // one search per word
        long oneMs = watch.ElapsedMilliseconds;

        watch.Restart();
        var root = new Node();
        foreach (string w in words) {
            Node node = root;
            foreach (char ch in w) {
                if (node.Children[ch - 'a'] == null) node.Children[ch - 'a'] = new Node();
                node = node.Children[ch - 'a'];
            }
            node.Word = w;
        }
        for (int r = 0; r < N; r++)
            for (int c = 0; c < N; c++) Walk(r, c, root);
        long trieMs = watch.ElapsedMilliseconds;

        bool same = foundOne.OrderBy(w => w, StringComparer.Ordinal)
            .SequenceEqual(foundTrie.OrderBy(w => w, StringComparer.Ordinal));
        Console.WriteLine($"found: {foundOne.Count} one word at a time, {foundTrie.Count} with the trie, the same words: {(same ? "true" : "false")}");
        Console.WriteLine($"cells stepped on: {stepsOne} one word at a time, {stepsTrie} with the trie");
        Console.WriteLine($"time: {oneMs} ms one word at a time, {trieMs} ms with the trie");
    }
}

// 4891 words on a 32 x 32 board
// found: 2454 one word at a time, 2454 with the trie, the same words: true
// cells stepped on: 718589 one word at a time, 55040 with the trie
// time: 43 ms one word at a time, 9 ms with the trie
package main

import (
	"fmt"
	"sort"
	"strings"
	"time"
)

var x int64 = 11 // the same board and words in every language

const N = 32

var board [N][N]byte
var stepsOne, stepsTrie int
var foundTrie []string

func nextNumber() int {
	x = x * 48271 % 2147483647
	return int(x)
}

func letter() byte {
	return "abcdefgh"[nextNumber()%8]
}

func searchOne(r, c int, word string, k int) bool { // Word Search: does word[k:] lie on a path from (r, c)?
	if r < 0 || c < 0 || r >= N || c >= N || board[r][c] != word[k] {
		return false
	}
	stepsOne++
	if k == len(word)-1 {
		return true
	}
	board[r][c] = '#' // used by this path
	found := searchOne(r+1, c, word, k+1) || searchOne(r-1, c, word, k+1) ||
		searchOne(r, c+1, word, k+1) || searchOne(r, c-1, word, k+1)
	board[r][c] = word[k]
	return found
}

func onBoard(word string) bool {
	for r := 0; r < N; r++ {
		for c := 0; c < N; c++ {
			if searchOne(r, c, word, 0) {
				return true
			}
		}
	}
	return false
}

type Node struct {
	children [26]*Node
	word     string // the word that ends here, until it's found
}

func walk(r, c int, node *Node) { // one search for every word: carry the trie node along
	if r < 0 || c < 0 || r >= N || c >= N || board[r][c] == '#' {
		return
	}
	here := board[r][c]
	child := node.children[here-'a']
	if child == nil { // no word goes on with this letter: turn back
		return
	}
	stepsTrie++
	if child.word != "" {
		foundTrie = append(foundTrie, child.word)
		child.word = "" // found once is enough
	}
	board[r][c] = '#'
	walk(r+1, c, child)
	walk(r-1, c, child)
	walk(r, c+1, child)
	walk(r, c-1, child)
	board[r][c] = here
}

func main() {
	for r := 0; r < N; r++ {
		for c := 0; c < N; c++ {
			board[r][c] = letter()
		}
	}
	words := []string{}
	seen := map[string]bool{}
	for k := 0; k < 5000; k++ {
		length := 4 + nextNumber()%5
		b := make([]byte, length)
		for i := range b {
			b[i] = letter()
		}
		if w := string(b); !seen[w] {
			seen[w] = true
			words = append(words, w)
		}
	}
	fmt.Printf("%d words on a %d x %d board\n", len(words), N, N)

	start := time.Now()
	foundOne := []string{}
	for _, w := range words { // one search per word
		if onBoard(w) {
			foundOne = append(foundOne, w)
		}
	}
	oneMs := time.Since(start).Milliseconds()

	start = time.Now()
	root := &Node{}
	for _, w := range words {
		node := root
		for i := 0; i < len(w); i++ {
			ch := w[i] - 'a'
			if node.children[ch] == nil {
				node.children[ch] = &Node{}
			}
			node = node.children[ch]
		}
		node.word = w
	}
	for r := 0; r < N; r++ {
		for c := 0; c < N; c++ {
			walk(r, c, root)
		}
	}
	trieMs := time.Since(start).Milliseconds()

	a := append([]string{}, foundOne...)
	b := append([]string{}, foundTrie...)
	sort.Strings(a)
	sort.Strings(b)
	same := strings.Join(a, " ") == strings.Join(b, " ")
	fmt.Printf("found: %d one word at a time, %d with the trie, the same words: %t\n", len(foundOne), len(foundTrie), same)
	fmt.Printf("cells stepped on: %d one word at a time, %d with the trie\n", stepsOne, stepsTrie)
	fmt.Printf("time: %d ms one word at a time, %d ms with the trie\n", oneMs, trieMs)
}

// 4891 words on a 32 x 32 board
// found: 2454 one word at a time, 2454 with the trie, the same words: true
// cells stepped on: 718589 one word at a time, 55040 with the trie
// time: 29 ms one word at a time, 7 ms with the trie

Both found the same 2,454 of the 4,891 words in all seven languages. One search per word stepped on 718,589 cells; the trie's one search stepped on 55,040, 13 times fewer. In time that was 932 ms against 75 ms in Python and 38 ms against 4 ms in C++, and the trie was 3 to 12 times faster across the seven languages, building included.

The searches one at a time repeat each other's work. Words that start alike walk the same cells: every word starting with ab steps onto the same a cells and the same b next to them, once per word. The trie does that walk once, for every word that starts that way.

Left, the lesson's board of three rows, c a t s, o r e a, d e n t, with the path c, o, r, e lit from the top left down to o, right to r and down to e, and the step from o down to d crossed out. Right, the c branch of the trie of the lesson's words, with d, r, s and t beside it greyed: c, then a and o; car, cat and cor; care, cats, cord and core, the words ringed and the path c, o, r, e lit. 1, a step onto a cell is one node down the trie; 2, co has no child d, so the walk turns back; 3, core's end mark finds the word. Focal line: one walk carries the trie for every word. Title: one walk for every word.

One walk for every word

Put all the words in one trie. Then search the board once, from every cell, and carry a trie node along with the path:

  • Step onto a cell only if the current node has a child for its letter, and move to that child. A path on the board is then always a prefix of some word.
  • Turn back the moment the node has no child for the letter: no word has this prefix, so nothing further along can be found.
  • A node with a word means the path spells it: report the word and take it off the node, so a second path to the same word doesn't report it again.
  • While the walk is on a cell, mark it used (here with a #), and give it its letter back on the way out, as in any backtracking search.

Here it is on a board of three rows, cats, orea and dent, with ten words:

class Node:
    def __init__(self):
        self.children = [None] * 26
        self.word = None                      # the word that ends here, until it's found


def find_words(rows, words):
    board = [list(row) for row in rows]
    root = Node()
    for w in words:                           # one trie of all the words
        node = root
        for ch in w:
            i = ord(ch) - ord("a")
            if node.children[i] is None:
                node.children[i] = Node()
            node = node.children[i]
        node.word = w
    found = []

    def walk(r, c, node):
        if r < 0 or c < 0 or r >= len(board) or c >= len(board[0]) or board[r][c] == "#":
            return
        here = board[r][c]
        child = node.children[ord(here) - ord("a")]
        if child is None:                     # no word goes on with this letter: turn back
            return
        if child.word is not None:
            found.append(child.word)
            child.word = None                 # found once is enough
        board[r][c] = "#"                     # used by this path
        walk(r + 1, c, child)
        walk(r - 1, c, child)
        walk(r, c + 1, child)
        walk(r, c - 1, child)
        board[r][c] = here                    # give the cell its letter back

    for r in range(len(board)):
        for c in range(len(board[0])):
            walk(r, c, root)
    return found


words = ["cat", "cats", "care", "core", "cord", "dent", "tent", "rent", "tea", "seat"]
found = find_words(["cats", "orea", "dent"], words)
print("found, in the order the walk met them:", " ".join(found))
print("not found:", " ".join(w for w in words if w not in found))

# found, in the order the walk met them: core care cat cats tent tea rent dent
# not found: cord seat
import java.util.*;

public class Main {
    static class Node {
        final Node[] children = new Node[26];
        String word;                                 // the word that ends here, until it's found
    }

    static char[][] board;
    static List<String> found;

    static void walk(int r, int c, Node node) {
        if (r < 0 || c < 0 || r >= board.length || c >= board[0].length || board[r][c] == '#') return;
        char here = board[r][c];
        Node child = node.children[here - 'a'];
        if (child == null) return;                   // no word goes on with this letter: turn back
        if (child.word != null) {
            found.add(child.word);
            child.word = null;                       // found once is enough
        }
        board[r][c] = '#';                           // used by this path
        walk(r + 1, c, child);
        walk(r - 1, c, child);
        walk(r, c + 1, child);
        walk(r, c - 1, child);
        board[r][c] = here;                          // give the cell its letter back
    }

    static List<String> findWords(String[] rows, String[] words) {
        board = new char[rows.length][];
        for (int r = 0; r < rows.length; r++) board[r] = rows[r].toCharArray();
        Node root = new Node();
        for (String w : words) {                     // one trie of all the words
            Node node = root;
            for (char ch : w.toCharArray()) {
                if (node.children[ch - 'a'] == null) node.children[ch - 'a'] = new Node();
                node = node.children[ch - 'a'];
            }
            node.word = w;
        }
        found = new ArrayList<>();
        for (int r = 0; r < board.length; r++)
            for (int c = 0; c < board[0].length; c++) walk(r, c, root);
        return found;
    }

    public static void main(String[] args) {
        String[] words = {"cat", "cats", "care", "core", "cord", "dent", "tent", "rent", "tea", "seat"};
        List<String> got = findWords(new String[] {"cats", "orea", "dent"}, words);
        System.out.println("found, in the order the walk met them: " + String.join(" ", got));
        StringJoiner missing = new StringJoiner(" ");
        for (String w : words) if (!got.contains(w)) missing.add(w);
        System.out.println("not found: " + missing);
    }
}

// found, in the order the walk met them: core care cat cats tent tea rent dent
// not found: cord seat
#include <bits/stdc++.h>
using namespace std;

struct Node {
    Node* children[26] = {};
    string word;                              // the word that ends here, until it's found
};

vector<string> board;
vector<string> found;

void walk(int r, int c, Node* node) {
    if (r < 0 || c < 0 || r >= (int) board.size() || c >= (int) board[0].size() || board[r][c] == '#') return;
    char here = board[r][c];
    Node* child = node->children[here - 'a'];
    if (!child) return;                       // no word goes on with this letter: turn back
    if (!child->word.empty()) {
        found.push_back(child->word);
        child->word.clear();                  // found once is enough
    }
    board[r][c] = '#';                        // used by this path
    walk(r + 1, c, child);
    walk(r - 1, c, child);
    walk(r, c + 1, child);
    walk(r, c - 1, child);
    board[r][c] = here;                       // give the cell its letter back
}

vector<string> findWords(const vector<string>& rows, const vector<string>& words) {
    board = rows;
    Node* root = new Node();
    for (const string& w : words) {           // one trie of all the words
        Node* node = root;
        for (char ch : w) {
            if (!node->children[ch - 'a']) node->children[ch - 'a'] = new Node();
            node = node->children[ch - 'a'];
        }
        node->word = w;
    }
    found.clear();
    for (int r = 0; r < (int) board.size(); r++)
        for (int c = 0; c < (int) board[0].size(); c++) walk(r, c, root);
    return found;
}

int main() {
    vector<string> words = {"cat", "cats", "care", "core", "cord", "dent", "tent", "rent", "tea", "seat"};
    vector<string> got = findWords({"cats", "orea", "dent"}, words);
    cout << "found, in the order the walk met them:";
    for (const string& w : got) cout << " " << w;
    cout << "\nnot found:";
    for (const string& w : words)
        if (find(got.begin(), got.end(), w) == got.end()) cout << " " << w;
    cout << "\n";
}

// found, in the order the walk met them: core care cat cats tent tea rent dent
// not found: cord seat
class Node {
    constructor() {
        this.children = new Array(26).fill(null);
        this.word = null;                     // the word that ends here, until it's found
    }
}

function findWords(rows, words) {
    const board = rows.map((row) => [...row]);
    const root = new Node();
    for (const w of words) {                  // one trie of all the words
        let node = root;
        for (const ch of w) {
            const i = ch.charCodeAt(0) - 97;
            if (node.children[i] === null) node.children[i] = new Node();
            node = node.children[i];
        }
        node.word = w;
    }
    const found = [];
    const walk = (r, c, node) => {
        if (r < 0 || c < 0 || r >= board.length || c >= board[0].length || board[r][c] === "#") return;
        const here = board[r][c];
        const child = node.children[here.charCodeAt(0) - 97];
        if (child === null) return;           // no word goes on with this letter: turn back
        if (child.word !== null) {
            found.push(child.word);
            child.word = null;                // found once is enough
        }
        board[r][c] = "#";                    // used by this path
        walk(r + 1, c, child);
        walk(r - 1, c, child);
        walk(r, c + 1, child);
        walk(r, c - 1, child);
        board[r][c] = here;                   // give the cell its letter back
    };
    for (let r = 0; r < board.length; r++)
        for (let c = 0; c < board[0].length; c++) walk(r, c, root);
    return found;
}

const words = ["cat", "cats", "care", "core", "cord", "dent", "tent", "rent", "tea", "seat"];
const found = findWords(["cats", "orea", "dent"], words);
console.log("found, in the order the walk met them:", found.join(" "));
console.log("not found:", words.filter((w) => !found.includes(w)).join(" "));

// found, in the order the walk met them: core care cat cats tent tea rent dent
// not found: cord seat
class TrieNode {
    children: (TrieNode | null)[] = new Array(26).fill(null);
    word: string | null = null;               // the word that ends here, until it's found
}

function findWords(rows: string[], words: string[]): string[] {
    const board = rows.map((row) => [...row]);
    const root = new TrieNode();
    for (const w of words) {                  // one trie of all the words
        let node = root;
        for (const ch of w) {
            const i = ch.charCodeAt(0) - 97;
            if (node.children[i] === null) node.children[i] = new TrieNode();
            node = node.children[i]!;
        }
        node.word = w;
    }
    const found: string[] = [];
    const walk = (r: number, c: number, node: TrieNode): void => {
        if (r < 0 || c < 0 || r >= board.length || c >= board[0].length || board[r][c] === "#") return;
        const here = board[r][c];
        const child = node.children[here.charCodeAt(0) - 97];
        if (child === null) return;           // no word goes on with this letter: turn back
        if (child.word !== null) {
            found.push(child.word);
            child.word = null;                // found once is enough
        }
        board[r][c] = "#";                    // used by this path
        walk(r + 1, c, child);
        walk(r - 1, c, child);
        walk(r, c + 1, child);
        walk(r, c - 1, child);
        board[r][c] = here;                   // give the cell its letter back
    };
    for (let r = 0; r < board.length; r++)
        for (let c = 0; c < board[0].length; c++) walk(r, c, root);
    return found;
}

const words = ["cat", "cats", "care", "core", "cord", "dent", "tent", "rent", "tea", "seat"];
const found = findWords(["cats", "orea", "dent"], words);
console.log("found, in the order the walk met them:", found.join(" "));
console.log("not found:", words.filter((w) => !found.includes(w)).join(" "));

// found, in the order the walk met them: core care cat cats tent tea rent dent
// not found: cord seat
using System;
using System.Collections.Generic;
using System.Linq;

class Node {
    public readonly Node[] Children = new Node[26];
    public string Word;                       // the word that ends here, until it's found
}

class Program {
    static char[][] board;
    static List<string> found;

    static void Walk(int r, int c, Node node) {
        if (r < 0 || c < 0 || r >= board.Length || c >= board[0].Length || board[r][c] == '#') return;
        char here = board[r][c];
        Node child = node.Children[here - 'a'];
        if (child == null) return;            // no word goes on with this letter: turn back
        if (child.Word != null) {
            found.Add(child.Word);
            child.Word = null;                // found once is enough
        }
        board[r][c] = '#';                    // used by this path
        Walk(r + 1, c, child);
        Walk(r - 1, c, child);
        Walk(r, c + 1, child);
        Walk(r, c - 1, child);
        board[r][c] = here;                   // give the cell its letter back
    }

    static List<string> FindWords(string[] rows, string[] words) {
        board = rows.Select(row => row.ToCharArray()).ToArray();
        var root = new Node();
        foreach (string w in words) {         // one trie of all the words
            Node node = root;
            foreach (char ch in w) {
                if (node.Children[ch - 'a'] == null) node.Children[ch - 'a'] = new Node();
                node = node.Children[ch - 'a'];
            }
            node.Word = w;
        }
        found = new List<string>();
        for (int r = 0; r < board.Length; r++)
            for (int c = 0; c < board[0].Length; c++) Walk(r, c, root);
        return found;
    }

    static void Main() {
        string[] words = {"cat", "cats", "care", "core", "cord", "dent", "tent", "rent", "tea", "seat"};
        List<string> got = FindWords(new[] {"cats", "orea", "dent"}, words);
        Console.WriteLine("found, in the order the walk met them: " + string.Join(" ", got));
        Console.WriteLine("not found: " + string.Join(" ", words.Where(w => !got.Contains(w))));
    }
}

// found, in the order the walk met them: core care cat cats tent tea rent dent
// not found: cord seat
package main

import (
	"fmt"
	"strings"
)

type Node struct {
	children [26]*Node
	word     string // the word that ends here, until it's found
}

var board [][]byte
var found []string

func walk(r, c int, node *Node) {
	if r < 0 || c < 0 || r >= len(board) || c >= len(board[0]) || board[r][c] == '#' {
		return
	}
	here := board[r][c]
	child := node.children[here-'a']
	if child == nil { // no word goes on with this letter: turn back
		return
	}
	if child.word != "" {
		found = append(found, child.word)
		child.word = "" // found once is enough
	}
	board[r][c] = '#' // used by this path
	walk(r+1, c, child)
	walk(r-1, c, child)
	walk(r, c+1, child)
	walk(r, c-1, child)
	board[r][c] = here // give the cell its letter back
}

func findWords(rows []string, words []string) []string {
	board = make([][]byte, len(rows))
	for r, row := range rows {
		board[r] = []byte(row)
	}
	root := &Node{}
	for _, w := range words { // one trie of all the words
		node := root
		for i := 0; i < len(w); i++ {
			ch := w[i] - 'a'
			if node.children[ch] == nil {
				node.children[ch] = &Node{}
			}
			node = node.children[ch]
		}
		node.word = w
	}
	found = []string{}
	for r := range board {
		for c := range board[0] {
			walk(r, c, root)
		}
	}
	return found
}

func main() {
	words := []string{"cat", "cats", "care", "core", "cord", "dent", "tent", "rent", "tea", "seat"}
	got := findWords([]string{"cats", "orea", "dent"}, words)
	fmt.Println("found, in the order the walk met them:", strings.Join(got, " "))
	missing := []string{}
	for _, w := range words {
		in := false
		for _, g := range got {
			if g == w {
				in = true
			}
		}
		if !in {
			missing = append(missing, w)
		}
	}
	fmt.Println("not found:", strings.Join(missing, " "))
}

// found, in the order the walk met them: core care cat cats tent tea rent dent
// not found: cord seat

The walk met core first: from the c at the top left it went down to o, then r, then the e below it. Then care, cat and cats, still from that first c, then tent, tea, rent and dent from later cells. cord isn't on the board, since the r has no d beside it, and neither is seat: the s's only neighbours are t and a.

Walk the Board. Walk the board yourself with the trie beside it: step from cell to cell, watch the trie node follow, see where the walk has to turn back, and find every word.

Walk the Board: step across the lesson's board with the trie of its words, turn back where no word goes on, and find every word on it

Why the walk is short

A walk on the board can branch in up to four directions at the first step and three after that, so the paths of length L from one cell grow like 3L3^L. Searching each of k words on an m × n board can try that many paths per word: O(k · m · n · 3L3^L) in the worst case.

With the trie, each path on the board is walked once for all the words together: the factor k is gone, O(m · n · 3L3^L) for the walk, plus the total number of letters in the words to build the trie. And that worst case is rarely reached, because a path survives only while it's a prefix of some word, and most paths stop being one within a few letters: in the 32 × 32 example the trie's walk stepped on 55,040 cells for all 4,891 words.

Found once, then cut out

Clearing a found word's mark stops it being reported twice. But its nodes stay in the trie, and the walk keeps following them, though there's nothing left to find down there. On a board where every path matches, that's every path. The fix is to cut a node out of the trie once it has no word and no children left: on the way back out of a cell, if the child it used is empty, remove it from its parent. Here both are run on a 4 × 4 board of a's with the words a, aa, up to aaaaaaaa:

class Node:
    def __init__(self):
        self.children = [None] * 26
        self.kids = 0                         # children that exist
        self.word = None


def search(rows, words, cut):
    board = [list(row) for row in rows]
    root = Node()
    for w in words:
        node = root
        for ch in w:
            i = ord(ch) - ord("a")
            if node.children[i] is None:
                node.children[i] = Node()
                node.kids += 1
            node = node.children[i]
        node.word = w
    found, steps = [], 0

    def walk(r, c, node):
        nonlocal steps
        if r < 0 or c < 0 or r >= len(board) or c >= len(board[0]) or board[r][c] == "#":
            return
        here = board[r][c]
        i = ord(here) - ord("a")
        child = node.children[i]
        if child is None:
            return
        steps += 1
        if child.word is not None:
            found.append(child.word)
            child.word = None
        board[r][c] = "#"
        walk(r + 1, c, child)
        walk(r - 1, c, child)
        walk(r, c + 1, child)
        walk(r, c - 1, child)
        board[r][c] = here
        if cut and child.word is None and child.kids == 0:   # nothing left to find below it: cut it out
            node.children[i] = None
            node.kids -= 1

    for r in range(len(board)):
        for c in range(len(board[0])):
            walk(r, c, root)
    return len(found), steps


rows = ["aaaa"] * 4
words = ["a" * k for k in range(1, 9)]        # a, aa, ..., aaaaaaaa
kept = search(rows, words, False)
cut = search(rows, words, True)
print(f"found: {kept[0]} with found words kept, {cut[0]} with them cut out")
print(f"cells stepped on: {kept[1]} with found words kept, {cut[1]} with them cut out")

# found: 8 with found words kept, 8 with them cut out
# cells stepped on: 4856 with found words kept, 8 with them cut out
public class Main {
    static class Node {
        final Node[] children = new Node[26];
        int kids;                                    // children that exist
        String word;
    }

    static char[][] board;
    static int found, steps;
    static boolean cut;

    static void walk(int r, int c, Node node) {
        if (r < 0 || c < 0 || r >= board.length || c >= board[0].length || board[r][c] == '#') return;
        char here = board[r][c];
        Node child = node.children[here - 'a'];
        if (child == null) return;
        steps++;
        if (child.word != null) {
            found++;
            child.word = null;
        }
        board[r][c] = '#';
        walk(r + 1, c, child);
        walk(r - 1, c, child);
        walk(r, c + 1, child);
        walk(r, c - 1, child);
        board[r][c] = here;
        if (cut && child.word == null && child.kids == 0) {   // nothing left to find below it: cut it out
            node.children[here - 'a'] = null;
            node.kids--;
        }
    }

    static int[] search(String[] rows, String[] words, boolean cutOut) {
        board = new char[rows.length][];
        for (int r = 0; r < rows.length; r++) board[r] = rows[r].toCharArray();
        Node root = new Node();
        for (String w : words) {
            Node node = root;
            for (char ch : w.toCharArray()) {
                if (node.children[ch - 'a'] == null) {
                    node.children[ch - 'a'] = new Node();
                    node.kids++;
                }
                node = node.children[ch - 'a'];
            }
            node.word = w;
        }
        found = 0;
        steps = 0;
        cut = cutOut;
        for (int r = 0; r < board.length; r++)
            for (int c = 0; c < board[0].length; c++) walk(r, c, root);
        return new int[] {found, steps};
    }

    public static void main(String[] args) {
        String[] rows = {"aaaa", "aaaa", "aaaa", "aaaa"};
        String[] words = new String[8];
        for (int k = 1; k <= 8; k++) words[k - 1] = "a".repeat(k);   // a, aa, ..., aaaaaaaa
        int[] kept = search(rows, words, false);
        int[] cutOut = search(rows, words, true);
        System.out.println("found: " + kept[0] + " with found words kept, " + cutOut[0] + " with them cut out");
        System.out.println("cells stepped on: " + kept[1] + " with found words kept, " + cutOut[1] + " with them cut out");
    }
}

// found: 8 with found words kept, 8 with them cut out
// cells stepped on: 4856 with found words kept, 8 with them cut out
#include <bits/stdc++.h>
using namespace std;

struct Node {
    Node* children[26] = {};
    int kids = 0;                             // children that exist
    string word;
};

vector<string> board;
int found, steps;
bool cut;

void walk(int r, int c, Node* node) {
    if (r < 0 || c < 0 || r >= (int) board.size() || c >= (int) board[0].size() || board[r][c] == '#') return;
    char here = board[r][c];
    Node* child = node->children[here - 'a'];
    if (!child) return;
    steps++;
    if (!child->word.empty()) {
        found++;
        child->word.clear();
    }
    board[r][c] = '#';
    walk(r + 1, c, child);
    walk(r - 1, c, child);
    walk(r, c + 1, child);
    walk(r, c - 1, child);
    board[r][c] = here;
    if (cut && child->word.empty() && child->kids == 0) {   // nothing left to find below it: cut it out
        node->children[here - 'a'] = nullptr;
        node->kids--;
    }
}

pair<int, int> search(const vector<string>& rows, const vector<string>& words, bool cutOut) {
    board = rows;
    Node* root = new Node();
    for (const string& w : words) {
        Node* node = root;
        for (char ch : w) {
            if (!node->children[ch - 'a']) {
                node->children[ch - 'a'] = new Node();
                node->kids++;
            }
            node = node->children[ch - 'a'];
        }
        node->word = w;
    }
    found = 0;
    steps = 0;
    cut = cutOut;
    for (int r = 0; r < (int) board.size(); r++)
        for (int c = 0; c < (int) board[0].size(); c++) walk(r, c, root);
    return {found, steps};
}

int main() {
    vector<string> rows(4, "aaaa");
    vector<string> words;
    for (int k = 1; k <= 8; k++) words.push_back(string(k, 'a'));   // a, aa, ..., aaaaaaaa
    auto kept = search(rows, words, false);
    auto cutOut = search(rows, words, true);
    cout << "found: " << kept.first << " with found words kept, " << cutOut.first << " with them cut out\n";
    cout << "cells stepped on: " << kept.second << " with found words kept, " << cutOut.second << " with them cut out\n";
}

// found: 8 with found words kept, 8 with them cut out
// cells stepped on: 4856 with found words kept, 8 with them cut out
class Node {
    constructor() {
        this.children = new Array(26).fill(null);
        this.kids = 0;                        // children that exist
        this.word = null;
    }
}

function search(rows, words, cut) {
    const board = rows.map((row) => [...row]);
    const root = new Node();
    for (const w of words) {
        let node = root;
        for (const ch of w) {
            const i = ch.charCodeAt(0) - 97;
            if (node.children[i] === null) {
                node.children[i] = new Node();
                node.kids++;
            }
            node = node.children[i];
        }
        node.word = w;
    }
    let found = 0, steps = 0;
    const walk = (r, c, node) => {
        if (r < 0 || c < 0 || r >= board.length || c >= board[0].length || board[r][c] === "#") return;
        const here = board[r][c];
        const i = here.charCodeAt(0) - 97;
        const child = node.children[i];
        if (child === null) return;
        steps++;
        if (child.word !== null) {
            found++;
            child.word = null;
        }
        board[r][c] = "#";
        walk(r + 1, c, child);
        walk(r - 1, c, child);
        walk(r, c + 1, child);
        walk(r, c - 1, child);
        board[r][c] = here;
        if (cut && child.word === null && child.kids === 0) {   // nothing left to find below it: cut it out
            node.children[i] = null;
            node.kids--;
        }
    };
    for (let r = 0; r < board.length; r++)
        for (let c = 0; c < board[0].length; c++) walk(r, c, root);
    return [found, steps];
}

const rows = ["aaaa", "aaaa", "aaaa", "aaaa"];
const words = Array.from({ length: 8 }, (_, k) => "a".repeat(k + 1));   // a, aa, ..., aaaaaaaa
const kept = search(rows, words, false);
const cut = search(rows, words, true);
console.log(`found: ${kept[0]} with found words kept, ${cut[0]} with them cut out`);
console.log(`cells stepped on: ${kept[1]} with found words kept, ${cut[1]} with them cut out`);

// found: 8 with found words kept, 8 with them cut out
// cells stepped on: 4856 with found words kept, 8 with them cut out
class TrieNode {
    children: (TrieNode | null)[] = new Array(26).fill(null);
    kids = 0;                                 // children that exist
    word: string | null = null;
}

function search(rows: string[], words: string[], cut: boolean): [number, number] {
    const board = rows.map((row) => [...row]);
    const root = new TrieNode();
    for (const w of words) {
        let node = root;
        for (const ch of w) {
            const i = ch.charCodeAt(0) - 97;
            if (node.children[i] === null) {
                node.children[i] = new TrieNode();
                node.kids++;
            }
            node = node.children[i]!;
        }
        node.word = w;
    }
    let found = 0, steps = 0;
    const walk = (r: number, c: number, node: TrieNode): void => {
        if (r < 0 || c < 0 || r >= board.length || c >= board[0].length || board[r][c] === "#") return;
        const here = board[r][c];
        const i = here.charCodeAt(0) - 97;
        const child = node.children[i];
        if (child === null) return;
        steps++;
        if (child.word !== null) {
            found++;
            child.word = null;
        }
        board[r][c] = "#";
        walk(r + 1, c, child);
        walk(r - 1, c, child);
        walk(r, c + 1, child);
        walk(r, c - 1, child);
        board[r][c] = here;
        if (cut && child.word === null && child.kids === 0) {   // nothing left to find below it: cut it out
            node.children[i] = null;
            node.kids--;
        }
    };
    for (let r = 0; r < board.length; r++)
        for (let c = 0; c < board[0].length; c++) walk(r, c, root);
    return [found, steps];
}

const rows = ["aaaa", "aaaa", "aaaa", "aaaa"];
const words = Array.from({ length: 8 }, (_, k) => "a".repeat(k + 1));   // a, aa, ..., aaaaaaaa
const kept = search(rows, words, false);
const cut = search(rows, words, true);
console.log(`found: ${kept[0]} with found words kept, ${cut[0]} with them cut out`);
console.log(`cells stepped on: ${kept[1]} with found words kept, ${cut[1]} with them cut out`);

// found: 8 with found words kept, 8 with them cut out
// cells stepped on: 4856 with found words kept, 8 with them cut out
using System;
using System.Linq;

class Node {
    public readonly Node[] Children = new Node[26];
    public int Kids;                          // children that exist
    public string Word;
}

class Program {
    static char[][] board;
    static int found, steps;
    static bool cut;

    static void Walk(int r, int c, Node node) {
        if (r < 0 || c < 0 || r >= board.Length || c >= board[0].Length || board[r][c] == '#') return;
        char here = board[r][c];
        Node child = node.Children[here - 'a'];
        if (child == null) return;
        steps++;
        if (child.Word != null) {
            found++;
            child.Word = null;
        }
        board[r][c] = '#';
        Walk(r + 1, c, child);
        Walk(r - 1, c, child);
        Walk(r, c + 1, child);
        Walk(r, c - 1, child);
        board[r][c] = here;
        if (cut && child.Word == null && child.Kids == 0) {   // nothing left to find below it: cut it out
            node.Children[here - 'a'] = null;
            node.Kids--;
        }
    }

    static (int, int) Search(string[] rows, string[] words, bool cutOut) {
        board = rows.Select(row => row.ToCharArray()).ToArray();
        var root = new Node();
        foreach (string w in words) {
            Node node = root;
            foreach (char ch in w) {
                if (node.Children[ch - 'a'] == null) {
                    node.Children[ch - 'a'] = new Node();
                    node.Kids++;
                }
                node = node.Children[ch - 'a'];
            }
            node.Word = w;
        }
        found = 0;
        steps = 0;
        cut = cutOut;
        for (int r = 0; r < board.Length; r++)
            for (int c = 0; c < board[0].Length; c++) Walk(r, c, root);
        return (found, steps);
    }

    static void Main() {
        string[] rows = {"aaaa", "aaaa", "aaaa", "aaaa"};
        string[] words = Enumerable.Range(1, 8).Select(k => new string('a', k)).ToArray();   // a, aa, ..., aaaaaaaa
        var kept = Search(rows, words, false);
        var cutOut = Search(rows, words, true);
        Console.WriteLine($"found: {kept.Item1} with found words kept, {cutOut.Item1} with them cut out");
        Console.WriteLine($"cells stepped on: {kept.Item2} with found words kept, {cutOut.Item2} with them cut out");
    }
}

// found: 8 with found words kept, 8 with them cut out
// cells stepped on: 4856 with found words kept, 8 with them cut out
package main

import (
	"fmt"
	"strings"
)

type Node struct {
	children [26]*Node
	kids     int // children that exist
	word     string
}

var board [][]byte
var found, steps int
var cut bool

func walk(r, c int, node *Node) {
	if r < 0 || c < 0 || r >= len(board) || c >= len(board[0]) || board[r][c] == '#' {
		return
	}
	here := board[r][c]
	child := node.children[here-'a']
	if child == nil {
		return
	}
	steps++
	if child.word != "" {
		found++
		child.word = ""
	}
	board[r][c] = '#'
	walk(r+1, c, child)
	walk(r-1, c, child)
	walk(r, c+1, child)
	walk(r, c-1, child)
	board[r][c] = here
	if cut && child.word == "" && child.kids == 0 { // nothing left to find below it: cut it out
		node.children[here-'a'] = nil
		node.kids--
	}
}

func search(rows []string, words []string, cutOut bool) (int, int) {
	board = make([][]byte, len(rows))
	for r, row := range rows {
		board[r] = []byte(row)
	}
	root := &Node{}
	for _, w := range words {
		node := root
		for i := 0; i < len(w); i++ {
			ch := w[i] - 'a'
			if node.children[ch] == nil {
				node.children[ch] = &Node{}
				node.kids++
			}
			node = node.children[ch]
		}
		node.word = w
	}
	found, steps, cut = 0, 0, cutOut
	for r := range board {
		for c := range board[0] {
			walk(r, c, root)
		}
	}
	return found, steps
}

func main() {
	rows := []string{"aaaa", "aaaa", "aaaa", "aaaa"}
	words := []string{}
	for k := 1; k <= 8; k++ {
		words = append(words, strings.Repeat("a", k)) // a, aa, ..., aaaaaaaa
	}
	keptFound, keptSteps := search(rows, words, false)
	cutFound, cutSteps := search(rows, words, true)
	fmt.Printf("found: %d with found words kept, %d with them cut out\n", keptFound, cutFound)
	fmt.Printf("cells stepped on: %d with found words kept, %d with them cut out\n", keptSteps, cutSteps)
}

// found: 8 with found words kept, 8 with them cut out
// cells stepped on: 4856 with found words kept, 8 with them cut out

Both found all 8 words. With the found words kept, the walk stepped on 4,856 cells: every path of up to eight a's from every cell is a prefix of aaaaaaaa, so it never had a reason to stop. With them cut out it stepped on 8: the first path found all eight words on its way down, its nodes were cut on the way back up, and the trie was empty before the second cell. Boards like this are where Word Search II solutions without the cut run out of time.

A 4 × 4 board of a's beside the trie a, aa, up to aaaaaaaa, drawn twice. Left, found words kept: the trie stays whole, and the walk follows every path, 4,856 cells stepped on. Right, found words cut out: 1, the first path finds all eight words on its way down; 2, on the way back up each empty node is cut from its parent; 3, the trie is empty, and the walk stops after 8 cells. Focal line: a found word leaves the trie. Title: cut what you found.

Words at the end of a stream

Stream of Characters turns the question around. Letters arrive one at a time, and after each one you say whether some word ends at the stream's last letters. A trie of the words as they are would have to start a walk at every earlier letter. A trie of the words reversed needs one walk: "the stream ends with a word" becomes "the reversed stream starts with a reversed word", so read the stream from its newest letter backward down the trie. Stop with true at the first end mark, and with false where a node has no child. Never read further back than the longest word:

class Node:
    def __init__(self):
        self.children = [None] * 26
        self.word = None


class StreamChecker:
    def __init__(self, words):
        self.root = Node()
        self.longest = max(len(w) for w in words)
        for w in words:
            node = self.root
            for ch in reversed(w):            # the word backwards: its last letter first
                i = ord(ch) - ord("a")
                if node.children[i] is None:
                    node.children[i] = Node()
                node = node.children[i]
            node.word = w
        self.stream = []
        self.read = 0                         # letters the last query read
        self.matched = None                   # the word it found, if any

    def query(self, letter):
        self.stream.append(letter)
        node, self.read, self.matched = self.root, 0, None
        # the newest letter first, and never further back than the longest word
        for k in range(len(self.stream) - 1, max(-1, len(self.stream) - 1 - self.longest), -1):
            node = node.children[ord(self.stream[k]) - ord("a")]
            self.read += 1
            if node is None:
                return False
            if node.word is not None:
                self.matched = node.word
                return True
        return False


checker = StreamChecker(["ode", "dex", "code"])
for letter in "codex":
    hit = checker.query(letter)
    tail = f" ({checker.matched})" if hit else ""
    print(f"{letter}: {'true' if hit else 'false'}, read {checker.read} {'letter' if checker.read == 1 else 'letters'}{tail}")

# c: false, read 1 letter
# o: false, read 1 letter
# d: false, read 1 letter
# e: true, read 3 letters (ode)
# x: true, read 3 letters (dex)
import java.util.*;

public class Main {
    static class Node {
        final Node[] children = new Node[26];
        String word;
    }

    static class StreamChecker {
        final Node root = new Node();
        final int longest;
        final StringBuilder stream = new StringBuilder();
        int read;                                    // letters the last query read
        String matched;                              // the word it found, if any

        StreamChecker(String[] words) {
            int most = 0;
            for (String w : words) {
                most = Math.max(most, w.length());
                Node node = root;
                for (int k = w.length() - 1; k >= 0; k--) {   // the word backwards: its last letter first
                    int i = w.charAt(k) - 'a';
                    if (node.children[i] == null) node.children[i] = new Node();
                    node = node.children[i];
                }
                node.word = w;
            }
            longest = most;
        }

        boolean query(char letter) {
            stream.append(letter);
            Node node = root;
            read = 0;
            matched = null;
            // the newest letter first, and never further back than the longest word
            for (int k = stream.length() - 1; k >= 0 && k > stream.length() - 1 - longest; k--) {
                node = node.children[stream.charAt(k) - 'a'];
                read++;
                if (node == null) return false;
                if (node.word != null) {
                    matched = node.word;
                    return true;
                }
            }
            return false;
        }
    }

    public static void main(String[] args) {
        StreamChecker checker = new StreamChecker(new String[] {"ode", "dex", "code"});
        for (char letter : "codex".toCharArray()) {
            boolean hit = checker.query(letter);
            System.out.println(letter + ": " + hit + ", read " + checker.read + (checker.read == 1 ? " letter" : " letters")
                + (hit ? " (" + checker.matched + ")" : ""));
        }
    }
}

// c: false, read 1 letter
// o: false, read 1 letter
// d: false, read 1 letter
// e: true, read 3 letters (ode)
// x: true, read 3 letters (dex)
#include <bits/stdc++.h>
using namespace std;

struct Node {
    Node* children[26] = {};
    string word;
};

struct StreamChecker {
    Node* root = new Node();
    int longest = 0;
    string stream;
    int read = 0;                             // letters the last query read
    string matched;                           // the word it found, if any

    StreamChecker(const vector<string>& words) {
        for (const string& w : words) {
            longest = max(longest, (int) w.size());
            Node* node = root;
            for (int k = (int) w.size() - 1; k >= 0; k--) {   // the word backwards: its last letter first
                int i = w[k] - 'a';
                if (!node->children[i]) node->children[i] = new Node();
                node = node->children[i];
            }
            node->word = w;
        }
    }

    bool query(char letter) {
        stream += letter;
        Node* node = root;
        read = 0;
        matched.clear();
        // the newest letter first, and never further back than the longest word
        for (int k = (int) stream.size() - 1; k >= 0 && k > (int) stream.size() - 1 - longest; k--) {
            node = node->children[stream[k] - 'a'];
            read++;
            if (!node) return false;
            if (!node->word.empty()) {
                matched = node->word;
                return true;
            }
        }
        return false;
    }
};

int main() {
    StreamChecker checker({"ode", "dex", "code"});
    for (char letter : string("codex")) {
        bool hit = checker.query(letter);
        cout << letter << ": " << (hit ? "true" : "false") << ", read " << checker.read
             << (checker.read == 1 ? " letter" : " letters") << (hit ? " (" + checker.matched + ")" : "") << "\n";
    }
}

// c: false, read 1 letter
// o: false, read 1 letter
// d: false, read 1 letter
// e: true, read 3 letters (ode)
// x: true, read 3 letters (dex)
class Node {
    constructor() {
        this.children = new Array(26).fill(null);
        this.word = null;
    }
}

class StreamChecker {
    constructor(words) {
        this.root = new Node();
        this.longest = Math.max(...words.map((w) => w.length));
        for (const w of words) {
            let node = this.root;
            for (let k = w.length - 1; k >= 0; k--) {   // the word backwards: its last letter first
                const i = w.charCodeAt(k) - 97;
                if (node.children[i] === null) node.children[i] = new Node();
                node = node.children[i];
            }
            node.word = w;
        }
        this.stream = [];
        this.read = 0;                        // letters the last query read
        this.matched = null;                  // the word it found, if any
    }

    query(letter) {
        this.stream.push(letter);
        let node = this.root;
        this.read = 0;
        this.matched = null;
        // the newest letter first, and never further back than the longest word
        for (let k = this.stream.length - 1; k >= 0 && k > this.stream.length - 1 - this.longest; k--) {
            node = node.children[this.stream[k].charCodeAt(0) - 97];
            this.read++;
            if (node === null) return false;
            if (node.word !== null) {
                this.matched = node.word;
                return true;
            }
        }
        return false;
    }
}

const checker = new StreamChecker(["ode", "dex", "code"]);
for (const letter of "codex") {
    const hit = checker.query(letter);
    console.log(`${letter}: ${hit}, read ${checker.read} ${checker.read === 1 ? "letter" : "letters"}${hit ? ` (${checker.matched})` : ""}`);
}

// c: false, read 1 letter
// o: false, read 1 letter
// d: false, read 1 letter
// e: true, read 3 letters (ode)
// x: true, read 3 letters (dex)
class TrieNode {
    children: (TrieNode | null)[] = new Array(26).fill(null);
    word: string | null = null;
}

class StreamChecker {
    root = new TrieNode();
    longest: number;
    stream: string[] = [];
    read = 0;                                 // letters the last query read
    matched: string | null = null;            // the word it found, if any

    constructor(words: string[]) {
        this.longest = Math.max(...words.map((w) => w.length));
        for (const w of words) {
            let node = this.root;
            for (let k = w.length - 1; k >= 0; k--) {   // the word backwards: its last letter first
                const i = w.charCodeAt(k) - 97;
                if (node.children[i] === null) node.children[i] = new TrieNode();
                node = node.children[i]!;
            }
            node.word = w;
        }
    }

    query(letter: string): boolean {
        this.stream.push(letter);
        let node: TrieNode | null = this.root;
        this.read = 0;
        this.matched = null;
        // the newest letter first, and never further back than the longest word
        for (let k = this.stream.length - 1; k >= 0 && k > this.stream.length - 1 - this.longest; k--) {
            node = node.children[this.stream[k].charCodeAt(0) - 97];
            this.read++;
            if (node === null) return false;
            if (node.word !== null) {
                this.matched = node.word;
                return true;
            }
        }
        return false;
    }
}

const checker = new StreamChecker(["ode", "dex", "code"]);
for (const letter of "codex") {
    const hit = checker.query(letter);
    console.log(`${letter}: ${hit}, read ${checker.read} ${checker.read === 1 ? "letter" : "letters"}${hit ? ` (${checker.matched})` : ""}`);
}

// c: false, read 1 letter
// o: false, read 1 letter
// d: false, read 1 letter
// e: true, read 3 letters (ode)
// x: true, read 3 letters (dex)
using System;
using System.Linq;
using System.Text;

class Node {
    public readonly Node[] Children = new Node[26];
    public string Word;
}

class StreamChecker {
    readonly Node root = new Node();
    readonly int longest;
    readonly StringBuilder stream = new StringBuilder();
    public int Read;                          // letters the last query read
    public string Matched;                    // the word it found, if any

    public StreamChecker(string[] words) {
        longest = words.Max(w => w.Length);
        foreach (string w in words) {
            Node node = root;
            for (int k = w.Length - 1; k >= 0; k--) {   // the word backwards: its last letter first
                int i = w[k] - 'a';
                if (node.Children[i] == null) node.Children[i] = new Node();
                node = node.Children[i];
            }
            node.Word = w;
        }
    }

    public bool Query(char letter) {
        stream.Append(letter);
        Node node = root;
        Read = 0;
        Matched = null;
        // the newest letter first, and never further back than the longest word
        for (int k = stream.Length - 1; k >= 0 && k > stream.Length - 1 - longest; k--) {
            node = node.Children[stream[k] - 'a'];
            Read++;
            if (node == null) return false;
            if (node.Word != null) {
                Matched = node.Word;
                return true;
            }
        }
        return false;
    }
}

class Program {
    static void Main() {
        var checker = new StreamChecker(new[] {"ode", "dex", "code"});
        foreach (char letter in "codex") {
            bool hit = checker.Query(letter);
            Console.WriteLine($"{letter}: {(hit ? "true" : "false")}, read {checker.Read} {(checker.Read == 1 ? "letter" : "letters")}{(hit ? $" ({checker.Matched})" : "")}");
        }
    }
}

// c: false, read 1 letter
// o: false, read 1 letter
// d: false, read 1 letter
// e: true, read 3 letters (ode)
// x: true, read 3 letters (dex)
package main

import "fmt"

type Node struct {
	children [26]*Node
	word     string
}

type StreamChecker struct {
	root    *Node
	longest int
	stream  []byte
	read    int    // letters the last query read
	matched string // the word it found, if any
}

func NewStreamChecker(words []string) *StreamChecker {
	s := &StreamChecker{root: &Node{}}
	for _, w := range words {
		if len(w) > s.longest {
			s.longest = len(w)
		}
		node := s.root
		for k := len(w) - 1; k >= 0; k-- { // the word backwards: its last letter first
			i := w[k] - 'a'
			if node.children[i] == nil {
				node.children[i] = &Node{}
			}
			node = node.children[i]
		}
		node.word = w
	}
	return s
}

func (s *StreamChecker) Query(letter byte) bool {
	s.stream = append(s.stream, letter)
	node := s.root
	s.read, s.matched = 0, ""
	// the newest letter first, and never further back than the longest word
	for k := len(s.stream) - 1; k >= 0 && k > len(s.stream)-1-s.longest; k-- {
		node = node.children[s.stream[k]-'a']
		s.read++
		if node == nil {
			return false
		}
		if node.word != "" {
			s.matched = node.word
			return true
		}
	}
	return false
}

func main() {
	checker := NewStreamChecker([]string{"ode", "dex", "code"})
	stream := "codex"
	for i := 0; i < len(stream); i++ {
		hit := checker.Query(stream[i])
		unit := "letters"
		if checker.read == 1 {
			unit = "letter"
		}
		tail := ""
		if hit {
			tail = " (" + checker.matched + ")"
		}
		fmt.Printf("%c: %t, read %d %s%s\n", stream[i], hit, checker.read, unit, tail)
	}
}

// c: false, read 1 letter
// o: false, read 1 letter
// d: false, read 1 letter
// e: true, read 3 letters (ode)
// x: true, read 3 letters (dex)

The words ode, dex and code went in backwards: edo, xed, edoc. After c, o and d, one letter was enough to say no: no word ends in c, o or d. After e the walk read e, d, o and met ode's end mark (code ends there too, one letter further back, but one word is all the answer needs). After x it read x, e, d: dex. Each query reads at most as many letters as the longest word, however many words there are and however long the stream gets.

The stream c, o, d, e, x as tiles, and below it the trie of ode, dex and code reversed: edo, xed, edoc, drawn from the root rightwards. 1, the words go in backwards, last letter first; 2, after x arrives the walk reads x, then e, then d, going back through the stream; 3, d's node has dex's end mark, so the query answers true after three letters. Focal line: read the newest letter first. Title: read the stream backwards.

Spotting the pattern

Clues that one trie of all the words is the tool:

  • Many words against one board, grid or text, where checking each word on its own repeats the same walk.
  • A backtracking search that should stop early: the trie says at every step whether any word still has this prefix.
  • Letters arriving one at a time with a question about the last few: reverse the words.

Near misses:

  • One word on the board: that's Word Search, plain backtracking, no trie needed.
  • Many words, each looked up whole in a list: a hash set answers each in one lookup.
  • One pattern in one long text: string matching (KMP, Z), not a trie.

Mistakes that look right

Two of these are run below in every language. The others are just as common:

  • The found word's mark isn't cleared: a word that lies on two paths is reported twice.
  • The cell isn't given its letter back on the way out: later paths can't use it, and words through it are missed.
  • One search per word, which repeats the shared prefixes' walks for every word, as the first example measured.
  • A stream checked with the words as they are, or by testing every word against the stream's end: both cost a walk or a comparison per word for every letter.
class Node:
    def __init__(self):
        self.children = [None] * 26
        self.word = None


def find_words(rows, words, clear, give_back):
    board = [list(row) for row in rows]
    root = Node()
    for w in words:
        node = root
        for ch in w:
            i = ord(ch) - ord("a")
            if node.children[i] is None:
                node.children[i] = Node()
            node = node.children[i]
        node.word = w
    found = []

    def walk(r, c, node):
        if r < 0 or c < 0 or r >= len(board) or c >= len(board[0]) or board[r][c] == "#":
            return
        here = board[r][c]
        child = node.children[ord(here) - ord("a")]
        if child is None:
            return
        if child.word is not None:
            found.append(child.word)
            if clear:
                child.word = None
        board[r][c] = "#"
        walk(r + 1, c, child)
        walk(r - 1, c, child)
        walk(r, c + 1, child)
        walk(r, c - 1, child)
        if give_back:
            board[r][c] = here

    for r in range(len(board)):
        for c in range(len(board[0])):
            walk(r, c, root)
    return sorted(found)


rows = ["cats", "orea", "dent"]
words = ["cat", "cats", "care", "core", "cord", "dent", "tent", "rent", "tea", "seat"]
for name, clear, give_back in (("the mark not cleared", False, True), ("the cell not given back", True, False),
                               ("both right", True, True)):
    found = find_words(rows, words, clear, give_back)
    print(f"{name}: {len(found)} found: {' '.join(found)}")

# the mark not cleared: 11 found: care care cat cats core core dent rent rent tea tent
# the cell not given back: 3 found: cat cats core
# both right: 8 found: care cat cats core dent rent tea tent
import java.util.*;

public class Main {
    static class Node {
        final Node[] children = new Node[26];
        String word;
    }

    static char[][] board;
    static List<String> found;
    static boolean clear, giveBack;

    static void walk(int r, int c, Node node) {
        if (r < 0 || c < 0 || r >= board.length || c >= board[0].length || board[r][c] == '#') return;
        char here = board[r][c];
        Node child = node.children[here - 'a'];
        if (child == null) return;
        if (child.word != null) {
            found.add(child.word);
            if (clear) child.word = null;
        }
        board[r][c] = '#';
        walk(r + 1, c, child);
        walk(r - 1, c, child);
        walk(r, c + 1, child);
        walk(r, c - 1, child);
        if (giveBack) board[r][c] = here;
    }

    static List<String> findWords(String[] rows, String[] words, boolean clearMark, boolean giveCellBack) {
        board = new char[rows.length][];
        for (int r = 0; r < rows.length; r++) board[r] = rows[r].toCharArray();
        Node root = new Node();
        for (String w : words) {
            Node node = root;
            for (char ch : w.toCharArray()) {
                if (node.children[ch - 'a'] == null) node.children[ch - 'a'] = new Node();
                node = node.children[ch - 'a'];
            }
            node.word = w;
        }
        found = new ArrayList<>();
        clear = clearMark;
        giveBack = giveCellBack;
        for (int r = 0; r < board.length; r++)
            for (int c = 0; c < board[0].length; c++) walk(r, c, root);
        Collections.sort(found);
        return found;
    }

    public static void main(String[] args) {
        String[] rows = {"cats", "orea", "dent"};
        String[] words = {"cat", "cats", "care", "core", "cord", "dent", "tent", "rent", "tea", "seat"};
        String[] names = {"the mark not cleared", "the cell not given back", "both right"};
        boolean[][] ways = {{false, true}, {true, false}, {true, true}};
        for (int k = 0; k < 3; k++) {
            List<String> got = findWords(rows, words, ways[k][0], ways[k][1]);
            System.out.println(names[k] + ": " + got.size() + " found: " + String.join(" ", got));
        }
    }
}

// the mark not cleared: 11 found: care care cat cats core core dent rent rent tea tent
// the cell not given back: 3 found: cat cats core
// both right: 8 found: care cat cats core dent rent tea tent
#include <bits/stdc++.h>
using namespace std;

struct Node {
    Node* children[26] = {};
    string word;
};

vector<string> board;
vector<string> found;
bool clearMark, giveBack;

void walk(int r, int c, Node* node) {
    if (r < 0 || c < 0 || r >= (int) board.size() || c >= (int) board[0].size() || board[r][c] == '#') return;
    char here = board[r][c];
    Node* child = node->children[here - 'a'];
    if (!child) return;
    if (!child->word.empty()) {
        found.push_back(child->word);
        if (clearMark) child->word.clear();
    }
    board[r][c] = '#';
    walk(r + 1, c, child);
    walk(r - 1, c, child);
    walk(r, c + 1, child);
    walk(r, c - 1, child);
    if (giveBack) board[r][c] = here;
}

vector<string> findWords(const vector<string>& rows, const vector<string>& words, bool clear, bool back) {
    board = rows;
    Node* root = new Node();
    for (const string& w : words) {
        Node* node = root;
        for (char ch : w) {
            if (!node->children[ch - 'a']) node->children[ch - 'a'] = new Node();
            node = node->children[ch - 'a'];
        }
        node->word = w;
    }
    found.clear();
    clearMark = clear;
    giveBack = back;
    for (int r = 0; r < (int) board.size(); r++)
        for (int c = 0; c < (int) board[0].size(); c++) walk(r, c, root);
    sort(found.begin(), found.end());
    return found;
}

int main() {
    vector<string> rows = {"cats", "orea", "dent"};
    vector<string> words = {"cat", "cats", "care", "core", "cord", "dent", "tent", "rent", "tea", "seat"};
    vector<tuple<string, bool, bool>> ways = {
        {"the mark not cleared", false, true}, {"the cell not given back", true, false}, {"both right", true, true}};
    for (auto& [name, clear, back] : ways) {
        vector<string> got = findWords(rows, words, clear, back);
        cout << name << ": " << got.size() << " found:";
        for (const string& w : got) cout << " " << w;
        cout << "\n";
    }
}

// the mark not cleared: 11 found: care care cat cats core core dent rent rent tea tent
// the cell not given back: 3 found: cat cats core
// both right: 8 found: care cat cats core dent rent tea tent
class Node {
    constructor() {
        this.children = new Array(26).fill(null);
        this.word = null;
    }
}

function findWords(rows, words, clear, giveBack) {
    const board = rows.map((row) => [...row]);
    const root = new Node();
    for (const w of words) {
        let node = root;
        for (const ch of w) {
            const i = ch.charCodeAt(0) - 97;
            if (node.children[i] === null) node.children[i] = new Node();
            node = node.children[i];
        }
        node.word = w;
    }
    const found = [];
    const walk = (r, c, node) => {
        if (r < 0 || c < 0 || r >= board.length || c >= board[0].length || board[r][c] === "#") return;
        const here = board[r][c];
        const child = node.children[here.charCodeAt(0) - 97];
        if (child === null) return;
        if (child.word !== null) {
            found.push(child.word);
            if (clear) child.word = null;
        }
        board[r][c] = "#";
        walk(r + 1, c, child);
        walk(r - 1, c, child);
        walk(r, c + 1, child);
        walk(r, c - 1, child);
        if (giveBack) board[r][c] = here;
    };
    for (let r = 0; r < board.length; r++)
        for (let c = 0; c < board[0].length; c++) walk(r, c, root);
    return found.sort();
}

const rows = ["cats", "orea", "dent"];
const words = ["cat", "cats", "care", "core", "cord", "dent", "tent", "rent", "tea", "seat"];
for (const [name, clear, giveBack] of [["the mark not cleared", false, true], ["the cell not given back", true, false],
    ["both right", true, true]]) {
    const found = findWords(rows, words, clear, giveBack);
    console.log(`${name}: ${found.length} found: ${found.join(" ")}`);
}

// the mark not cleared: 11 found: care care cat cats core core dent rent rent tea tent
// the cell not given back: 3 found: cat cats core
// both right: 8 found: care cat cats core dent rent tea tent
class TrieNode {
    children: (TrieNode | null)[] = new Array(26).fill(null);
    word: string | null = null;
}

function findWords(rows: string[], words: string[], clear: boolean, giveBack: boolean): string[] {
    const board = rows.map((row) => [...row]);
    const root = new TrieNode();
    for (const w of words) {
        let node = root;
        for (const ch of w) {
            const i = ch.charCodeAt(0) - 97;
            if (node.children[i] === null) node.children[i] = new TrieNode();
            node = node.children[i]!;
        }
        node.word = w;
    }
    const found: string[] = [];
    const walk = (r: number, c: number, node: TrieNode): void => {
        if (r < 0 || c < 0 || r >= board.length || c >= board[0].length || board[r][c] === "#") return;
        const here = board[r][c];
        const child = node.children[here.charCodeAt(0) - 97];
        if (child === null) return;
        if (child.word !== null) {
            found.push(child.word);
            if (clear) child.word = null;
        }
        board[r][c] = "#";
        walk(r + 1, c, child);
        walk(r - 1, c, child);
        walk(r, c + 1, child);
        walk(r, c - 1, child);
        if (giveBack) board[r][c] = here;
    };
    for (let r = 0; r < board.length; r++)
        for (let c = 0; c < board[0].length; c++) walk(r, c, root);
    return found.sort();
}

const rows = ["cats", "orea", "dent"];
const words = ["cat", "cats", "care", "core", "cord", "dent", "tent", "rent", "tea", "seat"];
const ways: [string, boolean, boolean][] = [["the mark not cleared", false, true],
    ["the cell not given back", true, false], ["both right", true, true]];
for (const [name, clear, giveBack] of ways) {
    const found = findWords(rows, words, clear, giveBack);
    console.log(`${name}: ${found.length} found: ${found.join(" ")}`);
}

// the mark not cleared: 11 found: care care cat cats core core dent rent rent tea tent
// the cell not given back: 3 found: cat cats core
// both right: 8 found: care cat cats core dent rent tea tent
using System;
using System.Collections.Generic;
using System.Linq;

class Node {
    public readonly Node[] Children = new Node[26];
    public string Word;
}

class Program {
    static char[][] board;
    static List<string> found;
    static bool clear, giveBack;

    static void Walk(int r, int c, Node node) {
        if (r < 0 || c < 0 || r >= board.Length || c >= board[0].Length || board[r][c] == '#') return;
        char here = board[r][c];
        Node child = node.Children[here - 'a'];
        if (child == null) return;
        if (child.Word != null) {
            found.Add(child.Word);
            if (clear) child.Word = null;
        }
        board[r][c] = '#';
        Walk(r + 1, c, child);
        Walk(r - 1, c, child);
        Walk(r, c + 1, child);
        Walk(r, c - 1, child);
        if (giveBack) board[r][c] = here;
    }

    static List<string> FindWords(string[] rows, string[] words, bool clearMark, bool giveCellBack) {
        board = rows.Select(row => row.ToCharArray()).ToArray();
        var root = new Node();
        foreach (string w in words) {
            Node node = root;
            foreach (char ch in w) {
                if (node.Children[ch - 'a'] == null) node.Children[ch - 'a'] = new Node();
                node = node.Children[ch - 'a'];
            }
            node.Word = w;
        }
        found = new List<string>();
        clear = clearMark;
        giveBack = giveCellBack;
        for (int r = 0; r < board.Length; r++)
            for (int c = 0; c < board[0].Length; c++) Walk(r, c, root);
        return found.OrderBy(w => w, StringComparer.Ordinal).ToList();
    }

    static void Main() {
        string[] rows = {"cats", "orea", "dent"};
        string[] words = {"cat", "cats", "care", "core", "cord", "dent", "tent", "rent", "tea", "seat"};
        var ways = new (string, bool, bool)[] {
            ("the mark not cleared", false, true), ("the cell not given back", true, false), ("both right", true, true)};
        foreach (var (name, clearMark, back) in ways) {
            List<string> got = FindWords(rows, words, clearMark, back);
            Console.WriteLine($"{name}: {got.Count} found: {string.Join(" ", got)}");
        }
    }
}

// the mark not cleared: 11 found: care care cat cats core core dent rent rent tea tent
// the cell not given back: 3 found: cat cats core
// both right: 8 found: care cat cats core dent rent tea tent
package main

import (
	"fmt"
	"sort"
	"strings"
)

type Node struct {
	children [26]*Node
	word     string
}

var board [][]byte
var found []string
var clearMark, giveBack bool

func walk(r, c int, node *Node) {
	if r < 0 || c < 0 || r >= len(board) || c >= len(board[0]) || board[r][c] == '#' {
		return
	}
	here := board[r][c]
	child := node.children[here-'a']
	if child == nil {
		return
	}
	if child.word != "" {
		found = append(found, child.word)
		if clearMark {
			child.word = ""
		}
	}
	board[r][c] = '#'
	walk(r+1, c, child)
	walk(r-1, c, child)
	walk(r, c+1, child)
	walk(r, c-1, child)
	if giveBack {
		board[r][c] = here
	}
}

func findWords(rows []string, words []string, clear, back bool) []string {
	board = make([][]byte, len(rows))
	for r, row := range rows {
		board[r] = []byte(row)
	}
	root := &Node{}
	for _, w := range words {
		node := root
		for i := 0; i < len(w); i++ {
			ch := w[i] - 'a'
			if node.children[ch] == nil {
				node.children[ch] = &Node{}
			}
			node = node.children[ch]
		}
		node.word = w
	}
	found, clearMark, giveBack = []string{}, clear, back
	for r := range board {
		for c := range board[0] {
			walk(r, c, root)
		}
	}
	sort.Strings(found)
	return found
}

func main() {
	rows := []string{"cats", "orea", "dent"}
	words := []string{"cat", "cats", "care", "core", "cord", "dent", "tent", "rent", "tea", "seat"}
	ways := []struct {
		name        string
		clear, back bool
	}{{"the mark not cleared", false, true}, {"the cell not given back", true, false}, {"both right", true, true}}
	for _, way := range ways {
		got := findWords(rows, words, way.clear, way.back)
		fmt.Printf("%s: %d found: %s\n", way.name, len(got), strings.Join(got, " "))
	}
}

// the mark not cleared: 11 found: care care cat cats core core dent rent rent tea tent
// the cell not given back: 3 found: cat cats core
// both right: 8 found: care cat cats core dent rent tea tent

Without clearing the mark, care, core and rent came out twice: each lies on two paths, one through the e in the middle row and one through the e in the bottom row. Without giving the cells back, the walk from the first c left its path covered in #'s, and only cat, cats and core were found. With both right, the eight words come out once each.

Check yourself

Quiz

1 / 6

Multiple choice

Word Search II with 500 words on one board, solved with one trie of all of them. How many times does the board search run?

Practice

The section's problem for this lesson:

  1. Word Search II: one trie of all the words, one walk from every cell, found words taken off their nodes and cut out of the trie.

The walk down a trie with a choice at each step comes back in Bitwise Tries: Maximum XOR, over bits instead of letters.