diff options
| author | Mohammad Sajid Anwar <Mohammad.Anwar@yahoo.com> | 2025-06-08 23:00:27 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-06-08 23:00:27 +0100 |
| commit | 14829a58811688c318cdca7c2985cd72178093ce (patch) | |
| tree | fc85debcf9231de77a4284fd4f70b8ad3a41df54 /challenge-324 | |
| parent | 8090e2dfa2afa2127025fe33be7e8089a33e5b34 (diff) | |
| parent | 1dfc39b89c5057a99e44b6b1ee11987312c32de1 (diff) | |
| download | perlweeklychallenge-club-14829a58811688c318cdca7c2985cd72178093ce.tar.gz perlweeklychallenge-club-14829a58811688c318cdca7c2985cd72178093ce.tar.bz2 perlweeklychallenge-club-14829a58811688c318cdca7c2985cd72178093ce.zip | |
Merge pull request #12145 from MatthiasMuth/muthm-324
Challenge 324 Task 1 and 2 solutions in Perl by Matthias Muth
Diffstat (limited to 'challenge-324')
| -rw-r--r-- | challenge-324/matthias-muth/README.md | 358 | ||||
| -rw-r--r-- | challenge-324/matthias-muth/blog.txt | 1 | ||||
| -rwxr-xr-x | challenge-324/matthias-muth/perl/ch-1.pl | 124 | ||||
| -rwxr-xr-x | challenge-324/matthias-muth/perl/ch-2.pl | 61 |
4 files changed, 395 insertions, 149 deletions
diff --git a/challenge-324/matthias-muth/README.md b/challenge-324/matthias-muth/README.md index fd768ecbc4..e662bafcee 100644 --- a/challenge-324/matthias-muth/README.md +++ b/challenge-324/matthias-muth/README.md @@ -1,223 +1,283 @@ -# Decrement Your Tax Amount. +# Reduce and Reduce, and Reduce! -**Challenge 323 solutions in Perl by Matthias Muth** +**Challenge 324 solutions in Perl by Matthias Muth** -## Task 1: Increment Decrement +## Task 1: 2D Array -> You are given a list of operations.<br/> -> Write a script to return the final value after performing the given operations in order. The initial value is always 0.<br/> -> ```text -> Possible Operations: -> ++x or x++: increment by 1 -> --x or x--: decrement by 1 ->``` -> +> You are given an array of integers and two integers \$r and \$c.<br/> +> Write a script to create two dimension array having \$r rows and \$c columns using the given array. +> > **Example 1** > > ```text -> Input: @operations = ("--x", "x++", "x++") -> Output: 1 -> -> Operation "--x" => 0 - 1 => -1 -> Operation "x++" => -1 + 1 => 0 -> Operation "x++" => 0 + 1 => 1 +> Input: @ints = (1, 2, 3, 4), $r = 2, $c = 2 +> Output: ([1, 2], [3, 4]) >``` > >**Example 2** > >```text -> Input: @operations = ("x++", "++x", "x++") -> Output: 3 +> Input: @ints = (1, 2, 3), $r = 1, $c = 3 +> Output: ([1, 2, 3]) > ``` > > **Example 3** > > ```text ->Input: @operations = ("x++", "++x", "--x", "x--") -> Output: 0 -> -> Operation "x++" => 0 + 1 => 1 ->Operation "++x" => 1 + 1 => 2 -> Operation "--x" => 2 - 1 => 1 -> Operation "x--" => 1 - 1 => 0 +>Input: @ints = (1, 2, 3, 4), $r = 4, $c = 1 +> Output: ([1], [2], [3], [4]) > ``` +We have to walk through the input array steps of size `$c`, +copying row after row to an output array.<br/> + +I started with a solution that is easy to implement. +Then I tried to **reduce the code** +(with the intention to keep it easy to understand), +and to **reduce the runtime**. -I simplify the task specification by saying that -'anything containing `++`' increments the result value, -and 'anything containing `--`' decrements the result value. -Using regular expressions, of course, -and a combination of `sum0` and `map`, -this makes my solution even shorter: +My first solution uses the `natatime` function from `List::MoreUtils`, +which is built for just that: +walking through an array or a list +and extracting multiple entries in every iteration (hence the name, '$n$ at a time') . +The function creates an iterator, which we then use in a loop.<br/> +Quite simple: ```perl use v5.36; +use List::MoreUtils qw( natatime ); + +sub two_d_array_natatime( $ints, $r, $c ) { + my @matrix; + my $iterator = natatime $c, $ints->@*; + while ( my @row = $iterator->() ) { + push @matrix, [ @row ]; + } + return @matrix; +} +``` + +Then I started to think:<br/> +Calling the iterator function might be quite an overhead +over just keeping an index ourselves +and locating and returning a number of elements. +We still can use Perl's nice array slice operator +to extract a whole row of elements 'at a time'. + +If we loop over the row indexes using `$_`, +each row's data start at index `$_ * $c`, +and it will end just before the next row, +which is at `( $_ + 1 ) * $c - 1` .<br/> +We also can use `map` instead of a `for` loop and `push`, +effectively eliminating the need of a result variable. +The row indexes run from `0` to `$r - 1 `. -use List::Util qw( sum0 ); +That looks like this: -sub increment_decrement( @operations ) { - return sum0( map /\Q++/ ? +1 : /--/ ? -1 : 0, @operations ); +```perl +sub two_d_array_map( $ints, $r, $c ) { + return map [ $ints->@[ $_ * $c .. ( $_ + 1 ) * $c - 1 ] ], 0 .. $r - 1; } ``` -`sum0` returns a value of `0` when there are no values to sum up at all, -which should be the correct behaviour for our little solution -when the list of operations is empty. -The `\Q` in the `/\Q++/` pattern serves to quote everything -from that point onward, -which removes the special meaning of the `+` signs -within the regular expression, and makes it nicer to read. +This **reduces our code** a lot, +and I think it has not turned into anything unreadable. + +At the same time, as very often, this also **reduces the runtime** +by avoiding several overheads.<br/> +I have run some small benchmarks, +for matrix sizes of 2x2, 4x4, 10x10, 100x100, and even 1000x1000: + +```text +array size: 2 x 2 + Rate natatime map +natatime 635496/s -- -47% +map 1205315/s 90% -- +array size: 4 x 4 + Rate natatime map +natatime 395930/s -- -37% +map 631484/s 59% -- +array size: 10 x 10 + Rate natatime map +natatime 127245/s -- -32% +map 188504/s 48% -- +array size: 100 x 100 + Rate natatime map +natatime 2045/s -- -39% +map 3329/s 63% -- +array size: 1000 x 1000 + Rate natatime map +natatime 21.0/s -- -43% +map 37.0/s 76% -- +``` + +It shows that our Perlish home-brewed one-line-of-code solution +beats `natatime` at any array size.<br/> +I find it interesting that for medium sizes, the two solutions approach each other with respect to performance, but then with larger sizes, `map` performs better again.<br/> +I cannot really explain this effect. +I would have guessed that for bigger matrices, most time is spent for really copying the data, so the way of controlling it becomes less important in general. + +A quite simple task, yet a lot to learn. + -And that's it! -## Task 2: Tax Amount +## Task 2: Total XOR -> You are given an income amount and tax brackets.<br/> -> Write a script to calculate the total tax amount. +> You are given an array of integers.<br/> +> Write a script to return the sum of total XOR for every subset of given array. > > **Example 1** > > ```text -> Input: $income = 10, @tax = ([3, 50], [7, 10], [12,25]) -> Output: 2.65 +> Input: @ints = (1, 3) +> Output: 6 > -> 1st tax bracket upto 3, tax is 50%. -> 2nd tax bracket upto 7, tax is 10%. -> 3rd tax bracket upto 12, tax is 25%. +> Subset [1], total XOR = 1 +> Subset [3], total XOR = 3 +> Subset [1, 3], total XOR => 1 XOR 3 => 2 > -> Total Tax => (3 * 50/100) + (4 * 10/100) + (3 * 25/100) -> => 1.50 + 0.40 + 0.75 -> => 2.65 +> Sum of total XOR => 1 + 3 + 2 => 6 >``` > >**Example 2** > >```text -> Input: $income = 2, @tax = ([1, 0], [4, 25], [5,50]) -> Output: 0.25 +> Input: @ints = (5, 1, 6) +> Output: 28 > ->Total Tax => (1 * 0/100) + (1 * 25/100) -> => 0 + 0.25 -> => 0.25 -> ``` +>Subset [5], total XOR = 5 +> Subset [1], total XOR = 1 +> Subset [6], total XOR = 6 +> Subset [5, 1], total XOR => 5 XOR 1 => 4 +> Subset [5, 6], total XOR => 5 XOR 6 => 3 +> Subset [1, 6], total XOR => 1 XOR 6 => 7 +> Subset [5, 1, 6], total XOR => 5 XOR 1 XOR 6 => 2 +> +>Sum of total XOR => 5 + 1 + 6 + 4 + 3 + 7 + 2 => 28 +> ``` > > **Example 3** > > ```text ->Input: $income = 0, @tax = ([2, 50]) -> Output: 0 +>Input: @ints = (3, 4, 5, 6, 7, 8) +> Output: 480 > ``` -I think my solution this week is less 'elegant' than I would wish for.<br/> -What I hope, though, is that it is still readable and comprehensible -even without a lot of comments.<br/> -I put in some effort to choose good variable names, -and to avoid overloaded or too long statements. +There are these things we need to implement: -So let's calculate taxes! +* get all possible subsets of the input data, +* compute the XOR sum of each subset, +* sum up and return the XOR sums. -We loop over the tax brackets in order, -and cumulate the partial amounts from each bracket in a variable `$tax_amount`. +For the first part, getting all subsets of a given set of elements, +it is easiest to use an existing and well tested piece of software from CPAN. +I used the `subsets` function from `Algorithm::Combinatorics` +for my first solution. +It returns an iterator that delivers the next subset with every call, +with the empty subset being the last. +The empty subset can then be used as an end criteria for the loop. -In each iteration, we need to decide what part of the income -falls into the current bracket. -We always need the lower limit of the bracket for this, -which happens to be the upper limit of the previous bracket. -So we use a variable `$prev_bracket` -to transport that limit from iteration to iteration.<br/> -Both variables are initialized to zero: +Simple as this: ```perl - my ( $tax_amount, $prev_bracket ) = ( 0, 0 ); -``` - -Within the loop, if the bracket's lower limit -(which we have in `$prev_bracket`) is higher than the income, -we are done, and we can exit the loop right away.<br/> -If not, we assign the two values of the current tax bracket to variables, -to be clear about what they mean: - -```perl - for ( $tax->@* ) { - last if $income <= $prev_bracket; - my ( $bracket, $percentage ) = $_->@*; -``` - -Now we can determine the amount that will be taxed -using the current bracket and percentage.<br/> -If the income is higher than the bracket's upper limit, -the complete bracket interval is taxed, -from the lower limit all the way up to the upper limit.<br/> -If the income is lower than the upper limit, -we only tax the amount up to there.<br/> -To make it easy, -we first determine the amount up to which we apply the tax: +use v5.36; +use Algorithm::Combinatorics qw( subsets ); -```perl - my $bracketed_amount = $income < $bracket ? $income : $bracket; +sub total_xor( @ints ) { + my $sum = 0; + my $iterator = subsets( \@ints ); + while ( my $subset = $iterator->next ) { + $sum += ...; # Add XOR sum of the current subset. + } + return $sum; +} ``` -Based on that, we can add the partial tax amount to the cumulated sum: +**Reduce to the XOR sum** -```perl - $tax_amount += ( $bracketed_amount - $prev_bracket ) * $percentage; -``` +But how do we compute the XOR sum of a list of numbers? -In fact, we should divide by 100 to get the correct amount. -But we will postpone that division -until all the partial amounts have been summed up. +Traditional thinking would use a loop for this.<br/> +But let's think a bit more 'functional' today.<br/> +The **`reduce`** function is the perfect choice +for walking through a list and building up a final result +by combining elements one by one.<br/> +Its code block combines the current partial result +(in the `$a` special variable) with the next element (in `$b`). +In the end, all the elements will have been combined. -At the end of the loop definition, -we store the bracket limit for the next round: +We start with a `0` as the first element, +to not get an `undef` result for the empty subset.<br/> +Our implementation of an 'XOR sum' function is simply this: ```perl - $prev_bracket = $bracket; - } + reduce { $a ^ $b } 0, @elements ``` -Once the loop is done, we return the cumulated tax amount, -not forgetting the division by 100: +So here is the complete (first) solution: ```perl - return $tax_amount / 100; +use v5.36; +use Algorithm::Combinatorics qw( subsets ); +use List::Util qw( reduce ); + +sub total_xor( @ints ) { + my $sum = 0; + my $iterator = subsets( \@ints ); + while ( my $subset = $iterator->next ) { + $sum += reduce { $a ^ $b } 0, $subset->@*; + } + return $sum; +} ``` -This concludes the solution: +**BONUS: Roll your own subset iterator:** + +I was wondering how difficult it would be to implement the `subsets` iterator myself.<br/>Actually it isn't! + +The theory behind creating all possible subsets is this: + +Every possible subset of a set with $n$ elements is determined +by which of the elements are contained or not.<br/> +If we use one bit $b_i$ to represent whether element $i$ is contained or not, +the bits form a number between $0$ (for the empty set) and $2^n - 1$ +(all bits set, all elements contained in that subset, which is the set itself).<br/> +We can enumerate the subsets using that number, calling it a 'subset id'<br/> +The overall number of possible subsets is $2^n$. + +For the creation of our iterator, +we initialize a `$subset_id` variable with $2^n$.<br/> +I know, that's one too high, +but it's actually easier to pre-decrement this variable within the iterator:<br/> +This way, we can at the same time determine whether we have +reached the end of the possible subsets, return `undef` if so, +and continue to directly return the result if not.<br/> +If the `$subset_id` already is zero upon entry, +we have returned the last subset (the empty one) in the previous call. +We then set the `$subset_id` to `undef`, +and return that value from this and all following calls to this iterator. + +If we are still good, we determine the elements of the subset, +based on the bits set or not set in `$subset_id`.<br/> +This is done by `map`ping the element indexes to a one-bit +shifted into the corresponding position, +and doing a bitwise And with the subset_id. +If the bit is set, +we include the element into the anonymous array that will be returned. + +So, here is my bonus subset iterator. ```perl -use v5.36; - -sub tax_amount( $income, $tax ) { - my ( $tax_amount, $prev_bracket ) = ( 0, 0 ); - for ( $tax->@* ) { - last if $income <= $prev_bracket; - my ( $bracket, $percentage ) = $_->@*; - my $bracketed_amount = $income < $bracket ? $income : $bracket; - $tax_amount += ( $bracketed_amount - $prev_bracket ) * $percentage; - $prev_bracket = $bracket; - } - return $tax_amount / 100; +sub subset_iterator( $ints ) { + my $subset_id = 1 << $ints->@*; + return sub() { + return defined ( $subset_id ? --$subset_id : undef ) + && [ map $subset_id & 1 << $_ ? $ints->[$_] : (), keys $ints->@* ]; + }; } ``` -In the real world, I would probably consider -doing correct rounding of the result.<br/> -But correct rounding in financial applications? That is a Pandora's Box ... - -I would probably also wonder what happens -when the income is higher than the last bracket.<br/> -Can we specify an upper limit of 'infinity'? - -I think that 'in the real world', -the tax brackets would not be defined like -'*up to this amount*, this percentage is applied', -but like like -'*from this amount onward*, this percentage is applied'. -The first bracket would always start from zero, -and the last bracket would be valid for any income -that is larger than that bracket's limit, -no matter how high the income is. - -But hey! Nice challenge! +Just make sure that your set doesn't contain more than 63 elements... ;-) + #### **Thank you for the challenge!** diff --git a/challenge-324/matthias-muth/blog.txt b/challenge-324/matthias-muth/blog.txt new file mode 100644 index 0000000000..0f8f9309f4 --- /dev/null +++ b/challenge-324/matthias-muth/blog.txt @@ -0,0 +1 @@ +https://github.com/MatthiasMuth/perlweeklychallenge-club/tree/muthm-324/challenge-324/matthias-muth#readme diff --git a/challenge-324/matthias-muth/perl/ch-1.pl b/challenge-324/matthias-muth/perl/ch-1.pl new file mode 100755 index 0000000000..3692734e1e --- /dev/null +++ b/challenge-324/matthias-muth/perl/ch-1.pl @@ -0,0 +1,124 @@ +#!/usr/bin/env perl +# +# The Weekly Challenge - Perl & Raku +# (https://theweeklychallenge.org) +# +# Challenge 324 Task 1: 2D Array +# +# Perl solution by Matthias Muth. +# + +use v5.36; + +use List::MoreUtils qw( natatime ); + +sub two_d_array_natatime( $ints, $r, $c ) { + my @matrix; + my $iterator = natatime $c, $ints->@*; + while ( my @row = $iterator->() ) { + push @matrix, [ @row ]; + } + return @matrix; +} + +sub two_d_array_map( $ints, $r, $c ) { + return map [ $ints->@[ $_ * $c .. $_ * $c + $c - 1 ] ], 0 .. $r - 1; +} + +# Other implementations for benchmarking: + +sub two_d_array_1_for( $ints, $r, $c ) { + my @matrix; + for ( my $i = 0; $i < $ints->@*; $i += $c ) { + push @matrix, [ $ints->@[ $i .. $i + $c - 1 ] ]; + } + return @matrix; +} + +sub two_d_array_2_incr( $ints, $r, $c ) { + my @matrix; + my $i = 0; + for ( 1 .. $r ) { + push @matrix, [ $ints->@[ $i .. $i + $c - 1 ] ]; + $i += $c; + } + return @matrix; +} + +sub two_d_array_4_map_add( $ints, $r, $c ) { + my $i = 0; + return map { $i += $c; [ $ints->@[ $i - $c .. $i - 1 ] ] } 1..$r; +} + +sub two_d_array_5_dyn_range( $ints, $r, $c ) { + my $i = 0; + # Could be a Perl bug! + # Just '$i' as the starting point doesn't work! + # It has to be an expression! + return map [ $ints->@[ 0+$i .. ( $i += $c ) - 1 ] ], 1..$r; +} + +# Most interesting! +# And performing very well for anything but real large arrays! +sub two_d_array_6_splice( $ints, $r, $c ) { + my @copy = $ints->@*; + return map [ splice @copy, 0, $c ], 1..$r; +} + +# +# Flexible testing. +# + +use Test2::V0 qw( -no_srand ); +use Data::Dump qw( pp ); + +my $sub_name = "two_d_array"; +my @tests = ( + [ "Example 1:", [ [1 .. 4], 2, 2 ], [ [1, 2], [3, 4] ] ], + [ "Example 2:", [ [1, 2, 3], 1, 3 ], [ [1, 2, 3] ] ], + [ "Example 3:", [ [1 .. 4], 4, 1 ], [ [1], [2], [3], [4] ] ], +); + +# This runs the tests not only for the sub named "$sub_name", +# but also for all variants with any suffix ("$subname<suffix>"). +for my $sub ( sort grep /^${sub_name}/, keys %:: ) { + note "\n", "Testing $sub:\n", "\n"; + for ( @tests ) { + my ( $descr, $input, $expected ) = $_->@*; + $descr .= " $sub" . pp( $input->@* ) . " == " + . pp $expected->@* + if substr( $descr, -1, 1 ) eq ":"; + no strict 'refs'; + is [ $sub->( $input->@* ) ], $expected, $descr; + } +} + +done_testing; + +# __END__ + +use Benchmark qw( :all :hireswallclock ); + +my @benchmark_data = ( + [ [1 .. 4], 2, 2 ], + [ [1..16], 4, 4 ], + [ [1..100], 10, 10 ], + [ [1..10000], 100, 100 ], + [ [1..1000000], 1000, 1000 ], +); + +for my $args ( @benchmark_data ) { + say "array size: $args->[1] x $args->[2]"; + cmpthese( -5, { + "natatime" => sub { two_d_array_natatime( $args->@* ) }, + "1_for" => sub { two_d_array_1_for( $args->@* ) }, + "2_incr" => sub { two_d_array_2_incr( $args->@* ) }, + "map" => sub { two_d_array_map( $args->@* ) }, + "4_map_add" => sub { two_d_array_4_map_add( $args->@* ) }, + "5_dyn_range" => sub { two_d_array_5_dyn_range( $args->@* ) }, + "6_splice" => sub { two_d_array_6_splice( $args->@* ) }, + } ); +} + +exit 0; + diff --git a/challenge-324/matthias-muth/perl/ch-2.pl b/challenge-324/matthias-muth/perl/ch-2.pl new file mode 100755 index 0000000000..262f7f8023 --- /dev/null +++ b/challenge-324/matthias-muth/perl/ch-2.pl @@ -0,0 +1,61 @@ +#!/usr/bin/env perl +# +# The Weekly Challenge - Perl & Raku +# (https://theweeklychallenge.org) +# +# Challenge 324 Task 2: Total XOR +# +# Perl solution by Matthias Muth. +# + +use v5.36; + +use Algorithm::Combinatorics qw( subsets ); +use List::Util qw( reduce ); + +sub total_xor_AC( @ints ) { + my $sum = 0; + my $iterator = subsets( \@ints ); + while ( my $subset = $iterator->next ) { + $sum += reduce { $a ^ $b } 0, $subset->@*; + } + return $sum; +} + +sub subset_iterator( $ints ) { + # The number of all possible subsets $n is 2^^(number of elements). + # Each bit in the subset order number corresponds to an element in the + # array. If the bit is 1, the element is included in the subset, if it's 0, + # it isn't. + # We use $n to count down the subset order numbers from $n minus 1 + # (all elements included) down to 0 (empty subset). + my $subset_id = 1 << $ints->@*; + return sub() { + # Pre-decrement $n, set it to undef once we have *passed* zero + # (the empty subset will still be delivered). + # If still defined, create and return the subset, containing exactly + # those elements whose bits in the current subset order number are set. + return defined ( $subset_id ? --$subset_id : undef ) + && [ map $subset_id & 1 << $_ ? $ints->[$_] : (), keys $ints->@* ]; + }; +} + +sub total_xor( @ints ) { + my $sum = 0; + my $iterator = subset_iterator( \@ints ); + while ( my $subset = $iterator->() ) { + $sum += reduce { $a ^ $b } 0, $subset->@*; + } + return $sum; +} + +use Test2::V0 qw( -no_srand ); + +is total_xor( 1, 3 ), 6, + 'Example 1: total_xor( 1, 3 ) == 6'; +is total_xor( 5, 1, 6 ), 28, + 'Example 2: total_xor( 5, 1, 6 ) == 28'; +is total_xor( 3, 4, 5, 6, 7, 8 ), 480, + 'Example 3: total_xor( 3, 4, 5, 6, 7, 8 ) == 480'; + +done_testing; |
