blog/posts/dyalog-apl-competition-2020-phase-2.org

12 KiB
Raw Blame History

— title: "Dyalog APL Problem Solving Competition 2020 — Phase II" subtitle: "Annotated Solutions" date: 2020-08-02 toc: true —

Introduction

After Phase I, here are my solutions to Phase II problems. The full code is included in the post, but everything is also available on GitHub.

A PDF of the problems descriptions is available on the competition website, or directly from my GitHub repo.

The submission guidelines gave a template where everything is defined in a Contest2020.Problems Namespace. I kept the default values for ⎕IO and ⎕ML because the problems were not particularly easier with ⎕IO←0.

  :Namespace Contest2020

	  :Namespace Problems
		  (⎕IO ⎕ML ⎕WX)←1 1 3

This post is still a work in progress! I will try to write explanations for every problem below.

Problem 1 Take a Dive

  ∇ score←dd DiveScore scores
    :If 7=≢scores
	    scores←scores[¯2↓2↓⍋scores]
    :ElseIf 5=≢scores
	    scores←scores[¯1↓1↓⍋scores]
    :Else
	    scores←scores
    :EndIf
    score←2(⍎⍕)dd×+/scores
  ∇

This is a very straightforward implementation of the algorithm describe in the problem description. I decided to switch explicitly on the size of the input vector because I feel it is more natural. For the cases with 5 or 7 judges, we use Drop () to remove the lowest and highest scores.

At the end, we sum up the scores with +/ and multiply them by dd. The last operation, 2(⍎⍕), is a train using Format (Dyadic) to round to 2 decimal places, and Execute to get actual numbers and not strings.

Problem 2 Another Step in the Proper Direction

  ∇ steps←{p}Steps fromTo;segments;width
    width←|-/fromTo
    :If 0=⎕NC'p' ⍝ No left argument: same as Problem 5 of Phase I
	    segments←0,width
    :ElseIf p<0 ⍝ -⌊p is the number of equally-sized steps to take
	    segments←(-⌊p){0,⍵×⍺÷⍨⍳⍺}width
    :ElseIf p>0 ⍝ p is the step size
	    segments←p{⍵⌊×0,⍳⌈⍵÷⍺}width
    :ElseIf p=0 ⍝ As if we took zero step
	    segments←0
    :EndIf
    ⍝ Take into account the start point and the direction.
    steps←fromTo{(⊃⍺)+(-×-/)×⍵}segments
  ∇

This is an extension to Problem 5 of Phase I. In each case, we compute the "segments", i.e., the steps starting from 0. In a last step, common to all cases, we add the correct starting point and correct the direction if need be.

To compute equally-sized steps, we first divide the segment $[0, 1]$ in p equal segments with (p)÷p. This subdivision can then be multiplied by the width to obtain the required segments.

When p is the step size, we just divide the width by the step size (rounded to the next largest integer) to get the required number of segments. If the last segment is too large, we "crop" it to the width with Minimum ().

Problem 3 Past Tasks Blast

  ∇ urls←PastTasks url;r;paths
    r←HttpCommand.Get url
    paths←('[a-zA-Z0-9_/]+\.pdf'⎕S'&')r.Data
    urls←('https://www.dyalog.com/'∘,)¨paths
  ∇

I decided to use HttpCommand for this task, since it is simply one ]load HttpCommand away and should be platform-independent.

Parsing XML is not something I consider "fun" in the best of cases, and I feel like APL is not the best language to do this kind of thing. Given how simple the task is, I just decided to find the relevant bits with a regular expression using Replace and Search (⎕S).

After finding all the strings vaguely resembling a PDF file name (only alphanumeric characters and underscores, with a .pdf extension), I just concatenate them to the base URL of the Dyalog domain.

Problem 4 Bioinformatics

The first task can be solved by decomposing it into several functions.

  ⍝ Test if a DNA string is a reverse palindrome.
  isrevp←{⍵≡⌽'TAGC'['ATCG'⍳⍵]}

