Swiftコードバトル2025で優勝しました #iosdc #iwillblog

こんにちは。株式会社メルカリ iOSエンジニアのkntkです。
私kntkは9月19日から9月21日にかけて開催された「iOSDC Japan 2025」のday 0に開催された企画、Swiftコードバトル2025で優勝しました。
iOSDC Japan 2025の運営の皆さんには貴重な体験をさせていただき、本当にありがとうございました。

せっかくの機会なので、この記事では私がどのような戦略やプロセスで参戦したのかを言語化しようと思います。

後日iOSDC Japan 2025運営から頂いた優勝記念品

また、メルカリは「iOSDC Japan 2025」にゴールドスポンサーとして参加しており、その様子はこちらをご確認ください!
iOSDC Japan 2025に参加しました #iosdc #iwillblog

Swiftコードバトルとは

Swiftコードバトルはお題で指示された動作をするSwiftコードをより短く書けた方が勝ち、という競技です。 お題は決して難しいものではなく、少し練習すればSwiftプログラマであればどなたでも参加できる難易度を目指しています。
お題例1: 入力された文字列を逆順にして出力するプログラムを書いてください。
お題例2: 与えられた整数リストの要素の合計を計算するプログラムを書いてください。

Swiftコードバトル2025予選の説明文から引用

一般的にはコードゴルフと呼ばれる競技になります。
昨年のiOSDC Japan 2024から開催されている企画で、今年が第2回目となります。
第1回目は私も参加し準優勝しました。
iOSDC Japan 2024に参加・登壇しました #iosdc #iwillblog

昨年からの変化として、問題の複雑度が上がったように感じました。その結果回答が多様化し、戦略的な立ち回りが求められるようになったと思います。
その影響もあってか、今年は問題文と一緒にサンプルコードが提供されるようになりました。

何をする競技なのか

Swiftコードバトルの問題には文字数短縮余地のある「最適化ポイント」が複数個存在し、我々選手は個々の最適化ポイントに対して文字数短縮方法を考え、文字数短縮を行っています。
問題が複数個の小問で構成されており、小問の合計スコアで競っている、というイメージを私は持っています。(実際には小問が相互に関係しあう場合があり、単純に可分できるわけではない。)

最適化ポイントについて、昨年の決勝問題を例に紹介します。

標準入力の各行に、ちょうど5文字からなる英単語が一つずつ並んでいます。
与えられた単語と「iOSDC」のハミング距離を出力してください。大文字と小文字は区別します。
「iOSDC」なら「0」、「CDSOi」なら「4」、「iosos」なら「4」を出力します。
すべての行についてこの手順を繰り返してください。

// 回答a
while let a = readLine() {
    print(zip("iOSDC", a).filter { $0 != $1 }.count)
}

// 回答b
while let a = readLine() {
    print(zip(a, "iOSDC").reduce(0) { $0 + ($1.0 != $1.1 ? 1 : 0) })
}

この問題には結果的に以下の最適化ポイントがありました。

  1. “iOSDC”と入力にどのようにアクセスするか?
    • a. それぞれにインデックスアクセス
    • b. zipでまとめてからアクセス (最短解)
  2. 異なる文字数をどの様にカウントするか?
    • a. カウンタを持ってfor文でインクリメント
    • b. reduceでカウンタをインクリメント
    • c. filter.count (最短解)

これらの最適化ポイントは問題に明示されていないので、まず最適化ポイントを発見する必要があります。「ここは複数の書き方ができるな」と、一目見ただけで発見できるわかりやすいポイントもあれば、問題を解く中で問題の理解度が上がることで発見できるポイントや、間違い探しのようにじっくり考え込んでやっと発見できるポイントもあります。

最適化ポイントを発見した後は文字数短縮方法を考えて実装します。Swiftの知識と競プロ的な知識を総合的に活用して短縮方法を考える必要があります(詳しくは後述)。

実際の回答を見てみると、回答aだと1, 2両方のポイントで最短解を採用できている一方で、回答bだと2で非最短解であるreduceを採用しており、2.の短縮方法で勝敗が決まった結果になっています。

このように、問題に存在する最適化ポイントを発見し、最適な短縮方法を考えることがこの競技の基本的な作業になると考えています。問題が複雑な場合は全ての最適化ポイントを発見・回答しきれず時間切れになることもあり、最適化ポイントを発見する速さと短縮方法を考える速さも重要になってきます。

サンプルコード vs 0から実装

