4
2

More than 1 year has passed since last update.

AtCoder に Swift5.2 で挑戦する ための準備(主に入出力)

Last updated at Posted at 2020-04-16

はじめに

これまでACoderで使用できるSwiftのバージョンは2.2とかなり古く、はっきり言ってこれを使う気は起きず、Kotlinでコードを書いていました。しかし、2020.4.12に実施されたABC162からオンラインジャッジ環境が一新され、Swiftもついに5.2.1にアップされました。 1
これを機にSwiftで挑戦しようかと考え、入出力周りの便利な関数を揃えいこうと思います。

ちなみに、私のローカル環境はApple Swift version 5.2 (swiftlang-1103.0.32.1 clang-1103.0.32.29)です。

私が思うKotlinの欠点

Kotlinの欠点

Swift2.2と比べると、Kotlinの方が何倍もイケてると思いますが、私が思うKotlinの最大の欠点は『配列の要素数にLong(型)を指定するとコンパイルエラーになる』『配列の添字にLong(型)を指定するとコンパイルエラーになる』ことです。添字の範囲を超えているならいざ知らず、コンパイル時にLongだからと言ってエラーにするとは・・・面倒です。
Kotlinの整数型変換はlong.toInt()でLongからIntへ、int.toLong()でIntからLongへ、簡単に書けます。また、配列の添字にLongを指定できるExtention(演算子オーバーロード)を書けば吸収できることも知っていますが、『Int32ビットでそれ以上の整数を扱う場合は、64ビットのLongと使い分ける必要がある』であり、これで何度もWAを喰らいました。正直面倒です。(愚痴)
(SwiftのIntは環境依存となっていますが、私のローカル環境もAtCoderの新ジャッジ環境とも64ビットです)

標準入力

AtCoderではすべてのプログラムは標準入力からデータを受け取り、結果を標準出力に出力します。俗に言うstdin、stdoutというやつです。
SwiftではFileHandle.standardInputが標準入力のファイルハンドルで、これを使うと次の2種類の方法でデータを入力できます。

let fileHandle = FileHandle.standardInput
let contentData = fileHandle.readDataToEndOfFile()       //その1 
let contentData = fileHandle.readData(ofLength: バイト長) //その2
let contentString = String(data: contentData, encoding: .utf8)! 

1番目の方法はファイルをEOFまで全部一度に読み込みます。2番目は入力するサイズを指定する方法です。
一般的にAtCoderの入力データはすべて文字形式が決まっているものの、例えば1行のデータ長が何バイトなのかなどは分かりませんし、テストケースによっては入力データのサイズが数メガバイトであることもあります。また、一度に読み込んだとしても、その後に改行コードで区切ったり、1行の中をスペースで区切ったりする必要があります。よって、私が考える入力方法の最善は上記のいずれでもなく、readLineメソッドを使うことです。シンプルに1行を入力する方法ですね。(JavaScannerクラス相当がSwiftにもあればいいのでしょうが)

AtCoderを始めとする競プロの場合は、まずここからですから、このreadLineの使い方に関しては、これまで多くの方が記事を書かれています。2 3 4

AtCoderでの入力データの代表的な形式は、次の3パターンです。
1. 1行で数値を1つだけ受け取る
2. 1行で数値を24個受け取る
3. 1行で文字列を1つ受け取る

また、問題データ1-indexedで与えられることが多いですが、プログラム上は0-indexedの方が都合が良かったりします。配列も0始まりですし。そこで、私が準備したメソッドは次の3パターンです。順番に説明します。

パターン1(1つシリーズ)
func getString() -> String { readLine()! }
func getInt() -> Int { Int(getString())! }
func getDouble() -> Double { Double(getString())! }

上記は一目瞭然なので説明は省略します。

パターン2(複数シリーズ)
func listOfString() -> [String] { getString().split(separator: " ").map { String($0) } }
func listOfInt(_ offset: Int = 0) -> [Int] { listOfString().map { Int($0)! + offset } }
func listOfDouble(_ offset: Double = 0.0) -> [Double] { listOfString().map { Double($0)! + offset } }

func listOfInt(_ offsets: Int ...) -> [Int] { listOfString().enumerated().map { Int($0.element)! + offsets[$0.offset % offsets.count] } }

let A = listOfInt(-1)のようにoffsetを指定すれば、一律に+/-できます。Doblue型でこれを使う場面は無いかも知れませんが、省略可能ですから(そもそもDoubleを入力すること自体が無いかも)。
パターン2は数列のように配列として受け取る場合ですが、タプルで受け取りたい場合があります。例えば平面座標の大きさのH,Wとか、グラフの頂点と辺の数N,Mとか。そこで、最後にパターン3です。

パターン3(タプルシリーズ)
extension Array where Element == Int {
    func toTupple() -> (Int, Int) { (self[0], self[1]) }
    func toTupple() -> (Int, Int, Int) { (self[0], self[1], self[2]) }
    func toTupple() -> (Int, Int, Int, Int) { (self[0], self[1], self[2], self[3]) }
}

