Understand
30-second explanation
Build the interview-ready mental model before comparing implementations.
Treat each valid gene string as a node and connect two genes with an edge when they differ by exactly one character (and the target is in the bank). The fewest mutations from start to end is then the shortest path on this unweighted graph → BFS. From a gene, generate all single-character mutations (8 positions × the other 3 of {A,C,G,T}), keep the ones present in the bank, and explore them level by level. The layer at which you first dequeue endGene is the answer; if the queue empties first, return -1. A hash set of the bank gives O(1) validity checks.
Interview contextWhat is testedWhere candidates slipHow to stand outExplore interview contextHide interview context
What's being tested
Whether you recognise a 'minimum number of single edits through valid states' problem as a shortest-path BFS over an implicit graph, and use a hash set for O(1) bank membership.
Where candidates slip
Using DFS (finds a path, not the shortest), forgetting to mark genes visited (revisiting and looping), or doing O(n) linear scans of the bank instead of a hash-set lookup.
How to stand out
Frame it as Word Ladder with alphabet {A,C,G,T}, explain why BFS guarantees the minimum, choose the cheaper neighbour-generation strategy for the given sizes, and mention bidirectional BFS as a scaling option.
Choose
Compare the available approaches
Use constraints and trade-offs to select the route you can defend in an interview.
Interactive decision treeInteractiveChoose the right approachFirst decision previewFewest single-char edits start → endEdges equal cost?Yes (1 per mutation)NoExplore the full treeCollapse the tree
Learn & trace
Build each solution step by step
Move from the core idea to a visual trace, implementation, proof, and edge cases.
Approach 1 of 2 · Optimal
BFS by Generating One-Character Mutations (Optimal)
1Hint 1Explore hintHide hint
Picture each valid gene as a node; two genes are connected if they differ in exactly one position. The answer is the shortest path from start to end.
2Hint 2Explore hintHide hint
Every mutation costs the same (one step), so BFS finds the minimum. Put the bank in a hash set so checking 'is this gene valid?' is O(1).
3Hint 3Explore hintHide hint
From a gene, generate all neighbours: for each of the 8 positions, swap in each of the other 3 bases. Keep the ones in the bank and not yet visited; explore level by level and count layers.
A · Core idea
The phrase 'minimum number of mutations' over equal-cost steps is the unmistakable signature of a shortest-path BFS.
Why this is a graph problem. Make every valid gene a node. Draw an edge between two genes whenever they differ in exactly one of the 8 characters — that's a single legal mutation. The number of mutations from start to end is the length of the shortest path between those two nodes. Because each edge costs exactly one mutation, the graph is unweighted, and BFS visits nodes in strictly increasing distance from the source. So the first time BFS reaches endGene, it has reached it via the fewest possible mutations. DFS, by contrast, would dive down a chain and might find a long route first — it cannot guarantee the minimum.
Why a hash set for the bank. A mutation is only legal if the resulting gene is in the bank. We'll test that condition once per generated candidate, so it must be fast. Loading the bank into a hash set turns each validity test into an O(1) lookup; a linear scan of the bank per candidate would multiply the running time by the bank size for no reason. This is the 'hash-map' half of the pattern — the set is what makes the BFS efficient.
Generating neighbours. From the current gene we enumerate every one-character mutation: for each of the 8 positions, replace its base with each of the other three letters in {A, C, G, T}. That's at most 8 × 3 = 24 candidates per gene. We keep a candidate only if it's in the bank and hasn't been visited, then mark it visited and enqueue it. Skipping the position's current letter avoids generating the gene unchanged.
Mark visited on enqueue. As with every BFS, we record a gene as visited the instant we enqueue it, not when we dequeue it. Otherwise the same gene can be enqueued by several neighbours, inflating the queue and re-expanding nodes. The visited set also doubles as cycle protection — gene graphs can have cycles (A↔B mutations), and without visited tracking BFS would loop forever.
Counting layers. We drain the queue one full layer per outer iteration and bump the mutation counter after each layer. The layer index when endGene is first dequeued is the minimum count. Seeding the queue with startGene (marked visited) and checking equality on dequeue means start == end correctly returns 0. If the queue empties without ever reaching endGene, no valid path exists and we return -1.
B · Visual dry run
Interactive dry run
Select a frame to connect the decision with its visual state.
start = "AACCGGTT", end = "AAACGGTA", bank = ["AACCGGTA","AACCGCTA","AAACGGTA"]Interactive diagram
Gene graph for example 2
steps=0Each edge is a single-character mutation through a valid bank gene; BFS finds the shortest chain.
2C · Code with teaching comments
D · Why it works and complexityO(B × L × 4) timeO(B) spaceOpen the invariant and proofExplore the proofHide the proof
This is Word Ladder with the alphabet pinned to four letters and the length pinned to eight. The shape — 'BFS over one-edit neighbours, validity checked against a hash set' — is the reusable template. Recognising that two superficially different problems (genes vs words) are the same graph is exactly the transfer interviewers look for.
ComplexityO(B × L × 4) time, O(B) space — Let B be the bank size and L = 8 the gene length. Each gene is dequeued once and generates L × 3 candidate mutations, each an O(L) string build and O(1) set lookup → O(B × L × 4) ≈ O(B) for the tiny fixed L. The visited set and bank set are O(B) space.
E · Edge cases and pitfalls7start == end — Dequeued immediately and equals end → returns 0.Explore edge casesHide edge cases
Edge cases handled
start == endDequeued immediately and equals end → returns 0.endGene not in bank (and != start)It's never generated as a valid neighbour → queue empties → return -1.empty bankNo valid mutations exist; unless start == end, returns -1.
Pitfalls
- Using DFS — yields a path, not the minimum number of mutations.
- Scanning the bank list per candidate instead of a hash-set lookup — needlessly O(B) each time.
- Forgetting the visited set — gene graphs have cycles, so BFS would loop forever.
- Not restoring the mutated character before trying the next position.
Approach 2 of 2
BFS by Scanning the Bank for One-Away Neighbours
1Hint 1Explore hintHide hint
Neighbour generation has two flavours: build all single-character mutations, or scan the candidate list for entries that differ in exactly one position.
2Hint 2Explore hintHide hint
When the bank is tiny, scanning it (B genes, each an O(L) Hamming-distance check) can be simpler than generating 24 mutations and hashing each.
3Hint 3Explore hintHide hint
Everything else — BFS, visited set, layer counting — is identical.
A · Core idea
This variant keeps the exact same BFS skeleton but swaps how neighbours are discovered.
Generate vs scan. The optimal approach generates every one-character mutation (8 × 3 = 24 strings) and tests each against the bank set. The alternative scans: for the current gene, walk the whole bank and keep any candidate whose Hamming distance from the current gene is exactly 1. Both produce the identical neighbour set — genes that differ in one position and are valid — they just trade off differently.
When scanning wins. The generate-and-hash approach costs roughly L × |alphabet| per gene, independent of bank size; scanning costs B × L per gene. With a four-letter alphabet and L = 8 the generate cost is ~24 hashes, while scanning a bank of B ≤ 10 is ~10 cheap comparisons. For these constraints scanning is marginally simpler and avoids allocating mutated strings. For a large dictionary (as in Word Ladder), generation wins decisively because it doesn't grow with the dictionary size.
The Hamming-distance helper. oneAway(a, b) counts mismatched positions and bails out the moment the count exceeds 1 — so it's O(L) but usually returns early. Two genes are neighbours exactly when that count equals 1.
Same correctness guarantees. Because the neighbour set is identical, the BFS still explores by increasing distance and the first arrival at endGene is still the minimum. The visited set still prevents revisits and cycles. The only thing that changed is the constant-factor cost of finding neighbours.
B · Visual dry run
Interactive dry run
Select a frame to connect the decision with its visual state.
start = "AAAAACCC", end = "AACCCCCC", bank = ["AAAACCCC","AAACCCCC","AACCCCCC"]steps=0
AAAAACCC: bank scan → AAAACCCC is 1-away, enqueue
queue=[AAAACCCC]3C · Code with teaching comments
Reference implementation previewpublic int minMutation(String startGene, String endGene, String[] bank) { Queue<String> queue = new LinkedList<>(); Set<String> visited = new HashSet<>(); queue.offer(startGene); visited.add(startGene);Explore the full codeHide the full code
D · Why it works and complexityO(B² × L) timeO(B) spaceOpen the invariant and proofExplore the proofHide the proof
Neighbour generation is a tunable knob in transformation BFS: generate candidates when the alphabet is small and the dictionary is large; scan the dictionary when it's small. Recognising that both strategies yield the same graph — and choosing based on the input sizes — is a mark of fluency.
ComplexityO(B² × L) time, O(B) space — Each of B genes, when dequeued, compares against all B bank genes with an O(L) Hamming check → O(B² × L). For B ≤ 10 and L = 8 that's trivial, and it avoids building mutated strings.
E · Edge cases and pitfalls6very small bank — Scanning beats generation here — fewer comparisons than 24 mutations.Explore edge casesHide edge cases
Edge cases handled
very small bankScanning beats generation here — fewer comparisons than 24 mutations.large dictionary (Word Ladder)Generation would win; scanning becomes O(B²·L) and grows with the dictionary.start == endReturns 0 immediately on the first dequeue.
Pitfalls
- Treating identical genes (Hamming distance 0) as neighbours — they're not.
- Scanning the bank when it's huge — O(B²) blows up; generate mutations instead.
- Omitting the early exit in the Hamming check on longer strings.
Practice
Try the solution yourself
Edit the optimal implementation, test another input, and observe the result.
Editable code previewpublic int minMutation(String startGene, String endGene, String[] bank) { Set<String> bankSet = new HashSet<>(Arrays.asList(bank)); char[] choices = {'A', 'C', 'G', 'T'}; Queue<String> queue = new LinkedList<>(); Set<String> visited = new HashSet<>();Explore the playgroundClose the playground
Avoid mistakes
Recognize the interview traps
Understand why common attempts fail before reviewing the corrected implementation.
1Forgetting the visited set
Gene graphs are full of cycles (A can mutate to B and back). Without tracking visited genes, BFS re-enqueues the same nodes forever and never terminates.
Explore the code comparisonHide the code comparison
Forgetting the visited set
Gene graphs are full of cycles (A can mutate to B and back). Without tracking visited genes, BFS re-enqueues the same nodes forever and never terminates.
Don't write this
queue = deque([startGene])
while queue:
gene = queue.popleft()
for candidate in bank:
if one_away(gene, candidate):
queue.append(candidate) # no visited check → infinite loopWrite this instead
queue = deque([startGene])
visited = {startGene}
while queue:
gene = queue.popleft()
for candidate in bank:
if candidate not in visited and one_away(gene, candidate):
visited.add(candidate)
queue.append(candidate)2Linear bank scan per candidate
When generating mutations, validating each against a list is O(B) per check. With many candidates this multiplies the runtime needlessly; a hash set makes each check O(1).
Explore the code comparisonHide the code comparison
Linear bank scan per candidate
When generating mutations, validating each against a list is O(B) per check. With many candidates this multiplies the runtime needlessly; a hash set makes each check O(1).
Don't write this
String mutated = new String(arr);
if (Arrays.asList(bank).contains(mutated)) { // O(B) per candidate
queue.offer(mutated);
}Write this instead
Set<String> bankSet = new HashSet<>(Arrays.asList(bank));
// ...
String mutated = new String(arr);
if (bankSet.contains(mutated)) { // O(1)
queue.offer(mutated);
}3Choosing DFS for a shortest-path question
DFS follows one mutation chain to its end and may report a long route before a short one exists. Only BFS, exploring by increasing distance, guarantees the minimum number of mutations.
Explore the code comparisonHide the code comparison
Choosing DFS for a shortest-path question
DFS follows one mutation chain to its end and may report a long route before a short one exists. Only BFS, exploring by increasing distance, guarantees the minimum number of mutations.
Don't write this
def dfs(gene, count):
if gene == endGene:
return count # not necessarily minimal
for candidate in bank:
if one_away(gene, candidate):
return dfs(candidate, count + 1)Write this instead
queue = deque([startGene]); steps = 0
while queue:
for _ in range(len(queue)):
gene = queue.popleft()
if gene == endGene:
return steps # BFS ⇒ first arrival is minimal
steps += 1Revise
Carry the pattern into the interview
Pattern to remember
Shortest transformation sequence → BFS over one-edit neighbours (hash set for the bank)
Code pattern to memorize
bankSet = set(bank); seen = {start}; q = [start]; steps = 0
while q:
for gene in this layer:
if gene == end: return steps
for each position p, each base b != gene[p]:
cand = mutate(gene, p, b)
if cand in bankSet and cand not in seen:
seen.add(cand); q.push(cand)
steps += 1
return -1Rules to remember
- 1Each single-character change is one move of equal cost, so BFS (not DFS) gives the minimum.
- 2A mutation is only legal if the result is in the bank — store the bank in a hash set for O(1) checks.
- 3Generate neighbours by trying each of the 8 positions with the other 3 bases of
{A, C, G, T}. - 4Mark a gene visited when enqueued so it isn't expanded twice.
- 5If
endGeneis unreachable (never dequeued) return -1; ifstart == end, the answer is 0.
Use this pattern when
- Shortest number of single-step edits between two strings/states
- Each intermediate state must be valid (in a dictionary / bank)
- Implicit graph where edges are 'differs by one unit' transformations
- Word-ladder-style minimum transformation problems
Choose another pattern when
- You need ALL transformation sequences → DFS / backtracking, not BFS alone
- Edits have different costs → weighted shortest path (Dijkstra)
- No validity constraint and unlimited alphabet → the graph is too dense to enumerate; rethink
Interview takeaway
Minimum Genetic Mutation is Word Ladder with a 4-letter alphabet and fixed length 8. Once you see 'fewest single-character changes through a dictionary', it's BFS over one-edit neighbours with a hash set for validity — the same template solves Word Ladder and Open the Lock.
Continue
Transfer the pattern to another problem
Use a direct variation or pattern sibling to prove the idea is reusable.
Transferable patternBFS for shortest transformation over an implicit graph of one-edit neighbours, with a hash set for O(1) validity checks. Same template as Word Ladder, Open the Lock, and Word Ladder II — the tell is 'minimum number of single-character/single-step changes through valid states'.
Practice queueDirect variations and pattern siblings
Direct follow-ups
- Word LadderSame BFS over one-edit neighbours, but with a 26-letter alphabet and variable word length — generation wins over scanning.
- Open the LockComing soonBFS over 4-digit lock states; each wheel turn (±1 mod 10) is a one-edit neighbour, with deadends blocked.
- Word Ladder IIComing soonReturn ALL shortest transformation sequences — BFS to build the layer graph, then DFS to reconstruct paths.
- Snakes and LaddersAnother unweighted shortest-path BFS, on a flattened board instead of strings.