diff options
| author | Mohammad S Anwar <Mohammad.Anwar@yahoo.com> | 2021-10-15 12:32:16 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2021-10-15 12:32:16 +0100 |
| commit | b20aa9b1c95c89fd1979bb5515f397913d6b1bc7 (patch) | |
| tree | dda28d54bb15d355df4bd4aadebcbbf7629706b8 | |
| parent | 5365a29aecdece7055d66b81e19aad7d8ecfed61 (diff) | |
| parent | 17f80bf213692c67f843cffe008e9e95401c4b8e (diff) | |
| download | perlweeklychallenge-club-b20aa9b1c95c89fd1979bb5515f397913d6b1bc7.tar.gz perlweeklychallenge-club-b20aa9b1c95c89fd1979bb5515f397913d6b1bc7.tar.bz2 perlweeklychallenge-club-b20aa9b1c95c89fd1979bb5515f397913d6b1bc7.zip | |
Merge pull request #5025 from drbaggy/master
Write up
| -rw-r--r-- | challenge-134/james-smith/README.md | 191 | ||||
| -rw-r--r-- | challenge-134/james-smith/perl/ch-1.pl | 34 |
2 files changed, 51 insertions, 174 deletions
diff --git a/challenge-134/james-smith/README.md b/challenge-134/james-smith/README.md index 188174521f..1d17e5726f 100644 --- a/challenge-134/james-smith/README.md +++ b/challenge-134/james-smith/README.md @@ -1,4 +1,4 @@ -# Perl Weekly Challenge #133 +# Perl Weekly Challenge #134 You can find more information about this weeks, and previous weeks challenges at: @@ -10,180 +10,57 @@ submit solutions in whichever language you feel comfortable with. You can find the solutions here on github at: -https://github.com/drbaggy/perlweeklychallenge-club/tree/master/challenge-133/james-smith/perl +https://github.com/drbaggy/perlweeklychallenge-club/tree/master/challenge-134/james-smith/perl -# Task 1 - Integer Square Root +# Task 1 - Pandigital Numbers -***You are given a positive integer `$N`. Write a script to calculate the integer square root of the given number.*** +***Write a script to generate first 5 Pandigital Numbers in base 10.*** ## The solution -We implement Newton's formulae to compute the integer square root.. This is the compact version. +Pandigital numbers (base 10) are numbers who contain all the digits between 0-9 (but don't start with 0) +The lowest pandigital numbers all are permutations of the digits 0..9 with 0 not being the first digit. -```perl -sub find_root { - my( $x, $y ) = ( my $n = shift ) >> 1; ## $x is half $n rounded down... - return $n unless $x; ## Return $n if it is <2 (i.e. 0 or 1) - $x = $y while ( $y = ( $x + $n / $x ) >> 1 ) < $x; ## The crux - the next no is 1/2 of $x & $n/$x - return $x; ## Return value -} -``` +To walk the pandigital numbers we can write a script which generates the next perumtation in "lexical" order. You don't need to do this recursively or with tightly nested loops a simple algorithm finds them... -# Task 2 - Smith Numbers + * Find the largest value of `$i` such that `$s[$i]` `<` `$s[$i+1]` + * Find the largest value of `$j` such that `$s[$i]` `<` `$s[$j]` + * Flip these to entries + * Flip all entries from $i+1 to end of the list... -***Write a script to generate first 10 Smith Numbers in base 10.*** - -## Solution +We note though that we don't have to start at 0123456789 (the lowest permutation) as all numbers starting with 0 are skipped. We can then pre-empty this loop by noting the largest permutation 0987654321 which isn't a pan-digital number, so when we find the next iteration we have our first pandigital number.... -We break the logic up into chunks and implement those in separate function. As we look at each -prime in turn we can effectively create a sieve to get the prime factors. - -Here is the main code. - * We check to see if the sum of digits is the same as the sum of primefactor digits {and that it is not prime} - * We add this to the list if it is the case - * Return when we have `$C` (`10`) in this case... ```perl -sub smith_numbers { - my ( $C, $n, @sn ) = (shift,3); - ( sum_digits( $n ) - sum map { sum_digits $_ } prime_factors( $n ) ) || - ( push @sn, $n ) && - ( @sn == $C ) && - ( return @sn ) while $n++; +my @s = reverse 1..9, 0; + +sub next_perm { + my( $i, $j ); + ( $s[$_] < $s[$_+1] ) && ( $i = $_ ) foreach 0 .. @s-2; + return unless defined $i; + ( $s[$i] < $s[$_] ) && ( $j = $_ ) foreach $i+1 .. @s-1; + @s[ $i, $j ] = @s[ $j, $i ]; + @s[ $i+1 .. @s-1 ] = @s[ reverse $i+1 .. @s-1 ]; + return 1; } -``` - -And the support functions: - * `sum` - Just sum an array; - * `sum_digits` - Sum digits of number, - We cache the digit sum for each number in `%ds`; - * `prime_factors` - for a prime returns nothing, for a composite returns the factors, - We keep a list of primes `@primes`, and also the prime factors for each composite `%comp`. - **Note:** We don't need to continue with primes bigger than `sqrt($N)` so short cut the loop. - -```perl -sub sum { my $t = 0; $t+=$_ foreach @_; $t; } - -sub sum_digits { return $ds{$_[0]} ||= sum split //, $_[0]; } -sub prime_factors { - my $N = shift; - ( $N % $_) ? ( ( $N < $_ * $_ ) && last ) - : ( return $sum_pf[$N] = $sum_pf[$N/$_] + $sum_pf[$_] ) foreach @primes; - $sum_pf[$N] = cl_sum_digits $N; - push @primes, $N; - return 0; -} +say @s while next_perm && $count--; ``` -## Performance - -A slight modification to the code ( to compute Smith Numbers < N ) computes the 29,928 Smith numbers less than 1 million in about 3.3 seconds, and for 10,000,000 about 45 seconds. For large N the script falls over with out-of-memory errors. - -## Re-write in C - -The algorithm does lend itself to rewriting in a lower level language - it doesn't use any nice features of Perl except for the "auto-sizing" of arrays - no hashes, complex IO etc. - -So here is my first C programme for probably 30 years! - -### Timings for C vs perl - -| Max value | Count | Time C | Time perl | -| ------------: | ---------: | ------: | --------: | -| 100 | 6 | 0.00s | 0.01s | -| 1,000 | 49 | 0.00s | 0.01s | -| 10,000 | 376 | 0.00s | 0.03s | -| 100,000 | 3,924 | 0.01s | 0.26s | -| 1,000,000 | 29,928 | 0.07s | 3.25s | -| 10,000,000 | 278,411 | 1.18s | 45.38s | -| 100,000,000 | 2,632,758 | 23.65s | *fail* | -| 1,000,000,000 | 25,154,060 | 514.17s | *fail* | +# Task 2 - Distinct Terms Count -This is the limit for the algorithm (would need 20+ Gbytes for the 10 billion case) +***You are given 2 positive numbers, `$m` and `$n`. Write a script to generate multiplcation table and display count of distinct terms.*** -Note there is an additional about 0.08 second penalty for compiling the C code - so infact for less than 100,000 the perl code is faster in total... - -### The C code - -```c -#include <stdio.h> - -// Compute all Smith Numbers below MAX_N - -// Use guess that PSIZE should approximately 1.1 * PFSIZE / ln(PFSIZE) - -// 10^6 - #define MAX_N 1000000 - #define PSIZE 42000 // Have to guess this! -// 10^7 -// #define MAX_N 10000000 -// #define PSIZE 400000 // Have to guess this! -// 10^8 -// #define MAX_N 100000000 -// #define PSIZE 3200000 // Have to guess this! -// 10^9 -// #define MAX_N 1000000000 -// #define PSIZE 28000000 // Have to guess this! - -#define PFSIZE (MAX_N/2) - -// Set up arrays -int sum_pf[ PFSIZE+1 ], primes[ PSIZE ], prime_index = 0; - -// Compute sum of digits - unlike Perl we can't use split -// so we use repeated modulus/divide by 10.. - -int sum_digits(int n) { - int total = 0; - do { total += n%10; } while( n /= 10 ); - return total; -} +## Solution -// Get the sum of prime factors - -// as we build this in order we only need to find a -// factorisation then we just add together the -// digit sum of the two factors (Here for speed we -// know one will be prime. -// We go through all primes we have until prime^2 -// is greater than the number itself. -// -// To make the last bit easier IF we have a prime -// we return 0 as not composite... -// -// Note to save memory we only store the sum if -// n < MAX_N/2 as we won't need it again (can't -// be a factor of a larger number less than MAX_N - -int sum_prime_factors( int n ) { - int p; - for(int i=0; i< prime_index; i++ ) { - p = primes[i]; - if( !(n % p) ) { - if( n <= PFSIZE ) { - return sum_pf[n] = sum_pf[n/p] + sum_pf[p]; - } else { - return sum_pf[ n/p ] + sum_pf[ p ]; - } - } - if( n < p*p ) { break; } - } - if( n <= PFSIZE ) { - sum_pf[ n ] = sum_digits(n); - primes[ prime_index++ ] = n; - } - return 0; -} +Number 2 again is the easier code this week... +We just loop through the two indicies and make a note of each product as a keys of a hash. And return scalar -// Main is simple just loop and search, printing out -// Smith numbers -int main() { - int count = 0, n = 1; - while( n++ <= MAX_N ) { - if( sum_digits(n) == sum_prime_factors(n) ) { - printf( "%11d\n", n ); - } +```perl + my($m,$n,%x) = @_; + for my $i (1..$m) { + $x{$i*$_}++ for 1..$n; } -} + return scalar keys %x; ``` -**Notes:** In this version we use an optimisation - we know that we will never need to use factorisations or primes greater than `MAX_N/2` so we don't store/cache these. - +If `$m` & `$n` are large (and similar) there may be gain in separating the rectangle into a square and a rectangle - and only compute products for one half of the triangle... diff --git a/challenge-134/james-smith/perl/ch-1.pl b/challenge-134/james-smith/perl/ch-1.pl index d18ca48222..d8d09d9c4f 100644 --- a/challenge-134/james-smith/perl/ch-1.pl +++ b/challenge-134/james-smith/perl/ch-1.pl @@ -8,30 +8,30 @@ use Test::More; use Benchmark qw(cmpthese timethis); use Data::Dumper qw(Dumper); -my $S = 10; -my @s = (1,0,2..$S-1); ## Cheat we jump into the first pemutation which - ## doesn't start with a "0" 1023456789 - avoids - ## us having to check for a leading 0... -my $count=5; - -do { - say @s; -} while next_perm && --$count; - +my @s = reverse 1..9,0; ## Cheat we start with the last perumation + ## starting with 0 - 0987654321 + ## Saves us looping through the first + ## combinations checking for number starting + ## with non-zero (362880 combinations) +my $count = @ARGV ? $ARGV[0] : 5; sub next_perm { - my($i,$j,$p); + my( $i, $j ); + ## Find largest index for which Si+1 > Si - ( $s[$_] < $s[$_+1] ) && ( $i=$_ ) foreach 0 .. $S-2; ## Find i + ( $s[$_] < $s[$_+1] ) && ( $i = $_ ) foreach 0 .. @s-2; ## Find i return unless defined $i; ## Got to the end of the list of permutations ## Find latest index for which Sj > Si for j>i - ( $s[$i] < $s[$_] ) && ( $j=$_ ) foreach $i+1 .. $S-1; ## Find j + ( $s[$i] < $s[$_] ) && ( $j = $_ ) foreach $i+1 .. @s-1; ## Find j - ## Flip i & jth elements..., then all numbers greater than i.. - ( $s[$i], $s[$j] ) = ( $s[$j], $s[$i] ); ## Flip numbers over... - @s[ $i+1 .. $S-1 ] = @s[ reverse $i+1 .. $S-1 ]; ## Flip remaining list - return 1; + ## Flip ith & jth elements..., then all numbers greater than i.. + @s[ $i, $j ] = @s[ $j, $i ]; + @s[ $i+1 .. @s-1 ] = @s[ reverse $i+1 .. @s-1 ]; + + return 1; ## Return true to say can continue... } +say @s while next_perm && $count--; + |
