Mathematics

The All-Points-Reachable (APR) Program

 

This is the second of the APR problem pages.

Data Structures

We will use LiveCode Community Edition, that can be downloaded free from RunRev.  I will not explain how this works since the LiveCode site does that in detail (e.g. in the beginners' guide).

We start by providing a text entry field in which the user can type a list of switch descriptions.  This field will look like a table with three columns and a line for each switch description.  I used a text field with the table option, tab stops at 30 pixels and a maximum of 3 editable columns.  I coloured the grid lines and chose to display only 16 lines since I only have 16 Lego Duplo switches.  I also put a label field on top and another lable field at the left to show the numbers of the switches.  It looks like this:

switches field
the switches field

Unfortunately typing into such a field will make the field scroll horizontally if the last visible column is used.  To avoid that I made four columns visible, then put a locked graphic with the card's background colour on top of the fourth column to hide it.  In the above image I already typed the switch descriptions of the oval with a diagonal.

We add a button to compute the APR, and a field "Diagnostic" where we will put the result and any other message about the computation:

switches field
the compute button

Validating the User's Input

As soon as the user has typed some switch descriptions in the switches field, we must check the validity of the input.

We will use the letters of the alphabet to name segments and label both segment ends by the segment's letter and either the digit 1 or 2.

That limits us to 26 segments, which is enough for 16 switches:  8 pairs need 3x8=24 segments.

The switches field contains a line for each switch, with three points (segment end labels) on each line, separated by the tab character.

A function ValidSwitches can perform this check:

1constant cAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" -- should not have Y and Z since they can't be used: 3*8=24, not 26.

2function ValidSwitches fS

3 put Clean(fS) into fS

4 put true into lResult

-- there should only be lines of the form X1-tab-Y1-tab-Z1

5 set the itemdelimiter to tab

6 repeat with iLine=1 to the number of lines of fS

7 put Clean(line iLine of fS) into lLine

8 if the number of items of lLine <> 3 then

9 answer "There is a missing point in line "&iLine&" of the switch table" with "OK"

10 put false into lResult

11 end if

12 repeat with j=1 to 3

13 put item j of lLine into lPoint

14 if (char 1 of lPoint is not in cAlphabet) or (char 2 of lPoint is not in "12") then

15 answer "There is an invalid point specification in column "&j&" of line "&iLine&" of the switch table" with "OK"

16 put false into lResult

17 end if

18 end repeat

19 end repeat

20 return lResult

21end ValidSwitches

For writing LiveCode programs I use some naming conventions that help me find my way among the variables I need to declare.  I will assume that you have read them.

Line 1:  declares the 26 upper case letters, used for segment names.

Line 3:  the function Clean() returns the given string without leading and trailing blanks and tabs.  It is one of a set of general routines that can be used in any stack's script.

Line 5:  ensures that we can use the item chunks of the switches.

Line 8:  checks each switch is described by three segment ends.

Line 14:  checks that each point name is of the form letter+digit where digit must be one of 1 or 2.

There is of course no verification that the given descriptions do correspond to some real layout.

The Switches Field

This is the script of the Switches field.  It gets the message CloseField when the user clicks somewhere else (except on Mac OSX) and this handler stops all processing if the user makes a typing error.

1on CloseField

2 if ValidSwitches(me) then

3 put Clean(field "Switches") into field "Switches"

4 else

5 answer "There are typing errors in your switch table" with "OK"

6 put the pendingmessages into lPendingMsgs

7 repeat for each line x in lPendingMsgs

8 cancel item 1 of x

9 end repeat

10 end if

11end CloseField

The Compute Button

A first attempt at the script of that button is to set up the first version of the table of reachable points.

Because it is interesting to see how the table filled up, we will keep its first state in a variable lFirstReachable, so we can compare later.

1on MouseUp

2 put empty into field "Diagnostic"

3 put empty into lFirstReachable

4 put 0 into lnPoints

5 SetupReachable lFirstReachable,lnPoints

6end MouseUp

Obviously we clear out the diagnostics first, then set up an empty table.

Before we go into the filling of the table, we consider this:  we can travel over a switch in both directions, each of which gives two marks in the table.  Take the example of the switch described by C1 A1 B1 :  going forward we actually come from C2, the other end of segment C.  Given our convention of labelling segment ends, to find the label of the other end of a segment means just changing the digit 1 into the digit 2, or 2 into 1.

We will index the tables by numbers rather than segment end labels because we will need to iterate over all rows and columns, which is easier to express in numeric loops.  We will therefore have to "translate" the points (segment end labels) into numbers while filling the table the first time:

A1 → 1; A2 → 2; B1 → 3; B2 → 4; C1 → 5; …

That is easily done by finding the place of the label's letter in the alphabet, multiplying by 2 and subtracting 1, and then adding the label's number and finally subtracting 1.

For that we use two routines numr and altnumr:

1constant cAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" -- should not have Y and Z since they can't be used: 3*8=24, not 26.

2function Numr fSegmentEndLabel

-- return the number of the lettered segment end if segments had been numbered instead of lettered

-- fSegmentEndLabel is of the form "L1" or "L2"; the result is: A1->1, A2->2, B1->3, B2->4, ...

3 return 2*offset(char 1 of fSegmentEndLabel,cAlphabet) - 1 + char 2 of fSegmentEndLabel -1

4end Numr

5function AltNumr fSegmentEndLabel

-- return the number of the lettered segment's other end if segments had been numbered instead of lettered

-- fSegmentEndLabel is of the form "X1" or "X2"

6 if char 2 of fSegmentEndLabel = 2 then put 1 into fn else put 2 into fn

7 return fn-1 + 2*offset(char 1 of fSegmentEndLabel,cAlphabet) - 1

8end AltNumr

The function altnumr returns the number corresponding to the other segment end of a given point, i.e. it will return 6, the number of point C2 when given C1

The SetupReachable routine can now fill up the table from the switch descriptions:

1command SetupReachable @fReachable,@fnPoints

-- Field Switches has the descriptions by segment ends.

2 put field "Switches" into lSwitches

3 put the number of lines of field "Switches" into nSwitches

4 put nSwitches*3 into fnPoints

5 set the itemdelimiter to tab

6 repeat with i=1 to nSwitches

7 put Clean(line i of lSwitches) into L

-- go forward to both choices:

8 put altnumr(item 1 of L) into i1

9 put numr(item 2 of L) into f1

10 put numr(item 3 of L) into f2

-- we can go from i1 to f1 and also from i1 to f2:

11 put true into fReachable[i1,f1]; put true into fReachable[i1,f2]

-- go backwards over same switch:

12 put numr(item 1 of L) into f1

13 put altnumr(item 2 of L) into i1

14 put altnumr(item 3 of L) into i2

-- we can go from i1 to f1 and from i2 to f1:

15 put true into fReachable[i1,f1]; put true into fReachable[i2,f1]

16 end repeat

17end SetupReachable

It gets its formal parameters by address, i.e. it can change the variables given in the MouseUp handler (this is indicated by the @-sign in front of the formal parameter name).  Lines 2 to 5 should be easy to understand.  The repeat loop runs over each switch description.  For each switch it puts marks into the table fReachable.  Finally it returns the total number of points to be considered, which is just the number of rows of the table.

Making it visible

Let's make the table visible in a field.  Make a table field, with 15 pixels to the tab stop and set the margin to 6.

reachable field
the "reachable" field

We now extend the mouse-up handler of the compute button:

1on MouseUp

2 ...

7 put empty into field "Reachable"

8 set the itemdelimiter to tab

9 repeat with iRow=1 to lnPoints

10 repeat with iColumn=1 to lnPoints

11 if lFirstReachable[iRow,iColumn] then

12 put "o" into item iColumn of line iRow of field "Reachable"

13 end if

14 end repeat

15 end repeat

16end MouseUp

I used a lower-case "o" instead of the checkmark, to avoid font problems on other platforms.

When the compute button is clicked, the Reachable field gets to show:

first reachable
the first reachables

Iterating the Table

The last bit of code will deal with iterating filling of the table to see if the APR property is true or not.

We do this with a procedure ("command" in LiveCode"):

1Command MultiplyReachable @fReachable,fnPoints,@fIterations

2 set the itemdelimiter to tab

3 put fReachable into lOldReachable -- to limit the number of iterations to the minimum

4 repeat with fIterations=1 to fnPoints

5 put fIterations & " iterations of " & fnPoints into field "Diagnostic"; wait 1 tick with messages

6 repeat with iDeparture=1 to fnPoints -- for this point, can we reach other points?

7 repeat with iArrival=1 to fnPoints

8 if fReachable[iDeparture,iArrival] then -- yes, so add the arrival's connections to the departure's

9 repeat with i=1 to fnPoints

10 put fReachable[iArrival,i] or fReachable[iDeparture,i] into fReachable[iDeparture,i]

11 end repeat

12 end if

13 end repeat

14 end repeat

15 if fReachable=lOldReachable then exit repeat

16 put fReachable into lOldReachable

17 end repeat

18end MultiplyReachable

I called it MultiplyReachable because the computation resembles the multiplication of matrices.

Line 3 keeps a copy of the given table, so we can compare at each iteration if the table has changed, and stop as soon as it no longer does.

Line 4 is the start of the main iteration loop.  It will run until

Line 5 just keeps the user informed of progress (though this is only observable in extremely large tables or on slow computers).

Lines 6 and 7 are the double loop over rows and columns looking for a reachable point, and when found, lines 9 to 11 then look at that point's reachable points to copy them over into the found one, thus filling more cells in the table.

Finally, line 15 checks to see if the table has changed, and if not stops the iterations.  If the table has changed, it preserves the new state to check against next time.

The procedure returns the number of iterations it performed.

Final Display

The only thing left to do is to tell the user the result.

To do that we could just output "APR=true" or "APR=false", but we will make it a little more attractive.  We change the code of the MouseUp handler of the Compute button to this:

1on MouseUp

2 send "CloseField" to field "Switches" -- necessary for Mac OSX only

3 put empty into field "Diagnostic"

4 put empty into lFirstReachable; put empty into lReachable

5 put 0 into lnPoints; put 0 into lIterations

6 SetupReachable lFirstReachable,lnPoints

7 put lFirstReachable into lReachable -- keep the original reachable points for user info only

8 MultiplyReachable lReachable,lnPoints,lIterations

-- display the original & the final matrices; the original's checked

-- cells are red, the iteratively added ones black.

9 put empty into field "Reachable"

10 put 0 into lTotalChecked

11 set the itemdelimiter to tab

12 lock screen

13 repeat with iRow=1 to lnPoints

14 repeat with iColumn=1 to lnPoints

15 if lReachable[iRow,iColumn] then

16 put "o" into item iColumn of line iRow of field "Reachable"

17 add 1 to lTotalChecked

18 end if

19 end repeat

20 end repeat

21 repeat with iRow=1 to lnPoints

22 repeat with iColumn=1 to lnPoints

23 if lFirstReachable[iRow,iColumn] then

24 set the textcolor of item iColumn of line iRow of field "Reachable" to red

25 set the textstyle of item iColumn of line iRow of field "Reachable" to "bold"

26 end if

27 end repeat

28 end repeat

29 if lTotalChecked < lnPoints*lnPoints then

30 put "Some points are unreachable"&CR&"("&(lIterations-1)&" iterations)" into field "Diagnostic"

31 set the textcolor of field "Diagnostic" to red

32 else

33 put "All points are reachable"&CR&"("&(lIterations-1)&" iterations)" into field "Diagnostic"

34 set the textcolor of field "Diagnostic" to green

35 end if

36end MouseUp

In line 6 we did the initial setup of the reachables table using the switch descriptions..  In line 7 we keep that original setup so we can display the original check marks differently from the added ones, and do the iterations on a copy.

Line 8 does the work.

Lines 9 to 12 prepare the display, which we will do in field "Reachable".

Lines 13 to 18 just put a mark in the field's cells that correspond to a reachable element in the table.  Line 17 counts the total number of marks we place.  They will by default be coloured black.  Then in 21 to 28 we use the first setup to colour the original marks red and make them bold.

Line 29 checks if the number of marks placed is equal to the total number of cells in the table (nxn) and puts a corresponding message in the diagnostics field.

The resulting display (after clicking the Compute button) looks like:

switches field
the diagnosis

For the layout:

A1 A2 B2 C2 B1 C1 2 1

we get:

switches field
a more pleasing diagnosis

Polishing

There is much left that we can do to improve this program.  We should use the cards of the stack so we can put more than one layout definition into it;  we should be able to past an image of the layouts onto the cards;  we should make the table larger or let it scroll, and it should have legends on its sides.

I have implemented much of that, and its code is available for inspection.

switches field
a more polished version of the program

I just still do not know why it always finds the solution after only two iterations.

There is a remark to make though.  We compute the next iteration of the matrix in a peculiar way:  we start from the previous state, but progressively we use the new state.  For example, suppose that the first line of the new state has several new cells filled in, and the second line refers to the first line.  We would normally change the second line by looking only at the first line in the previous matrix, but the program uses the new state, in which some cells have already been changed.  This is not an error, but it does give an advantage:  for filling information in the second line we are already using information from the first that we have just computed.  If you write a version of the multiplication that uses only the previous state, it will take more than two iterations before the matrix stops changing!