はじめに
これまで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)
です。
Swift2.2と比べると、Kotlinの方が何倍もイケてると思いますが、私が思うKotlinの最大の欠点は『配列の要素数にLong(型)を指定するとコンパイルエラーになる』『配列の添字にLong(型)を指定するとコンパイルエラーになる』ことです。添字の範囲を超えているならいざ知らず、コンパイル時にLongだからと言ってエラーにするとは・・・面倒です。私が思うKotlinの欠点
Kotlinの欠点
Kotlinの整数型変換はlong.toInt()
でLongからIntへ、int.toLong()
でIntからLongへ、簡単に書けます。また、配列の添字にLongを指定できるExtention(演算子オーバーロード)を書けば吸収できることも知っていますが、『Intが32ビットでそれ以上の整数を扱う場合は、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行を入力する方法ですね。(JavaのScannerクラス相当がSwiftにもあればいいのでしょうが)
AtCoderを始めとする競プロの場合は、まずここからですから、このreadLine
の使い方に関しては、これまで多くの方が記事を書かれています。2 3 4
AtCoderでの入力データの代表的な形式は、次の3パターンです。
1. 1行で数値を1つだけ受け取る
2. 1行で数値を2〜4個受け取る
3. 1行で文字列を1つ受け取る
また、問題とデータは1-indexedで与えられることが多いですが、プログラム上は0-indexedの方が都合が良かったりします。配列も0始まりですし。そこで、私が準備したメソッドは次の3パターンです。順番に説明します。
func getString() -> String { readLine()! }
func getInt() -> Int { Int(getString())! }
func getDouble() -> Double { Double(getString())! }
上記は一目瞭然なので説明は省略します。
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です。
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
メソッドで書きます。
var ans = 0
: :
print(ans)
print
はデフォルトで改行コードが付加されるので、C++のように\n
を付ける必要はありません。逆に\n
を付けたく無い場合は、print(・・・・, terminator: "")
と書くことができますが、AtCoderでは最後に改行コードが必須です。
また、2〜3個の数値を横に並べて出力する場合は、次のように書きます。
var a = 0, b = 0, c = 0
: :
print(a, b, c)
これでa b c
のようにスペース区切りで横に並び、最後に改行コードが付加されます。改行区切りで縦に並べたい場合は、print(a, b, c, separator: "\n")
とすることで可能です。
最後のパターンは、横もしくは縦(複数行)に多数の数値を出力する場合で、以下のように書きます。
var ans = [Int]()
: :
print(ans.map(String.init).joined(separator: " ")) //スペース区切り横並び
print(ans.map(String.init).joined(separator: "\n")) //改行区切り(縦並び)
大事なことは、I/Oは遅いのでprint
をループさせないことです。過去に数値を最大20万行出力するという問題 6 もありました。Intを20万行出力するときに、print
をループさせた場合と、joined
してprint
1回の場合を性能を比較してみました。
テストコードを表示
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です。)
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++ライクに)0が偽、0以外が真です。
if n.isTrue { 真の場合 } else { 偽の場合 }
使う場面は少ないと思いますが、最後はIntとBoolの加算です。trueを1、falseを0として足し算します。
let a = 10 + true
→ 11
<2021.9.19追加>
配列に関する便利関数を追加しました。
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)でまずは緑コーダを目指します。
もっと良い方法とかありましたら、ぜひ教えていただけると嬉しいです。