Dyalog APL Problem Solving Competition 2020 — Phase I

Annotated Solutions

Introduction

I’ve always been quite fond of APL and its “array-oriented” approach of programmingSee my previous post on simulating the Ising model with APL. It also contains more background on APL.

. Every year, Dyalog (the company behind probably the most popular APL implementation) organises a competition with various challenges in APL.

The Dyalog APL Problem Solving Competition consists of two phases:

In 2018, I participated in the competition, entering only Phase ISince I was a student at the time, I was eligible for a prize, and I won $100 for a 10-line submission, which is quite good!

(my solutions are on GitHub). This year, I entered in both phases. I explain my solutions to Phase I in this post. Another post will contain annotated solutions for Phase II problems.

The full code for my submission is on GitHub at dlozeve/apl-competition-2020, but everything is reproduced in this post.

1. Let’s Split!

Write a function that, given a right argument Y which is a scalar or a non-empty vector and a left argument X which is a single non-zero integer so that its absolute value is less or equal to ≢Y, splits Y into a vector of two vectors according to X, as follows:

If X>0, the first vector contains the first X elements of Y and the second vector contains the remaining elements.

If X<0, the second vector contains the last |X elements of Y and the first vector contains the remaining elements.

Solution: (0>⊣)⌽((⊂↑),(⊂↓))

There are three nested trains hereTrains are nice to read (even if they are easy to abuse), and generally make for shorter dfns, which is better for Phase I.

. The first one, ((⊂↑),(⊂↓)), uses the two functions Take () and Drop () to build a nested array consisting of the two outputs we need. (Take and Drop already have the behaviour needed regarding negative arguments.) However, if the left argument is positive, the two arrays will not be in the correct order. So we need a way to reverse them if X<0.

The second train (0>⊣) will return 1 if its left argument is positive. From this, we can use Rotate () to correctly order the nested array, in the last train.

2. Character Building

UTF-8 encodes Unicode characters using 1-4 integers for each character. Dyalog APL includes a system function, ⎕UCS, that can convert characters into integers and integers into characters. The expression 'UTF-8'∘⎕UCS converts between characters and UTF-8.

Consider the following:

      'UTF-8'∘⎕UCS 'D¥⍺⌊○9'
68 194 165 226 141 186 226 140 138 226 151 139 57
      'UTF-8'∘⎕UCS 68 194 165 226 141 186 226 140 138 226 151 139 57
D¥⍺⌊○9

How many integers does each character use?

      'UTF-8'∘⎕UCS¨ 'D¥⍺⌊○9' ⍝ using ]Boxing on
┌──┬───────┬───────────┬───────────┬───────────┬──┐
│68│194 165│226 141 186│226 140 138│226 151 139│57│
└──┴───────┴───────────┴───────────┴───────────┴──┘      

The rule is that an integer in the range 128 to 191 (inclusive) continues the character of the previous integer (which may itself be a continuation). With that in mind, write a function that, given a right argument which is a simple integer vector representing valid UTF-8 text, encloses each sequence of integers that represent a single character, like the result of 'UTF-8'∘⎕UCS¨'UTF-8'∘⎕UCS but does not use any system functions (names beginning with )

Solution: {(~⍵∊127+⍳64)⊂⍵}

First, we build a binary array from the string, encoding each continuation character as 0, and all the others as 1. Next, we can use this binary array with Partitioned Enclose () to return the correct output.

3. Excel-lent Columns

A Microsoft Excel spreadsheet numbers its rows counting up from 1. However, Excel’s columns are labelled alphabetically — beginning with A–Z, then AA–AZ, BA–BZ, up to ZA–ZZ, then AAA–AAZ and so on.

Write a function that, given a right argument which is a character scalar or non-empty vector representing a valid character Excel column identifier between A and XFD, returns the corresponding column number

Solution: 26⊥⎕A∘⍳

We use the alphabet ⎕A and Index Of () to compute the index in the alphabet of every character. As a train, this can be done by (⎕A∘⍳). We then obtain an array of numbers, each representing a letter from 1 to 26. The Decode () function can then turn this base-26 number into the expected result.

4. Take a Leap

Write a function that, given a right argument which is an integer array of year numbers greater than or equal to 1752 and less than 4000, returns a result of the same shape as the right argument where 1 indicates that the corresponding year is a leap year (0 otherwise).

A leap year algorithm can be found here.

Solution: 1 3∊⍨(0+.=400 100 4∘.|⊢)

According to the algorithm, a year is a leap year in two situations:

The train (400 100 4∘.|⊢) will test if each year in the right argument is divisible by 400, 100, and 4, using an Outer Product. We then use an Inner Product to count how many times each year is divisible by one of these numbers. If the count is 1 or 3, it is a leap year. Note that we use Commute () to keep the dfn as a train, and to preserve the natural right-to-left reading of the algorithm.

5. Stepping in the Proper Direction

Write a function that, given a right argument of 2 integers, returns a vector of the integers from the first element of the right argument to the second, inclusively.

Solution: {(⊃⍵)+(-×-/⍵)×0,⍳|-/⍵}

First, we have to compute the range of the output, which is the absolute value of the difference between the two integers |-/⍵. From this, we compute the actual sequence, including zeroIf we had ⎕IO←0, we could have written ⍳|1+-/⍵, but this is the same number of characters.

: 0,⍳|-/⍵.

This sequence will always be nondecreasing, but we have to make it decreasing if needed, so we multiply it by the opposite of the sign of -/⍵. Finally, we just have to start the sequence at the first element of .

6. Please Move to the Front

Write a function that, given a right argument which is an integer vector and a left argument which is an integer scalar, reorders the right argument so any elements equal to the left argument come first while all other elements keep their order.

