docs: def nfa_determinization(nfa: dict) -> dict:
      """ Returns a DFA that reads the same language of the input NFA.
  
      Let A be an NFA, then there exists a DFA :math:`A_d` such
      that :math:`L(A_d) = L(A)`. Intuitively, :math:`A_d`
      collapses all possible runs of A on a given input word into
      one run over a larger state set.
      :math:`A_d` is defined as:
  
      :math:`A_d = (Σ, 2^S , s_0 , ρ_d , F_d )`
  
      where:
  
      • :math:`2^S` , i.e., the state set of :math:`A_d` , consists
        of all sets of states S in A;
      • :math:`s_0 = S^0` , i.e., the single initial state of
        :math:`A_d` is the set :math:`S_0` of initial states of A;
      • :math:`F_d = \{Q | Q ∩ F ≠ ∅\}`, i.e., the collection of
        sets of states that intersect F nontrivially;
      • :math:`ρ_d(Q, a) = \{s' | (s,a, s' ) ∈ ρ\ for\ some\ s ∈ Q\}`.
  
      :param dict nfa: input NFA.
      :return: *(dict)* representing a DFA
      """
      def state_name(s):
          return str(set(sorted(s)))
  
      dfa = {
          'alphabet': nfa['alphabet'].copy(),
          'initial_state': None,
          'states': set(),
          'accepting_states': set(),
          'transitions': dict()
      }
  
      if len(nfa['initial_states']) > 0:
          dfa['initial_state'] = state_name(nfa['initial_states'])
          dfa['states'].add(state_name(nfa['initial_states']))
  
      sets_states = list()
      sets_queue = list()
      sets_queue.append(nfa['initial_states'])
      sets_states.append(nfa['initial_states'])
      if len(sets_states[0].intersection(nfa['accepting_states'])) > 0:
          dfa['accepting_states'].add(state_name(sets_states[0]))
  
      while sets_queue:
          current_set = sets_queue.pop(0)
          for a in dfa['alphabet']:
              next_set = set()
              for state in current_set:
                  if (state, a) in nfa['transitions']:
                      for next_state in nfa['transitions'][state, a]:
                          next_set.add(next_state)
              if len(next_set) == 0:
                  continue
              if next_set not in sets_states:
                  sets_states.append(next_set)
                  sets_queue.append(next_set)
                  dfa['states'].add(state_name(next_set))
                  if next_set.intersection(nfa['accepting_states']):
                      dfa['accepting_states'].add(state_name(next_set))
  
              dfa['transitions'][state_name(current_set), a] = state_name(next_set)
  
      return dfa