問題文と一緒にサンプルコードが提供されるようになったことにより、大きな方針として二つの作戦が存在していました。(コードバトル予選でも参加者間で話題になっていました。)

サンプルコードを元に文字数短縮を行う作戦

初期実装コストがなく、個々の最適化ポイントに順に対処していけば安定してスコアを伸ばせる利点があります。その一方で、サンプルコードに存在する冗長な表現の修正に時間を要したり、既存のロジックや構造に縛られて大幅な変更をしにくいなどの欠点があります。

サンプルコードを用いず0からコードを実装する作戦

サンプルコードの構造やロジックに縛られないため、最初から一気に短いプログラムを記述して高いスコアを狙える利点があります。その一方で、初期実装コストが必要な上、テストケースが通らない間違ったコードを書いてしまい、時間を消費する危険性があります。

実際に取った作戦

私はサンプルコードを元に文字数短縮を行う作戦で進めました。理由としては後者に必要となる競プロ的な能力に自信がなかったというのと、前者の安定してスコアを取れる点が複数試合を行う上で有利だと考えたためです。

対戦前の準備

競プロ等でも一般的に有効な手法だと思いますが、傾向を把握し、その対策を行っていました。事前に頻出パターンとその短縮方法(テクニック)調査して知識化し、対戦中はそれらの知識を適用できるようにしていました。これによって、問題に存在する複数の最適化ポイントのうち8-9割程度を事前知識で瞬時に発見して解くことができ、対戦時間を残りの1-2割(初見のパターンや一般化が難しい複雑なパターン)への対処に集中できていました。

具体的にどのような短縮テクニックを調査・利用していたのか、実際の準決勝の問題を例に紹介します。

標準入力の各行に、英字のみからなる文字列が与えられます。キャメルケースをスネークケースに変換し、改行区切りで出力してください。

出題側から提供されるサンプル回答(読みやすさのため一部変更)

while let line = readLine() {
    var result = ""
    for (index, character) in input.enumerated() {
        if character.isUppercase && index > 0 {
            result.append("_")
            result.append(character.lowercased())
        } else {
            result.append(character.lowercased())
        }
    }

    print(result)
}