Solution: {⍵[⍋⍺≠⍵]}

⍺≠⍵ will return a binary vector marking as 0 all elements equal to the left argument. Using this index to sort in the usual way with Grade Up will return the expected result.

7. See You in a Bit

A common technique for encoding a set of on/off states is to use a value of \(2^n\) for the state in position \(n\) (origin 0), 1 if the state is “on” or 0 for “off” and then add the values. Dyalog APL’s component file permission codes are an example of this. For example, if you wanted to grant permissions for read (access code 1), append (access code 8) and rename (access code 128) then the resulting code would be 137 because that’s 1 + 8 + 128.

Write a function that, given a non-negative right argument which is an integer scalar representing the encoded state and a left argument which is an integer scalar representing the encoded state settings that you want to query, returns 1 if all of the codes in the left argument are found in the right argument (0 otherwise).

Solution: {f←⍸∘⌽(2∘⊥⍣¯1)⋄∧/(f⍺)∊f⍵}

The difficult part is to find the set of states for an integer. We need a function that will return 1 8 128 (or an equivalent representation) for an input of 137. To do this, we need the base-2 representations of \(137 = 1 + 8 + 128 = 2^0 + 2^3 + 2^7 = 10010001_2\). The function (2∘⊥⍣¯1) will return the base-2 representation of its argument, and by reversing and finding where the non-zero elements are, we find the correct exponents (1 3 7 in this case). That is what the function f does.

Next, we just need to check that all elements of f⍺ are also in f⍵.

8. Zigzag Numbers

A zigzag number is an integer in which the difference in magnitude of each pair of consecutive digits alternates from positive to negative or negative to positive.

Write a function that takes a single integer greater than or equal to 100 and less than 1015 as its right argument and returns a 1 if the integer is a zigzag number, 0 otherwise.

Solution: ∧/2=∘|2-/∘×2-/(10∘⊥⍣¯1)

First, we decompose a number into an array of digits, using (10∘⊥⍣¯1) (Decode () in base 10). Then, we Reduce N Wise to compute the difference between each pair of digits, take the sign, and ensure that the signs are indeed alternating.

9. Rise and Fall

Write a function that, given a right argument which is an integer scalar or vector, returns a 1 if the values of the right argument conform to the following pattern (0 otherwise):

  • The elements increase or stay the same until the “apex” (the highest value) is reached
  • After the apex, any remaining values decrease or remain the same

Solution: {∧/(⍳∘≢≡⍋)¨(⊂((⊢⍳⌈/)↑⊢),⍵),⊂⌽((⊢⍳⌈/)↓⊢),⍵}

How do we approach this? First we have to split the vector at the “apex”. The train (⊢⍳⌈/) will return the index of () the maximum element.

      (⊢⍳⌈/)1 3 3 4 5 2 1
5

Combined with Take () and Drop (), we build a two-element vector containing both parts, in ascending order (we Reverse () one of them). Note that we have to Ravel (,) the argument to avoid rank errors in Index Of.

      {(⊂((⊢⍳⌈/)↑⊢),⍵),⊂⌽((⊢⍳⌈/)↓⊢),⍵}1 3 3 4 5 2 1
┌─────────┬───┐
│1 3 3 4 5│1 2│
└─────────┴───┘

Next, (⍳∘≢≡⍋) on each of the two vectors will test if they are non-decreasing (i.e. if the ranks of all the elements correspond to a simple range from 1 to the size of the vector).

10. Stacking It Up

Write a function that takes as its right argument a vector of simple arrays of rank 2 or less (scalar, vector, or matrix). Each simple array will consist of either non-negative integers or printable ASCII characters. The function must return a simple character array that displays identically to what {⎕←⍵}¨ displays when applied to the right argument.

Solution: {↑⊃,/↓¨⍕¨⍵}

The first step is to Format () everything to get strings.A lot of trial-and-error is always necessary when dealing with nested arrays, and this being about formatting exacerbates the problem.

The next step would be to “stack everything vertically”, so we will need Mix () at some point. However, if we do it immediately we don’t get the correct result:

      {↑⍕¨⍵}(3 3⍴⍳9)(↑'Adam' 'Michael')
1 2 3  
4 5 6  
7 8 9  

Adam   
Michael

Mix is padding with spaces both horizontally (necessary as we want the output to be a simple array of characters) and vertically (not what we want). We will have to decompose everything line by line, and then mix all the lines together. This is exactly what SplitSplit is the dual of Mix.

() does:

      {↓¨⍕¨⍵}(3 3⍴⍳9)(↑'Adam' 'Michael')(⍳10) '*'(5 5⍴⍳25)
┌───────────────────┬─────────────────┬──────────────────────┬─┬───────────────
│┌─────┬─────┬─────┐│┌───────┬───────┐│┌────────────────────┐│*│┌──────────────
││1 2 3│4 5 6│7 8 9│││Adam   │Michael│││1 2 3 4 5 6 7 8 9 10││ ││ 1  2  3  4  5
│└─────┴─────┴─────┘│└───────┴───────┘│└────────────────────┘│ │└──────────────
└───────────────────┴─────────────────┴──────────────────────┴─┴───────────────

      ─────────────────────────────────────────────────────────────┐
      ┬──────────────┬──────────────┬──────────────┬──────────────┐│
      │ 6  7  8  9 10│11 12 13 14 15│16 17 18 19 20│21 22 23 24 25││
      ┴──────────────┴──────────────┴──────────────┴──────────────┘│
      ─────────────────────────────────────────────────────────────┘

Next, we clean this up with Ravel (,) and we can Mix to obtain the final result.