Perl Weekly Challenge 133: Integer Square Roots and Smith Numbers

These are some answers for Week 133 of the Perl Weekly Challenge organized by Mohammad S. Anwar.

Spoiler Alert: This weekly challenge deadline is due in a few days from now (on October 10, 2021 at 23:59). This blog post offers some solutions to this challenge, please don’t read on if you intend to complete the challenge on your own.

Task 1: Integer Square Root

You are given a positive integer $N.

Write a script to calculate the integer square root of the given number.

Please avoid using built-in function. Find out more about it https://en.wikipedia.org/wiki/Integer_square_root.

Examples:

Input: $N = 10
Output: 3

Input: $N = 27
Output: 5

Input: $N = 85
Output: 9

Input: $N = 101
Output: 10

The standard method for calculating the square root of a number is the Newton method, a.k.a. Newton-Raphson method. For an integer square root, we may use a variation of that method called Heron’s method.

Integer Square Root in Raku

Using a Bisection Method

Before we implement the fairly fast standard method, let’s see what we can do with a bisection method, which is similar to a binary search algorithm on a list of values. If the input number is $N, then we look at the 1..$N interval and find the midpoint $est as the first estimate of the square root. If the square of the midpoint is larger than $N, then we continue with the 1..$est interval, else we continue with the $est..$N interval. We continue until the range is smaller than or equal to 1.

This is an implementation of this idea:

use v6;

sub sqroot (Int $a) {
    # Bisection approach
    my $start = 1;
    my $end = $a;
    my $est;
    loop {
        $est = ($start + $end) div 2;
        say "\tIntermediate values: $start, $est, and $end";
        my $est-sq = $est ** 2;
        last if abs($end-$start) <= 1;
        if $est ** 2 > $a {
            $end = $est;
        } else {
            $start = $est;
        }
    }
    return $est;
}
say "$_\t", sqroot $_ for 10, 27, 85, 101, 200_000_000;

This program displays the following output:

$ raku ./int_sqrt_bs.raku
    Intermediate values: 1, 5, and 10
    Intermediate values: 1, 3, and 5
    Intermediate values: 3, 4, and 5
    Intermediate values: 3, 3, and 4
10  3
    Intermediate values: 1, 14, and 27
    Intermediate values: 1, 7, and 14
    Intermediate values: 1, 4, and 7
    Intermediate values: 4, 5, and 7
    Intermediate values: 5, 6, and 7
    Intermediate values: 5, 5, and 6
27  5
    Intermediate values: 1, 43, and 85
    Intermediate values: 1, 22, and 43
    Intermediate values: 1, 11, and 22
    Intermediate values: 1, 6, and 11
    Intermediate values: 6, 8, and 11
    Intermediate values: 8, 9, and 11
    Intermediate values: 9, 10, and 11
    Intermediate values: 9, 9, and 10
85  9
    Intermediate values: 1, 51, and 101
    Intermediate values: 1, 26, and 51
    Intermediate values: 1, 13, and 26
    Intermediate values: 1, 7, and 13
    Intermediate values: 7, 10, and 13
    Intermediate values: 10, 11, and 13
    Intermediate values: 10, 10, and 11
101 10
    Intermediate values: 1, 100000000, and 200000000
    Intermediate values: 1, 50000000, and 100000000
    Intermediate values: 1, 25000000, and 50000000
    Intermediate values: 1, 12500000, and 25000000
    Intermediate values: 1, 6250000, and 12500000
    Intermediate values: 1, 3125000, and 6250000
    Intermediate values: 1, 1562500, and 3125000
    Intermediate values: 1, 781250, and 1562500
    Intermediate values: 1, 390625, and 781250
    Intermediate values: 1, 195313, and 390625
    Intermediate values: 1, 97657, and 195313
    Intermediate values: 1, 48829, and 97657
    Intermediate values: 1, 24415, and 48829
    Intermediate values: 1, 12208, and 24415
    Intermediate values: 12208, 18311, and 24415
    Intermediate values: 12208, 15259, and 18311
    Intermediate values: 12208, 13733, and 15259
    Intermediate values: 13733, 14496, and 15259
    Intermediate values: 13733, 14114, and 14496
    Intermediate values: 14114, 14305, and 14496
    Intermediate values: 14114, 14209, and 14305
    Intermediate values: 14114, 14161, and 14209
    Intermediate values: 14114, 14137, and 14161
    Intermediate values: 14137, 14149, and 14161
    Intermediate values: 14137, 14143, and 14149
    Intermediate values: 14137, 14140, and 14143
    Intermediate values: 14140, 14141, and 14143
    Intermediate values: 14141, 14142, and 14143
    Intermediate values: 14142, 14142, and 14143
200000000   14142

This works well, but isn’t very efficient because our first estimates of the square root are quite far away from the actual root. We can improve the process significantly by starting the process with an estimate which is much closer to the target. For example, we can use 2 ** (((log2 N)/2).Int + 1), which is the least power of 2 larger than the square root of N, as our first estimate. This improved version may look like this:

