The hard way to do it
You might be able to deduce this system by pure insight, especially after seeing the answer that this method comes up with. But as Einstein once said in a lecture: "What follows has been done much more elegantly by Minkowski, but chalk is cheaper than gray [brain] matter, so we will take it as it comes."
The dumbest machine which builds strings in this language looks like:
[even, odd]
(append "b") / \ (append "a")
/ \
{start} --> [even, even] [odd, odd]
\ /
(append "a") \ / (append "b")
[odd, even]
|
|
{end}
where []
are states of the system, ()
are actions taken while following an edge (either way), and {}
are special pointers to enter and terminate the process. From now on the actions will all be "append a string matching this regex", so I will just include the regex in those parentheses and be done with it.
Now we remove states. When we remove a state X, we take every inbound arrow to that state from every other state Y, and create an arrow in its place from Y to every Z, where Z ranges over all of the destinations of outbound arrows of X. There are two subtleties: first, when X has an arrow pointing to itself (none of these do yet, but they will), in which case our regexes all need to include a repetition operator *
; they all take the form (Y-to-X)(X-to-X)*(X-to-Z)
. The other subtlety is that if an arrow of the same start and end already exists, we can (and should) concatenate their regular expressions with the alternation operator |
that matches if either the regex on the right or left matches.
Now, it matters which states you remove, and in which order, for getting this regex to look simple. There are other forms which look less simple. We will remove the right node [odd, odd]
first, then [even, even]
, in order to preserve an aesthetic symmetry that will keep our regex simple.
So let's remove [odd, odd]
. This means that you need to add four arrows: one arrow each from [even, odd]
and [odd, even]
back to themselves, and then one arrow from [even, odd]
to [odd, even]
, and one arrow back.
The arrow from [even, odd]
to itself is labeled (aa)
while the arrow from [odd, even]
to itself is labeled (bb)
. The path from [even, odd]
to [odd, even]
is (ab)
and the reverse is (ba)
.
Now we want to remove [even, even]
. This is tricky because the {start}
pointer is pointing at it, so we will instead point the {start}
at some node [start]
which is otherwise exactly like [even, even]
but has no other inbound arrows. This removal symmetrizes the system nicely:
{start} ---> [ start ]
/ \
(b) / \ (a)
V (ab|ba) V
[even, odd]=======[odd, even] ---> {end}
V ^ V ^
|___| |___|
(aa|bb) (aa|bb)
(Here the double line is again a symmetric transition.)
Eliminating [even, odd]
is the tricky part; it induces a loop in [odd, even]
which must now take the form of:
aa | bb | (ab | ba) (aa | bb)* (ab | ba)
where I've added spaces added for clarity, but it also has to add onto the [start] -> [odd, even]
arrow:
a | b (aa | bb)* (ab | ba)
Thus we have simply
(a|b(aa|bb)*(ab|ba))(aa|bb|(ab|ba)(aa|bb)*(ab|ba))*
It has two parts: the first parses the minimal string that contains an odd number of a
and an even number of b
, by saying "either we start on an a
and all is good or we start on a b
and take further characters in pairs: if we see an aa
or bb
we keep going, if we see an ab
or ba
we have found the minimal string."
The second parses any string which has an even number of a
and an even number of b
, again by taking characters in pairs: if it sees an aa
or a bb
it continues, otherwise if it sees an ab
or a ba
it skips as many aa
and bb
tokens as it can until it finds a corresponding ab
or ba
(doesn't matter which).
b's
which is `even number of b's = (ababa*)* $\endgroup$