Beauty and the nest
Weekly challenge 300 — 16 December 2024
Week 300: 16 Dec 2024
You are given a positive integer, $n
.
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 .. $n)
either of the following is true:
$perm[$i]
is divisible by $i
$i
is divisible by $perm[!i]
Example 1 Input: $n = 2 Output: 2 1st arrangement: [1, 2] perm[1] is divisible by i = 1 perm[2] is divisible by i = 2 2nd arrangement: [2, 1] perm[1] is divisible by i = 1 i=2 is divisible by perm[2] = 1 Example 2 Input: $n = 1 Output: 1 Example 3 Input: $n = 10 Output: 700
The obvious way to meet this challege is
my solution 1, which simply takes all the permutations of
1 .. $n
and tests them for being beautiful. It comes up with the same
answer as the provided example for $n == 10
in 25 seconds.
For $n
in the range 0-8 it takes less than a second,
for $n == 9
it takes 3 seconds and for $n == 11
it takes 277 seconds - nearly 5 minutes.
In my solution I have included a couple of optimisations:
$i == 1 or $i == $perm[$i]
it is not necessary to
apply the two conditions, and
$i
is greater or less than $perm[$i]
.
But even so, its execution time rises rather alarmingly
as $n
increases.
So is there a faster way? I can't immediately think of a way that doen't involve checking every permutation, but I can make a modest improvement - my solution 2.
The basis of solution 2 is that for each $i
there are only a
few possible values of $perm[$i]
, which are:
$i
$i
which are <= $n
$i
including 1So for example for $n == 10
the only beautiful
possibilities are:
$i | $perm[$i] |
1 | 1 .. 10 |
2 | 1, 2, 4, 6, 8, 10 |
3 | 1, 3, 6, 9 |
4 | 1, 2, 4, 8 |
5 | 1, 5, 10 |
6 | 1, 2, 3, 6 |
7 | 1, 7 |
8 | 1, 2, 4, 8 |
9 | 1, 3, 9 |
10 | 1, 2, 5, 10 |
So when checking a permutation, let's start by checking
$perm[7]
, because that has only two possible values -
1 or 7 - so 80% of perms can be discarded without looking
at any further components of the perm.
My solution 2 therefore creates an order for checking
the beautifulness of each perm by checking the components
in the increasing order of the number of beautiful
possibilities: so for$n == 10
it checks in the order
7, 5, 9, 3, 4, 6, 8, 10, 2. It doesn't need to check 1,
because any number is divisible by 1 and 1 can therefore
appear anywhere in @perm
, and it doesn't need to
check the elements where $perm[$i] == $i
.
I had rather hoped for a substantial increase in speed by doing that, but in fact it only reduces the time by around 10%. I am guessing that's because the generation of the perms is what takes the bulk of the time.
#!/usr/bin/perl # Blog: http://ccgi.campbellsmiths.force9.co.uk/challenge use v5.26; # The Weekly Challenge - 2024-12-16 use utf8; # Week 300 - task 1 - Beautiful arrangement use warnings; # Peter Campbell Smith use Algorithm::Combinatorics 'permutations'; binmode STDOUT, ':utf8'; for (2 .. 11) { beautiful_arrangement1($_); beautiful_arrangement2($_); } sub beautiful_arrangement1 { my ($n, @array, $iter, $next, $i, $start, @order, @possible, $beautiful); # initialise $n = shift; $beautiful = 0; $start = time(); push(@array, $_) for 1 .. $n; # loop over perms $iter = permutations(\@array); PERM: while ($next = $iter->next) { for $i (1 .. $n) { # test for ugliness unless ($i == $next->[$i - 1]) { next PERM unless $i > $next->[$i - 1] ? $i % $next->[$i - 1] == 0 : $next->[$i - 1] % $i == 0; } } # not ugly $beautiful ++; } say qq[\nInput: \$n = $n]; say qq[Output1: $beautiful (] . (time() - $start) . qq[ secs)]; } sub beautiful_arrangement2 { my ($i, $j, $k, $start, @count, $n, $iter, $next, $m, $beautiful, @possible, @order, @array); # initialise $n = shift; $beautiful = 0; $start = time(); # find possible values of each $array[$i] for $i (1 .. $n) { $possible[$i] = '~'; for $j (1 .. 999) { # multiples if ($i * $j <= $n) { $possible[$i] .= $i * $j . '~'; } # divisors if ($j > 1 and $i / $j >= 1 and $i % $j == 0) { $possible[$i] .= $i / $j . '~'; } } } # $count[$i] is the no of possible values at $array[$i] for $i (1 .. $n) { $count[$i] = ($possible[$i] =~ s|~|~|g) - 1; } # @order is the hardest to easiest order to fill for $i (1 .. $n) { for $j (1 .. $n) { if ($count[$j] == $i) { push @order, $j; } } } # now start as before push(@array, $_) for 1 .. $n; # loop over perms $iter = permutations(\@array); PERM: while ($next = $iter->next) { for $k (0 .. $n - 1) { # test for ugliness in order of probability $i = $order[$k]; $m = $next->[$i - 1]; unless ($i == 1 or $i == $m) { next PERM unless $i > $m ? $i % $m == 0 : $m % $i == 0; } } # not ugly $beautiful ++; } say qq[Output2: $beautiful (] . (time() - $start) . qq[ secs)]; }
Input: $n = 2 Output1: 2 (0 secs) Output2: 2 (0 secs) Input: $n = 3 Output1: 3 (0 secs) Output2: 3 (0 secs) Input: $n = 4 Output1: 8 (0 secs) Output2: 8 (0 secs) Input: $n = 5 Output1: 10 (0 secs) Output2: 10 (0 secs) Input: $n = 6 Output1: 36 (0 secs) Output2: 36 (0 secs) Input: $n = 7 Output1: 41 (0 secs) Output2: 41 (0 secs) Input: $n = 8 Output1: 132 (0 secs) Output2: 132 (0 secs) Input: $n = 9 Output1: 250 (3 secs) Output2: 250 (2 secs) Input: $n = 10 Output1: 700 (25 secs) Output2: 700 (23 secs) Input: $n = 11 Output1: 750 (277 secs) Output2: 750 (255 secs)
Any content of this website which has been created by Peter Campbell Smith is in the public domain