sub sqroot (Int $a) {
    # Bisection approach
    my $start = 1;
    my $end = $a;
    my $est = 2 ** (((log2 $a)/2).Int + 1);
    loop {
        say "\tIntermediate values: $start, $est, and $end";
        last if abs($end-$start) <= 1;
        if $est ** 2 > $a {
            $end = $est;
        } else {
            $start = $est;
        }
        $est = ($end + $start) div 2;
    }
    return $est;
}
say "$_\t", sqroot $_ for 85, 101, 200_000_000;

This improved version displays the following output:

$ raku ./int_sqrt_bs2.raku
    Intermediate values: 1, 16, and 85
    Intermediate values: 1, 8, and 16
    Intermediate values: 8, 12, and 16
    Intermediate values: 8, 10, and 12
    Intermediate values: 8, 9, and 10
    Intermediate values: 9, 9, and 10
85  9
    Intermediate values: 1, 16, and 101
    Intermediate values: 1, 8, and 16
    Intermediate values: 8, 12, and 16
    Intermediate values: 8, 10, and 12
    Intermediate values: 10, 11, and 12
    Intermediate values: 10, 10, and 11
101 10
    Intermediate values: 1, 16384, and 200000000
    Intermediate values: 1, 8192, and 16384
    Intermediate values: 8192, 12288, and 16384
    Intermediate values: 12288, 14336, and 16384
    Intermediate values: 12288, 13312, and 14336
    Intermediate values: 13312, 13824, and 14336
    Intermediate values: 13824, 14080, and 14336
    Intermediate values: 14080, 14208, and 14336
    Intermediate values: 14080, 14144, and 14208
    Intermediate values: 14080, 14112, and 14144
    Intermediate values: 14112, 14128, and 14144
    Intermediate values: 14128, 14136, and 14144
    Intermediate values: 14136, 14140, and 14144
    Intermediate values: 14140, 14142, and 14144
    Intermediate values: 14142, 14143, and 14144
    Intermediate values: 14142, 14142, and 14143
200000000   14142

Using Heron’s Method

This method looks iteratively for a solution to the x² - N = 0 equation. With Newton’s method which applies to general numbers (not only integers), if you start with almost any estimate, p, you can compute a better estimate q with the following formula: q = (p + a / x) / 2. Geometrically, this is like drawing a tangent to the curve at the point where the abscissa is the first estimate, and take the point where the tangent meets the x axis as the new estimate. As it can be seen, we quickly get closer to the searched value (the square root), which is where the curve intersects the x-axis.

Newton_method.jpg

For integer square roots, we essentially need to replace standard rational division by Euclidean division (or integer division).

We use again 2 ** (((log2 N)/2).Int + 1) as our first estimate (see above).

use v6;

sub sqroot (Int $a) {
    my $x = 2 ** (((log2 $a)/2).Int + 1);
    my $y;
    loop {
        say "\tIntermediate value: $x";
        $y = ($x + $a div $x) div 2;
        last if abs($x - $y) < 1;
        $x = $y;
    }
    return $y;
}
say "$_\t", sqroot $_ for 10, 27, 85, 101, 200_000_000;

This program displays the following output:

$ raku ./int_sqrt.raku
    Intermediate value: 4
    Intermediate value: 3
10  3
    Intermediate value: 8
    Intermediate value: 5
27  5
    Intermediate value: 16
    Intermediate value: 10
    Intermediate value: 9
85  9
    Intermediate value: 16
    Intermediate value: 11
    Intermediate value: 10
101 10
    Intermediate value: 16384
    Intermediate value: 14295
    Intermediate value: 14142
200000000   14142

As it can be seen, this method converges toward the root much faster than the bisection method.

Integer Square Root in Perl

Using a Bisection Method

We again start with a bisection method. Please refer to the Bisection Method sub-section in the Raku section above for further explanations.

Perl doesn’t have a div Euclidean division operator, but it is easy to replace it with a combination of the standard division operator / and the int built-in function. Similarly, there is no built-in log2 binary logarithm function in Perl, but we can easily do without it, since we have the following mathematical property: log2 x = log x / log 2 (where log is the logarithm in any base, including the natural or the decimal logarithm).

use strict;
use warnings;
use feature "say";

sub sqroot {
    # Bisection method
    my $c = shift;
    my $start = 1;
    my $end = $c;
    my $est = 2 ** (int((log($c)/log(2))/2) + 1);
    while (1) {
        say "\tIntermediate values: $start, $est, and $end";
        last if abs($end-$start) <= 1;
        if ($est ** 2 > $c) {
            $end = $est;
        } else {
            $start = $est;
        }
        $est = int (($end + $start) / 2);
    }
    return $est;
}
say "$_\t", sqroot $_ for 85, 101, 200_000_000;

This script displays the same output as the equivalent Raku script above, so, for brevity, we only show the beginning of the output:

$ perl int_sqrt_BS.pl
        Intermediate values: 1, 16, and 85
        Intermediate values: 1, 8, and 16
        Intermediate values: 8, 12, and 16
        Intermediate values: 8, 10, and 12
        Intermediate values: 8, 9, and 10
        Intermediate values: 9, 9, and 10
