Perl Weekly Challenge 191: Twice Largest and Cute List

These are some answers to the Week 191 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 November, 20, 2022 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: Twice Largest

You are given list of integers, @list.

Write a script to find out whether the largest item in the list is at least twice as large as each of the other items.

Example 1

Input: @list = (1,2,3,4)
Output: -1

The largest in the given list is 4. However 4 is not greater than twice of every remaining elements.
1 x 2 < 4
2 x 2 > 4
2 x 3 > 4

Example 2

Input: @list = (1,2,0,5)
Output: 1

The largest in the given list is 5. Also 5 is greater than twice of every remaining elements.
1 x 2 < 5
2 x 2 < 5
0 x 2 < 5

Example 3

Input: @list = (2,6,3,1)
Output: 1

The largest in the given list is 6. Also 6 is greater than twice of every remaining elements.
2 x 2 < 6
3 x 2 < 6
1 x 2 < 6

Example 4

Input: @list = (4,5,2,3)
Output: -1

The largest in the given list is 5. Also 5 is not greater than twice of every remaining elements.
4 x 2 > 5
2 x 2 < 5
3 x 2 > 5

Our task is to find out whether the largest item in the list is at least twice as large as each of the other items. This is not the same thing as twice larger, which may be construed to mean larger than twice each other item. In other words, we need to use >=, not >. For example, in example 1, we have:

2 x 2 > 4

which is obviously wrong. Similarly, in example 3, 3 x 2 < 6 is also wrong. The required output provided with the examples is in line with this understanding, in spite of the somewhat erroneous notation in the explanations.

Twice Largest in Raku

We only need to compare the largest item with twice the second largest item. Since the lists of integers are very small, we can just sort the item (in descending order) and work with the first two items in the list.

Note that we lazily use sort to find the two largest items of the list; as already discusses in PWC 189 and elsewhere, this is not the best algorithmic method (and it might not be good for very long lists), but it is the fastest to develop. Saving development time is sometimes more important than saving a few CPU cycles. The resulting code is fairly concise and very easy to understand.

sub is-twice-as-large (@input) {
    my @sorted = reverse sort @input;
    return @sorted[0] >= 2 * @sorted[1];
}
for <1 2 3 4>, <1 2 0 5>, <2 6 3 1>, <4 5 2 3> -> @test {
    say @test, " -> ", is-twice-as-large(@test) ?? 1 !! -1;
}

This script displays the following output:

$ raku ./twice.raku
(1 2 3 4) -> -1
(1 2 0 5) -> 1
(2 6 3 1) -> 1
(4 5 2 3) -> -1

Twice Largest in Perl

This is a port to Perl of the Raku program above. The explanations and comments made above also apply here.

use strict;
use warnings;
use feature qw/say/;

sub is_twice_as_large {
    my @sorted = sort { $b <=> $a } @_;
    return $sorted[0] >= 2 * $sorted[1];
}
for my $test ( [<1 2 3 4>], [<1 2 0 5>], 
               [<2 6 3 1>], [<4 5 2 3>] ) {
    say "@$test -> ", is_twice_as_large(@$test) ? 1 : -1;
}

This script displays the following output:

$ perl twice.pl
1 2 3 4 -> -1
1 2 0 5 -> 1
2 6 3 1 -> 1
4 5 2 3 -> -1

Task 2: Cute List

You are given an integer, 0 < $n <= 15.

Write a script to find the number of orderings of numbers that form a cute list.

With an input @list = (1, 2, 3, .. $n) for positive integer $n, an ordering of @list is cute if for every entry, indexed with a base of 1, either

1) $list[$i] is evenly divisible by $i or 2) $i is evenly divisible by $list[$i]

Example

Input: $n = 2
Ouput: 2

Since $n = 2, the list can be made up of two integers only i.e. 1 and 2.
Therefore we can have two list i.e. (1,2) and (2,1).

@list = (1,2) is cute since $list[1] = 1 is divisible by 1 
and $list[2] = 2 is divisible by 2.

I wish we had a non-trivial example with more items in the input list, but my understanding is as follows: for a given integer $n, we first build the list of positive integers from 1 to $n and then check every permutation of the list to verify whether it is a cute list, as defined by the two divisibility properties stated above.

Cute List in Raku

The count-cute subroutine generates all permutations of the input list and, for each permutation, calls the is-cute subroutine to figure out whether such permutation is a cute list.

For the purpose of testing, we run the count-cute subroutine for every integer between 1 and 10. We did no go further because the program is becoming very slow for even moderately large input values: the number of permutations of a list grows as the factorial of the number of its items, so that we have essentially an exponential explosion. The performance is further slowed down by the fact that checking each permutation takes longer when the permutations have more items.

sub is-cute (@list) {
    my @new = (0, @list).flat;
    for 1..@list.elems -> $i {
        return False unless $i %% @new[$i] or @new[$i] %% $i;
    }
    return True;
}

sub count-cute ($k) {
    my $count = 0;
    for (1..$k).permutations -> @perm {
        $count++ if is-cute @perm;
    }
    return $count;
}

for 1..10 -> $j {
    say "$j -> ", count-cute $j;
}

This script displays the following output and timings:

$ time raku ./cute-list.raku
1 -> 1
2 -> 2
3 -> 3
4 -> 8
5 -> 10
6 -> 36
7 -> 41
8 -> 132
9 -> 250
10 -> 700

real    0m36,083s
user    0m0,000s
sys     0m0,031s

Using the Native Calling Interface and replacing the is-cute subroutine by a C is_cute function improves the performance quite a bit, but not enough to solve or even significantly alleviate the combinatorial nightmare:

real    0m26,208s
user    0m0,000s
sys     0m0,015s

Cute List in Perl

This is essentially a port to Perl of the Raku program above. Please refer to the comments above for further information. Since Perl doesn’t have a built-in permutations routine, we build our own recursive permute subroutine. Note that the Perl implementation is about twice faster than the Raku implementation. Raku is still significantly slower than Perl, but the good news is that its performance is slowly catching up.

use strict;
use warnings;
use feature qw/say/;

my @permutations;

sub is_cute {
    my @new = (0, @_);
    for my $i (1.. scalar @_) {
        return 0 if $i % $new[$i] and $new[$i] % $i;
    }
    return 1;
}

sub permute {
    my ($done, $left) = @_;
    if (scalar @$left == 0) {
        push @permutations, $done;
        return;
    }
    my @left = @$left;
    permute([ @$done, $left[$_]], [@left[0..$_-1], @left[$_+1..$#left]]) for 0..$#left;
}

sub count_cute {
    my $k = shift;
    my $count = 0;
    @permutations = ();
    permute([], [1..$k]);
    for my $perm (@permutations) {
        $count++ if is_cute @$perm;
    }
    return $count;
}

for my $j (1..10) {
    say "$j -> ", count_cute $j;
}

This script displays the following output and timings:

$ time perl ./cute-list.pl
1 -> 1
2 -> 2
3 -> 3
4 -> 8
5 -> 10
6 -> 36
7 -> 41
8 -> 132
9 -> 250
10 -> 700

real    0m18,773s
user    0m17,687s
sys     0m0,858s

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 November 27, 2022. 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.