Contest Python: ACSL

unit

Unit 14 — What Does This Program Do? (Strings)

The problem: The Password Machine

The coding club gives every locker a password. The password machine takes a secret word, cuts it into pieces, and glues the pieces back together in a new order. This is its ACSL program:

INPUT S
A = S[:2]
B = S[3:]
C = S[1:3]
P = B + C + A
OUTPUT P, len(P)

What is output when the input is S = "ELEPHANT"?

This is the Junior What Does This Program Do? question for Contest 4: a short program that works on a string, a row of characters, traced by hand with no computer. The IFs and loops are the ones you already know from Units 3, 6 and 10. What is new are the pieces. S[3:] and S[1:3] look exactly like Python, but in ACSL they mean something different. Read them the Python way and every line after them goes wrong.

This unit teaches it in three steps: how ACSL numbers the characters of a string and cuts pieces out of it, and how to write each piece in Python (Lesson 1), how programs walk along a string to count, build, reverse and test for palindromes (Lesson 2), and how programs move a piece along a string, compare letters, and shrink a string in a loop (Lesson 3). Write down your first guess now. You will check it at the end of Lesson 1.

You will learn

  • Read ACSL string positions, which start at 0, with len(S) and S[j].
  • Apply ACSL's three substring forms, translate them to Python, and predict each piece's length.
  • Trace loops that count, build, reverse and compare strings, including palindrome tests.
  • Compare letters by alphabetical order, move a piece along a string, and shrink a string with WHILE.

Lesson 1 — Strings and pieces in ACSL and in Python

Division: Junior and above.

A string is a row of characters: letters, digits, spaces and signs. Each character has a position, and in ACSL, as in Python, the first position is 0. len(S) is the number of characters, and S[j] is the character at position j.

For S = "ELEPHANT":

position 0 1 2 3 4 5 6 7
character E L E P H A N T

On paper, write the positions above the letters like this before you trace anything.

S = "ELEPHANT"
print(len(S))
print(S[0], S[3], S[len(S) - 1])

Output

8
E P T

Notice

len(S) is 8, but the last position is 7, one less, because the positions start at 0. S[len(S) - 1] is always the last character, whatever the length. S[8] would be past the end: there is no position 8.

Three ways to cut a piece

A piece of a string (a substring) is written with a colon inside the square brackets. ACSL has exactly three forms, and only the first one means the same as in Python.

ACSL means for "ELEPHANT" Python
S[:n] the first n characters S[:2] is "EL" S[:n]
S[n:] the last n characters S[3:] is "ANT" S[len(S) - n:]
S[a:b] positions a through b, both included S[1:3] is "LEP" S[a:b + 1]

Here is the table as Python.

S = "ELEPHANT"
print(S[:2])
print(S[len(S) - 3:])
print(S[1:3 + 1])

Output

EL
ANT
LEP

Notice

The three pieces are EL, ANT and LEP. In S[n:] the number is a count from the right-hand end, not a position. In S[a:b] both ends are positions, and the piece includes both of them.

The trap: reading ACSL with Python's eyes

You already know Python's slices, and that is exactly the danger. Python's S[3:] starts at position 3 and runs to the end. Python's S[1:3] starts at position 1 and stops before position 3.

S = "ELEPHANT"
print(S[3:])
print(S[1:3])

Output

PHANT
LE

Notice

Python gives PHANT and LE, while ACSL's S[3:] is ANT and ACSL's S[1:3] is LEP. When a question shows ACSL pseudocode, use ACSL's rules; when it shows Python, use Python's rules. Before you trace, look at which language the program is in.

How long is each piece?

Counting the characters of a piece is a quick check on your work.

ACSL number of characters
S[:n] n
S[n:] n
S[a:b] b - a + 1

So S[j:j] is one character, the same as S[j], and S[j:j + 1] is two characters. Two pieces come up again and again:

  • S[:len(S) - 1] is every character except the last one.
  • S[len(S) - 1:] is the last len(S) - 1 characters: every character except the first one.
T = "PENCIL"
print(T[2:2 + 1], len(T[1:4 + 1]))
print(T[:len(T) - 1])
print(T[len(T) - (len(T) - 1):])

Output

N 4
PENCI
ENCIL

Notice

ACSL's T[2:2] is N, a single character, and ACSL's T[1:4] has 4 - 1 + 1, which is 4, characters. ACSL's T[len(T) - 1:] asks for the last 5 characters of PENCIL, which is ENCIL: the first letter has gone. In Python that is T[1:], which the translation T[len(T) - (len(T) - 1):] works out to.

