Talentd
FleetCode
Make and follow sheets.Sign in

Word Squares solution and explanation

HardArrayBacktrackingStringTrie4 min read54.7% acceptanceUpdated 23 Mar 2026Premium elsewhere, free here
425 Word Squares | Leetcode | Google Interview QuestionSonu Raj · 15:32 · 5K viewsWatch on YouTube
9 more videos on this problem
  1. 425 Word Squares | Leetcode | Google Interview QuestionSonu Raj · 15:32 · 5K views
  2. LeetCode 425. Word SquaresHappy Coding · 33:55 · 3K views
  3. Leetcode 425 Word Squares Analysis & ImplementationJessieW · 17:54 · 3K views
  4. LeetCode 425. Word SquarescoderZ · 22:06 · 985 views
  5. 425. Word Squares - Week 3/5 Leetcode July ChallengeProgramming Live with Larry · 26:52 · 416 views
  6. 425. Word Squares | Leetcode Premium | Trie | DFS + BacktrackingNext Level(FAANG) · 18:22 · 390 views
  7. Leetcode 425. Word Squares. 详细解析Alina L · 21:10 · 371 views
  8. Word Squares | Leetcode #425Surjit Singh · 13:05 · 298 views
  9. 425 Word Squares with TriesAndrewHou · 19:04 · 102 views
  10. LeetCode "из грязи в князи" часть 28 (Trie) 425. Word SquaresRomander · 1:51:53 · 27 views

The problem

Given an array of unique strings words, return all the word squares you can build from words. The same word from words can be used multiple times. You can return the answer in any order.

A sequence of strings forms a valid word square if the kth row and column read the same string, where 0 <= k < max(numRows, numColumns).

  • For example, the word sequence ["ball","area","lead","lady"] forms a word square because each word reads the same both horizontally and vertically.

Example 1:

Input: words = ["area","lead","wall","lady","ball"]
Output: [["ball","area","lead","lady"],["wall","area","lead","lady"]]
Explanation:
The output consists of two word squares. The order of output does not matter (just the order of words in each word square matters).

Example 2:

Input: words = ["abat","baba","atan","atal"]
Output: [["baba","abat","baba","atal"],["baba","abat","baba","atan"]]
Explanation:
The output consists of two word squares. The order of output does not matter (just the order of words in each word square matters).

Constraints:

  • 1 <= words.length <= 1000
  • 1 <= words[i].length <= 4
  • All words[i] have the same length.
  • words[i] consists of only lowercase English letters.
  • All words[i] are unique.

Approach overview

Problem Overview: Given a list of words with the same length, build all possible word squares. A word square means the k-th row and k-th column form the same string. If the first row starts with "ball", the first column must also read "ball". The task is to generate every valid arrangement of words that satisfies this constraint.

Approach 1: Backtracking with Prefix Scanning (Brute Force) (Time: O(N^L * L), Space: O(L))

Start with each word as the first row of the square and build the rest using backtracking. At step k, derive the required prefix for the next word using the characters from column k of previously placed rows. Scan the entire word list and pick candidates whose prefix matches. Continue recursively until the square reaches size L. This approach works but repeatedly scanning the array makes prefix lookups expensive.

Approach 2: Trie + Backtracking (Optimized) (Time: O(N * L^2) average, Space: O(N * L))

Speed up candidate lookup by indexing all words in a Trie. Each node stores the list of word indices that share the prefix represented by that node. During backtracking, compute the prefix needed for the next row and query the Trie to instantly retrieve all valid candidates. This removes the repeated full-array scans and turns prefix lookup into O(L). The recursive search builds squares row by row while maintaining the row/column symmetry constraint.

The key observation: when you already placed k rows, the next word must start with the characters formed by column k. Using a Trie makes this prefix query efficient. The remaining logic is standard depth‑first exploration over candidate words.

This problem heavily combines array traversal, string prefix construction, and structured pruning using a Trie.

Recommended for interviews: Trie + backtracking is the expected solution. Brute force backtracking demonstrates understanding of the square constraint, but efficient prefix lookup using a Trie shows stronger algorithmic design and pruning strategy.

Solution

class Trie:
    def __init__(self):
        self.children = [None] * 26
        self.v = []

    def insert(self, w, i):
        node = self
        for c in w:
            idx = ord(c) - ord('a')
            if node.children[idx] is None:
                node.children[idx] = Trie()
            node = node.children[idx]
            node.v.append(i)

    def search(self, w):
        node = self
        for c in w:
            idx = ord(c) - ord('a')
            if node.children[idx] is None:
                return []
            node = node.children[idx]
        return node.v


class Solution:
    def wordSquares(self, words: List[str]) -> List[List[str]]:
        def dfs(t):
            if len(t) == len(words[0]):
                ans.append(t[:])
                return
            idx = len(t)
            pref = [v[idx] for v in t]
            indexes = trie.search(''.join(pref))
            for i in indexes:
                t.append(words[i])
                dfs(t)
                t.pop()

        trie = Trie()
        ans = []
        for i, w in enumerate(words):
            trie.insert(w, i)
        for w in words:
            dfs([w])
        return ans
