DEV Community

Bob Lied
Bob Lied

Posted on

PWC 238 Running and Persistence

I belong to a Facebook group called "Running after 60" (because, yes, I am that old, and yes, I run). There are some truly amazing stories in that group. Last month, there was a runner who tripped and broke his ankle on a 3:00am training run, two weeks before a marathon in the Utah mountains. He ran the marathon anyway. This past weekend, at the back of the pack in the Chicago marathon, was a runner who finished despite having three broken ribs. He had to finish because he didn't want his guides to be disqualified -- he's blind. Did I mention these people are over 60 years old?

So a little programming challenge is nothing. Let's do week 238!

Task 1 Running Sum

You are given an array of integers.

Write a script to return the running sum of the
given array. The running sum can be calculated as
sum[i] = num[0] + num[1] + ... + num[i].
Enter fullscreen mode Exit fullscreen mode
  • Example 1 Input: @int = (1, 2, 3, 4, 5) Output: (1, 3, 6, 10, 15)

  • Example 2 Input: @int = (1, 1, 1, 1, 1) Output: (1, 2, 3, 4, 5)

  • Example 3 Input: @int = (0, -1, 1, 2) Output: (0, -1, 0, 2)

Warmups

This is a straight-forward task. We could set up a loop over the elements and accumulate the sum, or we could do something more Perl-ish.

There is List::Util::reductions. It provides all the intermediate results from applying a block to consecutive pairs, leading to a single line solution:

use List::Util qw/reductions/;
say "(", join( ", ", reductions { $a + $b } @ARGV), ")";
Enter fullscreen mode Exit fullscreen mode

But what if the 'a' and 'b' keys on the keyboard were broken? How would we persist? How about shifting things off the input and pushing them onto a list of sums; it's almost a one-liner:

sub runningSum(@int)
{
    my @running;
    my $sum = 0;
    push @running, ($sum += shift @int) while @int;
    return \@running;
}
Enter fullscreen mode Exit fullscreen mode

Task 2 Persistence Sort

You are given an array of positive integers.

Write a script to sort the given array in increasing
order with respect to the count of steps required to
obtain a single-digit number by multiplying its digits
recursively for each array element. If any two numbers
have the same count of steps, then print the smaller
number first.
Enter fullscreen mode Exit fullscreen mode

Example 1

Input: @int = (15, 99, 1, 34)
Output: (1, 15, 34, 99)

15 => 1 x 5 => 5 (1 step)
99 => 9 x 9 => 81 => 8 x 1 => 8 (2 steps)
 1  => 0 step
34 => 3 x 4 => 12 => 1 x 2 => 2 (2 steps)
Enter fullscreen mode Exit fullscreen mode

Example 2

Input: @int = (50, 25, 33, 22)
Output: (22, 33, 50, 25)

50 => 5 x 0 => 0 (1 step)
25 => 2 x 5 => 10 => 1 x 0 => 0 (2 steps)
33 => 3 x 3 => 9 (1 step)
22 => 2 x 2 => 4 (1 step)
Enter fullscreen mode Exit fullscreen mode

Persistence of Memory

Thank goodness for examples; parsing that problem description was a challenge in itself. So it's a two-part problem: (1) we need to be able to convert a number to its step count, and (2) we want to sort by that step count.

Let's start by assuming we have part 1 in hand; let's say there's a function for Steps-To-Reduce-Number-To-One-Digit -- let's call it strnt1d -- that returns the number of steps like in the examples. Given that, we can sort the list by invoking the function in the comparison:

sort { strnt1d($a) <=> strnt1d($b) || $a <=> $b } @int 
Enter fullscreen mode Exit fullscreen mode

Good enough, probably. But strnt1d is going to be invoked twice every time that sort needs to do a comparison. Could we do better? How about if we make one pass just to get the step counts. If we save that list, then we can use both lists together to do the sorting. We can operate on indices, using the well-known computer science principal of solving problems via indirection.

my @step = map { strnt1d($_) } @int; 
my @sorted = sort { $step[$a] <=> $step[$b] || $int[$a] <=> $int[$b] } 0..$#int;
return @int[@sorted] 
Enter fullscreen mode Exit fullscreen mode

What's that last step? Well, we sorted a list of indices, not the list of numbers. Once we know the order of indices, we have to take a slice of the original list to get the sorted order.

This indirection with an extra array is looking a little awkward. How could we do better? What if we had the step count attached to each value instead of off on the side? We can map each single value in @int to a pair: the original value, and its step count:

map { [ $_, strnt1d($_) ] } @int 
Enter fullscreen mode Exit fullscreen mode

which gives us an array of pairs that looks like

[ [15, 1], [99, 2], [1, 1], [34, 2] ]
Enter fullscreen mode Exit fullscreen mode

Now we sort that list using the second element of each pair for the step count, and the first element of each pair to break ties:

sort { $a->[1] <=> $b->[1] || $a->[0] <=> $b->[0] } map {...}
Enter fullscreen mode Exit fullscreen mode

The result is still an array of pairs, but now in order. From that we just need to extract the first member of each pair

map { $_->[0] } sort {...} map {...}
Enter fullscreen mode Exit fullscreen mode

In one piece, it looks like

sub persistenceSort(@int)
{
    return [ map { $_->[0] }
        sort { $a->[1] <=> $b->[1] || $a->[0] <=> $b->[1] }
        map { [ $_, strnt1d($_) ] } @int ];
}
Enter fullscreen mode Exit fullscreen mode

I've made the result return an array reference instead of a list, because that makes it easier to fit into a testing framework.

In Perl lore, this trick of augmenting data with an additional trait, sorting it, and then discarding the trait is known as the Schwartzian Transform. I've also seen it done in shell scripts when specifying a sort field would be awkward. Instead, an awk or perl pipeline selects the sort criteria, prepends it to each line, and then after the sort runs, the sort field is cut off again.

Cool down

One bit of housekeeping left: we need to implement the strnt1d function that we assumed existed. There are two pretty easy ways to do it. If we treat a number as a string, we can split it into digits and multiply the digits:

use List::Util qw/product/;
sub strnt1d($n)
{
    my $step = 0;
    while ( $n > 9 )
    {
        ++$step;
        $n = product split(//, $n);
    }
    return $step;
}
Enter fullscreen mode Exit fullscreen mode

Or we can apply math, picking off the least significant digit with modular arithmetic.

sub strnt1d($n)
{
    my $step = 0;
    while ( $n > 9 ) # Has more than one digit
    {
        $step++;
        my $p = 1;
        while ( $n ) # Create product of digits
        {
            $p *= $n % 10;  # Least significant digit
            $n = int($n/10); # Drop least significant digit
        }
        $n = $p; # Recursively apply to product
    }
    return $step;
}
Enter fullscreen mode Exit fullscreen mode

There's also a purely recursive solution, which could use Memoize to cache intermediate results, but I believe this horse has been beaten enough. Time to finish the course and grab my banana and free T-shirt.

Top comments (0)