Joining and comparing

+ joins two strings end to end. It adds no space: "ANT" + "EATER" is "ANTEATER". == is true only when two strings are exactly the same: the same length, and the same character at every position. A space counts as a character, and so does its absence.

print("ANT" + "EATER")
print("CAT" == "CA" + "T")
print("CAT" == "CAT ")
print(len("ICE" + " " + "CREAM"))

Output

ANTEATER
True
False
9

Notice

"CAT ", with a space on the end, is not equal to "CAT", because it is one character longer. "ICE" + " " + "CREAM" has 3 + 1 + 5, which is 9, characters, because the space you joined in counts too.

When an ACSL program outputs several values, as in OUTPUT P, len(P), this book writes them on one line separated by one space, just as Python's print(P, len(P)) does. The strings are written without quotation marks.

Checking the opening problem

Here is the password machine, translated line by line. S[3:] becomes S[len(S) - 3:] and S[1:3] becomes S[1:4].

S = "ELEPHANT"
A = S[:2]
B = S[len(S) - 3:]
C = S[1:4]
P = B + C + A
print(P, len(P))

Output

ANTLEPEL 8

Notice

A is EL, B is ANT and C is LEP, so the password is ANTLEPEL, with 8 characters. Reading it with Python's eyes gives PHANT, LE and a wrong password, PHANTLEEL, with 9. Did it match your guess? Running a translation is how you check an answer while you practise; in the contest you work it out by hand, piece by piece.

Put it together: this program reads a word S and a number n, and makes a badge: the first n letters joined to the last n letters. It also outputs the middle: the letters left over between them, positions n through len(S) - 1 - n.

INPUT S
INPUT n
badge = S[:n] + S[n:]
middle = S[n:len(S) - 1 - n]
OUTPUT badge, middle

The first input line holds the word, in capital letters, and the second line holds n. The word has at least 2 * n + 1 letters, so the middle is never empty. It is saved as assets/l1.py.

Try it yourself

S = input()
n = int(input())
badge = S[:n] + S[len(S) - n:]
middle = S[n:len(S) - n]
print(badge, middle)

Run it from this unit's folder, feeding it an input file:

python assets/l1.py < assets/l1/1.in

Notice

All three forms appear, and each one is translated differently. ACSL's S[n:len(S) - 1 - n] ends at position len(S) - 1 - n, so the Python slice must stop one later, at len(S) - n. With the word CAT and n = 1 (assets/l1/2.in) the middle is the single letter A.

Lesson 2 — Walking along a string

Division: Junior and above.

Many programs look at a string one character at a time. The positions run from 0 to len(S) - 1, so the loop is

FOR j = 0 TO len(S) - 1
   ...S[j]...
NEXT j

The - 1 matters: ACSL's FOR includes its end value, and position len(S) does not exist. In Python it becomes for j in range(len(S)):, because range stops before its end.

Counting one character

To count how often one character appears, compare each S[j] with it and add 1 when they match.

S = "EXCELLENCE"
c = 0
FOR j = 0 TO len(S) - 1
   IF S[j] == "E" THEN
      c = c + 1
   END IF
NEXT j
OUTPUT c

Python can also walk along a string without positions, with for ch in S:. Both loops are shown here.

S = "EXCELLENCE"
c = 0
for j in range(len(S)):
    if S[j] == "E":
        c = c + 1
print(c)
d = 0
for ch in S:
    if ch == "E":
        d = d + 1
print(d)

Output

4
4

Notice

Both loops count 4. for ch in S is shorter, but it does not know where each character is. When a program uses the position too, as in S[j + 1] or S[len(S) - 1 - j], use the loop over positions.

Counting a group of characters: vowels

