解 PWC 080:以 Raku、Perl、Kotlin

作者:   發佈於: ,更新於:

晚上看了 PWC 080 的兩個題目,以 Raku、Perl、Kotlin 三種語言來試解。原則上是想要不使用 for / while 迴圈,以各語言中提供的 collection 來解成。

以第一題來說,Raku 與 Kotlin 有 Set 這種無序集合物件可用,而且可以一步把列表轉成集合,比起 Perl 而言算是比較能表意。以下是三種語言併列,把命令列參數轉換成「整數」後,做成集合的步驟:

Kotlin

val N = args.map({ it.toIntOrNull() }).filter({ it != null });
val seen = N.toSet();

Raku

my @N = @*ARGS.map({ .Int })
my $seen = @N.Set();

Perl

my @N = map { int($_) } @ARGV;
my %seen = map { $_ => 1 } @N;

寫 Perl 寫熟練了,很容易就能看出 map { $_ => 1 } 就是要拿來做出 HashSet 物件來用。不過,相對於其他兩都似乎就是少了些字面上的線索。

第二題的題目是要發糖果給 N 個人,先每人發一顆,再根據輸入 @N 陣列的內容多發幾根。說明文看來有點長,但其實不複雜。其規則原文如下:

a) You must given at least one candy to each candidate.
b) Candidate with higher ranking get more candies than their mmediate neighbors on either side.

輸入陣列 @N 的內容數字所代表的是每位人選的權重,若某人之權重數字比其隔壁兩人都高,則該人多得兩根,若只比其中一人高,則多得一顆。

雖然乍看之下對於每人 @N[$i],需要先後檢查 @N[$i - 1]@N[$i + 1],還得同時注意不能超過陣列邊界。但其實有比較容易的處理方式。

如果把這問題轉換成去檢查數字間的「間隔」的話,就比較容易理解,也不必處理邊際條件。先把兩間隔數字之間的關係寫出來如下:

1 < 2 < 3 > 1 = 1 = 1

然後由左至右逐一看過符號,只要符號是 =,就表示左右兩方的人選都不會因此而多拿到一顆糖果。如果是大於(或小於),則表示其左方(或右方)的人選會多拿到一顆糖果。一個符號會對應到零根或一顆糖果。換句話說,只要符號兩端的數字彼此不相等(符號不是 =),總糖果數就要多一顆。

求額外發出的糖果數目這部分解法在各語言對照如下:

Kotlin:

val extra = (1..N.lastIndex).filter({ N[it] != N[it-1] }).size;

Raku:

my $extra = (1..@N.end).grep(-> $i { @N[$i] != @N[$i-1] }).elems;

Perl:

my $extra = grep { @N[$_] != @N[$_-1] } (1..@N-1);

Perl 由於有純量語境,相對於其他兩者少了個最後表示「取得個數」的字樣。

Raku 中用來表示陣列最尾端索引的函式為 .end,跟 Kotlin 採用的 .lastIndex 比起來短了不少。不知會不會被誤認為其意義是「最後一個元素內容」。附帶一提,陣列 N 最後最後一個元素內容,在 Kotlin 中表示為 N.last(),在 Raku 中表示為 @N.tail

這幾個名詞與其對應的意義其實都還算好記。附帶一提,對我而言會常常忘記的是 Kotlin 中 .size 是屬性而非函式,偶爾還是會不小心寫出 N.size() 然後發生編譯錯誤。

完整程式碼在:gugod/perlweeklychallenge-club