I’ve been doing the Weekly
Challenges. The
latest
involved permutations and array recursion. (Note that this ends
today.)
Task 1: Beautiful Arrangement
You are given a positive integer, $int.
Write a script to return the number of beautiful arrangements that you can construct.
A permutation of n integers, 1-indexed, is considered a beautiful arrangement
if for every i (1 <= i <= n) either of the following is true:
- 
perm[i] is divisible by i 
- 
i is divisible by perm[i] 
The sequence is at the OEIS but there
doesn't appear to any short-cut to calculating it. But I got a factor
of about 100 speed-up in going from the initial naive version (using
permutations) to the rather more ferociously optimised final code. For
a start, there are no divisions needed.
Crystal:
def beautifularrangement(a)
Generate the modulus table. Start with every entry false. (Some
languages make this hard by duplicating the inner object when
initialising an array.)
  precalc = Array.new(a + 1) { Array.new(a + 1, false) }
Step through one axis.
  1.upto(a) do |i|
Step through the other generating the divisible entries, and set them
to true in both directions.
    i.step(to: a, by: i) do |j|
      precalc[i][j] = true
      precalc[j][i] = true
    end
  end
Initialise the count, and the DFS stack. Each entry in the stack is a
list of the remaining numbers.
  ct = 0
  stack = Array(Array(Int32)).new
  stack.push((1 .. a).to_a)
  while stack.size > 0
    trail = stack.pop
If there's only one number left (and we wouldn't get here if it hadn't
been tested), increment the count.
    if trail.size == 1
      ct += 1
    else
Work out the place value of the next number.
      p = a - trail.size + 2
For each candidate,
      trail.each do |i|
If it's valid here,
        if precalc[i][p]
Generate a list of the candidates without it,
          tt = trail.select{|x| x != i}
And push them to the stack.
          stack.push(tt)
        end
      end
    end
  end
  ct
end
This lets me take great short-cuts: if 2 doesn't fit in position 3,
that whole branch can be abandoned rather than worked out in detail.
This seemed like a decent benchmarking challenge since I implemented
essentially the same compute-intensive code in each language, Tests
are for values of 1, 2, 10 and 20, in sequence. This time I gathered
maximum memory use too.
| Language | time/s | RSS/M | 
| Rust (release) | 1.88 | 3.0 | 
| JavaScript | 2.28 | 63.0 | 
| Crystal (release) | 2.45 | 5.3 | 
| Kotlin | 2.79 | 203.3 | 
| Crystal (debug) | 9.62 | 7.2 | 
| Rust (debug) | 13.68 | 3.0 | 
| Python | 15.24 | 13.5 | 
| Scala* | 17.46 | 322.3 | 
| Perl | 26.26 | 13.7 | 
| PostScript | 41.88 | 30.8 | 
| Lua | 41.90 | 2.9 | 
| Ruby | 63.51 | 18.4 | 
| Raku | 300.61 | 178.7 | 
* (I wasn't able to get the -savecompiled option to work with my
copy of the Scala compiler, so figures for that include compilation
time and memory.)
The genuinely compiled languages have a bit of competition, especially
from JavaScript (Node 18.19.0) and Kotlin, but they're vastly more
memory-efficient without the need for an interpretation environment.
(And Lua uses least of all, since it's designed for embedding.) And oh
dear Raku. I mean, there are plenty of good things about the language,
but.
Task 2: Nested Array
You are given an array of integers, @ints of length n containing
permutation of the numbers in the range [0, n - 1].
Write a script to build a set, set[i] = ints[i], ints[ints[i]], ints[ints[ints[i]]], ..., subjected to the following rules:
- 
The first element in set[i] starts with the selection of elements
ints[i]. 
- 
The next element in set[i] should be ints[ints[i]], and then
ints[ints[ints[i]]], and so on. 
- 
We stop adding right before a duplicate element occurs in
set[i].Return the longest length of a setset[i].
 
This was altogether more straightforward, since we don't actually need
to retain the sequences in full. Perl:
sub nestedarray($a) {
  my $arr = 0;
Iterate over each possible starting value.
  foreach my $i (0 .. scalar @{$a} - 1) {
Initialise the empty sequence.
    my %trail;
    my $j = $i;
Loop infinitely.
    while (1) {
Take one step through the sequence.
      $j = $a->[$j];
Ir we've seen this value before, exit.
      if (exists $trail{$j}) {
        last;
      }
Otherwise, set the seen-flag for this number. (In most languages I can
use a hash-set for this.)
      $trail{$j} = 1;
    }
Increase the maximum sequence length if needed.
    $arr = max($arr, scalar keys %trail);
  }
  $arr;
}
Full code on
github.
Comments on this post are now closed. If you have particular grounds for adding a late comment, comment on a more recent post quoting the URL of this one.