1. Home
  2. Theory of Computation
  3. DFA Minimization

DFA Minimization

Many DFAs accept the same language. Remove unreachable states, then split groups of states until only truly equivalent states share a group.

Interactive 3DIntermediate12 min readTOCUpdated

Drag to rotate · Right-drag to pan · Click, then scroll to zoom · Space play · ←→ step

What's happening

Pseudocode

    Try this in the 3D model

    • Press Minimize. Why is q6 removed before anything else happens?
    • Watch the signature tags in round 1. Why does q5 split away from q0 and q1?
    • How many states does the minimal DFA for "exactly one 1" have, and what does each one mean?
    • Choose Even number of 1s. Which pairs of states turn out to be equivalent?

    Why minimize?

    A DFA built by hand, or produced automatically from a regular expression or an NFA, often has more states than it needs. Some states can never be reached, and some pairs of states do exactly the same job. A smaller DFA uses less memory in a lexer or a hardware circuit, and it is easier to understand.

    Two states p and q are equivalent if, for every possible remaining input string, starting in p and starting in q give the same answer (accept or reject). Minimization merges each group of equivalent states into one state.

    The algorithm (partition refinement)

    1. Remove unreachable states. Run a BFS from the start state and delete everything it does not reach.
    2. Initial split. Accepting and non-accepting states can never be equivalent, because on the empty input one accepts and the other rejects. Start with P = { accepting, non-accepting }.
    3. Refine. For every state, write its signature: the group each symbol leads to. In each group, keep states together only if their signatures are equal. Otherwise split the group.
    4. Repeat step 3 until a round splits nothing.
    5. Merge. Every final group becomes one state of the minimal DFA.

    Worked example: exactly one 1

    The DFA in the 3D model has states q0…q6, start q0, accepting q2, q3, q4, q6:

    State on 0 on 1
    → q0 q1 q2
    q1 q0 q3
    *q2 q4 q5
    *q3 q4 q5
    *q4 q4 q5
    q5 q5 q5
    *q6 q4 q6
    • Unreachable: nothing leads to q6, so remove it.
    • P₀ = { q2, q3, q4 } (accepting), { q0, q1, q5 } (non-accepting).
    • Round 1. q0 and q1 go to non-accepting on 0 and to accepting on 1. q5 goes to non-accepting on both, so q5 splits off. q2, q3 and q4 all go to accepting on 0 and to q5’s group on 1, so they stay together.
    • P₁ = { q2, q3, q4 }, { q0, q1 }, { q5 }.
    • Round 2: no group splits. Done.

    The minimal DFA has 3 states: {q0, q1} = “no 1 seen yet”, {q2, q3, q4} = “exactly one 1” (accepting), and {q5} = “two or more 1s” (a dead state).

    Code

    def minimize(states, alphabet, delta, start, accept):
        reach, todo = {start}, [start]                 # 1. reachable states
        while todo:
            s = todo.pop()
            for a in alphabet:
                if delta[s][a] not in reach:
                    reach.add(delta[s][a])
                    todo.append(delta[s][a])
        states = [s for s in states if s in reach]
    
        groups = [g for g in ([s for s in states if s in accept],
                              [s for s in states if s not in accept]) if g]
        while True:                                    # 2. refine until stable
            index = {s: i for i, g in enumerate(groups) for s in g}
            new = []
            for g in groups:
                buckets = {}
                for s in g:
                    signature = tuple(index[delta[s][a]] for a in alphabet)
                    buckets.setdefault(signature, []).append(s)
                new.extend(buckets.values())
            if len(new) == len(groups):
                return new                             # each group = one new state
            groups = new
    
    d = {"q0": {"0": "q1", "1": "q2"}, "q1": {"0": "q0", "1": "q3"},
         "q2": {"0": "q4", "1": "q5"}, "q3": {"0": "q4", "1": "q5"},
         "q4": {"0": "q4", "1": "q5"}, "q5": {"0": "q5", "1": "q5"},
         "q6": {"0": "q4", "1": "q6"}}
    print(minimize(list(d), "01", d, "q0", {"q2", "q3", "q4", "q6"}))
    # [['q2', 'q3', 'q4'], ['q0', 'q1'], ['q5']]

    Table-filling method

    Many textbooks use the equivalent table-filling (Myhill–Nerode) method instead. Draw a triangle table of all state pairs. First mark every pair with one accepting and one non-accepting state. Then repeatedly mark a pair (p, q) if some symbol takes it to an already-marked pair. Pairs never marked are equivalent. It gives the same result as partition refinement and works well by hand for small DFAs.

    Facts worth knowing

    • The minimal DFA is unique (up to renaming states). This is the Myhill–Nerode theorem. So you can test whether two DFAs accept the same language by minimizing both and comparing them.
    • A dead state (all transitions loop back to itself, not accepting) is reachable and must stay in a complete DFA. It is different from an unreachable state.
    • NFAs have no such neat minimization; finding a minimal NFA is a much harder (PSPACE-hard) problem.

    Common mistakes

    • Merging states that look alike but differ in being accepting.
    • Comparing the target states instead of the target groups. q2 → q4 and q3 → q4 match, but so would q2 → q4 and q3 → q3 if q3 and q4 are in the same group.
    • Forgetting to remove unreachable states first.
    • Stopping after one round. Refinement continues until a whole round causes no split.

    Complexity at a glance

    Case / operationTimeWhy
    Removing unreachable statesO(n · |Σ|)One BFS from the start state.
    Partition refinement (Moore)O(n² · |Σ|)At most n rounds, each looks at every transition.
    Hopcroft's algorithmO(n · |Σ| · log n)The fastest known method.
    Extra spaceO(n)

    Quick check

    Test yourself — pick an answer to see if you got it.

    1. Why do we start by separating accepting and non-accepting states?

    2. When does the partition algorithm stop?

    3. Which states must be removed before refining the partition?

    4. Is the minimal DFA of a regular language unique?

    Saved only in this browser — no account needed.
    Spotted a mistake or a bug in the 3D model?

    Report a mistake

    in DFA Minimization. Thank you — every report makes the lesson better for the next reader.

    We'll also include a link to the step of the 3D model you're on and your browser type, so we can reproduce it.