はじめに
みなさん、先日は iOSDC Japan 2024 へのご参加ありがとうございました!
今日は、8/22 の day0 にトラック A で開催された「Swift コードバトル」という対戦型企画について、試合結果を振り返りつつ、それぞれのコードを紹介しようと思います。
Swift コードバトルとは
Swift コードバトルとは、指示された動作をする Swift のソースコードをより短く書けた方が勝ちという 1対1 の対戦コンテンツです。今回の iOSDC Japan 2024 の初企画となります。
本選に先立って 8/12 に予選がおこなわれ、上位8名のプレイヤーが iOSDC Japan 2024 当日の本選に出場しました。
予選
https://iosdc.connpass.com/event/326837
予選は、3つの問題を全員で解き、コードの合計サイズが短い順に順位をつける形式で実施されました。予選順位は以下のとおりです。
- toshi0383 さん
- kntkymt さん
- kishikawakatsumi さん
- S-Shimotori さん
- akkey さん
- Kota1021 さん
- shin-usu さん
- Ryu0118 さん
予選の中から、最終試合の1問を紹介します。
予選第三試合
問題
標準入力の各行に、空白区切りで二つの英単語が並んでいます。
単語どうしがアナグラムになっているかどうか判定し、アナグラムならば「Yes」、そうでないなら「No」を標準出力へ出力してください。
「tea eat」なら「Yes」、「apple google」なら「No」を出力します。
すべての行についてこの手順を繰り返してください。
1位回答 (Kota1021 さん)
同じコードサイズの回答が複数あるため、ここでは最も提出の早かった Kota1021 さんの回答を紹介します。
while let a = readLine()?.split(separator: " ").map {$0.sorted()} {
print(a[0]==a[1] ?"Yes":"No")
}
※今回のシステムでは、スコア計算は空白文字を除いたコードサイズでおこなわれています。
解説
解説というほど解説はできないのですが、作問者からのちょっとした説明と感想を書こうと思います。
アナグラムかどうかの判定は、ソート後の文字列が等しくなるかでおこなえます。これを素早くシンプルに実装したコードが1位回答となりました。データがより大きい場合は、各文字の出現頻度を数えることでより効率的に判定できますが、今回の入力に対しては過剰な最適化であり、コードが長くなってしまうので向いていません。
この問題の感想戦では、空白区切りの文字列の分割を、s.split(separator:" ")
ではなく s.split{$0==" "}
と書くことで短縮するテクニックが発見されました。
本選
準々決勝第一試合 (toshi0383 さん vs. Ryu0118 さん)
問題
標準入力の各行に、英単語が一つずつ書かれています。
奇数番目の文字だけ、偶数番目の文字だけをそれぞれ繋げて、空白区切りで出力してください。
「abcdef」なら「ace bdf」、「xyz」なら「xz y」を出力します。
すべての行についてこの手順を繰り返してください。
toshi0383 さん (スコア: 135, 提出: 13 m 54 s)
while let a = readLine() {
let t = a.enumerated().reduce(("", "")) { c, v in
let e = String(v.1)
return v.0 % 2 == 0 ? (c.0 + e, c.1) : (c.0, c.1 + e)
}
print(t.0 + " " + t.1)
}
Ryu0118 さん (スコア: 200, 提出: 13 m 55 s)
while let l = readLine() {
var i = 0
let a = l.enumerated().reduce(into: [String: String]()) { a, b in
if i % 2 == 0 {
a["0", default: ""].append(b.element)
} else {
a["1", default: ""].append(b.element)
}
i += 1
}
print(a["0"]! + " " + a["1"]!)
}
解説
enumerated()
でインデックスを取りつつ reduce()
で結合していくという基本戦略は同じでしたが、勝利コードはタプルをうまく使って文字数を削減しています。c
が、(ここまでの奇数番目の文字を結合した文字列, ここまでの偶数番目の文字を結合した文字列)
、v
が、(今処理しているインデックス、今処理している文字)
となっています。
準々決勝第二試合 (S-Shimotori さん vs. akkey さん)
問題
標準入力の各行に、空白区切りで整数が一つ以上並んでいます。
一行に含まれる値の中で、最も大きいものと最も小さいものを空白区切りで出力してください。
「1 2 3 4 5」なら「5 1」、「1」なら「1 1」を出力します。
すべての行についてこの手順を繰り返してください。
S-Shimotori さん (スコア: 86, 提出: 13 m 14 s)
while let n = readLine()?.split { $0 == " " }.map { Int($0)! }.sorted() {
print("\(n.last!) \(n[0])")
}
akkey さん (スコア: 88, 提出: 08 m 46 s)
while let r = readLine()?.split(separator: " ").map { Int($0)! } {
print("\(r.max()!) \(r.min()!)")
}
解説
S-Shimotori さんの勝利コードでは、予選に発見された文字列分割のテクニックがさっそく使われています。なお、もし akkey さんが split()
の部分をクロージャで書くテクニックを使っていればスコアは逆転していました。
準々決勝第三試合 (kishikawakatsumi さん vs. Kota1021 さん)
問題
1 から 100 までの FizzBuzz を改行区切りで出力してください。
3の倍数に対して「Fizz」、5の倍数に対して「Buzz」、15の倍数に対して「FizzBuzz」、それ以外に対してその数を出力します。
kishikawakatsumi さん (スコア: 75, 提出: 03 m 57 s)
for i in 1...100 {
print(i % 15 == 0 ? "FizzBuzz" : i % 5 == 0 ? "Buzz" : i % 3 == 0 ? "Fizz" : "\(i)")
}
Kota1021 さん (スコア: 75, 提出: 04 m 59 s)
for i in 1...100 {
print(i%15 == 0 ? "FizzBuzz":i%3==0 ?"Fizz":i%5==0 ?"Buzz": "\(i)")
}
解説
ご存知 FizzBuzz です。この問題に関しては、短縮できそうでそれほど短縮余地のない問題になってしまいました。これは作問のミスだと思います。
両者とも本質的には同じコードに到達し、最終的に提出の早かった kishikawakatsumi さんの勝利となりました。
準々決勝第四試合 (kntkymt さん vs. shin-usu さん)
問題
標準入力の各行に、空白区切りで整数が一つ以上並んでいます。
一行に含まれる値の種類数を出力してください。
「1 2 3 4 5 」なら「5」、「0 1 0 1 0」なら「2」を出力します。
すべての行についてこの手順を繰り返してください。
kntkymt さん (スコア: 55, 提出: 11 m 51 s)
while let a = readLine() {
print(Set(a.split { $0 == " " }).count)
}
shin-usu さん (スコア: 61, 提出: 02 m 24 s)
while let l = readLine()?.split { $0 == " "} {
print("\(Set(l).count)")
}
※スペースは原文ママ
解説
重複除去を Set
でおこないカウントするという考えかたは同じですが、細かいテクニックが勝敗を分けた試合となりました。
readLine()
の返り値を直接その場で加工しないことにより、readLine()
の後ろに必要だった ?
の1バイトを削ることに成功しています。
準決勝第一試合 (toshi0383 さん vs. S-Shimotori さん)
問題
標準入力の各行に、正の整数が一つずつ並んでいます。
与えられた整数が平方数であれば「Yes」そうでなければ「No」を出力してください。
「9」なら「Yes」、「5」なら「No」を出力します。
すべての行についてこの手順を繰り返してください。
toshi0383 さん (スコア: 75, 提出: 02 m 10 s)
while let a = Int(readLine() ?? "") {
print((0...a).contains { $0 * $0 == a } ? "Yes" : "No")
}
S-Shimotori さん (スコア: 75, 提出: 06 m 36 s)
while let n = Int(readLine() ?? "") {
print((1 ... n).contains {
$0 * $0 == n
} ? "Yes" : "No")
}
解説
アルゴリズムは実質的に同じで、圧倒的な提出速度により toshi0383 さんの勝利となりました。
作問時の想定解は、平方根を取って切り捨ててから二乗して元の数と比較するというものでしたが、squareRoot()
や Double
と Int
の変換に文字数を消費し、今回の勝利コードには及ばないようです。
準決勝第二試合 (kntkymt さん vs. kishikawakatsumi さん)
問題
標準入力の各行に、正の整数が一つずつ並んでいます。
与えられた整数を N としたとき、N 番目のフィボナッチ数を出力してください。
「1」なら「1」、「2」なら「1」、「3」なら「2」、「6」なら「8」を出力します。
すべての行についてこの手順を繰り返してください。
kntkymt さん (スコア: 71, 提出: 12 m 16 s)
let f = { b in
b < 2 ? b : f(b - 1) + f(b - 2)
}
while let a = Int(readLine() ?? "") {
print(f(a))
}
kishikawakatsumi さん (スコア: 82, 提出: 05 m 57 s)
while let n = Int(readLine() ?? "") {
print(f(n))
}
func f(_ n: Int) -> Int {
n <= 1 ? n : f(n - 1) + f(n - 2)
}
解説
フィボナッチ数の実装部分が勝敗を分ける結果となりました。kntkymt さんの勝利コードは、クロージャを使うことで関数の場合に必要な func
キーワードなどの文字数を削減することに成功しています。
kntkymt さんが書かれたような再帰的なクロージャは、かつての Swift では書けませんでしたが、現在はコンパイル・実行できるように変わったようです。
決勝 (toshi0383 さん vs. kntkymt さん)
問題
標準入力の各行に、ちょうど5文字からなる英単語が一つずつ並んでいます。
与えられた単語と「iOSDC」のハミング距離を出力してください。大文字と小文字は区別します。
「iOSDC」なら「0」、「CDSOi」なら「4」、「iosos」なら「4」を出力します。
すべての行についてこの手順を繰り返してください。
toshi0383 さん (スコア: 64, 提出: 11 m 24 s)
while let a = readLine() {
print(zip("iOSDC", a).filter { $0 != $1 }.count)
}
kntkymt さん (スコア: 74, 提出: 09 m 54 s)
while let a = readLine() {
print(zip(a, "iOSDC").reduce(0) { $0 + ($1.0 != $1.1 ? 1 : 0) })
}
解説
ハミング距離は、同じ文字数の2つの文字列がどの程度異なるかを表す指標で、同じ位置にある異なる文字の個数を数えて算出されます。
両者のコードともに、2つの文字列を並べて比較するのに zip()
を使って短縮しています。優勝コードは、filter()
をうまく使って異なる文字 (のタプル) だけを取り出し、それをカウントするという方法で書かれています。
優勝に相応しくシンプルかつ美しい回答であり、この先はないと思っていたのですが……
おまけ
決勝戦では、観戦者の方々も同時に同じ問題へ挑戦できるよう即席でシステム改修がおこなわれ、紹介したプレイヤー以外の方も挑戦されていました。優勝した toshi0383 さんのコードを元に、さらに短縮された回答がありましたのでご紹介します。
41772ki さん (スコア: 60, 提出: 12 m 41 s)
while let a = readLine() {
print(zip("iOSDC", a).filter(!=).count)
}
Swift の演算子は関数であり、それを直接 filter()
へ渡すことで { $0 != $1 }
と同等の動作をさせています。非常にコードゴルフらしい解で、これ以上の短縮は難しいでしょう。
※Swift 6 だともう少し短縮余地があります。今回のシステムは Swift 5 なので通りませんが、気になるかたは考えてみてください。
おわりに
まずは toshi0383 さん、優勝おめでとうございます!予選から本選まで、終始早く短く正答されていて、納得の優勝でした。
また、予選時も含めて、今回の企画にプレイヤーとして参加いただいたみなさま、第一回の得体の知れない企画に参戦し、熱い戦いを繰り広げられたみなさまのおかげで、企画を成立させることができました。本当にありがとうございました!
そして、day0 で観戦していただいたみなさま、コーディング対戦の見せ方を運営側も模索しながらの企画となりましたが、多くの方々に来場していただきました。ありがとうございました!
次回……があるかどうかはわかりませんが、もし機会があれば今回の反省を生かしつつ、より楽しめる企画にしたいと思います。
なお、今回の試合の様子は、ニコニコ生放送の day0 トラック A で観ることができます。当日観戦できなかったかたは、是非そちらもご覧ください。