4.6.4 · D3Theory of Computation

Worked examples — NFA to DFA conversion — subset construction

2,984 words14 min readBack to topic

Before starting, make sure these are solid: Finite Automata — NFA and ε-NFA (what an NFA and an ε-move are), Finite Automata — DFA basics (what a single-state machine is), and Power Set and Subsets (why there are subsets of an -element set).


The scenario matrix

Every subset-construction problem is really one of these case classes. If we work an example of each, you will never meet a cell you haven't seen.

# Case class What's tricky about it Covered by
C1 No ε-edges (plain NFA) ECLOSE does nothing — pure MOVE Example 1
C2 With ε-edges (chained / ε-cycle) must close before start and after every move; closures can iterate Example 2
C3 Dead / trap state appears () a MOVE returns the empty set Example 3
C4 Start state is itself accepting the empty string must be accepted Example 4
C5 Blow-up: near reachable subsets many subsets actually reachable Example 5
C6 Degenerate: no final state at all language is empty — every subset rejects Example 6
C7 Word problem (real-world spec) translate English → NFA → DFA Example 7
C8 Exam twist: unreachable subsets don't list all ; only reachable ones Example 8

Notation reminder, so nothing is used before it's earned:

  • A subset-state is written — literally the set of NFA states the NFA might be sitting in. This is one DFA state.
  • = union of all -arrows leaving the states in .
  • = plus everything reachable by following -arrows only.
  • means " shares at least one state with the final set " — that "" is set intersection, and "" means "the overlap is not empty".

Throughout, the pictures carry the work: magenta circles are non-accepting DFA states, violet double circles are accepting, orange is the dead/trap state, and the orange start arrow marks the start. Watch the arrows, not just the tables.


Example 1 — Case C1 (no ε)

Step 1 — start state. . Call it . Why this step? Because there are no ε-edges, ECLOSE cannot add anything — for case C1 the start is just .

Step 2 — on . . Why? Only state in the set is ; its -arrow returns . No ε ⇒ ECLOSE unchanged. Self-loop.

Step 3 — on . . New subset — call it . Why? 's -arrow is . This subset hasn't appeared, so it's a brand-new DFA state.

Step 4 — on . . Self-loop.

Step 5 — on . . Self-loop.

Step 6 — accepting? → no. yes.

The finished DFA — look at how the orange start arrow enters , and how every arrow out of curls back to (once you have seen a you can never lose it):

Figure — NFA to DFA conversion — subset construction
DFA state on 0 on 1 accept?
(start) no
yes

Verify: we reach exactly once we've read a , and once in we stay there. So the DFA accepts every string containing at least one . Trace "": , reject (correct — no 1). Trace "": , accept. ✔ Only 2 of subsets reachable.


Example 2 — Case C2 (ε-edges, with a chained ε-closure and an ε-cycle)

Step 1 — start (chained closure, needs several iterations). :

  • Iteration 1: from , ε-edge to → set now .
  • Iteration 2: from the newly added , ε-edge to → set now .
  • Iteration 3: from the newly added , no ε-edge → nothing new, stop.

Start . Why iterate? ECLOSE is a fixed-point computation: after adding you must re-scan 's ε-edges, which reveal . A single pass that only looked at would wrongly stop at . This is the chained-ε pitfall the parent note only hinted at.

Step 2 — on . (only has an -arrow). Then : from , ε-edge to ; from , no ε → stop. New state . Why does the ε-cycle not loop forever? The set already contains both and ; ECLOSE only adds states, so once nothing new can be added it halts — even though visually cycles.

Step 3 — on . . ECLOSE. Dead state — call it . (Foreshadows C3.) Why is MOVE empty here? We scan every state in for a -arrow: state has none, state has none, state has none. The union of three empty sets is empty — so there is literally nowhere to go on .

Step 4 — on . : 's -arrow is , has no -arrow → . ECLOSE. Self-loop.

Step 5 — on . : has no -arrow, 's -arrow is . ECLOSE. Self-loop.

Step 6 — accepting? contains yes. has no → no. → no.

The DFA — note the orange dead state hanging off on , and looping to itself on both letters:

Figure — NFA to DFA conversion — subset construction
DFA state on on accept?
(start) no
yes
(dead) no

Verify: to accept, we must first read an (moving ); after that any mix of 's and 's keeps us in . So the language is " followed by any string over " = . Trace "": (accept). Trace "": (accept). Trace "": (reject). Trace "": (reject). ✔ And the start subset really needed three closure iterations to build.


Example 3 — Case C3 (dead/trap state)

Step 1 — start. (no ε).

Step 2 — on . .

