解 Perl Weekly Challenge 092 -- 同構字串與合併範圍

作者:   發佈於:   #raku

Perl Weekly Challenge 092 感覺是有點實用的兩個題目。

TASK #1 › Isomorphic Strings

Submitted by: Mohammad S Anwar

You are given two strings $A and $B.

Write a script to check if the given strings are Isomorphic. Print 1 if they are otherwise 0.

Example 1:

Input: $A = "abc"; $B = "xyz"
Output: 1

Example 2:

Input: $A = "abb"; $B = "xyy"
Output: 1

Example 3:

Input: $A = "sum"; $B = "add"
Output: 0

解 #1 › Isomorphic Strings

從題目所附上的 How to check if two strings are isomorphic 一文的內容得知這「同構字串」的定義。看來是對 $A$B 兩字串,存在有一字符對照表,能將 $A 字串轉換為 $B 字串,且能將 $B 字串轉換為 $A 字串。嗯,看來就是參考一般數學上「同構」的意義。

既然只是要檢查兩字串是否為同構的,那麼只要逐一看過兩字串中每個位置,逐步建立雙向對照表。若在建表途中發生衝突( $a$b$b$a 已在表中 ),則表示 $A, $B 兩字串並非同構的。若無事完成,則兩字串為同構的。

把上述演算法逐步翻譯成 Raku 程式後如下:

sub isomorphic (Str $A, Str $B) {
    my %trans;
    return False unless $A.chars == $B.chars;

    # [1]
    for 0..^$A.chars -> $i {
        my $a = $A.substr($i, 1);
        my $b = $B.substr($i, 1);

        if %trans{"ab"}{$a}:exists and %trans{"ab"}{$a} ne $b {
            return False
        }
        %trans{"ab"}{$a} = $b;

        if %trans{"ba"}{$b}:exists and %trans{"ba"}{$b} ne $a {
            return False
        }
        %trans{"ba"}{$b} = $a;
    }
    return True;
}

其實跟其他語言寫起來也相差無幾,標點符號不同而已。

其中 [1] 處 for 迴圈開頭部分,可以用 .comb 函式與 Z 算符 改寫如下:

    for $A.comb Z $B.comb -> ($a, $b) {
        ...
    }

稍微短了一些。

附帶說明:Raku 中的 for 迴圈支援一次迭代多個元素。只要在後方加上變數即可:

my @S = (1,1,2,3,5,8,13,21,34);
for @S -> $a,$b,$c {
    say "$a, $b, $c";
}
## Output
# 1, 1, 2
# 3, 5, 8
# 13, 21, 34

Z 算符會建出的的是雙層序列,所以得多加一層小括號,才能讓 $a, $b 接到第二層序列之內的元素。沒加那層括號的話,$a, $b 會接到第二層序列本身。可參考下列兩版本之區別。

my @S = (1,1,2,3,5,8,13,21);
my @T = (1,2,3,5,8,13,21,34);

# [2]
for @S Z @T -> $a,$b {
    say "$a, $b";
}

# [3]
for @S Z @T -> ($a,$b) {
    say "$a, $b";
}

## Output of [2]
# 1 1, 1 2
# 2 3, 3 5
# 5 8, 8 13
# 13 21, 21 34

## Output of [3]
# 1, 1
# 1, 2
# 2, 3
# 3, 5
# 5, 8
# 8, 13
# 13, 21
# 21, 34

若想一窺 @S Z @T 之結構,可利用 .gist.raku 函式:

say (@S Z @T).gist;
#=> ((1 1) (1 2) (2 3) (3 5) (5 8) (8 13) (13 21) (21 34))

say (@S Z @T).raku;
#=> ((1, 1), (1, 2), (2, 3), (3, 5), (5, 8), (8, 13), (13, 21), (21, 34)).Seq

TASK #2 › Insert Interval

Submitted by: Mohammad S Anwar

You are given a set of sorted non-overlapping intervals and a new interval.

Write a script to merge the new interval to the given set of intervals. Example 1:

Input $S = (1,4), (8,10); $N = (2,6)
Output: (1,6), (8,10)

Example 2:

Input $S = (1,2), (3,7), (8,10); $N = (5,8)
Output: (1,2), (3,10)

Example 3:

Input $S = (1,5), (7,9); $N = (10,11)
Output: (1,5), (7,9), (10,11)

解 #2 › Insert Interval

將多個範圍重疊部分合併在一起,也算是個簡單的資料壓縮方式。

這題的輸入 $S 內容是已經排序過的,又是沒有互相重疊的。但 $N 加入後,就有可能會跟某處重疊。不確定是否有什麼很有效率的解法,總之先想出了如下的 Raku 程式碼。我將 $S (List) 改用 @S (Array) 來裝,為的是要直接修改其內容(List 的內容是不可變的):

sub insert-intervals (@S is copy, $N) {
    # [1]
    my $i = @S.first(-> $s { $s[0] <= $N[0] <= $s[1] }, :k);
    my $j = @S.first(-> $s { $s[0] <= $N[1] <= $s[1] }, :k);

    # [2]
    if $i.defined {
        @S[$i] = (@S[$i][0], max(@S[$i][1], $N[1]));
    }

    # [3]
    if $j.defined {
        @S[$j] = (@S[$j][0], max(@S[$j][1], $N[1]));

        # [4]
        if $i.defined and @S[$i][0] <= @S[$j][0] <= @S[$i][1] {
            @S[$i] = (@S[$i][0], @S[$j][1]);

            for $i^..$j -> $x {
                @S[$x] = Nil;
            }
        }
    }

    # [5]
    if none($i.defined, $j.defined) {
        @S.push($N);
        @S = @S.sort({ $^a[0] <=> $^b[0] });
    }

    # [6]
    return @S.grep({ .defined });
}

在 [1] 處,檢查 $N 的開頭 $N[0] 與結尾 $N[1] 是否有與 @S 中某個元素重疊。如果有重疊的話,@S[$i] 範圍會包含 $N[0],而 @S[$j] 會包含 $N[1]。如果 $i$j 兩者皆存在,由於 @S 本身是排序過的,此處可以額外確定 $i ≤ $j。依 $i$j 的存在與否,執行 [2] 到 [5] 各處不同的程式碼。

在 [2] 處,如果 @S[$i] 包含 $N[0],就將 @S[$i] 本身擴大。

在 [3] 處,如果 @S[$j] 包含 $N[1],就將 @S[$j] 本身擴大。

在 [4] 處,如果 $i$j 皆存在,表示自 @S$i..$j 這段區間會有重疊。此時,將所有重疊部分合併至 @S[$i],其餘位置清空。

在 [5] 處,處理 $i$j 皆不存在、也就是 $N@S 完全沒有重疊部分的狀況。

在 [6] 處,則是把 @S 中空出來的位置給去除掉。

有不少狀況要處理,多半並不是最佳解。但在有什麼新的想法之前,就先到此為止。