class Solution:
def wordSquares(self, words):
from collections import defaultdict
N = len(words[0])
prefix_map = defaultdict(list)
for word in words:
for i in range(N+1):
prefix_map[word[:i]].append(word)
results = []
def backtrack(square):
if len(square) == N:
results.append(square[:])
return
prefix = ''.join([word[len(square)] for word in square])
for candidate in prefix_map.get(prefix, []):
square.append(candidate)
backtrack(square)
square.pop()
for word in words:
backtrack([word])
return results
class Solution {
public:
vector<vector<string>> wordSquares(vector<string>& words) {
int N = words[0].size();
unordered_map<string, vector<string>> prefix_map;
for (const string& word : words) {
for (int i = 0; i <= N; ++i) {
prefix_map[word.substr(0, i)].push_back(word);
}
}
vector<vector<string>> results;
vector<string> square;
function<void()> backtrack = [&]() {
if (square.size() == N) {
results.push_back(square);
return;
}
string prefix;
for (int i = 0; i < square.size(); ++i) {
prefix += square[i][square.size()];
}
for (const string& candidate : prefix_map[prefix]) {
square.push_back(candidate);
backtrack();
square.pop_back();
}
};
for (const string& word : words) {
square = {word};
backtrack();
}
return results;
}
};
class Solution {
public List<List<String>> wordSquares(String[] words) {
int N = words[0].length();
Map<String, List<String>> prefixMap = new HashMap<>();
for (String word : words) {
for (int i = 0; i <= N; ++i) {
String prefix = word.substring(0, i);
prefixMap.computeIfAbsent(prefix, k -> new ArrayList<>()).add(word);
}
}
List<List<String>> results = new ArrayList<>();
List<String> square = new ArrayList<>();
backtrack(words, N, prefixMap, results, square);
return results;
}
private void backtrack(String[] words, int N, Map<String, List<String>> prefixMap,
List<List<String>> results, List<String> square) {
if (square.size() == N) {
results.add(new ArrayList<>(square));
return;
}
StringBuilder prefix = new StringBuilder();
int idx = square.size();
for (String s : square) {
prefix.append(s.charAt(idx));
}
for (String candidate : prefixMap.getOrDefault(prefix.toString(), new ArrayList<>())) {
square.add(candidate);
backtrack(words, N, prefixMap, results, square);
square.remove(square.size() - 1);
}
}
}
var wordSquares = function(words) {
const N = words[0].length;
const prefixMap = {};
for (let word of words) {
for (let i = 0; i <= N; ++i) {
let prefix = word.substring(0, i);
if (!prefixMap[prefix]) prefixMap[prefix] = [];
prefixMap[prefix].push(word);
}
}
const results = [];
function backtrack(square) {
if (square.length === N) {
results.push(square.slice());
return;
}
let prefix = '';
let idx = square.length;
for (let i = 0; i < idx; ++i) {
prefix += square[i][idx];
}
let candidates = prefixMap[prefix] || [];
for (let candidate of candidates) {
square.push(candidate);
backtrack(square);
square.pop();
}
}
for (let word of words) {
backtrack([word]);
}
return results;
};
Given an array of strings words
, all of the same length, you are to find all possible word squares that can be built from them.
A word square is a set of words arranged in a square grid so that the words read the same horizontally and vertically.
That is, the word at row i
is the same as the word at column i
.
Constraints:
words
as many times as you like.N
. The square is N x N
.
Example:
Input: ["area","lead","wall","lady","ball"]
Output: [ ["wall","area","lead","lady"], ["ball","area","lead","lady"] ]
At first glance, the problem seems to require generating all possible arrangements of words in a square, then checking if the vertical and horizontal words match. This brute-force approach would involve trying every possible combination, which can be very slow as the number of words and their length grows.
However, we can notice that for a word square, the k-th word must have its first k letters match the k-th column so far. This means that at each step, we can use the current partial square to determine a prefix and only consider words that match this prefix for the next row. This insight allows us to prune invalid combinations early.
To do this efficiently, we can build a prefix map (like a dictionary or trie) that lets us quickly find all words starting with a given prefix. Using backtracking, we can build the square row by row, always ensuring the columns match.
N
), add it to the results.Using a prefix map allows us to efficiently find only those words that can possibly fit, drastically reducing the number of combinations we need to check.
Let's use the sample input ["area","lead","wall","lady","ball"]
(all words of length 4).
This process efficiently finds all valid word squares.
Brute-force approach: Try all possible combinations of N
words for the square, check if columns match. If there are W
words of length N
, the number of combinations is W^N
. Checking validity for each square takes O(N^2)
time.
Optimized approach (with prefix map):
O(W * N^2)
(since each word has N
prefixes, each of length up to N
).W^N
because at each step, only words matching the current prefix are considered. In the worst case (if all prefixes are present), it's still exponential, but in practice, the prefix map prunes the search space significantly.O(W * N^2)
space. The recursion stack uses up to O(N)
space.The word squares problem is a classic example of using backtracking with efficient pruning. By recognizing that each new row must match the current columns, and using a prefix map to quickly find candidates, we avoid brute-force enumeration of all possibilities. This makes the solution elegant and efficient, and highlights the power of combining recursion with smart data structures.