First, we compute the complement of a DNA string (using simple indexing) and test if its Reverse () is equal to the original string.

  ⍝ Generate all subarrays (position, length) pairs, for 4 ≤ length ≤ 12.
  subarrays←{⊃,/(⍳⍵),¨¨3↓¨¨12⌊1+⍵-⍳⍵}

We first compute all the possible lengths for each starting point. For instance, the last element cannot have any (position, length) pair associated to it, because there is no three element following it. So we crop the possible lengths to $[3, 12]$. For instance for an array of size 10:

        {3↓¨¨12⌊1+⍵-⍳⍵}10
┌──────────────┬───────────┬─────────┬───────┬─────┬───┬─┬┬┬┐
│4 5 6 7 8 9 10│4 5 6 7 8 9│4 5 6 7 8│4 5 6 7│4 5 6│4 5│4││││
└──────────────┴───────────┴─────────┴───────┴─────┴───┴─┴┴┴┘

Then, we just add the corresponding starting position to each length (1 for the first block, 2 for the second, and so on). Finally, we flatten everything.

  ∇ r←revp dna;positions
    positions←subarraysdna
    ⍝ Filter subarrays which are reverse palindromes.
    r←↑({isrevp dna[¯1+⍵[1]+⍳⍵[2]]}¨positions)/positions
  ∇

For each possible (position, length) pair, we get the corresponding DNA substring with dna[¯1+⍵[1]+⍳⍵[2]] (adding ¯1 is necessary because ⎕IO←1). We test if this substring is a reverse palindrome using isrevp above. Replicate (/) then selects only the (position, length) pairs for which the substring is a reverse palindrome.

The second task is just about counting the number of subsets modulo 1,000,000. So we just need to compute $2^n \mod 1000000$ for any positive integer $n\leq1000$.

  sset←{((1E6|2∘×)⍣⍵)1}

Since we cannot just compute $2^n$ directly and take the remainder, we use modular arithmetic to stay mod 1,000,000 during the whole computation. The dfn (1E6|2∘×) doubles its argument mod 1,000,000. So we just apply this function $n$ times using the Power operator (), with an initial value of 1.

Problem 5 Future and Present Value

  ⍝ First solution: ((1+⊢)⊥⊣) computes the total return
  ⍝ for a vector of amounts  and a vector of rates
  ⍝ ⍵. It is applied to every prefix subarray of amounts
  ⍝ and rates to get all intermediate values. However,
  ⍝ this has quadratic complexity.
  ⍝ rr←(,\⊣)((1+⊢)⊥⊣)¨(,\⊢)

  ⍝ Second solution: We want to be able to use the
  ⍝ recurrence relation (recur) and scan through the
  ⍝ vectors of amounts and rates, accumulating the total
  ⍝ value at every time step. However, APL evaluation is
  ⍝ right-associative, so a simple Scan
  ⍝ (recur\amounts,¨values) would not give the correct
  ⍝ result, since recur is not associative and we need
  ⍝ to evaluate it left-to-right. (In any case, in this
  ⍝ case, Scan would have quadratic complexity, so would
  ⍝ not bring any benefit over the previous solution.)
  ⍝ What we need is something akin to Haskell's scanl
  ⍝ function, which would evaluate left to right in O(n)
  ⍝ time. This is what we do here, accumulating values
  ⍝ from left to right. (This is inspired from
  ⍝ dfns.ascan, although heavily simplified.)
  rr←{recur←{⍵[1]+×1+⍵[2]} ⋄ 1↓⌽⊃{(⊂(⊃⍵)recur),⍵}/⌽⍺,¨⍵}
  ⍝ Simply apply the formula for cashflow calculations.
  pv←{+/⍺÷×\1+⍵}