この問題では次のような文字数短縮テクニックが利用できます。

  • String.append(Character)+=で代替可能
    • RangeReplaceableCollectionの実装由来。StringRangeReplaceableCollectionに準拠しているため。
  • If aaa && bbbif aaa, bbbに代替可能
  • 変数名の一文字化
  • 共通項の抜き出し( r += c.lowercased()

それらのテクニックを利用したプログラム

while let l = readLine() {
    var r = ""
    for (i, c) in l.enumerated() {
        if c.isUppercase, i > 0 {
            r += "_"
        }

        r += c.lowercased()
    }

    print(r)
}

その他、多数のテクニックをドキュメントにまとめておき、対戦前に眺めていました。
また、「この手法では短縮できない」という知識も持っておくことで、対戦中の試行錯誤時におけるノイズを減らしていました。

例を一つ紹介すると

for文より.forEachで書いた方が短い?-> for文の方が常に短い。

for i in a {a.forEach { i inを比較すると後者の方が5文字多くなります。
また、forEachのクロージャーで省略引数名$0を使った場合i inを省略できますが、ここでの省略数は3文字なのでメソッド呼び出し部分だけで結果に2文字多くなり赤字です。

// 8文字
for i in a {
// 13文字
a.forEach { i in
// 10文字
a.forEach {

対戦中

先述の通り、対戦中は冒頭に事前知識を用いて頻出のパターンの文字数短縮を行い、ほとんどの時間を初見パターンや一般化が難しい複雑なパターンの文字数短縮方法を考えることに集中していました。

具体的に文字数短縮方法を考えるプロセスについて紹介すると、
各最適化ポイントについて、文字数短縮方法には主に二つの側面があると考えていました。

  • A: 文法・記法を工夫する
  • B: ロジックを工夫する

A: 文法・記法を工夫する

既存の記述の文法や記法を工夫することで文字数短縮を行う側面です。先述の準決勝での例にあるように変数名を一文字にしたり、String.append(Character)+=に置き換えたりするなどが当たります。

また、予選の問題ではSwiftの型推論や標準ライブラリの関数・メソッドを応用して文字数を減らす場面がありました。(Swiftらしさが強く現れる問題で特に記憶に残っています。)

コードバトル予選、ROT13変換問題の私の回答

while let p = readLine() {
    print(String(p.map { a in
        let k = a.asciiValue! &- 52

        return a.isLetter
        ? .init(
            .init(
                13...38 ~= k
                ? k % 26 + 65
                : (k - 32) % 26 + 97
            )
        )
        : a
    }))
}
  • Character.init(UnicodeScalar.init(...)).init(.init())で代替
    • 型推論によって型名を省略
    • String.initの期待する型([Character])がp.mapの帰り値の型を経由してクロージャーのreturn statementの型推論まで伝播
  • Range.contains~=で代替
    • 標準ライブラリに演算子定義が存在
  • オーバーフロー演算子&-を利用して事前計算
    • 通常の演算子だとランタイムエラーになるテストケースが存在

このように、Swiftの知識(型推論・文法・言語機能・標準ライブラリなど)が活かされる側面です。Appleの公式ドキュメントやTSPL (The Swift Programming Language)を読むことでこの側面の思考力が鍛えられると思います。

B: ロジックを工夫する

既存のロジックをより短い記述のロジックに変えることで文字数短縮を行う側面です。こちらは平たく言えば競プロ的な側面です。ただし、実行速度向上ではなく文字数短縮が目的です。

実は先述した準決勝の私の回答は最短解ではなく、さらにロジックを工夫することで短縮できる余地がありました。(前回優勝者にご指摘いただきました。)

while let l = readLine() {
    var r = ""
    for c in l {
        if c.isUppercase, r != "" {
            r += "_"
        }

        r += c.lowercased()
    }

    print(r)
}

index > 0というのは、つまり「最初以外の要素の時」を意味する条件なので、こちらはr != ""で代替可能なわけです。また、コードバトル予選では有名な「番兵法」を利用することで文字数を大幅に減らすシーンもありました。

このように、問題・プログラムの意図を読みとるのがこちらの側面になり、競プロ的な力が活かされる側面です。競技プログラミングサイト等で過去問を解くことで、この側面の思考力を鍛えることができると思います。

また、AとBの側面は排他的ではなく、Bを適用した後の記述に新たにAが適用できる場合もあり、AとBの組み合わせによって文字数短縮方法が多数存在していました。

戦略

A・Bの例で示したように、より短い解に辿り着くにはAとB両方の側面を組み合わせて解法を考えることが必要になってきます。
しかし、解法を考える際はコードを書いてみないと実際に文字数短縮が可能か分からないことが多く、制限時間の中で試せる解法の候補数も限られてくるため、ある程度解法に”あたり”を付ける必要があります。ここで、私は迷ったらAを優先して攻める戦略を立てました。理由としては私がAの方が得意というのと、「Aの側面を極めた回答の方がSwift特有のテクニックが現れて面白いだろう」と考えたからです。

結果的に決勝でもSwiftの文字列展開を利用した手法を選択し、Swiftらしいコードで優勝ができたのは良かったなと考えています。

アスキーアートを出力する決勝問題の私の回答(見やすさのため一部スペースを省略)

let a = "######", i = a + a, f = "\(i)##", j = f + f (中略)
print(
"""
\(f)                                    \(j)
\(f)                                    \(j)
                      ##                    \(a)              \(a)
                      ##                    \(a)              \(a)
\(a)####         ##              \(a)          \(f+a)            \(a)
\(a)####         ##              \(a)          \(f+a)            \(a)
(中略)
"""
)

まとめ

Swiftコードバトルは問題に存在する最適化ポイントを発見し、Swiftの知識と競プロ的な知識を総合的に活用して最適な短縮方法を考える競技です。制限時間があるため、事前に頻出パターンの対策を行うことによって対戦時間を有効に活用することができます。

Swiftコードバトルのテクニックは一見奇妙で役に立たなそうに見えるかもしれません。しかしその背景にはSwiftの文法・言語機能の知識やロジックを読みとる力などが隠れており、それらは日々の開発でも有益なものだと考えています。自分も参戦する中で多くを学ぶことができました。

また、参戦することで対戦中により良い解法を思いついた際の爽快感や、逆に思い付けなかった際の悔しさを味わうことができ、Swiftコードバトルをより深く楽しむことができます。是非皆さんも来年(開催された場合は)参戦してみてはいかがでしょうか。

最後に、改めてiOSDC Japan 2025・Swiftコードバトル2025 の運営の皆さん、予選・本戦で対戦してくれた選手の皆さん、本当にありがとうございました! #iosdc #iwillblog

  • X
  • Facebook
  • linkedin
  • このエントリーをはてなブックマークに追加