The problem: The Knock Code
The clubhouse door has no key. It has two buttons: k (a knock) and t (a tap). You press a code, one button at a time, and a little machine inside the door decides whether to open.
The club's inventor left this chart of the machine taped inside the door:
state k t → S (start) K — K K T T (final) K — The machine starts in state
S. Each button press moves it to the state in that row and column. A—means there is no move at all: the machine jams, and the door stays locked. When the code is finished, the door opens only if the machine is in the final stateT.Two questions:
- Which of these codes open the door:
kt,kkt,ktt,tkt,ktkkt?
- Which of these codes open the door:
- The inventor wants to write down, in one short line, a rule that describes every code that opens the door. What could that line be?
A machine like this is called a finite state automaton (FSA), and the one-line rule is a regular expression. Both are on the ACSL Intermediate and Senior papers. Lesson 1 teaches you to read and run an FSA, Lesson 2 to describe what it accepts with a regular expression, and Lesson 3 the short-hand symbols real regular expressions use. Along the way you will write Python programs that run an FSA and check a pattern, and at the end of Lesson 3 you will answer both of the inventor's questions.
-
Read a finite state automaton as a table or a diagram, and run it by hand on a string.
-
Read and write regular expressions built from concatenation, union and star.
-
Test two regular expressions for equality, or show they differ with one string.
-
Read ACSL's extended syntax:
?,+,.and classes such as[a-z]and[^abc]. -
Write an FSA simulator and a simple pattern matcher in Python.
-
Lesson 1 — Finite state automata
Division: Intermediate and above.
A finite state automaton (FSA) is a very simple machine that reads a string one symbol at a time. It has four parts:
-
a small number of states; the machine is in exactly one of them at any moment
-
transitions: rules of the form "in state
K, readingt, move to stateT" -
one initial state, where it starts
-
one or more final states
-
The FSA accepts a string if, after reading every symbol, it is in a final state. Otherwise it rejects the string. The symbols the FSA can read are its alphabet; the door's alphabet is
kandt.Two ways to draw an FSA
ACSL draws an FSA as a picture: a circle for each state, an arrow for each transition labelled with its symbol, an arrow from nowhere into the initial state, and a double circle for each final state. Here is the door, drawn in text, with
(( T ))as the double circle:+-k-+ | | | v start --> ( S ) --k--> ( K ) --t--> (( T )) ^ | | | +------k------+The loop above
Ksays: in stateK, readingk, stay inK.This book usually gives an FSA as a transition table instead. There is one row per state and one column per symbol.
-
→marks the initial state. -
(final)marks each final state. -
—means there is no transition: if the machine needs it, the string is rejected at once.
-
Running an FSA by hand
Keep track of the state as you read the string, one symbol at a time.
Example 1. Does the door accept
kkt?read — k k t state S K K T The machine ends in
T, a final state, sokktis accepted.Example 2. Does the door accept
ktt?read — k t t state S K T ? In state
Tthere is no move fort. The machine jams, sokttis rejected, even though it passed throughTon the way.Watch out:
-
Only the state at the end matters. Passing through a final state is not enough.
-
A missing transition rejects the string straight away, whatever comes after.
-
- The empty string, with no symbols at all, is written
λ(the Greek letter lambda). The machine reads nothing and stays in the initial state, so an FSA acceptsλexactly when its initial state is also final. The door rejectsλ, becauseSis not final.
Example 3. This FSA reads strings of
0s and1s.state 0 1 → E (final) E O O O E +-0-+ +-0-+ | | | | | v | v start --> (( E )) ----1----> ( O ) ^ | | | +--------1--------+- The empty string, with no symbols at all, is written
A
0never changes the state, and every1swaps betweenEandO. So the machine is inEexactly when it has read an even number of1s: it accepts0110and rejects1011. It acceptsλtoo, because its initial stateEis final (zero1s is an even number).Working out what an FSA is counting or checking, like this, is the key to the harder questions.
An FSA in Python
A transition table fits naturally in a Python dict. Each key is a tuple
(state, symbol), and its value is the next state. A missing transition is simply a key that is not in the dict.door = {("S", "k"): "K", ("K", "k"): "K", ("K", "t"): "T", ("T", "k"): "K"} print(door[("K", "t")]) print(("T", "t") in door) print(("T", "k") in door)Scroll the code for more ↓
Output
T False Trueintells you whether a key is in the dict. Asking fordoor[("T", "t")]would crash the program, so always check withinfirst.To run the machine, go through the string one symbol at a time. When a transition is missing, move to a made-up state
"STUCK"that has no transitions at all: the machine then stays stuck to the end, and"STUCK"is never final.door = {("S", "k"): "K", ("K", "k"): "K", ("K", "t"): "T", ("T", "k"): "K"} for code in ["kkt", "ktt", "ktkkt"]: state = "S" for button in code: if (state, button) in door: state = door[(state, button)] else: state = "STUCK" print(code, state)Scroll the code for more ↓
Output
kkt T ktt STUCK ktkkt TPut it together: this program reads one code and prints
OPENif the door accepts it andLOCKEDif not. It is saved asassets/l1.py.Try it door = {("S", "k"): "K", ("K", "k"): "K", ("K", "t"): "T", ("T", "k"): "K"} code = input() state = "S" for button in code: if (state, button) in door: state = door[(state, button)] else: state = "STUCK" if state == "T": print("OPEN") else: print("LOCKED")Scroll the code for more ↓
Run it from this unit's folder:
python assets/l1.py < assets/l1/1.inIt prints
OPENfor the codekt. Tryassets/l1/2.in(the codektt, which jams),assets/l1/3.inandassets/l1/4.intoo.Lesson 2 — Regular expressions
Division: Intermediate and above.
An FSA is a machine that checks strings. A regular expression is a short line of text that describes a set of strings. The two ideas fit together: every FSA's set of accepted strings can be written as a regular expression, and every regular expression has an FSA.
The building blocks
Regular expression The strings it describes a(any single symbol)just the string aλjust the empty string xy(concatenation: one after the other)a string from xfollowed by a string fromy`x y , also writtenxUy` (union)x*(Kleene star)zero or more strings from x, one after anotherSo
abdescribes onlyab;a|bdescribesaandb; anda*describesλ,a,aa,aaa, and so on forever.ACSL writes union both ways,
|andU, so0U1means the same as0|1. (In this book, a capitalUoutside square brackets always means union, andλalways means the empty string.) Round brackets group, as in maths:(ab)*describesλ,ab,abab,ababab, ….Precedence
When there are no brackets, the operations are done in this order:
-
star
*first -
then concatenation
-
then union
|last
So:
-
ab*meansa(b*): anafollowed by any number ofbs:a,ab,abbb, but notabab. -
(ab)*repeats the wholeab:λ,ab,abab, but notabb. -
a|bcmeansa|(bc): the stringsaandbc, notac.
-
This is just like arithmetic, where powers come before multiplication and multiplication before addition: star is like a power, concatenation like multiplication, and union like addition.
Example 4. Which of these strings does
00*1*1U11*0*0describe?0000001111111,1011,10,0Use precedence to split it at the union first:
00*1*1or11*0*0.-
00*1*1is one or more0s followed by one or more1s. -
11*0*0is one or more1s followed by one or more0s.
-
So
0000001111111(0s then 1s) and10(a 1 then a 0) are described.1011switches three times, and0has no1at all, so neither is.From an FSA to a regular expression
To describe what an FSA accepts, follow the paths from the initial state to a final state:
-
a chain of transitions becomes concatenation
-
a loop that you can go round any number of times becomes a star
-
a choice between two ways becomes a union
Example 5. The door. From
Syou must pressk, reachingK. InKyou can presskany number of times (the loop): that isk*. Thenttakes you toT. So one trip fromStoTiskk*t.-
From
T, akgoes back toK, and the same pattern repeats:k*again, and atback toT. So after the first trip you can take any number of further tripskk*t. The door accepts exactly the strings described bykk*t(kk*t)*Check it against the codes from the problem:
ktandkktare one trip, andktkktis two trips (kt, thenkkt).ktthas twots in a row, andtktstarts witht, so neither fits.Try it. Write a regular expression for the FSA of Example 3 (an even number of
1s). Work it out on paper before you read on.One answer: from
E, loop on0s (0*); a pair of1s, each followed by any0s, takes you fromEtoOand back toE(10*10*); repeat the pair any number of times.0*(10*10*)*Another correct answer is
(0|10*1)*: each step is either a0, or a1, some0s and a second1. Different regular expressions can describe the same set of strings. That is why contest questions on this usually give you options to choose from.Identities: when are two expressions equal?
Two regular expressions are equivalent when they describe exactly the same strings. These identities are always true (
a,bandccan stand for any regular expressions):Identity Why 1 (a*)* = a*repeating a repeat is still just a repeat 2 aa* = a*aboth are one or more as3 aa* U λ = a*one or more, or none, is zero or more 4 a(b U c) = ab U acconcatenation spreads over union, like multiplying out brackets 5 a(ba)* = (ab)*aboth are a,aba,ababa, …6 (a U b)* = (a* U b*)*any mixture of as andbs7 (a U b)* = (a*b*)*the same 8 (a U b)* = a*(ba*)*the same To show that two expressions are not equivalent, one string is enough: find a string that one describes and the other does not. For example,
(ab)* = ab*is false:abbfitsab*but not(ab)*. A good way to hunt is to try the short strings first:λ, then every string of length 1, then length 2, and so on.Counting accepted strings in Python
Trying every short string is exactly what a computer is good at. The function
countbelow builds every code of lengthnone button at a time, with recursion: a code that is not long enough yet branches into two longer codes, one ending inkand one ending int.def count(code, n): if len(code) == n: print(code) return 1 return count(code + "k", n) + count(code + "t", n) print(count("", 2))Scroll the code for more ↓
Output
kk kt tk tt 4With 2 buttons there are 2 × 2 = 4 codes of length 2, and in general 2 multiplied by itself
ntimes.Put it together: this program reads
nand prints how many codes of lengthnopen the door. It is saved asassets/l2.py.Try it door = {("S", "k"): "K", ("K", "k"): "K", ("K", "t"): "T", ("T", "k"): "K"} def opens(code): state = "S" for button in code: if (state, button) in door: state = door[(state, button)] else: state = "STUCK" return state == "T" def count(code, n): if len(code) == n: if opens(code): return 1 return 0 return count(code + "k", n) + count(code + "t", n) n = int(input()) print(count("", n))Scroll the code for more ↓
Run it from this unit's folder:
python assets/l2.py < assets/l2/1.inFor
n = 2it prints1(onlyktopens the door). Tryassets/l2/2.inandassets/l2/3.in, then check the answer forn = 3by hand withkk*t(kk*t)*.Lesson 3 — The extended syntax
Division: Intermediate and above.
Real regular expressions, like the ones in text editors and search boxes, add some short-hand symbols. ACSL uses them too:
Symbol Meaning Example Describes x?zero or one xab?a,abx+one or more xab+ab,abb,abbb, …x*zero or more xab*a,ab,abb, ….any one character c.tcat,cot,c9t, …[abc]any one of the characters listed [aeiou]one vowel [^abc]any one character not listed [^0-9]one character that is not a digit [a-z]any one character in the range [A-D]A,B,CorD(x)grouping (ab)+ab,abab, …?,+and*work on the one thing just before them: a single character, a., a class in square brackets, or a bracketed group. None of these add anything new; they are short ways to write the basic operations. For example,x+isxx*,x?isx|λ, and[abc]isa|b|c.Watch out:
-
Inside square brackets,
^means "not" only when it comes first:[^abc]. -
A class matches exactly one character.
[a-c]matchesbbut notbc;[a-c]+matchesbc. -
Capital and small letters are different characters:
[a-d]does not matchB.
Example 6. Which strings match
[A-D]*[a-d]*[0-9]?ABCD8,abcd5,ABcd9,AbCd7,DCCBBBaaaa5,ABCDE3The pattern is: any number of capitals from
AtoD, then any number of small letters fromatod, then exactly one digit.ABCD8,abcd5,ABcd9andDCCBBBaaaa5match.
-
-
AbCd7does not: once a small letter has appeared, a capital cannot come back. -
ABCDE3does not:Eis outsideAtoD.
Example 7. Which strings match
Hi?g+h+[^a-ceiou]?Highb,HigghhhC,Hihh,Highd,HgggggghXThe pattern is:
H, an optionali, one or moregs, one or morehs, and then one character that is nota,b,c,e,i,ooru.-
-
HigghhhC,HighdandHgggggghXmatch. (CandXare capitals, so they are not in the classa-ceiou;dis not in it either.) -
Highbdoes not: its last characterbis ina-c. -
Hihhdoes not: it has nog.
-
A pattern matcher in Python
Here is a program that checks whether a word matches a pattern made of letters, where a letter may be followed by
*. It works on two positions at once:i, how far it has got in the pattern, andj, how far it has got in the word.-
When the pattern is used up, the match succeeds only if the word is used up too.
-
A plain letter must match the next character of the word; then both move on by one.
-
- A letter with a
*gives two choices: use it zero times (skip the letter and its*in the pattern), or match one copy of it and stay on the same letter to look for more.
Recursion is the natural way to try both choices: if the first choice fails, the function comes back and tries the second.
- A letter with a
def match(p, i, s, j): if i == len(p): return j == len(s) fits = j < len(s) and s[j] == p[i] if i + 1 < len(p) and p[i + 1] == "*": if match(p, i + 2, s, j): return True return fits and match(p, i, s, j + 1) return fits and match(p, i + 1, s, j + 1) print(match("ab*c", 0, "abbbc", 0)) print(match("ab*c", 0, "ac", 0)) print(match("a*ab", 0, "aaab", 0)) print(match("ab*c", 0, "abcb", 0))Scroll the code for more ↓
Output
True True True FalseThe third test needs the second choice. For
a*abonaaab, thea*must take only two of the threeas, leaving oneafor the plainain the pattern. The recursion finds this by trying the choices one after another, without you telling it how manyas to take.fitsusesandso thats[j]is only looked at whenj < len(s): once the first part isFalse, Python does not work out the second part at all.Put it together: this program reads a pattern on the first line and a word on the second, and prints
ACCEPTorREJECT. It is saved asassets/l3.py.Try it def match(p, i, s, j): if i == len(p): return j == len(s) fits = j < len(s) and s[j] == p[i] if i + 1 < len(p) and p[i + 1] == "*": if match(p, i + 2, s, j): return True return fits and match(p, i, s, j + 1) return fits and match(p, i + 1, s, j + 1) pattern = input() word = input() if match(pattern, 0, word, 0): print("ACCEPT") else: print("REJECT")Scroll the code for more ↓
Run it from this unit's folder:
python assets/l3.py < assets/l3/1.inIt prints
ACCEPTfor the patternab*cand the wordabbbc. Tryassets/l3/2.in,assets/l3/3.inandassets/l3/4.intoo.Back to the door
Now both of the inventor's questions are easy.
-
Run each code through the table (or through
assets/l1.py):kt,kktandktkktopen the door;kttjams at its secondt, andtktjams at once, sinceShas no move fort. -
In Lesson 2 you found
kk*t(kk*t)*. With the extended syntax,kk*isk+, so one trip isk+t, and one or more trips is
(k+t)+-
In words: the door opens for one or more blocks, where each block is one or more knocks followed by a single tap.
-
An FSA accepts a string only if it ends in a final state; a missing move rejects the string.
-
Star goes first, then concatenation, then union.
-
One counterexample string is enough to show that two expressions are different.
-
x+means one or more (sokk*isk+),x?means zero or one, and a class matches exactly one character. -
In Python, a dict keyed by
(state, symbol)tuples holds the transition table, and recursion tries both choices of a star.
-
You can now describe the knock code in one line,
(k+t)+, and answer ACSL's FSA and pattern questions.