diff options
| author | Mohammad Sajid Anwar <Mohammad.Anwar@yahoo.com> | 2023-08-06 20:39:29 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-08-06 20:39:29 +0100 |
| commit | b25e973a2c758c8f39f3710a94b2cfdec11ea5b3 (patch) | |
| tree | 9b21e9c24bf9ca0744530eed45b612b5160f7afa | |
| parent | 7e719c7e41b50a80c397f7542e2bb7eeae3d6068 (diff) | |
| parent | e2133368f6dcc0c49565fd4919148e5c1995c23e (diff) | |
| download | perlweeklychallenge-club-b25e973a2c758c8f39f3710a94b2cfdec11ea5b3.tar.gz perlweeklychallenge-club-b25e973a2c758c8f39f3710a94b2cfdec11ea5b3.tar.bz2 perlweeklychallenge-club-b25e973a2c758c8f39f3710a94b2cfdec11ea5b3.zip | |
Merge pull request #8511 from MatthiasMuth/muthm-228
Challenge 228 Task 1 and 2 solutions in Perl by Matthias Muth
| -rw-r--r-- | challenge-228/matthias-muth/README.md | 349 | ||||
| -rw-r--r-- | challenge-228/matthias-muth/blog.txt | 1 | ||||
| -rw-r--r-- | challenge-228/matthias-muth/perl/TestExtractor.pm | 243 | ||||
| -rw-r--r-- | challenge-228/matthias-muth/perl/ch-1.pl | 32 | ||||
| -rw-r--r-- | challenge-228/matthias-muth/perl/ch-2.pl | 232 | ||||
| -rw-r--r-- | challenge-228/matthias-muth/perl/challenge-228.txt | 62 |
6 files changed, 820 insertions, 99 deletions
diff --git a/challenge-228/matthias-muth/README.md b/challenge-228/matthias-muth/README.md index 3cf35f627b..1705fce596 100644 --- a/challenge-228/matthias-muth/README.md +++ b/challenge-228/matthias-muth/README.md @@ -1,122 +1,273 @@ -# Friday XIII -**Challenge 227 solutions in Perl by Matthias Muth** +# Fast empty arrays, and the non-unique meaning of 'unique' +**Challenge 228 Task 1 and 2 solutions in Perl by Matthias Muth** -## Task 1: Friday 13th +## Task 1: Unique Sum -> You are given a year number in the range 1753 to 9999.<br/> -> Write a script to find out how many dates in the year are Friday 13th, assume that the current Gregorian calendar applies.<br/> +> You are given an array of integers.<br/> +> Write a script to find out the sum of unique elements in the given array.<br/> > <br/> -> Example<br/> -> Input: $year = 2023<br/> -> Output: 2<br/> -> Since there are only 2 Friday 13th in the given year 2023 i.e. 13th Jan and 13th Oct.<br/> - -Looking for an easy way to get the weekday for a given date, -the `Time::Piece` core module is an obvious choice. - -`Time::Piece`'s typical usage is for dealing with 'current' times, -which are returned by the `localtime` and `gmtime` subroutines when called without parameter. -If we want to supply a different date with them, -we need to compute Unix epoch time value to do so. -We will look into that later. - -But there is also the `Time::Piece->strptime(STRING, FORMAT)` subroutine that works as a constructor -for `Time::Piece` objects. -We hand in a date string like `"2023-07-13"`, and a format of `'%Y-%m-%d'`, -and there we have our time object. - -So everything in one statement (but not on one line, to make it more readable!): -- month numbers from 1 to 12, -- use a `grep` code block to create the `Time::Piece` objects on the fly, -and select those who return a day_of_week of 5 (Friday), -- use `scalar` to put `grep` into a scalar context -so it returns the number of elements found instead of the list. +> Example 1<br/> +> Input: @int = (2, 1, 3, 2)<br/> +> Output: 4<br/> +> In the given array we have 2 unique elements (1, 3).<br/> +> <br/> +> Example 2<br/> +> Input: @int = (1, 1, 1, 1)<br/> +> Output: 0<br/> +> In the given array no unique element found.<br/> +> <br/> +> Example 3<br/> +> Input: @int = (2, 1, 3, 4)<br/> +> Output: 10<br/> +> In the given array every element is unique.<br/> + +I think that the lesson that this task wants to teach us is +that the word 'unique' has several distinct (pun intended!) meanings.<br/> +A Wikipedia search tells us this: +>Unique primarily refers to:<br/> +> * Uniqueness, a state or condition wherein something is **unlike anything else**<br/> +> * In mathematics and logic, a unique object is the **only object** with a certain property, +see Uniqueness quantification<br/> + +(highlighting by me). + +Most of the times when we talk about extracting 'the unique elements' of a list +we think Unix `sort | uniq`, and what we want to get is *every* number from the list, +but each one only once.<br/> +Actually this means that every number in our *resulting list* is 'unique', +in the sense that is exists only once. + +But in this task, we are supposed to extract the numbers that exist only once +already in the *input list* (and thus are 'unique' there!). + +So we cannot use `List::Util`'s `uniq` (or better `uniq_int`) function for this.<br/> +But going through the list of functions in `List::MoreUtils`, +we find the `singleton` function, which is exactly what we need. +So we can make our life simple: ```perl use v5.36; -use Time::Piece; -sub friday_13th( $year ) { - return scalar grep { - Time::Piece->strptime( "$year-$_-13", "%Y-%m-%d" )->day_of_week == 5 - } 1..12; +use List::Util qw( sum ); +use List::MoreUtils qw( singleton ); + +sub unique_sum( @int ) { + return sum( singleton @int ) // 0; } ``` +We make sure that we return a zero instead of `undef` +when `singleton` returns an empty list +and `sum` has nothing to sum up. + +If you don't want to install `List::MoreUtils` from CPAN, +here is the more 'classical' solution +(still using `sum` from the `List::Util`core module): -Now maybe `strptime` is not the fastest solution, -and we could use `timegm` from `Time::Local` to create our dates -without the need of parsing a string with a format. -But using `strptime` like above looks much clearer to me than -converting month numbers from 1..12 to 0..11 and years to be offsets from 1900, -which would be necessary if we used `timegm`: ```perl use v5.36; -use Time::Local; -use Time::Piece; -sub friday_13th( $year ) { - return scalar grep { - gmtime( timegm( 0, 0, 0, 13, $_ - 1, $year - 1900 ) )->day_of_week == 5 - } 1..12; +use List::Util qw( sum ); + +sub unique_sum_core_only( @int ) { + my %frequencies; + $frequencies{$_}++ + for @int; + return sum( + grep { $frequencies{$_} == 1 } keys %frequencies + ) // 0; } ``` -It's also that we would be jumping between domains -(`localtime`/`gmtime` needing that 6-element list, returning an epoch time value, -then we create a Time::Piece object from that), -which does not really make it obvious what is going on.<br/> -I prefer the first version! :-) -## Task 2: Roman Maths +## Task 2: Empty Array -> Write a script to handle a 2-term arithmetic operation expressed in Roman numeral.<br/> +> You are given an array of integers in which all elements are unique.<br/> +> Write a script to perform the following operations until the array is empty and return the total count of operations.<br/> +> If the first element is the smallest then remove it otherwise move it to the end.<br/> > <br/> -> Example<br/> -> IV + V => IX<br/> -> M - I => CMXCIX<br/> -> X / II => V<br/> -> XI * VI => LXVI<br/> -> VII ** III => CCCXLIII<br/> -> V - V => nulla (they knew about zero but didn't have a symbol)<br/> -> V / II => non potest (they didn't do fractions)<br/> -> MMM + M => non potest (they only went up to 3999)<br/> -> V - X => non potest (they didn't do negative numbers)<br/> - -I'm sure it's an interesting exercise to convert Roman numerals to arabic (common) numbers -and vice versa, but here, I am not going to reinvent the wheel.<br/> -The `Roman` module from CPAN is my friend in this case. - -The more interesting aspect is how to implement the arithmetic operations -in a more elegant way than a nested if-then-else statement. - -I chose a hash lookup to return an anonymous subroutine that implements -the respective operation. - -The rest looks quite self-explanatory to me.<br/> -Or is it only in my eyes??? +> Example 1<br/> +> Input: @int = (3, 4, 2)<br/> +> Ouput: 5<br/> +> Operation 1: move 3 to the end: (4, 2, 3)<br/> +> Operation 2: move 4 to the end: (2, 3, 4)<br/> +> Operation 3: remove element 2: (3, 4)<br/> +> Operation 4: remove element 3: (4)<br/> +> Operation 5: remove element 4: ()<br/> +> <br/> +> Example 2<br/> +> Input: @int = (1, 2, 3)<br/> +> Ouput: 3<br/> +> Operation 1: remove element 1: (2, 3)<br/> +> Operation 2: remove element 2: (3)<br/> +> Operation 3: remove element 3: ()<br/> + +Let's start with a literal implementation of the algorithm that is described.<br/> +We count the operations in `$n_ops`.<br/> +In a loop, we either shift the first element away, or we push it to the end. ```perl use v5.36; -use Roman; - -my %ops = ( - '+' => sub { $_[0] + $_[1] }, - '-' => sub { $_[0] - $_[1] }, - '*' => sub { $_[0] * $_[1] }, - '/' => sub { $_[0] / $_[1] }, - '**' => sub { $_[0] ** $_[1] }, -); - -sub roman_maths( @input ) { - my $result = $ops{$input[1]}->( arabic( $input[0] ), arabic( $input[2] ) ); - return - $result == 0 - ? "nulla (they knew about zero but didn't have a symbol)" : - $result != int( $result ) - ? "non potest (they didn't do fractions)" : - $result > 3999 - ? "non potest (they only went up to 3999)" : - $result < 0 - ? "non potest (they didn't do negative numbers)" : - Roman( $result ); +use List::Util qw( min ); + +sub empty_array_1( @int ) { + my $n_ops = 0; + while ( @int ) { + $n_ops++; + if ( $int[0] == min @int ) { + shift @int; + } + else { + push @int, shift @int; + } + } + return $n_ops; } ``` +Looking closer at what is happening, +I find that all numbers from the start of the list up to the list's lowest number +are moved to the end, one by one, before then that lowest number is removed.<br/> +So why don't we do this in one single operation? + +To do so, we need to find the index of the lowest number: +```perl + my $min = $int[0]; + my $index = 0; + for ( 1..$#int ) { + if ( $int[$_] < $min ) { + $index = $_; + $min = $int[$_]; + } + } +``` +Actually, finding the index of a given element in the array is something that has been +invented and optimized before. +So let's use this version, which is much easier on the eyes +(sorry, I mean 'more readable and therefore more easily maintainable'): +```perl + my $min = min @int; + my $index = first_index { $_ == $min } @int; +``` +Any of these two will work nicely. + +But in fact, I don't like that we compute the minimum of the array again and again, +within each iteration of the loop. +And actually, the order in which elements are removed is from the lowest to the highest, +no matter how many operations it takes. +Which means that we can create a sorted copy of the array once, +and then process the 'lowest' numbers in that order.<br/> +Like this: +```perl + my @sorted = sort { $a <=> $b } @int; + for my $current_smallest ( @sorted ) { + my $index = first_index { $_ == $current_smallest } @int; + ... + } +``` + +Now for moving the numbers in one step, we recreate the array, +from the numbers *right* of the smallest number, +and putting everything that was *left* of the smallest number behind. +We just need to be cautious about the edge case of having +*no* numbers to the left, +because `$index == 0` results in `$index - 1 == -1`, +which 'wraps around' in a most unwanted way. +```perl + @int = ( + @int[ $index + 1 .. $#int ], + $index > 0 ? @int[ 0 .. $index - 1 ] : (), + ); +``` + +Maybe the next step is a bit of overdoing it, +but I think it is still worth it.<br/> +The idea is to not recreate the array, +but to use a combination of `splice` and `push` +to modify the existing array.<br/> +The wanted effect is to use Perl's internal optimizations +for array operations, and to avoid copying the whole array +over and over again within each iteration. + +So let's do it like that: +```perl + # Move all numbers left of the smallest one to the end, in one step, + # 'splicing' them away at the front and re-adding (push) them at the + # end. + # We only need to do that if there actually *are* any numbers left of + # the smallest number. + push @int, splice @int, 0, $index, () + if $index > 0; +``` +After that, the smallest number is in the first position (always!), +and can be `shift`ed away. +```perl + # Remove the smallest number, which is at the front now. + shift @int; +``` + +This is the whole function now (leaving comments in the code). +``` +use v5.36; +use List::Util qw( min ); +use List::MoreUtils qw( first_index ); + +sub empty_array_5( @int ) { + my $n_ops = 0; + + # Sort the numbers, to get the order in which we will remove them, + # Then walk through the numbers, starting with the lowest one. + my @sorted = sort { $a <=> $b } @int; + for my $current_smallest ( @sorted ) { + # Find the position of our smallest number in the array. + my $index = first_index { $_ == $current_smallest } @int; + + # Move all numbers left of the smallest one to the end, in one step, + # 'splicing' them away at the front and re-adding (push) them at the + # end. + # We only need to do this if there actually *are* any numbers left of + # the smallest number. + push @int, splice @int, 0, $index, () + if $index > 0; + + # Remove the smallest number, which is at the front now. + shift @int; + + # What we did was worth '$index' single move operations, plus one + # delete operation. + $n_ops += $index + 1; + } + return $n_ops; +} +``` + +For the small examples that we have, I probably really over-optimized my solution. +But I ran a little benchmark, comparing the first version and the last one: +``` +Benchmark using 10 values: + Rate empty_array_5 empty_array_1 +empty_array_5 74734/s -- -61% +empty_array_1 192152/s 157% -- + +Benchmark using 50 values: + Rate empty_array_1 empty_array_5 +empty_array_1 3792/s -- -25% +empty_array_5 5046/s 33% -- + +Benchmark using 100 values: + Rate empty_array_1 empty_array_5 +empty_array_1 323/s -- -84% +empty_array_5 1973/s 511% -- + +Benchmark using 1000 values: + s/iter empty_array_1 empty_array_5 +empty_array_1 2.14 -- -99% +empty_array_5 3.14e-02 6717% -- +``` + +It turns out that for all challenge examples, the simple first version is more efficient! +This is probably due to the startup cost for sorting the array.<br/> +At around 50 elements, the 'optimized' version starts being faster. +And for a 1000 elements array, the simple version needs more than 2 seconds for one run on my laptop, +while the optimized version does it in 31.4 milliseconds.<br/> +More than 600 times faster! + +It gives me a good feeling to have a PWC solution that scales so well! :-D + #### **Thank you for the challenge!** diff --git a/challenge-228/matthias-muth/blog.txt b/challenge-228/matthias-muth/blog.txt new file mode 100644 index 0000000000..88ff1bd463 --- /dev/null +++ b/challenge-228/matthias-muth/blog.txt @@ -0,0 +1 @@ +https://github.com/MatthiasMuth/perlweeklychallenge-club/tree/muthm-228/challenge-228/matthias-muth#readme
diff --git a/challenge-228/matthias-muth/perl/TestExtractor.pm b/challenge-228/matthias-muth/perl/TestExtractor.pm new file mode 100644 index 0000000000..412e2b7c1b --- /dev/null +++ b/challenge-228/matthias-muth/perl/TestExtractor.pm @@ -0,0 +1,243 @@ +# +# The Weekly Challenge - Perl & Raku +# (https://theweeklychallenge.org) +# +# The Test Data Extraction Machine (tm). +# +# Perl solution by Matthias Muth. +# + +use strict; +use warnings; +use feature 'say'; +use feature 'signatures'; +no warnings 'experimental::signatures'; + +package TestExtractor; +use Exporter 'import'; +our @EXPORT = qw( run_tests $verbose %options vprint vsay pp is ); + +use Data::Dump qw( pp ); +use Getopt::Long; +use Cwd qw( abs_path ); +use File::Basename; +use List::Util qw( any ); +use Test2::V0; +no warnings 'experimental::signatures'; + +our ( $verbose, %options ); +sub vprint { print @_ if $verbose }; +sub vsay { say @_ if $verbose }; + +sub run_tests() { + + $| = 1; + + GetOptions( + "v|verbose!" => \$verbose, + ) or do { say "usage!"; exit 2 }; + + my $dir = dirname abs_path $0; + my ( $challenge, $task ) = + abs_path( $0 ) =~ m{challenge-(\d+) .* (\d+)[^[/\\]*$}x; + unless ( $challenge && $task ) { + say STDERR "ERROR: ", + "Cannot determine challenge number or task number. Exiting."; + exit 1; + } + + my $local_tests; + ( undef, $local_tests ) = read_task( *::DATA ) + if fileno *::DATA; + + my ( $task_title, $task_description ) = + read_task( "$dir/challenge-${challenge}.txt", $task ); + # vsay $task_title; + + my @tests = ( + $local_tests ? extract_tests( $local_tests ) : (), + $task_description ? extract_tests( $task_description ) : (), + ); + # vsay pp( @tests ); + + ( my $sub_name = lc $task_title ) =~ s/\W+/_/g; + my $sub = \&{"::$sub_name"}; + + do { + my @input_params = + @{$_->{INPUT}} == 1 + ? ( ref $_->{INPUT}[0] eq 'ARRAY' + && ! grep( ref $_, @{$_->{INPUT}[0]} ) ) + ? @{$_->{INPUT}[0]} + : $_->{INPUT}[0] + : @{$_->{INPUT}}; + my $expected = $_->{OUTPUT}; + my $diag = + "$sub_name( " . pp( @input_params ) . " ) " + . ( ( @$expected == 1 && $expected->[0] =~ /^(?:(true)|false)/ ) + ? "is $expected->[0]" + : ( "== " . pp( @{$_->{OUTPUT}} ) ) ); + + my $name = "$_->{TEST}"; + $name .= ": $diag" + if $_->{TEST} =~ /^(Test|Example)(?:\s+\d+)?$/; + $diag = "test: $diag"; + + my @output = $sub->( @input_params ); + + if ( @$expected == 1 && $expected->[0] =~ /^(?:(true)|false)/ ) { + ok $1 ? $output[0] : ! $output[0], $name, $diag // (); + } + else { + is \@output, $expected, $name, $diag // (); + } + + # vsay ""; + + } for @tests; + + done_testing; +} + +sub read_task( $fd_or_filename, $wanted_task = undef ) { + + my $fd; + if ( ref \$fd_or_filename eq 'SCALAR' ) { + open $fd, "<", $fd_or_filename + or die "ERROR: cannot open '$fd_or_filename': $!\n"; + } + else { + # non-SCALARs, like __DATA__ GLOB. + $fd = $fd_or_filename; + } + + my ( $task, $task_title, $task_text ) = ( -1, undef ); + while ( <$fd> ) { + /^Task (\d+):\s*(.*?)\s*$/ and do { + $task = $1; + $task_title = $2 + if $wanted_task && $task == $wanted_task; + next; + }; + + next + if $wanted_task && $task != $wanted_task; + + $task_text .= $_; + } + + return $task_title, $task_text; +} + +sub extract_tests( $task_text ) { + # vsay "extract_tests( ", pp( $task_text ), " )"; + + # These regular expressions are used for extracting input or output + # test data. + my $var_name = qr/ [\@\$]\w+ /x; + my $literal = qr/ ".*?" | '.*?' | [+-]?\d+ | undef /x; + my $bracketed = qr/ \[ [^\[]*? \] /xs; + my $parenthesized = qr/ \( [^\[]*? \) /xs; + my $entry = qr/ $literal | $bracketed | $parenthesized /x; + my $list = qr/ $entry (?: \s*,\s* $entry )* \s*,? /xs; + + # The combination of what we expect as input or output data. + # Capture unparenthesized lists for special handling. + my $data_re = qr/ (?<lit> $literal ) + | (?<br_list> \[ \s* (?:$list)? \s* \] ) + | (?<par_list> \( \s* (?:$list)? \s* \) ) + | (?<no_paren> $list ) /x; + + my @tests; + while ( $task_text =~ + /^((?:Example|Test).*?)\s*:?\s*$ .*? + ^Input: \s* ( .*? ) \s* + ^Out?put: \s* ( .*? ) \s*? (?=(?: ^$ | ^\S | \Z )) + /xmsg ) + { + my ( $test, $input, $output) = ( $1, $2, $3 ); + # vsay pp $test, $input, $output; + + push @tests, { TEST => $test }; + + for ( $input, $output ) { + # To avoid misinterpretations of '@' or '$' when the data is + # 'eval'ed, we turn all double quotes into single quotes. + s/\"/'/g; + + # We convert 'barewords' into quoted strings. + # We search for these patterns, but we just skip them without + # changing them: + # * 'Input:', 'Output:' at the beginning of the string, + # * quoted strings, + # * variable names having a $ or @ sigil. + # After we are sure it's none of those, we also check unquoted + # 'barewords' (here: combinations of letters, digits or underscores, + # starting with a letter) and enclose them in single quotes. + my $bareword = qr/ \b (?!undef) [a-z_][a-z0-9_]* \b /ix; + while ( / ^Input: | ^Output: | '.*?' | [\$\@]$bareword + | ( $bareword ) /xg ) + { + if ( $1 ) { + my $p = pos(); + substr $_, $p - length( $1 ), length( $1 ), "'$1'"; + pos = $p + 2; + } + } + + # As all arrays will be stored as array references, so we just + # convert parentheses (...) to angle brackets [...]. + # s/\(/\[/g; + # s/\)/\]/g; + + # Add missing commas between literals. + while ( s/($literal)\s+($literal)/$1, $2/ ) {} + } + + while ( $input =~ / ($var_name) \s* = \s* ($data_re) /xg ) { + push @{$tests[-1]{VARIABLE_NAMES}}, $1; + push @{$tests[-1]{INPUT}}, + eval( ( $+{no_paren} || $+{par_list} ) ? "[ $2 ]" : $2 ); + }; + + while ( $output =~ /^\s* ($data_re) $/xg ) { + local $_ = $1; + # vsay "\$_: <$_>"; + # Special case: (1,2),(3,4),(5,6) + # should become: [1,2],[3,4],[5,6] ] + if ( $+{no_paren} && /$parenthesized/ ) { + # vsay "found special case <$_>"; + s/\(/\[/g; + s/\)/\]/g; + } + push @{$tests[-1]{OUTPUT}}, + eval( $+{no_paren} ? "( $_ )" : $_ ); + }; + } + + unless ( @tests ) { + # Try an alternative description format: + # <input...> => <output...> + my $n_examples = 0; + while ( $task_text =~ /^( .*? ) \s* => \s* ( .* )$/xmg ) { + # vsay pp @{^CAPTURE}; + push @tests, { + TEST => "Example " . ++$n_examples, + INPUT => [ split " ", $1 ], + OUTPUT => [ $2 ], + VARIABLE_NAMES => [ '@input' ], + } + } + } + + # Use array refs for all OUTPUT lists if at least one of tests does. + if ( any { ref $_->{OUTPUT}[0] } @tests ) { + $_->{OUTPUT} = [ $_->{OUTPUT} ] + for grep { ! ref $_->{OUTPUT}[0] } @tests; + } + + return @tests; +} + +1; + diff --git a/challenge-228/matthias-muth/perl/ch-1.pl b/challenge-228/matthias-muth/perl/ch-1.pl new file mode 100644 index 0000000000..16648ce9d8 --- /dev/null +++ b/challenge-228/matthias-muth/perl/ch-1.pl @@ -0,0 +1,32 @@ +#!/usr/bin/env perl +# +# The Weekly Challenge - Perl & Raku +# (https://theweeklychallenge.org) +# +# Challenge 228 Task 1: Unique Sum +# +# Perl solution by Matthias Muth. +# + +use v5.36; + +use lib '.'; +use TestExtractor; + +use List::Util qw( sum ); +use List::MoreUtils qw( singleton ); + +sub unique_sum( @int ) { + return sum( singleton @int ) // 0; +} + +sub unique_sum_core_only( @int ) { + my %frequencies; + $frequencies{$_}++ + for @int; + return sum( + grep { $frequencies{$_} == 1 } keys %frequencies + ) // 0; +} + +run_tests; diff --git a/challenge-228/matthias-muth/perl/ch-2.pl b/challenge-228/matthias-muth/perl/ch-2.pl new file mode 100644 index 0000000000..eb22430032 --- /dev/null +++ b/challenge-228/matthias-muth/perl/ch-2.pl @@ -0,0 +1,232 @@ +#!/usr/bin/env perl +# +# The Weekly Challenge - Perl & Raku +# (https://theweeklychallenge.org) +# +# Challenge 228 Task 2: Empty Array +# +# Perl solution by Matthias Muth. +# + +use v5.36; + +use lib '.'; +BEGIN { $ENV{T2_RAND_SEED} = 228; } +use TestExtractor; + +use List::Util qw( min ); + +sub empty_array_1( @int ) { + # $verbose && print say "empty_array( @int )"; + my $n_ops = 0; + while ( @int ) { + $n_ops++; + # $verbose && print print "Operation $n_ops: "; + if ( $int[0] == min @int ) { + # $verbose && print print "remove element $int[0]"; + shift @int; + } + else { + # $verbose && print print "move $int[0] to the end"; + push @int, shift @int; + } + # $verbose && print say ": ( ", join( ", ", @int ), " )"; + } + return $n_ops; +} + +sub empty_array_2( @int ) { + # $verbose && print say "empty_array( @int )"; + my $n_ops = 0; + while ( @int ) { + $n_ops++; + # $verbose && print print "Operation $n_ops: "; + my $min = min @int; + # Remove the first element (we need to do this in any case). + my $first = shift @int; + # Add the (former) first element to the end if it is not the minimum. + if ( $first != $min ) { + push @int, $first; + # $verbose && print print "move $first to the end"; + } + else { + # $verbose && print print "remove element $first"; + } + # $verbose && print say " ( @int )"; + } + return $n_ops; +} + +sub empty_array_3a( @int ) { + # $verbose && print say "empty_array( @int )"; + my $n_ops = 0; + while ( @int ) { + # Find the index of the lowest number. + my $min = $int[0]; + my $index = 0; + for ( 1..$#int ) { + if ( $int[$_] < $min ) { + $index = $_; + $min = $int[$_]; + } + } + + # Reorder the array, + # moving all numbers left of the smallest number to the end in one step, + # also deleting (that is: not copying) the smallest number. + @int = ( + @int[ $index + 1 .. $#int ], + $index > 0 ? @int[ 0 .. $index - 1 ] : (), + ); + + # This is worth '$index' single moving operations, plus one delete operation. + $n_ops += $index + 1; + } + return $n_ops; +} + +use List::MoreUtils qw( first_index ); + +sub empty_array_3b( @int ) { + # $verbose && print say "empty_array( @int )"; + my $n_ops = 0; + while ( @int ) { + # Find the index of the lowest number. + my $min = min @int; + my $index = first_index { $_ == $min } @int; + + # Reorder the array, + # moving all numbers left of the smallest number to the end in one step, + # also deleting (that is: not copying) the smallest number. + @int = ( + @int[ $index + 1 .. $#int ], + $index > 0 ? @int[ 0 .. $index - 1 ] : (), + ); + + # This is worth '$index' single moving operations, plus one delete operation. + $n_ops += $index + 1; + } + return $n_ops; +} + +sub empty_array_4( @int ) { + # $verbose && print say "empty_array( @int )"; + + # Walk through the numbers starting with the lowest one. + my $n_ops = 0; + my @sorted = sort { $a <=> $b } @int; + for my $current_lowest ( @sorted ) { + # Find the index of the current lowest number. + my $index = first_index { $_ == $current_lowest } @int; + + # Reorder the array, + # starting with all numbers right of the smallest number, + # and putting everything that was left of the smallest number behind. + @int = ( + @int[ $index + 1 .. $#int ], + $index > 0 ? @int[ 0 .. $index - 1 ] : (), + ); + # $verbose && print say ": ( ", join( ", ", @int ), " )"; + + # This is worth '$index' single moving operations, plus one delete + # operation. + # $verbose && print print "Operation $n_ops: move elements up to #$index,", + # " remove element $current_lowest"; + $n_ops += $index + 1; + } + return $n_ops; +} + +sub empty_array_5( @int ) { + # $verbose && print say "empty_array( @int )"; + + my $n_ops = 0; + + # Sort the numbers, to get the order in which we will remove them, + # Then walk through the numbers, starting with the lowest one. + my @sorted = sort { $a <=> $b } @int; + for my $current_smallest ( @sorted ) { + # Find the position of our smallest number in the array. + my $index = first_index { $_ == $current_smallest } @int; + + # $verbose && print print "Operation $n_ops: move elements up to #$index,", + # " remove element $current_smallest"; + + # Move all numbers left of the smallest one to the end, in one step, + # 'splicing' them away at the front and re-adding (push) them at the + # end. + # We only need to do that if there actually *are* any numbers left of + # the smallest number. + push @int, splice @int, 0, $index, () + if $index > 0; + + # Remove the smallest number, which is at the front now. + shift @int; + + # What we did was worth '$index' single move operations, plus one + # delete operation. + $n_ops += $index + 1; + + # $verbose && print say ": ( ", join( ", ", @int ), " )"; + } + return $n_ops; +} + +sub empty_array( @int ) { + # $verbose = 1; + my @results = empty_array_1( @int ); + is [ empty_array_2( @int ) ], \@results, "empty_array_2 results are the same"; + is [ empty_array_3a( @int ) ], \@results, "empty_array_3a results are the same"; + is [ empty_array_3b( @int ) ], \@results, "empty_array_3b results are the same"; + is [ empty_array_4( @int ) ], \@results, "empty_array_4 results are the same"; + is [ empty_array_5( @int ) ], \@results, "empty_array_5 results are the same"; + return @results; +} + +# run_tests; +# exit 0; + +use Benchmark qw( :all :hireswallclock ); + +sub random_list( $n ) { + my @a = 1..$n; + my @b = (); + while( @a ) { + push @b, splice @a, int(rand(@a)), 1, () + }; + return @b; +} + +my @input = ( + 47,12,38,26,25,31,41,37,1,6,10,23,22,32,17,48,43,2,27,13,19,30,35,29,40, + 11,5,18,36,33,15,46,50,28,14,45,4,21,8,42,20,16,7,39,9,24,34,49,44,3, +); + +$| = 1; +$verbose = 0; + +for ( 10, 50, 100, 1000 ) { + say "Benchmark using $_ values:"; + my @input = random_list( $_ ); + cmpthese( $_ > 100 ? -10 : -3, { + 'empty_array_1' => sub { empty_array_1( @input ) }, + # 'empty_array_2' => sub { empty_array_2( @input ) }, + # 'empty_array_3a' => sub { empty_array_3a( @input ) }, + # 'empty_array_3b' => sub { empty_array_3b( @input ) }, + # 'empty_array_4' => sub { empty_array_4( @input ) }, + 'empty_array_5' => sub { empty_array_5( @input ) }, + } ); + say ""; +} + +1; + +__DATA__ +Test 1: Test with 10 values +Input: @int = ( 47, 12, 38, 26, 25, 31, 41, 37, 1, 6 ) +Output: 28 + +Test 2: Test with 50 values +Input: @int = (47,12,38,26,25,31,41,37,1,6,10,23,22,32,17,48,43,2,27,13,19,30,35,29,40,11,5,18,36,33,15,46,50,28,14,45,4,21,8,42,20,16,7,39,9,24,34,49,44,3) +Output: 702 + diff --git a/challenge-228/matthias-muth/perl/challenge-228.txt b/challenge-228/matthias-muth/perl/challenge-228.txt new file mode 100644 index 0000000000..09de3bd250 --- /dev/null +++ b/challenge-228/matthias-muth/perl/challenge-228.txt @@ -0,0 +1,62 @@ +The Weekly Challenge - 228
+Monday, Jul 31, 2023
+
+
+Task 1: Unique Sum
+Submitted by: Mohammad S Anwar
+
+You are given an array of integers.
+Write a script to find out the sum of unique elements in the given array.
+Example 1
+
+Input: @int = (2, 1, 3, 2)
+Output: 4
+
+In the given array we have 2 unique elements (1, 3).
+
+Example 2
+
+Input: @int = (1, 1, 1, 1)
+Output: 0
+
+In the given array no unique element found.
+
+Example 3
+
+Input: @int = (2, 1, 3, 4)
+Output: 10
+
+In the given array every element is unique.
+
+
+Task 2: Empty Array
+Submitted by: Mohammad S Anwar
+
+You are given an array of integers in which all elements are unique.
+Write a script to perform the following operations until the array is empty and return the total count of operations.
+
+If the first element is the smallest then remove it otherwise move it to the end.
+
+
+Example 1
+
+Input: @int = (3, 4, 2)
+Ouput: 5
+
+Operation 1: move 3 to the end: (4, 2, 3)
+Operation 2: move 4 to the end: (2, 3, 4)
+Operation 3: remove element 2: (3, 4)
+Operation 4: remove element 3: (4)
+Operation 5: remove element 4: ()
+
+Example 2
+
+Input: @int = (1, 2, 3)
+Ouput: 3
+
+Operation 1: remove element 1: (2, 3)
+Operation 2: remove element 2: (3)
+Operation 3: remove element 3: ()
+
+
+Last date to submit the solution 23:59 (UK Time) Sunday 6th August 2023.
|