class Trie {
    Trie[] children = new Trie[26];
    List<Integer> v = new ArrayList<>();

    void insert(String word, int i) {
        Trie node = this;
        for (char c : word.toCharArray()) {
            c -= 'a';
            if (node.children[c] == null) {
                node.children[c] = new Trie();
            }
            node = node.children[c];
            node.v.add(i);
        }
    }

    List<Integer> search(String pref) {
        Trie node = this;
        for (char c : pref.toCharArray()) {
            c -= 'a';
            if (node.children[c] == null) {
                return Collections.emptyList();
            }
            node = node.children[c];
        }
        return node.v;
    }
}

class Solution {
    private Trie trie = new Trie();
    private String[] words;
    private List<List<String>> ans = new ArrayList<>();

    public List<List<String>> wordSquares(String[] words) {
        this.words = words;
        for (int i = 0; i < words.length; ++i) {
            trie.insert(words[i], i);
        }

        List<String> t = new ArrayList<>();
        for (String w : words) {
            t.add(w);
            dfs(t);
            t.remove(t.size() - 1);
        }
        return ans;
    }

    private void dfs(List<String> t) {
        if (t.size() == words[0].length()) {
            ans.add(new ArrayList<>(t));
            return;
        }
        int idx = t.size();
        StringBuilder pref = new StringBuilder();
        for (String x : t) {
            pref.append(x.charAt(idx));
        }
        List<Integer> indexes = trie.search(pref.toString());
        for (int i : indexes) {
            t.add(words[i]);
            dfs(t);
            t.remove(t.size() - 1);
        }
    }
}
type Trie struct {
	children [26]*Trie
	v        []int
}

func newTrie() *Trie {
	return &Trie{}
}
func (this *Trie) insert(word string, i int) {
	node := this
	for _, c := range word {
		c -= 'a'
		if node.children[c] == nil {
			node.children[c] = newTrie()
		}
		node = node.children[c]
		node.v = append(node.v, i)
	}
}
func (this *Trie) search(word string) []int {
	node := this
	for _, c := range word {
		c -= 'a'
		if node.children[c] == nil {
			return []int{}
		}
		node = node.children[c]
	}
	return node.v
}

func wordSquares(words []string) [][]string {
	trie := newTrie()
	for i, w := range words {
		trie.insert(w, i)
	}
	ans := [][]string{}
	var dfs func([]string)
	dfs = func(t []string) {
		if len(t) == len(words[0]) {
			cp := make([]string, len(t))
			copy(cp, t)
			ans = append(ans, cp)
			return
		}
		idx := len(t)
		pref := []byte{}
		for _, v := range t {
			pref = append(pref, v[idx])
		}
		indexes := trie.search(string(pref))
		for _, i := range indexes {
			t = append(t, words[i])
			dfs(t)
			t = t[:len(t)-1]
		}
	}
	for _, w := range words {
		dfs([]string{w})
	}
	return ans
}
Try this approach in the editor

Complexity analysis

ApproachTimeSpaceWhen to Use
Backtracking with Prefix ScanningO(N^L * L)O(L)Small input sizes or when demonstrating the base backtracking idea
Trie + BacktrackingO(N * L^2) averageO(N * L)Optimal approach for interviews and large word lists with many prefix queries

Frequently asked questions

Is Word Squares easy or hard?

Word Squares is a Hard difficulty problem. The challenge comes from combining backtracking with efficient prefix queries. A naive solution works conceptually but becomes slow without a Trie-based optimization.

How to solve Word Squares efficiently?

Build a Trie that maps every prefix to the list of words starting with that prefix. Use backtracking to build the square row by row. At step k, compute the prefix formed by column k and query the Trie for candidate words. This pruning ensures only valid prefixes are explored.

What is the best approach for Word Squares?

The best approach uses Trie with backtracking. Store all words in a Trie where each node tracks words sharing that prefix. During backtracking, compute the required column prefix and fetch candidates directly from the Trie. This avoids scanning the full list each time and reduces the search cost significantly.

Is Word Squares asked at Google/Amazon/Meta?

Word Squares is considered a classic hard backtracking + Trie problem and has appeared in interviews at companies like Google and Amazon. It tests recursive search, prefix pruning, and efficient string indexing structures.

What data structure is used in Word Squares?

The optimized solution uses a Trie for fast prefix lookup combined with backtracking to build valid squares. Arrays or lists store candidate words while strings are used to construct the column prefix during recursion.

What is the time complexity of Word Squares?

Trie construction takes O(N * L) where N is the number of words and L is the word length. The backtracking search typically runs around O(N * L^2) on average because prefix queries take O(L) and only matching candidates are explored. Worst-case complexity can grow exponentially if many words share the same prefixes.

Word Squares Python or Java solution approach?

Both Python and Java implementations follow the same pattern: build a Trie mapping prefixes to word indices, then perform DFS backtracking. At each level compute the required prefix and retrieve matching words from the Trie before continuing the recursion.

Comments

Now write it yourselfClose this page and solve Word Squares in the editor, without looking.
Practise Word Squares