85      9
        Intermediate values: 1, 16, and 101
        Intermediate values: 1, 8, and 16
        Intermediate values: 8, 12, and 16
        Intermediate values: 8, 10, and 12
        Intermediate values: 10, 11, and 12
        Intermediate values: 10, 10, and 11
101     10
...

Using Heron’s Method

This is essentially a port to Perl of the Raku program above (please look there for further explanations):

use strict;
use warnings;
use feature "say";

sub sqroot {
    my $c = shift;  # my $x = 2 ** (int((log($c)/log(2))/2) + 1);
    my $x = 2 ** (int((log($c)/log(2))/2) + 1);
    my $y;
    while (1) {
        say "\tIntermediate value: $x";
        $y = int(($x + $c / $x) / 2);
        last if abs($x - $y) < 1;
        $x = $y;
    }
    return $y;
}
say "$_ -> ", sqroot $_ for 10, 27, 85, 101, 200_000_000;

Output:

$ perl int_sqrt.pl
        Intermediate value: 2
        Intermediate value: 3
10 -> 3
        Intermediate value: 4
        Intermediate value: 5
27 -> 5
        Intermediate value: 8
        Intermediate value: 9
85 -> 9
        Intermediate value: 8
        Intermediate value: 10
101 -> 10
        Intermediate value: 8192
        Intermediate value: 16303
        Intermediate value: 14285
        Intermediate value: 14142
200000000 -> 14142

Task 2: Smith Numbers

Write a script to generate first 10 Smith Numbers in base 10.

According to https://en.wikipedia.org/wiki/Smith_number, in number theory, a Smith number is a composite number for which, in a given number base, the sum of its digits is equal to the sum of the digits in its prime factorization in the given number base.

Smith Numbers in Raku

For this task, we use a prime-factors subroutine that performs a prime factorization of the input parameter. We then iterates over successive integers (except those that are prime) and push the integer on a @result array if the sum of its digits is equal to the sum of the digits of its prime factors

use v6;

my @primes = grep {.is-prime}, 1..*;

sub prime-factors (UInt $num-in) {
    my @factors;
    my $num = $num-in;
    for @primes -> $div {
        while ($num %% $div) {
            push @factors, $div;
            $num div= $div;
        }
        return @factors if $num == 1;
    }
    push @factors, $num unless $num == $num-in;
    return @factors;
}

my @result;
my $count = 0;
for 1..* -> $i {
    next if $i.is-prime; # we want composite numbers only
    my @factors = prime-factors $i;
    push @result, $i and ++$count if (|@factors).comb.sum == $i.comb.sum;
    last if $count >= 10;
}
say @result;

This program displays the following output:

$ raku ./smith_nums.raku
[4 22 27 58 85 94 121 166 202 265]

Smith Numbers in Perl

This is essentially the same algorithm as in Raku, except that we have to build our own prime-factors and add_digits subroutines and implement a loop to populate the @primes array.

#!/usr/bin/perl
use strict;
use warnings;
use feature qw/say/;
use Data::Dumper;
use constant MAX => 400;

my @primes = (2, 3, 5, 7);
my $current = 9;
while (1) {
    my $prime = 1;
    for my $i (@primes) {
        my $i_sq = $i * $i;
        last if $i_sq > $current;
        $prime = 0, last if $current % $i == 0;  
    }
    push @primes, $current if $prime;;
    $current += 2;
    last if $current > MAX;
}
my %primes = map { $_ => 1} @primes;

sub prime_factors {
    my $num = shift;
    my $origin_num = $num;
    my @factors;
    for my $div (@primes) {
        while ($num % $div == 0) {
            push @factors, $div;
            $num /= $div;
        }
        return @factors if $num == 1;
    }
    push @factors, $num unless $num == $origin_num;
    return @factors;
}

sub add_digits {
my $sum = 0;
    for my $i (@_) {
        $sum += $_ for split //, $i;
    }
    return $sum;
}
# say join " ", prime_factors $_ for grep {not exists $primes{$_}} 2..50;

my @result;
my $count = 0;
my $i = 2;
while (1) {
    $i++;
    next if exists $primes{$i}; # we want composite numbers only
    my @factors = prime_factors $i;
    push @result, $i and $count++ if add_digits($i) == add_digits(@factors);
    last if $count >= 10;
}
say "@result";

The program displays the following output:

$ perl ./smith_nums.pl 4 22 27 58 85 94 121 166 202 265

Wrapping up

The next week Perl Weekly Challenge will start soon. If you want to participate in this challenge, please check https://perlweeklychallenge.org/ and make sure you answer the challenge before 23:59 BST (British summer time) on October 17, 2021. And, please, also spread the word about the Perl Weekly Challenge if you can.

Leave a comment

About laurent_r

user-pic I am the author of the "Think Perl 6" book (O'Reilly, 2017) and I blog about the Perl 5 and Raku programming languages.