This post is part of a series on Mohammad Anwar’s excellent Weekly Challenge, where hackers submit solutions in Perl, Raku, or any other language, to two different challenges every week. (It’s a lot of fun, if you’re into that sort of thing.)
Task 1 › Large Numbers
The first task this week has us taking a list of numbers and finding the permutation whose concatenation gives the largest number. For example: largest(20,3) = 320
.
At first, I thought I might get away with a greedy join('', reverse sort @ints)
but the greedy approach doesn’t work in this case. For example, join('', reverse sort(5, 50, 51))
would give 51505, but the correct answer is 55150. So, a more deliberate approach is needed. However, see the update below this section!
What I settled on is a relatively simple recursive algorithm that starts with the full list of @ints
, sort that alphanumerically (in reverse order), and will then recurse for each integer that starts with the highest remaining digit. Clear as mud? Here’s a worked example:
largest(5 50 51):
$sub->('' => reverse sort(5 50 51)) # 51 50 5
$sub->(51 => 50 5)
$sub->(5150 => 5) = 51505 # new max
$sub->(515 => 50) = 51550 # new max
$sub->(50 => 51 5)
$sub->(5051 => 5) = 50515
$sub->(505 => 51) = 50551
$sub->(5 => 51 50)
$sub->(551 => 50) = 55150 # new max
$sub->(550 => 51) = 55051
The maximum value is kept in a closure and returned by the parent function.
Here is the complete subroutine:
sub largest(@ints) {
my $sub = sub {
my $num = shift;
return $num.($_[0]//0) if @_ <= 1;
my $max_val = 0; # Our candidate answer
# Loop until current number starts with a lower digit than previous
for my $i (0..$#_) {
last if $i > 0 and substr($_[$i], 0, 1) ne substr($_[0], 0, 1);
my @new_ints = @_;
splice @new_ints, $i, 1; # Remove $i-th element
my $val = __SUB__->($num.$_[$i], @new_ints);
$max_val = length($val) > length($max_val) ? $val
: $val gt $max_val ? $val : $max_val;
}
$max_val;
};
return $sub->('',reverse sort @ints);
}
2025-01-04: Updated to use a stringwise numerical max calculation, which handles large numbers better.
I also got this great suggestion from long-time contributor wanderdoc in the comments below this post, which bears highlighting. His version is as follows:
sub largest(@ints) { join '', sort { $b.$a <=> $a.$b } @ints }
It seems my Holiday brain didn’t take my initial hunch of join '', sort { ... }
quite far enough! This is exactly the kind of concise solution I like, and even handles large results just fine, so long as no pair of numbers in @ints
concatenate into a value larger than the maximum integer.
Task 2 › Hamming Distance
Hamming distance calculations are a simple form of error detection. It measures the number of changes needed to turn one value into another. With integers x and y (represented in binary), the Hamming distance is simply the number of bits that differ between the two numbers. Note that x ^ b
(XOR or exclusive or) will give us a 1 for any bit that differs, so we simply count the 1s in x ^ b
. For example:
x = 15 = 1111b
y = 4 = 0100b
----------------
x^y = 11 = 1011b -> 3 1s, so hamming(15,4) = 3
In Perl, the Hamming distance between two numbers can therefore be implemented simply:
sub hamming($x,$y) { ()= sprintf('%b', $x ^ $y) =~ /1/g }
If you are squinting at the ()=
in that expression, then let me introduce you to the =()=
operator in Perl!
You can see I’m converting $x ^ $y
to binary, and then running it through a simple regex, /1/g
. The /g
makes the regex return all of the matches, but we’re not interested in getting a bunch of 1s; we want to know how many matches there were. So the ()=
puts the expression into list context, and then back into scalar context.
If you instead wanted to assign the result to a variable, you could write it as follows:
my $hamming_distance =()= sprintf('%b', $x ^ $y) =~ /1/g
That, of course, only calculates the Hamming distance between two numbers, whereas the task asks us to calculate the Hamming distance between all numbers, and sum the result. Why? I have no idea. I’m not aware of any good reason to do this, except that it’s a fun bit of recreational coding.
sub hamming_multi(@ints) {
my $dist = 0;
for my $i (0..$#ints-1) {
$dist += hamming($ints[$i], $_) for @ints[$i+1..$#ints];
}
$dist;
}
As you can see, a simple nested loop does the trick.
Going faster: C implementation
The gcc compiler provides access to the __builtin_popcount()
builtin, which can often calculate a single Hamming distance extremely quickly. Most modern CPUs have some kind of “popcount” instruction that counts the number of 1s in an integer, in a single instruction. On x86 for example, the POPCNT
instruction does all of the heavy lifting, and __builtin_popcount()
will use this platform-specific instruction (or one like it) if available. So our C function is simply:
int hamming(int x, int y) {
return __builtin_popcount(x ^ y);
}
The hamming_multi()
instruction is very similar to the Perl version:
int hamming_multi(int count, int ints[]) {
int dist = 0; // Total Hamming distance
for (int i = 0; i < count - 1; i++)
for (int j = i + 1; j < count; j++)
dist += hamming(ints[i], ints[j]);
return dist;
}
x = 15 = 1111b
y = 8 = 0100b
—————-
x^y = 11 = 1011b -> 3 1s, so hamming(15,8) = 3
=================
except that 0100b is 4, not 8.
x = 15 = 1111b
y = 8 = 1000b
—————-
x^y = 7 = 0111b -> 3 1s, so hamming(15,8) = 3
Indeed. Thanks for the catch!
return join(”, sort { $b.$a $a.$b } @_); returns 55150 with (5, 50, 51).
Hey wanderdoc! Good to hear from you. That’s a lovely way to solve the problem. And to top it off, while checking it against my test cases, I also found that a couple of my test cases were wrong, and my code doesn’t handle large numbers as well as I thought it did. Dang. I blame it on being hopped up on turkey, sage, and cranberry sauce. 🙂
Hi, I am happy you found it useful 🙂 I learned a lot from you.