Problem 6 Merge

  ∇ val←ns getval var
    :If ''≡var ⍝ literal '@'
	    val←'@'
    :ElseIf (⊂var)∊ns.⎕NL ¯2
	    val←⍕ns⍎var
    :Else
	    val←'???'
    :EndIf
  ∇
  ∇ text←templateFile Merge jsonFile;template;ns
    template←⊃⎕NGET templateFile 1
    ns←⎕JSON⊃⎕NGET jsonFile
    ⍝ We use a simple regex search and replace on the
    ⍝ template.
    text←↑('@[a-zA-Z]*@'⎕R{ns getval ¯1↓1↓⍵.Match})template
  ∇

Problem 7 UPC

  CheckDigit←{10|-⍵+.×113 1}
  ⍝ Left and right representations of digits. Decoding
  ⍝ the binary representation from decimal is more
  ⍝ compact than writing everything explicitly.
  lrepr←⍉(72)13 25 19 61 35 49 47 59 55 11
  rrepr←~¨lrepr
  ∇ bits←WriteUPC digits;left;right
    :If (11=≢digits)∧∧/digits∊0,9
	    left←,lrepr[1+6↑digits;]
	    right←,rrepr[1+6↓digits,CheckDigit digits;]
	    bits←1 0 1,left,0 1 0 1 0,right,1 0 1
    :Else
	    bits←¯1
    :EndIf
  ∇
  ∇ digits←ReadUPC bits
    :If 95≠bits ⍝ incorrect number of bits
	    digits←¯1
    :Else
	    ⍝ Test if the barcode was scanned right-to-left.
	    :If 0=2|+/bits[3+7]
		    bits←⌽bits
	    :EndIf
	    digits←({¯1+lrepr⍵}¨(7/6)⊆42↑3↓bits),{¯1+rrepr⍵}¨(7/6)⊆¯42↑¯3↓bits
	    :If ~∧/digits∊0,9 ⍝ incorrect parity
		    digits←¯1
	    :ElseIf (⊃⌽digits)≠CheckDigit ¯1↓digits ⍝ incorrect check digit
		    digits←¯1
	    :EndIf
    :EndIf
  ∇

Problem 8 Balancing the Scales

  ∇ parts←Balance nums;subsets;partitions
    ⍝ This is a brute force solution, running in
    ⍝ exponential time. We generate all the possible
    ⍝ partitions, filter out those which are not
    ⍝ balanced, and return the first matching one. There
    ⍝ are more advanced approach running in
    ⍝ pseudo-polynomial time (based on dynamic
    ⍝ programming, see the "Partition problem" Wikipedia
    ⍝ page), but they are not warranted here, as the
    ⍝ input size remains fairly small.

    ⍝ Generate all partitions of a vector of a given
    ⍝ size, as binary mask vectors.
    subsets←{1↓2⊥⍣¯12*⍵}
    ⍝ Keep only the subsets whose sum is exactly
    ⍝ (+/nums)÷2.
    partitions←nums{((2÷⍨+/)=+.×⍵)/⍵}subsetsnums
    :If 0=≢,partitions
	    ⍝ If no partition satisfy the above
	    ⍝ criterion, we return ⍬.
	    parts←⍬
    :Else
	    ⍝ Otherwise, we return the first possible
	    ⍝ partition.
	    parts←nums{((⊂,(⊂~))⊃↓⍉⍵)/¨2}partitions
    :EndIf
  ∇

Problem 9 Upwardly Mobile

  ∇ weights←Weights filename;mobile;branches;mat
    ⍝ Put your code and comments below here

    ⍝ Parse the mobile input file.
    mobile←↑⊃⎕NGET filename 1
    branches←⍸mobile∊'┌┴┐'
    ⍝ TODO: Build the matrix of coefficients mat.

    ⍝ Solve the system of equations (arbitrarily setting
    ⍝ the first variable at 1 because the system is
    ⍝ overdetermined), then multiply the coefficients by
    ⍝ their least common multiple to get the smallest
    ⍝ integer weights.
    weights←((1∘,)×(∧/÷))mat[;1]⌹1↓[2]mat
  ∇
	  :EndNamespace
  :EndNamespace