Int配列のextensionとして実装しました。こうすることで、パターン2との組み合わせが実現できます。例えば、
let (u, v, c) = listOfInt(-1, -1, 0).toTupple() と書くと、「グラフ問題の辺(を結ぶ頂点)と辺のコスト」を(0-indexedにして)簡単に受け取ることができます。

@y_mazunさんの記事 5 で演算子(オーバーロード)でタプルを受け取れる実装を公開しておられます。これも便利ですね。パターン3もそれに倣うか今後考えます。

標準出力

標準出力は結果の出力先で、入力に比べると至ってシンプルです。大体は数値や文字列を1つ出力するパターンが多く、これはシンプルにprintメソッドで書きます。

パターン1
var ans = 0
   :    :
print(ans)

printはデフォルトで改行コードが付加されるので、C++のように\nを付ける必要はありません。逆に\nを付けたく無い場合は、print(・・・・, terminator: "")と書くことができますが、AtCoderでは最後に改行コードが必須です。
また、23個の数値を横に並べて出力する場合は、次のように書きます。

パターン2
var a = 0, b = 0, c = 0
   :    :
print(a, b, c)

これでa b cのようにスペース区切りで横に並び、最後に改行コードが付加されます。改行区切りで縦に並べたい場合は、print(a, b, c, separator: "\n") とすることで可能です。

最後のパターンは、横もしくは縦(複数行)に多数の数値を出力する場合で、以下のように書きます。

パターン3
var ans = [Int]()
   :    :
print(ans.map(String.init).joined(separator: " "))  //スペース区切り横並び
print(ans.map(String.init).joined(separator: "\n")) //改行区切り(縦並び)

大事なことは、I/Oは遅いのでprintをループさせないことです。過去に数値を最大20万行出力するという問題 6 もありました。Int20万行出力するときに、printをループさせた場合と、joinedしてprint1回の場合を性能を比較してみました。

テストコードを表示
テストコード
import Foundation

let main: () = {
    let N = 200000
    let A = ([Int])(0 ..< N)

    //Loop
    let t1 = measureTimeSeconds {
        for a in A { print(a) }
    }

    //Join
    let t2 = measureTimeSeconds {
        print(A.map(String.init).joined(separator: "\n"))
    }

    printErr("Loop: \(t1)\nJoin: \(t2)\n")

}()

func printErr(_ s: String) {
    FileHandle.standardError.write(s.data(using: .utf8)!)
}

func measureTimeSeconds(block: (() -> Void)) -> Double {
    let startTime = Date()
    block()
    return -startTime.timeIntervalSinceNow
}

結果[秒]
Loop: 0.07886505126953125
Join: 0.03861498832702637

ちょっと意外な結果でした。確かにjoin方式の方が倍ぐらい早いですが、loop方式でも100ミリ秒以下だったので、これならもしかしてAtCoderでの実行時間制限(通常2秒)に収まるかも知れません。(Kotlinだと確実にTLEです。

Kolinでの結果[秒]
Loop: 0.805783205
Join: 0.086151367

その他

入出力以外で、有ると便利な関数をいくつか準備しておきます。

便利関数
extension Int {
    var isEven: Bool { self.isMultiple(of: 2) }
    var isTrue: Bool { self != 0 }
}

func + (left: Int, right: Bool) -> Int { left + (right ? 1 : 0) }

まずは偶奇を判定する関数です。普通はn % 2 == 0のように判定しますが、Intのプロパティとして実装しました。偶奇を判定する問題は多いです。
if n.isEven { 偶数の場合 } else { 奇数の場合 }

同様に、数値で真偽を判定したい場面もあります。(C++ライクに)00以外がです。
if n.isTrue { 真の場合 } else { 偽の場合 }

使う場面は少ないと思いますが、最後はIntBool加算です。true1false0として足し算します。
let a = 10 + true11

<2021.9.19追加>
配列に関する便利関数を追加しました。

便利関数(その2)
extension Array {
    init(_ size: Int, _ initValue: ((Int) -> Element)) {
        self = [Element]()
        for index in 0 ..< size {
            let value = initValue(index)
            self.append(value)
        }
    }
    init(_ size: Int, _ initValue: (() -> Element)) {
        self = [Element]()
        for _ in 0 ..< size {
            let value = initValue()
            self.append(value)
        }
    }
    static func += (lhs: inout Self, rhs: Element) { lhs.append(rhs) }
}

Kotlin風に配列を初期化するinit(コンストラクタ)2つと、配列に要素を追加する+=演算子です。
配列の初期化は、
let A = [String](N) { getString() } や、
let B = [(Int, Int)](N) { n in (n, getInt()) }のように書き、入力データを読みながら配列に詰め込むことができます。
+=演算子は見ての通り、配列の末尾に要素を追加(append)します。例えば、整数配列Cに要素3を追加するときに、
C += 3と書きます(C.append(3)と等価)。

終わりに

いくつか関数を準備できましたので、これからSwift de AtCoderを満喫したいと思います。また、この記事を読んでくださった方々の参考になればと思います。
競プロやるならC++だ」と言われていますが、私はSwift(もしくはKotlin)でまずは緑コーダを目指します。
もっと良い方法とかありましたら、ぜひ教えていただけると嬉しいです。

以上

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2