I’ve been doing the Perl Weekly
Challenges. The
latest
involved another numerical sequence and permutations. (Note that this
is open until 25 April 2021.)
TASK #1 › Chowla Numbers
Write a script to generate first 20 Chowla Numbers, named after
Sarvadaman D. S. Chowla, a London born Indian American
mathematician. It is defined as:
C(n) = (sum of divisors of n) - 1 - n
"Divisors" appears to mean what I'd normally call "all factors" (as
distinct from "prime factors"); I think this may be a difference
between British and American English, or possibly a change in maths
teaching over time. So anyway this is OEIS
A048050… and I have now written some Perl
code which uses an HTTP client library simply to fetch and parse an
OEIS page, but let's do this the proper way.
To get the divisors of a number, I can either prime factorise it and
then build up combinations of those, or just trial-divide. Since we
don't need to consider the factor of n
that is n
, the easiest
(though certainly not the most efficient) approach is to trial-divide,
or more to the point trial-modulus, from 2
up to n/2
(rounded
down).
Thus Perl:
sub chowla {
my $count=shift;
my @a;
foreach my $n (1..$count) {
push @a,sum(0,grep {$n % $_ == 0} (2..int($n/2)));
}
return \@a;
}
That initial "0" is because the sum of an empty list is, quite
properly, undefined.
Raku looks the same, but I thought I'd try out take
/gather
.
sub chowla($count) {
my @a;
for 1..$count -> $n {
push @a,sum(gather {
take 0;
for (2..floor($n/2)) {
take $_ if $n %% $_
}
});
}
return @a;
}
The sympy
library for Python has a divisors method, but I already
have the code and I'm trying to avoid external dependencies. Ruby's
grep
is called select
and I must try to remember this. In Rust I
made the loops explicit.
fn chowla(count: usize) -> Vec<usize> {
let mut a: Vec<usize>=vec![];
for n in 1..=count {
let mut s=0;
for i in 2..=n/2 {
if n % i == 0 {
s += i
}
}
a.push(s);
}
return a;
}
There's another approach which I thought of after I'd written this
code. If one were building the sequence at greater length and trying
to minimise execution time, I suspect that a Sieve of
Eratosthenes-like approach would make sense: have a running total for
each target number, and add the divisors to each total in turn, so 2
would be added to the totals for 4, 6, 8, …; then 3 to the totals for
6, 9, 12, …; and so on. The output sequence is just the list of
totals. That would avoid all the modulus operations; it still looks
like O(N^2), but it shouldn't particularly use more memory either.
I've coded that as well, with this drop-in replacement function:
sub chowla_sieve {
my $count=shift;
my @a=(0) x $count;
foreach my $n (2..int($count/2)) {
for (my $k=$n*2;$k<=$count;$k+=$n) {
$a[$k-1]+=$n;
}
}
return \@a;
}
With the first-20 problem as presented it speeds things up only by a
factor of about 2.6 (from 30-odd to 12-odd microseconds on my test
machine). First 100 is speeded up by a factor of 6.5, first 1,000 by
about 40, and first 10,000 by about 270.
TASK #2 › Four Squares Puzzle
You are given four squares as below with numbers named a,b,c,d,e,f,g.
Write a script to place the given unique numbers in the square box
so that sum of numbers in each box is the same.
I won't duplicate the ASCII art here. This comes down to: arrange
seven numbers into order a..g
such that
a + b == b + c + d
b + c + d == d + e + f
d + e + f == f + g
One could probably deploy some cleverness in solving the system of
equations to reduce the search space, but I didn't bother: I ran a
simple permutation and tested the results.
In Perl that meant Algorithm::Permute
; that's breaking my usual
guideline about avoiding the use of non-core modules, but I've written
permutor code before (for the Weekly Challenge, even) and I couldn't
be bothered to do it again. One hiccup: because of the way the code
block is enclosed, I can't abandon the permutation when a solution is
found as I can in the other languages.
(Since there are 8 possible solutions for the test case 1..7, and
there will clearly be at least 2 for any case which has a solution at
all (which I think is probably all cases), testing becomes mildly
challenging.)
The notional variables $a
to $d
represent each box sum; they're
calculated only as needed and assigned to actual variables only if
they'll be used more than once, so only $b
and $c
actually exist.
sub foursquare {
my @in=@_;
my @sol;
Algorithm::Permute::permute {
my $b=$in[1]+$in[2]+$in[3];
if ($in[0]+$in[1]==$b) {
my $c=$in[3]+$in[4]+$in[5];
if ($b==$c && $c == $in[5]+$in[6]) {
@sol=@in;
}
}
} @in;
return \@sol;
}
Raku has the permutations
method, and we can bail out of that
iterator when we get a result.
sub foursquare(@in) {
my @sol;
for @in.permutations -> @t {
my $b=@t[1]+@t[2]+@t[3];
if (@t[0]+@t[1]==$b) {
my $c=@t[3]+@t[4]+@t[5];
if ($b==$c && $c == @t[5]+@t[6]) {
@sol=@t;
last;
}
}
}
return @sol;
}
Python has permutations
in itertools
which is a core module, and
Ruby has permutation
in the core language. Rust doesn't permute in
core, but offers the Permutohedron
crate, which insists on modifying
the array in place; that's doubtless useful if you're doing
computationally challenging stuff where performance is relevant, but
not really important here.
This week felt to me like a return to form after some recent
challenges that weren't particularly enjoyable. Thanks to Mohammed,
who does this largely single-handedly. (Send him problems! I'm working
on some of my own.)
Full code on
github.