Step 3 — on . . This is the empty subset — the DFA's dead state . Why this step matters? The NFA has no live states after , so it can never reach a final state again. In a DFA every transition must be defined, so we make a real state that loops to itself forever (Mistake 5 from parent).

Step 4 — on . Self-loop, never accepts.

The trap is easiest to see — once the orange dead state is entered, every arrow curls back into it:

Figure — NFA to DFA conversion — subset construction
DFA state on accept?
(start) no
yes
(dead) no

Verify: language is exactly the single string "". Trace "": , accept. Trace "": , reject forever. Trace "" : stays , reject. ✔ This DFA recognises — one string only.


Example 4 — Case C4 (start is accepting, accepts )

Step 1 — start. .

Step 2 — is the START accepting? yes, is accepting. Why this step is easy to miss? People check accepting states after building transitions and forget the start can already be final — that is exactly how you accept the empty string (the DFA halts on having read nothing).

Step 3 — on . .

Step 4 — on . .

DFA state on accept?
(start) yes
no

Verify: language is "even number of 's" (including zero) = . Trace "" : at , accept. Trace "": , reject. Trace "": , accept. ✔


Example 5 — Case C5 (near blow-up)

Step 1 — start. (no ε).

Step 2 — on : . on : . Why is new? The -arrow of is nondeterministic (), giving a genuinely bigger subset.

Step 3 — on : . on : .

Step 4 — on : . on : . Why does contribute nothing? has no outgoing arrows — it's a sink in the NFA.

Step 5 — on : . on : .

DFA state on on accept?
(start) no
no
yes
yes

Verify: both contain → accepting. Language: strings whose 2nd-last symbol is . Trace "": (accept — 2nd-last is ). Trace "": (reject — 2nd-last is ). Trace "": (accept). ✔ 4 of 8 subsets reachable — for the general "-th from end" NFA, all become reachable, the true exponential blow-up.


Example 6 — Case C6 (no final state → empty language)

Step 1 — start. .

Step 2 — transitions: on ; on .

Step 3 — accepting? For any subset , . Why this settles it? Intersecting with the empty set is always empty, so no subset can be accepting — no matter how the transitions look.

DFA state on accept?
(start) no
no

Verify: language (the empty language — not the same as !). Every string is rejected because no DFA state is accepting. ✔ This is the mechanical reason "no final states ⇒ empty language" — the subset construction reproduces it automatically.


Example 7 — Case C7 (word problem)

Step 0 — build the NFA (why an NFA first?). NFAs let us "guess" when the crucial occurs, which is far easier than tracking it deterministically. States , start , final : , , , , . (In , reading either ignores it or "commits" to it via ; from a later unlocks to , which then loops forever.)

Step 1 — start. .

Step 2 — on : . on : . Why? Before any , a is useless — stay at .

Step 3 — on : . on : .

Step 4 — on : . on : .

Step 5 — on : . on : .

Step 6 — accepting? Contains : and yes. → no.

DFA state on on accept?
(start) no
no
yes
yes

Verify: once we hit or (unlocked), every arrow keeps us among — so it stays unlocked, matching the forecast. Trace "": (unlock). Trace "": (still locked — the came before ). Trace "": (unlocked). ✔


Example 8 — Case C8 (exam twist: don't enumerate all )

Step 1 — start. (no ε).

Step 2 — reachability only. on : ; on : . Why only reachable? Mistake 3 from the parent note: unreachable subsets are dead weight. We generate a subset only when an arrow lands on it.

Step 3 — process . on : ; on : .

Step 4 — process . on : ; on : . Both self-loops.

Step 5 — process . on . Dead state.

Step 6 — accepting? contains yes. → no.

DFA state on on accept?
(start) no
no
yes
(dead) no

Verify: reachable subsets = 4 of 8. The 4 unreachable subsets (, , , ) never occur because this NFA's arrows are single-valued (no nondeterministic branching), so no subset ever grows past size 1. Language: = "starts with ". Trace "": (accept). Trace "": (reject). Trace "": (accept). ✔


Recall Self-test: name the case class

Which cell does an NFA with no final states hit, and what's the resulting language? ::: Case C6 — the empty language ; every subset rejects because . Which cell forces you to accept the empty string ? ::: Case C4 — the start subset already contains a final state. When does the dead state get created? ::: Case C3 — whenever a MOVE returns the empty set; it self-loops and rejects. Why did Example 2's start subset need three closure iterations? ::: The ε-edges chain ; you must re-scan each newly added state, so a single pass would wrongly stop at . Why did Example 5 reach 4 subsets while Example 8's subsets never grew past size 1? ::: Example 5's NFA branches nondeterministically (subsets grow); Example 8's arrows are single-valued, so subsets stay singletons plus the dead state.


Connections