Opening the reading…
Opening the reading…
GRAPHS › DFS AND BFS ON GRAPHS
Open — the attempt gate is not wired up yet
This editorial is meant to unlock after you have run the problem at least once, with the worked solution behind one further deliberate click. That needs per-learner unlock state nothing stores today, so for now the whole article is open.
Try it yourself first →Treat every valid word as a vertex. Connect two vertices when their words differ at exactly one position. A transformation sequence is then a path through this graph, and its length is the number of vertices visited, including beginWord and endWord. Because every allowed change costs one step, the shortest sequence is the shortest path in an unweighted graph.
You do not need to build all graph edges explicitly. From a current word, change each of its L positions to each of the other 25 letters, and check whether the resulting word is still in the dictionary. BFS explores these generated neighbors in increasing distance order, so the first time you reach endWord, no later path can use fewer words.
| MEASURE | BOUND | WHY |
|---|---|---|
| Time | O(N x L x 26) | Each dictionary word is enqueued at most once. When it is removed from the queue, the algorithm tests 26 replacements at each of its L positions, so the total number of generated candidates is bounded by N x L x 26; hash-set operations are O(1) on average. |
| Space | O(N x L) extra | The dictionary stores up to N words and the queue can hold up to N words, each of length L. The returned integer uses no output storage; the extra memory is therefore proportional to the stored word characters, and this bound is already reached when many dictionary words are queued or retained. |
#include <string>
#include <unordered_set>
#include <utility>
#include <vector>
#include <queue>
using namespace std;
class Solution {
public:
int ladderLength(string beginWord, string endWord, vector<string>& wordList) {
unordered_set<string> dict(wordList.begin(), wordList.end());
if (!dict.count(endWord)) return 0;
queue<pair<string, int>> q;
q.push({beginWord, 1});
dict.erase(beginWord);
while (!q.empty()) {
auto [word, distance] = q.front();
q.pop();
if (word == endWord) return distance;
string candidate = word;
for (int i = 0; i < static_cast<int>(candidate.size()); ++i) {
char original = candidate[i];
for (char replacement = 'a'; replacement <= 'z'; ++replacement) {
if (replacement == original) continue;
candidate[i] = replacement;
if (dict.count(candidate)) {
dict.erase(candidate);
q.push({candidate, distance + 1});
}
}
candidate[i] = original;
}
}
return 0;
}
};The candidate string is a reusable copy of the word currently being explored. Each inner loop changes one position, and the restoration after that loop is essential: without it, changing the next position would build mutations from an already modified word rather than from the original word. The dictionary removal is also part of the BFS invariant, not just a performance detail.
A second reasonable solution searches outward from both beginWord and endWord. Expand the smaller frontier, generate its one-letter mutations, and stop when a generated word already belongs to the opposite frontier. The two searches meet near the middle, which can greatly reduce the number of explored words when the graph branches heavily.
#include <string>
#include <unordered_set>
#include <vector>
using namespace std;
class Solution {
public:
int ladderLength(string beginWord, string endWord, vector<string>& wordList) {
unordered_set<string> dictionary(wordList.begin(), wordList.end());
if (!dictionary.count(endWord)) return 0;
unordered_set<string> front{beginWord};
unordered_set<string> back{endWord};
dictionary.erase(beginWord);
dictionary.erase(endWord);
int words = 2;
while (!front.empty() && !back.empty()) {
if (front.size() > back.size()) {
swap(front, back);
}
unordered_set<string> next;
for (const string& word : front) {
string candidate = word;
for (int i = 0; i < static_cast<int>(candidate.size()); ++i) {
char original = candidate[i];
for (char replacement = 'a'; replacement <= 'z'; ++replacement) {
if (replacement == original) continue;
candidate[i] = replacement;
if (back.count(candidate)) return words;
if (dictionary.count(candidate)) {
dictionary.erase(candidate);
next.insert(candidate);
}
}
candidate[i] = original;
}
}
front = move(next);
++words;
}
return 0;
}
};This is an optimisation, not a different answer: both methods use the same implicit neighbors and preserve shortest-path order. The one-sided version has a simpler distance invariant and is usually easier to verify. Bidirectional BFS buys fewer explored layers, but it costs two frontiers and more careful bookkeeping when combining the distances.