Peter’s blog ✴ Week 300 ✴ 16 December 2024
THE WEEKLY CHALLENGE
Beauty and the nest
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.
Happy to see the optimisation and discussion around it. Thanks for being the most consistent contributor.
#!/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)]; }
45 lines of code
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