To count several different characters, join the tests with || (Python's or).

IF S[j] == "A" || S[j] == "E" || S[j] == "I" || S[j] == "O" || S[j] == "U" THEN
S = "EDUCATION"
v = 0
for j in range(len(S)):
    ch = S[j]
    if ch == "A" or ch == "E" or ch == "I" or ch == "O" or ch == "U":
        v = v + 1
print(v, len(S) - v)

Output

5 4

Notice

EDUCATION has 5 vowels and 4 other letters. Each test must name ch again: ch == "A" or "E" does not test for E. In a trace, tick each vowel as you pass it, then count the ticks.

Building a new string

A program can build a new string, starting from the empty string "" and joining one character on each pass.

S = "BASKETBALL"
T = ""
FOR j = 0 TO len(S) - 1
   IF j % 2 == 0 THEN
      T = T + S[j]
   END IF
NEXT j
OUTPUT T
S = "BASKETBALL"
T = ""
for j in range(len(S)):
    if j % 2 == 0:
        T = T + S[j]
print(T)

Output

BSEBL

Notice

j % 2 == 0 keeps the positions 0, 2, 4, 6 and 8, which hold B, S, E, B and L: the answer is BSEBL. It is easy to count letters from 1 by mistake and keep A, K, T, A, L instead. Write the positions above the word first.

Reversing: which side do you join on?

T = T + S[j] joins each character on the end, so it copies the string in order. R = S[j] + R joins each character on the front, so the first character ends up last: the string comes out reversed.

A loop that runs backwards and joins on the end reverses too:

FOR j = len(S) - 1 TO 0 STEP -1
   R = R + S[j]
NEXT j
S = "DRAWER"
T = ""
R = ""
for j in range(len(S)):
    T = T + S[j]
    R = S[j] + R
print(T, R)
B = ""
for j in range(len(S) - 1, -1, -1):
    B = B + S[j]
print(B)

Output

DRAWER REWARD
REWARD

Notice

T is DRAWER again, and R is REWARD. The backwards loop also gives REWARD: in Python its end is -1, one past 0, so that position 0 is included. In a trace, write a table with a column for j, one for S[j] and one for the string after the pass:

j S[j] R after the pass
0 D D
1 R RD
2 A ARD
3 W WARD
4 E EWARD
5 R REWARD

Palindromes

A palindrome reads the same forwards and backwards, like RADAR or NOON. A program can test it in two ways:

  • build the reverse and compare it with ==;
  • compare each character with its mirror, S[j] with S[len(S) - 1 - j], from the outside in.

For the mirror test, the loop only needs to reach the middle: FOR j = 0 TO int(len(S) / 2) - 1.

S = "RADAR"
R = ""
for j in range(len(S)):
    R = S[j] + R
print(R == S)
W = "ROTOR BLADE"
same = 0
for j in range(len(W) // 2):
    if W[j] == W[len(W) - 1 - j]:
        same = same + 1
print(len(W) // 2, same)

Output

True
5 0

Notice

RADAR reversed is RADAR, so the first test prints True. ROTOR BLADE has 11 characters, so the mirror loop checks 5 pairs: R E, O D, T A, O L and R B. None of them match, so same is 0: the test found that it is not a palindrome. With an odd length, the middle character is its own mirror and is never checked, which is fine.

Put it together: this program reads a word, reverses it, and says whether it is a palindrome.

INPUT S
R = ""
FOR j = 0 TO len(S) - 1
   R = S[j] + R
NEXT j
IF R == S THEN
   OUTPUT R, "YES"
ELSE
   OUTPUT R, "NO"
END IF

The input is one word in capital letters. It is saved as assets/l2.py.

Try it yourself

S = input()
R = ""
for j in range(len(S)):
    R = S[j] + R
if R == S:
    print(R, "YES")
else:
    print(R, "NO")

Trace it by hand on each input before you run it from this unit's folder:

python assets/l2.py < assets/l2/1.in

Notice

A one-letter word (assets/l2/3.in) is always a palindrome. ABCA (assets/l2/5.in) starts and ends with the same letter but is not a palindrome: every pair of mirrors must match, not only the outside pair.

Lesson 3 — Moving pieces, comparing letters, shrinking strings

Division: Junior and above.

The hardest string questions put a piece inside a loop, compare letters, or chop a string shorter on every pass. Each one is traced the same way: work out the piece first, write it down, and only then compare or join it.

A piece that moves along the string

S[j:j + 1] is positions j through j + 1: a piece of two characters that moves along as j grows. The loop must stop at len(S) - 2, so that j + 1 is still inside the string.

S = "ASSESSMENT"
c = 0
FOR j = 0 TO len(S) - 2
   IF S[j:j + 1] == "SS" THEN
      c = c + 1
   END IF
NEXT j
OUTPUT c

In Python the same piece is S[j:j + 2].

S = "ASSESSMENT"
c = 0
for j in range(len(S) - 1):
    if S[j:j + 2] == "SS":
        c = c + 1
print(c)
print(S[3:3 + 2], S[3:4])

Output

2
ES E

Notice

SS sits at positions 1 and 4, so c is 2. At j = 3, ACSL's S[3:4] is the two letters ES, but Python's S[3:4] is only E. A reader who uses Python's rule never finds a two-letter match and answers 0.

Endings: the last k letters

S[k:] is the last k characters, so a loop over k looks at longer and longer endings of the word.

FOR k = 1 TO len(S)
   ...S[k:]...
NEXT k

In Python the ending is S[len(S) - k:].

S = "TIGER"
for k in range(1, len(S) + 1):
    print(k, S[len(S) - k:], S[:k])

Output

1 R T
2 ER TI
3 GER TIG
4 IGER TIGE
5 TIGER TIGER

Notice

The endings are R, ER, GER, IGER and TIGER, and the beginnings S[:k] grow from the other side. A program that compares S[:k] with S[k:] is asking whether the word starts and ends with the same k letters.

Comparing letters

< and > compare two capital letters by their order in the alphabet: "A" < "B" is true, and "M" > "Z" is false. Every string in this unit that is compared this way uses capital letters only. (In Python every capital letter comes before every small letter, so "Z" < "a" is true: never mix the two when you compare.)

Comparing letters lets a program find the letter latest in the alphabet, just as it finds the largest number.

S = "SQUIRREL"
big = S[0]
where = 0
early = 0
for j in range(len(S)):
    if S[j] > big:
        big = S[j]
        where = j
    if S[j] < "M":
        early = early + 1
print(big, where, early)

Output

U 2 3

Notice

U, at position 2, is the latest letter, so the answer starts U 2. The letters before M in the alphabet are I, E and L, so early is 3. With >, a later copy of the same letter never replaces the first one; with >= it would.

Putting two letters in order

Compare two letters and join them smaller first:

a = S[j]
b = S[j + 1]
IF a > b THEN
   T = T + b + a
ELSE
   T = T + a + b
END IF
S = "TOPCAT"
T = ""
for j in range(0, len(S), 2):
    a = S[j]
    b = S[j + 1]
    if a > b:
        T = T + b + a
    else:
        T = T + a + b
print(T)

Output

OTCPAT

Notice

The pairs are TO, PC and AT. T comes after O, so the first pair becomes OT; PC becomes CP; AT is already in order. The result is OTCPAT.

Shrinking a string with WHILE

A WHILE loop can chop a string shorter on every pass until it is used up.

  • S = S[len(S) - 1:] keeps the last len(S) - 1 characters: it drops the first character.
  • S = S[:len(S) - 1] keeps the first len(S) - 1 characters: it drops the last character.

Remember to compute len(S) for the string as it is now, since it shrinks on every pass.

S = "PLANET"
while len(S) > 0:
    print(S)
    S = S[len(S) - (len(S) - 1):]

Output

PLANET
LANET
ANET
NET
ET
T

Notice

The lines are PLANET, LANET, ANET, NET, ET and T. On the pass that prints T, len(S) - 1 is 0, so the next S is the last 0 characters: the empty string, and the loop stops. The Python piece S[len(S) - (len(S) - 1):] is the translation rule applied as it is; it works out to S[1:].

Put it together: this program reads a word S and a shorter pattern P. It counts how many times P appears in S, overlaps included, and outputs the count and the first position where P starts (or -1 if it never does).

INPUT S
INPUT P
m = len(P)
c = 0
first = -1
FOR j = 0 TO len(S) - m
   IF S[j:j + m - 1] == P THEN
      c = c + 1
      IF first == -1 THEN
         first = j
      END IF
   END IF
NEXT j
OUTPUT c, first

The first input line holds the word and the second holds the pattern, both in capital letters. It is saved as assets/l3.py.

Try it yourself

S = input()
P = input()
m = len(P)
c = 0
first = -1
for j in range(len(S) - m + 1):
    if S[j:j + m] == P:
        c = c + 1
        if first == -1:
            first = j
print(c, first)

Run it from this unit's folder:

python assets/l3.py < assets/l3/1.in

Notice

The ACSL piece S[j:j + m - 1] has (j + m - 1) - j + 1, which is m, characters: exactly the length of the pattern. In BANANA the pattern ANA is found at positions 1 and 3, which overlap (assets/l3/1.in). When the pattern is longer than the word (assets/l3/5.in), the loop FOR j = 0 TO -1 runs zero times, so the output is 0 -1.

Recap

  • In ACSL, S[:n] is the first n characters, S[n:] the last n, and S[a:b] positions a through b.
  • In Python those are S[:n], S[len(S) - n:] and S[a:b + 1].
  • Joining each character on the end copies a string; joining on the front reverses it.
  • A palindrome reads the same forwards and backwards.
  • Read ACSL with ACSL's rules: the Password Machine prints ANTLEPEL 8, not PHANTLEEL 9.

You can now trace an ACSL program that works on strings and write its exact output.

Go to the exercises (19)

For parents and teachers: report a problem