👊

Go 1.21で競プロを始めてみる

2023/08/16に公開

はじめに

2023年8月8日、Go 1.21.0がリリースされました。バージョン1.21からナンバリングの規則が変わり、パッチバージョンまで含めてのリリースとなります。

Go 1.21ではランタイム、コンパイラ、ライブラリなどに複数の変更がなされています。本記事では特にbuilt-inとライブラリに焦点を当てて、Goで競プロを始めるにあたって役に立つ知識を紹介していきます。

https://tip.golang.org/doc/go1.21

どうして今更Goで競プロ記事を書くの?

Go 1.21において、競プロで頻繁に用いられる機能に対してアップデートがありました。具体的には min, maxのbuilt-inサポートと、スライスのソートの高速化です。
AtCoderのジャッジ環境が最新のGoのバージョンに追いつくのはまだ先ではあるものの、今後利用されてゆくことになるので紹介していきます。

max, minがbuild-inになった

Go 1.21では、新たに3つのbuild-in関数が実装されました。

  • max: 比較可能な値を任意個受け取り、その最大値を返す
  • min: 比較可能な値を任意個受け取り、その最小値を返す
  • clear: map, sliceを初期化する

その中でも、競プロでよく用いられるmin, maxに焦点を当てます。

min、maxは自作しなければならない

1.21以前は、自らmax, minを実装する必要がありました。その上、引数として用いることのできる型が限られてしまいます。

func main() {
    a := 1
    b := 5

    m := myMin(a, b)

    fmt.Printf("%dと%dの最小値は%dです\n", a, b, m)
    // 1と5の最小値は1です
}

// 自作のmin関数
// intにしか対応できない
func myMin(a int, b int) int {
    if a > b {
        return b
    } else {
        return a
    }
}

genericsで複数の型に対応する

上記の実装の改善策として、1.18から導入されたgenericsを用いる方法があります。
これによって、複数の型に対して最小値を計算することができます。

func main() {
    a := 1
    b := 5

    m := myMin(a, b)

    fmt.Printf("%dと%dの最小値は %d です\n", a, b, m)
    // 1と5の最小値は 1 です
}

// genericsを用いた自作のmin関数
// aとbの型が異なるとコンパイルエラーになる
func myMin[T cmp.Ordered](a T, b T) T {
    if a > b {
        return b
    } else {
        return a
    }
}

今回導入されたmin, maxは、さらに高性能

今回実装されたmin, maxは上記のmyMinよりも優れた機能を提供してくれます。

具体的な仕様はこちら
https://tip.golang.org/ref/spec#Min_and_max

主な特徴は以下の2点です。

  • 一つの関数で、型によらず大小比較が行える
  • 任意個の引数を受け取ることができる

前者は先ほどgenericsとして説明しましたが、後者は以下のように利用することができます。
引数の個数に囚われることなく、直感的に利用できるようになりました。

func main() {
    a := 1.0
    b := 5.0
    c := 10.0

    m := min(a, b, c) // 任意個の引数を受け取ることができる

    fmt.Printf("%f, %f, %fの最小値は%fです", a, b, c, m)
    // 1.000000, 5.000000, 10.000000の最小値は1.000000です
}

sliceのソートが速くなった

正確には []intのソートが速くなりました。 これまで用いられていたsort.Ints()関数のドキュメントに、今後はslices.Sort()を使うよう記載があります。

Ints sorts a slice of ints in increasing order.
Note: consider using the newer slices.Sort function, which runs faster.

https://pkg.go.dev/sort#Ints

slicesパッケージの使い方

sort.Ints()同様、直感的にソートを実装することができます。
また、int型に限らず、大小比較が可能な型を要素にもつスライスであればslices.Sort()の引数に置くだけでソートできます。

func main() {
    s := []int{3, 1, 4, 1, 5}

    slices.Sort(s)

    fmt.Println(s)
    // [1, 1, 3, 4, 5]
}

実際に使ってみる

Go 1.21によって、min, max, sortが平易に記述できるようになりました。
これらを利用して解くことのできる問題を実際に解いてみます。

https://atcoder.jp/contests/abc212/tasks/abc212_c

2つの配列が与えられたとき、それぞれの配列から1要素ずつ選んだ時の差の最小値を求める問題です。

例えば、配列Aから31、配列Bから6を選択した場合は、その差が15となります。
15と18を選択した場合が最小となります。

ここまで紹介したbuild-inの関数やslicesパッケージを用いると、以下のようになります。

func main() {
    // 以下を標準入力で受け取る
    // A, B: 配列
    // N: Aの要素数
    // M: Bの要素数

    slices.Sort(A) // スライスのソート
    slices.Sort(B) // スライスのソート
    
    A = append(A, 2000000000)
    B = append(B, 2000000000)
    
    i, j := 0, 0
    ans := abs(A[i] - B[j])
    for i < N && j < M {
        if A[i] < B[j] {
            i++
        } else {
            j++
        }
        
        ans = min(ans, abs(A[i] - B[j])) // 自身で実装せずとも利用できる
    }
    
    fmt.Println(ans)
}

func abs(a int) int {
    return int(math.Abs(float64(a)))
}

平易にソートを実装でき、minを自身で実装しなくても良くなりました。

おわりに

記事を書くことが、Goのドキュメントを熟読するいい機会になりました。
試験運用パッケージの保管場所としてのexpモジュールや、comparableの性質を知ることができました。

https://pkg.go.dev/golang.org/x/exp#section-readme
https://go.dev/blog/comparable

set(集合)に対応したパッケージの実装が待ち遠しいです。

GitHubで編集を提案
フィシルコム

Discussion