5
5
# Contact: andrewbadr@gmail.com
6
6
# Code contributions are welcome.
7
7
8
- from debug import prints
9
8
from copy import copy
9
+ from UnionFind import UnionFind
10
10
11
11
class DFA :
12
12
"""This class represents a deterministic finite automaton."""
@@ -106,13 +106,9 @@ def state_hash(self, value):
106
106
"""
107
107
d = {}
108
108
for state in self .states :
109
- if value == {}:
110
- d [state ] = {}
111
- elif value == []:
112
- d [state ] = []
113
- else :
114
- d [state ] = value
109
+ d [state ] = copy (value )
115
110
return d
111
+
116
112
def state_subset_hash (self , subset ):
117
113
"""Creates a hash with one key for every state in the DFA, with
118
114
the value True for states in 'subset' and False for all others.
@@ -121,11 +117,11 @@ def state_subset_hash(self, subset):
121
117
for q in subset :
122
118
hash [q ] = True
123
119
return hash
120
+
124
121
def state_merge (self , q1 , q2 ):
125
122
"""Merges q1 into q2. All transitions to q1 are moved to q2.
126
123
If q1 was the start or current state, those are also moved to q2.
127
124
"""
128
- prints ("Merging '%s' into '%s'" % (q1 , q2 ))
129
125
self .states .remove (q1 )
130
126
if q1 in self .accepts :
131
127
self .accepts .remove (q1 )
@@ -138,13 +134,11 @@ def state_merge(self, q1, q2):
138
134
transitions [state ] = {}
139
135
for char in self .alphabet :
140
136
next = self .delta (state , char )
141
- prints ("Checking transition [%s][%s]->[%s]" % (state , char , next ))
142
137
if next == q1 :
143
- prints ("Changing transition [%s][%s]->[%s]" % (state , char , next ))
144
138
next = q2
145
- prints (" Now it's [%s][%s]->[%s]" % (state , char , next ))
146
139
transitions [state ][char ] = next
147
140
self .delta = (lambda s , c : transitions [s ][c ])
141
+
148
142
def reachable_from (self , q0 , inclusive = True ):
149
143
"""Returns the set of states reachable from given state q0. The optional
150
144
parameter "inclusive" indicates that q0 should always be included.
@@ -161,9 +155,11 @@ def reachable_from(self, q0, inclusive=True):
161
155
reached [next ] = True
162
156
to_process .append (next )
163
157
return filter (lambda q : reached [q ], self .states )
158
+
164
159
def reachable (self ):
165
160
"""Returns the reachable subset of the DFA's states."""
166
161
return self .reachable_from (self .start )
162
+
167
163
def delete_unreachable (self ):
168
164
"""Deletes all the unreachable states."""
169
165
reachable = self .reachable ()
@@ -223,7 +219,6 @@ def collapse(self, partition):
223
219
state_map = {}
224
220
#build new_states, new_start, new_current_state:
225
221
for state_class in partition :
226
- prints ("Processing state class: %s" % state_class )
227
222
representative = state_class [0 ]
228
223
new_states .append (representative )
229
224
for state in state_class :
@@ -253,19 +248,16 @@ def minimize(self):
253
248
"""Classical DFA minimization, using the simple O(n^2) algorithm.
254
249
Side effect: can mix up the internal ordering of states.
255
250
"""
256
- prints ("Starting states: %s" % self .states )
257
251
#Step 1: Delete unreachable states
258
252
self .delete_unreachable ()
259
- prints ("After deleting unreachable: %s" % self .states )
260
253
#Step 2: Partition the states into equivalence classes
261
254
classes = self .mn_classes ()
262
- prints ("Classes: %s" % classes )
263
255
#Step 3: Construct the new DFA
264
256
self .collapse (classes )
265
- prints ( "After collapsing: %s" % self . states )
266
- def find_fin_inf_parts (self ):
267
- """Returns the partition of the state-set into the finite-part and
268
- infinite-part as a 2-tuple. A state is in the finite part iff there
257
+
258
+ def preamble_and_kernel (self ):
259
+ """Returns the partition of the state-set into the preamble and
260
+ kernel as a 2-tuple. A state is in the preamble iff there
269
261
are finitely many strings that reach it from the start state.
270
262
271
263
See "The DFAs of Finitely Different Regular Languages" for context.
@@ -279,9 +271,10 @@ def find_fin_inf_parts(self):
279
271
if q in reachable [q ]:
280
272
for next in reachable [q ]:
281
273
in_fin [next ] = False
282
- finite_part = filter (lambda x : in_fin [x ], self .states )
283
- infinite_part = filter (lambda x : not in_fin [x ], self .states )
284
- return (finite_part , infinite_part )
274
+ preamble = filter (lambda x : in_fin [x ], self .states )
275
+ kernel = filter (lambda x : not in_fin [x ], self .states )
276
+ return (preamble , kernel )
277
+
285
278
def pluck_leaves (self ):
286
279
"""Only for minimized automata. Returns a topologically ordered list of
287
280
all the states that induce a finite language. Runs in linear time.
@@ -319,66 +312,94 @@ def pluck_leaves(self):
319
312
#prints("Adding '%s' to be plucked" % incoming)
320
313
plucked .reverse ()
321
314
return plucked
315
+
316
+ def right_finite_states (self , sink_states ):
317
+ """Given a DFA (self) and a list of states (sink_states) that are assumed to induce the
318
+ empty language, return the topologically-ordered set of states in the DFA that induce
319
+ finite languages.
320
+ """
321
+ #Step 1: Build the states' profiles
322
+ inbound = self .state_hash ([])
323
+ outbound = self .state_hash ([])
324
+ is_sink_state = self .state_subset_hash (sink_states )
325
+ for state in self .states :
326
+ if is_sink_state [state ]:
327
+ continue
328
+ for c in self .alphabet :
329
+ next = self .delta (state , c )
330
+ inbound [next ].append (state )
331
+ outbound [state ].append (next )
332
+
333
+ #Step 2: Pluck!
334
+ to_pluck = sink_states
335
+ plucked = []
336
+ while len (to_pluck ):
337
+ state = to_pluck .pop ()
338
+ plucked .append (state )
339
+ for incoming in inbound [state ]:
340
+ outbound [incoming ].remove (state )
341
+ if (len (outbound [incoming ]) == 0 ) and (incoming != state ):
342
+ to_pluck .append (incoming )
343
+ plucked .reverse ()
344
+ return plucked
345
+
322
346
def is_finite (self ):
323
347
"""Indicates whether the DFA's language is a finite set."""
324
348
D2 = self .copy ()
325
349
D2 .minimize ()
326
350
plucked = D2 .pluck_leaves ()
327
351
return (D2 .start in plucked )
352
+
328
353
def states_fd_equivalent (self , q1 , q2 ):
329
354
"""Indicates whether q1 and q2 only have finitely many distinguishing strings."""
330
355
d1 = DFA (states = self .states , start = q1 , accepts = self .accepts , delta = self .delta , alphabet = self .alphabet )
331
356
d2 = DFA (states = self .states , start = q2 , accepts = self .accepts , delta = self .delta , alphabet = self .alphabet )
332
357
sd_dfa = symmetric_difference (d1 , d2 )
333
358
return sd_dfa .is_finite ()
334
- def fd_classes (self ):
335
- """Returns a partition of the states into finite-difference equivalence classes."""
359
+
360
+ def f_equivalence_classes (self ):
361
+ """Returns a partition of the states into finite-difference equivalence clases, using
362
+ the experimental O(n^2) algorithm."""
336
363
sd = symmetric_difference (self , self )
337
- sd2 = sd .copy ()
338
- classes = sd .mn_classes ()
339
- state_map = sd2 .collapse (classes )
340
- plucked = sd2 .pluck_leaves ()
341
- plucked_h = sd2 .state_subset_hash (plucked )
342
- similar_states_list = filter (lambda q : plucked_h [state_map [q ]], sd .states )
343
- similar_states = sd .state_subset_hash (similar_states_list )
344
- state_classes = []
364
+ self_pairs = [(x , x ) for x in self .states ]
365
+ fd_equiv_pairs = sd .right_finite_states (self_pairs )
366
+ sets = UnionFind ()
345
367
for state in self .states :
346
- placed = False
347
- for sc in state_classes :
348
- rep = sc [0 ]
349
- if similar_states [(state ,rep )]:
350
- sc .append (state )
351
- placed = True
352
- break #only for speed, not logic -- like how I live
353
- if not placed :
354
- state_classes .append ([state ])
368
+ sets .make_set (state )
369
+ for (state1 , state2 ) in fd_equiv_pairs :
370
+ set1 , set2 = sets .find (state1 ), sets .find (state2 )
371
+ if set1 != set2 :
372
+ sets .union (set1 , set2 )
373
+ state_classes = sets .as_lists ()
355
374
return state_classes
356
- def finite_difference_minimize (self ):
375
+
376
+ def hyper_minimize (self ):
357
377
"""Alters the DFA into a smallest possible DFA recognizing a finitely different language.
358
378
In other words, if D is the original DFA and D' the result of this function, then the
359
379
symmetric difference of L(D) and L(D') will be a finite set, and there exists no smaller
360
380
automaton than D' with this property.
361
381
362
382
See "The DFAs of Finitely Different Regular Languages" for context.
363
383
"""
364
- #Step 1: Classical minimization
384
+ # Step 1: Classical minimization
365
385
self .minimize ()
366
- #Step 2: Partition states into equivalence classes
367
- state_classes = self .fd_classes ()
368
- #Step 3: Find finite and infinite parts
369
- (fin_part , inf_part ) = self .find_fin_inf_parts ()
370
- #Step 4: Merge
386
+ # Step 2: Partition states into equivalence classes
387
+ state_classes = self .f_equivalence_classes ()
388
+ # Step 3: Find preamble and kernel parts
389
+ (preamble , kernel ) = self .preamble_and_kernel ()
390
+ # Step 4: Merge (f_merge_states in the paper)
391
+ # (Could be done more efficiently)
371
392
for sc in state_classes :
372
- fins = filter (lambda s : s in fin_part , sc )
373
- infs = filter (lambda s : s in inf_part , sc )
374
- if len (infs ) != 0 :
375
- rep = infs [0 ]
376
- for fp_state in fins :
377
- self .state_merge (fp_state , rep )
393
+ pres = filter (lambda s : s in preamble , sc )
394
+ kers = filter (lambda s : s in kernel , sc )
395
+ if len (kers ) :
396
+ rep = kers [0 ]
397
+ for p_state in pres :
398
+ self .state_merge (p_state , rep )
378
399
else :
379
- rep = fins [0 ]
380
- for fp_state in fins [1 :]:
381
- self .state_merge (fp_state , rep )
400
+ rep = pres [0 ]
401
+ for p_state in pres [1 :]:
402
+ self .state_merge (p_state , rep )
382
403
def levels (self ):
383
404
"""Returns a dictionary mapping each state to its distance from the starting state."""
384
405
levels = {}
@@ -417,6 +438,7 @@ def long_path(q,length, longest):
417
438
longest = candidate
418
439
return longest
419
440
return long_path (self .start , 0 , None )
441
+
420
442
def DFCA_minimize (self , l = None ):
421
443
"""DFCA minimization"
422
444
Input: "self" is a DFA accepting a finite language
@@ -601,7 +623,7 @@ def finite_factor(self):
601
623
D1 = self .copy ()
602
624
D1 .minimize ()
603
625
D2 = D1 .copy ()
604
- D2 .finite_difference_minimize ()
626
+ D2 .hyper_minimize ()
605
627
D3 = symmetric_difference (D1 , D2 )
606
628
l = D3 .DFCA_minimize ()
607
629
return (D2 , (D3 , l ))
0 commit comments