LoginSignup
134
86

More than 3 years have passed since last update.

Goで競技プログラミングするときのテクニック

Last updated at Posted at 2019-12-16

これは品川 Advent Calendar 2019の17日目の記事です。

私は競技プログラミングにGo言語で参加することが多いのですが、その際に役立つ(かもしれない)小ネタをいくつか紹介したいと思います。

なお、本記事では次のような方を読者として想定しており、競技プログラミングそのもにについては解説しません。

  • 競技プログラミングにGoで参加しはじめた人
  • 競技プログラミングに興味があってGoで参加したいと思っている人

競技プログラミングそのものについて知りたい場合は drken さんの AtCoder に登録したら次にやること ~ これだけ解けば十分闘える!過去問精選 10 問 ~ を見ると雰囲気がわかると思います。また、これらの問題の Go によるACコードについては AtCoder に登録したら解くべき精選過去問 10 問 を Go で解いてみた をご覧ください。

0. C++での参戦を検討する

Goで競プロをやるのは C++ 利用と比較すると結構マゾいので、これから始める人はまずC++で参戦できないか検討したほうが良いです。
「ワイはそれでもGoがええんや!」という方は以下のコンテンツをどうぞ。

1. 入出力のバッファリング

fmt.Scan()fmt.Printf() はアンバッファードな入出力なので、 $3 \times 10^5$ 個くらいの数値を入出力したりすると TLE します。 例えば ABC138 D - Ki とか。

ヤバそうな入出力量がある問題の場合は、 bufio を使って入出力をバッファリングしましょう。例えば↓みたいな感じです。

package main

import (
    "bufio"
    "fmt"
    "os"
)

func main() {
    r := bufio.NewReader(os.Stdin)
    w := bufio.NewWriter(os.Stdout)
    defer w.Flush()

    var a, b, c int
    var s string

    fmt.Fscan(r, &a)
    fmt.Fscan(r, &b, &c)
    fmt.Fscan(r, &s)

    fmt.Fprintln(w, a+b+c, s)
}

bufio.Writer で出力バッファリングする場合は、最後に bufio.Writer.Flush() を呼ばなければならないことに注意してください。 Flush しないと、実際の出力が途中で切れてしまい、WA (誤答、 Wrong Answer) と判定されてしまいます。
上記の例では defer w.Flush() として実行しています。 defer で実行しておけば、計算途中で一部の解を特定できる問題などで、早めにリターンするケースがあるプログラムを書いても必ず実行されるので安心です。

「インタラクティブな問題」ではバッファリングしてはならない

もう一つ、「インタラクティブな問題」ではバッファリングしてはならないことにも注意してください。「インタラクティブな問題」とは、例えば practice B - | | Interactive Sorting のような問題のことです。
このような問題でバッファリングしてしまうと、「こちらのプログラムはジャッジに出力したつもりだが、実はバッファに貯められたままになっていて、ジャッジは入力待ち、こちらのプログラムもジャッジ出力を入力待ち」という状態になり、 TLE してしまいます。
「インタラクティブな問題」では、バッファリングなしの入出力でTLEするような問題は出ないので、バッファリングなしでプログラムすれば良いです。

応用

fmt.Fscan は事前に変数宣言が必要なので、初期化の構文で入力と代入を一度にできるような入出力ライブラリを作っておくと捗ります。

N := readi()  // int
A := readll() // int64
F := readf()  // float64
S := reads()  // string
B := readb()  // []byte

こんな感じで使えるようにしておくと楽です。
実装例としては 私が使っている入出力テンプレ を参考にしてください。 初期化時に SetInteractive(os.Stdout, os.Stdin) を加えるだけで インタラクティブ問題にも対応 しています。

なお、コード量に制限があるコンテストサイトだとこの手のテンプレライブラリは引っかかることがあるので、その場合は諦めて bufio.Reader + fmt.Fscan を使いましょう。

2. 変数や組み込み型関連

グローバル変数の利用やエクスポート有無

競プロの提出コードは基本的に main パッケージのみで完結するので、グローバル変数の利用や変数のエクスポート(キャピタライズするかどうか)は好みで決めると良いです。

AtCoderのような1入力1ケースのコンテンストサイトの場合は、グローバル変数を使ったほうが関数の引数を少なくできるので効率良くコーディングできます。(1入力nケースだとローカル変数を引数でやりとりするか、グローバル変数+初期化をきっちりやる、感じになります)
しかし、 Go はクロージャの構文がシンプルなので、main関数内のローカル変数とクロージャで書いてもさほど辛くないです。お好みで決めるといいと思います。

int か int64 か (2020/01/06 修正)

64bit整数が必要な問題はよく出題されます。

Go ではベースシステムが 64bit であれば int は 64bit 整数になるので、int64 を明示的に使う必要があるかどうかはジャッジ環境依存です。 ジャッジ環境が sizeof(int) == sizeof(int64) であれば、 何も考えずに int を使っておけば良いです。一方、 sizeof(int) == sizeof(int32) の場合、 int と int64 を使い分ける必要があります。

sizeof(int) == sizeof(int32) の場合であっても type int int64 して問題なければ楽なんですが、これをやってしまうと slice のインデックスを取り扱う関数やプリミティブなど結局でキャストが必要になり、逆に煩わしくなってしまいます。
代表的なところは i := len() とか for i := range X とかで、 この場合 iint になります。
あと整数定数で初期化すると int 型になるのも辛いところ。

i := 1000000007 // int

私の場合は、あきらめて intint64 は分けて取り扱い、必要に応じてキャストする、という形に落ち着いています。
以前は AtCoder において 64bit 整数が必要な問題で int を使うと WA になっていた記憶があり、 intint64 を使い分けていましたが、現在では int は 64bit 整数になるようです。やったね。
なお、CodeForces では int は 32bit 整数のようです。残念。

ジャッジ環境において sizeof(int) == sizeof(int64) かどうかは、ジャッジ環境に用意されたコードテスト用のページなどで以下のコードを実行するとわかります。

package main

import (
    "fmt"
    "unsafe"
)

func main() {
    var i int
    var i32 int32
    var i64 int64
    fmt.Println("int32", unsafe.Sizeof(i32))
    fmt.Println("int  ", unsafe.Sizeof(i))
    fmt.Println("int64", unsafe.Sizeof(i64))
}

64bit環境の場合の出力

$ go run main.go
int32 4
int   8
int64 8

32bit環境の場合の出力

$ go run main.go
int32 4
int   4
int64 8

(64bit環境で無理やり試すには、 env GOARCH=386 go run main.go のように実行します)

string か []byte か

string は string で便利なのですが、競プロでは文字列を1文字ずつ取り扱うことが多く、 内容は ASCII のみで utf8 な文字列がまず出てこないので、最初から []byte で扱うほうが簡単なことが多いです。

s := "foo" だとすると s[0]byte ですが、 for _, c := range s などしたときの crune になるのも煩わしいところです。

strings パッケージの各種関数はだいたい bytes パッケージでも定義されているので、あまり困ることはありません。

ただ、 mapのキーには []byte を使えないので、 string にキャストして使うことになります。

3. エラー処理および関数まわりのあれこれ

エラー処理

まずエラー処理について。

競プロでは基本的に真面目なエラー処理は不要だと思います。
むしろ、異常事態においてエラー処理をあれこれ頑張るよりは panic してくれたほうが、スタックトレースも出るのでデバッグしやすかったりします。好みの問題ではありますが。

なので、 error を返す関数を使う場合も原則 _ とかで受けて無視する、あるいは error が返ってきたら panic する、みたいなシンプルな処理で OK です。もちろん合理的な理由があって error を処理する必要がある場合もあると思います。

多値

Goの関数は多値を返せます(2番目の値として error を返す関数はよくありますよね)。 競プロでは上述のとおり error 利用の必要性はあまりありませんが、多値自体は結構便利に使えます。 「DFSで何か構築するときに、構築結果と一緒に構築可否もboolで返す」「探索の値と zero value が区別できないときに、探索結果と一緒に探索成否を bool で返す」などなどです。

あんまり多いと受ける方も記述量が多くなって大変ですが、積極的に使うと良いと思います。

クロージャ

Goのクロージャは

    f := func(x int) {
        // 何らかの処理
    }

のようにシンプルな構文なので、競プロでも割と使えます。

後述するbit全探索やnext_permutationなどはクロージャを引数に取るようなライブラリとして実装しておくと、楽に利用できます。

再帰クロージャ

クロージャで再帰関数を定義するときは少し注意が必要です。次の「だめな例」と「良い例」をご覧ください。

// だめな例 (コンパイルエラー)
func solve(x int) int {
    fact := func(n int) int {
        if n == 1 {
            return n
        }
        return n * fact(n-1)
    }
    return fact(x)
}

// 良い例
func solve(x int) int {
    var fact func(n int) int
    fact = func(n int) int {
        if n == 1 {
            return n
        }
        return n * fact(n-1)
    }
    return fact(x)
}

f := func() {...} というクロージャの変数を宣言と同時に初期化する構文では、クロージャのボディ定義時点では宣言される変数がスコープ外です。なので、上の「だめな例」では return n * fact(n-1)fact が見えず、コンパイルエラーになります。
再帰するクロージャを書く場合は、「良い例」のようにクロージャの変数を宣言してからクロージャ本体を定義する必要があります。

4. かんたんなデータ構造

C++と比較すると競プロ用途としてはかなり貧弱ですが、いくつか組み込みのデータ構造が使えます。

slice

slice は一番よく使うと思います。 C++ の vector と同じ使い方でいいんですが、 slice は一見参照を受け渡すように見えて、↓のような構造体を値渡し(コピー)しています。したがって、関数呼び出しに slice を渡して、呼び出された関数側で append したりすると意図しない動作になることが多いので注意が必要です。

type SliceHeader struct {
    Data uintptr
    Len  int
    Cap  int
}
// (go doc reflect.SliceHeader より引用)

詳細は以前まとめた Go の Slice の落とし穴 があるので、そちらをご覧ください。

array

array はたまに使い所があります。 slice の zero value は nil ですが、 array は zero value としてちゃんと領域が用意される、という特徴があるので、それを利用すると楽になるところで使います。 例えば C++ でいうところの vector<pair<int,int>> みたいなことをしたいときに、 [][2]int という array の slice を使うと、 [][]int を使う場合と比べて [2]int 部分を明示的に初期化しなくていい、という利点があります。

struct{a, b int} でもいいのですが、 インデックスによるアクセスが可能な array にしておくと

A := make([][2]int, N)
// Aになんらかのデータを詰める
var j int
for i := range A {
    fmt.Println(A[i][j])
    j = 1-j // jが0なら1に 1なら0に
}

みたいに処理対象を切り替えて次々処理したいときに簡潔に記述できます。

なお、 array は slice とは違って関数呼び出しでは配列全体をコピーして渡すことになるので、arrayを受け取る関数を書くときは注意が必要です。

map

slice より利用頻度が低いですが、 array よりは利用頻度が高いです。
有名な話ですが、 map m を for k, v := range m という構文でイテレーションを回すと要素がランダムに出てくるので、注意が必要です。 もちろん upper_bound みたいなものもありません。C++で言えば unordered_map 相当かと思います。

slice とは違って、 map は値渡しでも完全な参照渡し風の雰囲気で使えます。したがって、関数呼び出しに map を渡して、渡した先で 値を挿入してもOK(呼び出し側でも挿入された値が見えます)。
また、 for k, v := range m のループ内で delete(m, k) 等を行っても安全です。

先にも書きましたが []byte などの slice をキーにすることは出来ません。 string などに変換してキーにする必要があります。

map の map

例えば、「2次元グリッドのxy座標」 → 「string」 なmapを作りたいとします。この場合いわゆる map の map、 つまり map[int]map[int]string を使ってもよいのですが、初期化していない map に値を格納しようとすると panic するので、「最初の int に対応する値が nil だったらまずmake(map[int]string)して……」みたいなコードを書く必要があります。面倒ですね。
こういう場合はまず

type P struct{
    y, x int
}

を定義したうえで

var m map[P]string

なmapを使うと良いです。
この場合初期化は m に対して行うだけでよく、 s, ok := m[P{i, j}] みたいにアクセスできます。

キーを構成する複数の要素が同一の型であれば、 Array を使って

var m map[[2]int]string

のようにしても同じことができますが、[] をたくさん書かないといけないので個人的には struct を定義したほうが読み書きしやすいと思います。

スタック、キュー

スタックとキューは slice とクロージャの組み合わせで実現できます。

// Stack
st := make([]int, 0)
push := func(v int) {
    st = append(st, v)
}
pop := func() int {
    n := len(st)-1
    res := st[n]
    st = st[:n]
    return res
}
empty := func() bool {
    return len(st) == 0
}
// Queue
q := make([]int, 0)
push := func(v int) {
    q = append(q, v)
}
pop := func() int {
    res := st[0]
    st = st[1:]
    return res
}
empty := func() bool {
    return len(st) == 0
}

その場その場で毎回書き起こしてもいいですが、 res := st[0] とすべきところを res := st[len(st)-1] と書いてしまうミスがわりとありがちなので、ライブラリ化しておくのもおすすめです。

Priority Queue

container/heap にある Priority Queue を使えます。 これを利用するには、 slice 的なデータ構造について sort.Interface を満たすよう Len, Less, Swap メソッドを定義し、加えて Push(interface{})Pop() interface{} を定義する必要があります。

import (
        "container/heap"
)

type PriorityQueue []int64

func (pq PriorityQueue) Len() int {
        return len(pq)
}

func (pq PriorityQueue) Less(i, j int) bool {
        return pq[i] < pq[j]
}

func (pq PriorityQueue) Swap(i, j int) {
        pq[i], pq[j] = pq[j], pq[i]
}

func (pq *PriorityQueue) Push(x interface{}) {
        old := *pq
        *pq = append(old, x.(int64))
}

func (pq *PriorityQueue) Pop() interface{} {
        old := *pq
        n := len(old) - 1
        x := old[n]
        *pq = old[:n]
        return x
}

これを利用する際は次のように書きます。

func PriorityQueueExample() {
        pq := new(PriorityQueue)
        push := func(x int64) {
                heap.Push(pq, x)
        }
        pop := func() int64 {
                x := heap.Pop(pq)
                return x.(int64)
        }
        empty := func() bool {
                return pq.Len() == 0
        }

        a := []int64{1, 8, 2, 5, 6, 0, 4, 7, 9, 3}
        for _, x := range a {
                push(x)
        }
        for !empty() {
                fmt.Println(pop()) 
        }
}

push 操作と pop 操作には、自分でで定義した PushPop ではなく、 heap.Pushheap.Pop を使わなければならないことに注意が必要です。 また、 heap.Pop は interface{} を返すので、上の例のように本来の型にキャストするクロージャでくるんで上げると利用が楽になります。

結構記述量が多いので、テンプレを用意しておいて、型は問題にあわせて書き換える、というあたりが現実的かと思います。

container パッケージ配下には、 他に二重リンクトリストの container/list と循環リストの container/ring がありますが、私は競プロで利用したことがありません。

5. ソート

AtCoder では Go のバージョンが古く、 sort.Slice() が利用できません。(近い将来、言語アップデートにより利用可能になる見込み)
先日の言語アップデートにより、 sort.Slice() を利用可能になりました。 (2020/06/30修正)
競プロで Len, Less, Swap をいちいち定義するのは煩わしいので、 sort.Slice() を使いましょう。

なお、 Goのバージョンが古いジャッジサイトでも、新しい Go から sort.Slice() を移植することで無理やり利用はできます。 詳しくは ソートを使う問題の私の提出コード を参照してください。 sort.Slice() には reflect を使った黒魔術がたっぷり含まれていますが、実行速度が問題になったことはありません。

sort.SliceStable も移植しましたが、使ったことはありません。

6. 二分探索

単純な二分探索であれば、 sort.Search が使えます。 sort.Searchfunc Search(n int, f func(int) bool) int というAPIになっており、 $0$ から $n-1$ までの整数 $i$ について、 f($i$) == true となる最小の $i$ を見つける挙動をします。

一番わかりやすい使い方としては

    lower := 100
    sort.Ints(A)
    k := sort.Search(len(A), func(i int) bool { return lower <= A[i] })
    fmt.Println(k)

のようにソートされた slice に対して用いるケースですが、 探索対象が slice であることを要求しないAPIになっているので、0 から math.MaxInt32 の範囲の整数空間に対して二分探索する形に持ち込めるのであれば、 slice 以外でも探索可能です。

7. math パッケージ

定数

math パッケージ内には math.MaxInt64math.Pi などのいくつかの便利な定数が定義されているので、どんなものがあるか一度 go doc に目を通しておくと良いと思います。ただし、無限大的な意味で math.MaxInt... を使うのはやめたほうが良いでしょう(配列の最小値を探すときの初期値など、無限大がほしいことがあります)。 単純な数値のみを比較する場合は問題ないですが、 これを格納した変数同士を足し算してしまうなどすると math.MaxInt64 + math.MaxInt64 となり、オーバーラップしてしまうからです(実際には math.MaxIn64 は 1 足しただけでオーバーラップします)。また、 64 という名前がついてはいますが math.MaxInt64 は定数なので、 i := math.MaxInt64 とすると i の型が int になることには注意が必要です(このように初期化した i を別の int64 の型に代入しようとするとコンパイル時エラーになります)。

以下は比較的記述しやすくて便利な大きな数です。

  • 1000000007
    • いわゆる「割ったあまりを答えなさい」系の問題でよく出てくる素数の定数です。
    • 10億7 つまり、1を1個、0を8個、最後に7を書けば良いので覚えやすい。 const で定数を定義しておくのも良いでしょう。
    • よくある制約の上限値である $10^9$ よりもわずかに大きいので、最小値を求める際の初期値として使いやすい
    • 2倍したものを2乗しても int64 の正整数範囲に余裕で収まる
    • int(1e9+7) と書くこともできます
  • 1 << 62
    • 簡単に書き下せる巨大な数
    • MaxInt64 (= 1<<63 - 1)まで多少余裕があるので $10^{18}$ 以下程度の値であれば多少足しても大丈夫。
    • $10^9$ 以下の数同士を掛け算した結果の最小値、のような場合の初期値にも使える。
    • なお、 1 << 63 は MaxInt64 を 1 だけ超えているのでコンパイルエラーになります。

関数

math パッケージ内の関数については、使えるものもありますが、 float64 を対象にする関数が多く、 int64 の範囲が必要な問題では float64 がまばらになるような大きい数の領域(10621335997251793以上くらい)にかかることがあるので、使えない場合があります。必要に応じて自前でライブラリを用意しておくと良いです。

とくに、 min, max, abs, gcd は実装が簡単ですが、利用頻度が比較的高いので、int用、 int64 用それぞれでテンプレに用意しておくのがおすすめです。

8. その他

論理演算

var A, B bool とした場合、以下のような論理演算があります

  • A AND B: A && B
  • A OR B: A || B
  • NOT A: !A

また、XOR を取りたい場合は、論理演算子ではありませんが != を利用すれば XOR になります。

  • A XOR B: A != B

ビット演算

ビット演算は C++ などに似ています。

明らかに違うところとしては

  • NOT 演算子が ~ ではなく、単項演算子の ^ である(二項演算子の ^ は XOR です。)
  • ビットクリアするための二項演算子 &^ が存在する

あたりです。他にもシフト演算がいくらでも可能、などの細かい違いがあるようですが、競プロをやるうえではあまり意識したことがありません。

比較的新しめのGoのバージョンでは、math/bits 配下に bits.OnesCount (popcount) などの便利関数を使えます。一度 doc を流し読みしておくと良いと思います。

複素数

複素数を表す組み込み型 complex128 と関連ライブラリの math/cmplx があり、幾何問題などに利用可能です。
私はまだあまり使ったことがありませんが、最近ABCで幾何問題の出題頻度が上がってきている気がするので、自分用のライブラリを整備している最中です。

多倍長整数

math/big に多倍長整数ライブラリがありますが、私は使ったことがありません。

9. 応用的なライブラリ

整数論系ライブラリ

10億7などの大きい素数を法とする冪剰余、階乗, その逆関数、二項係数を計算するライブラリは、たまに必要になります。
これらの関数はお互いに密接な関係があるので、まとめて用意しておくと良いと思います。↓は実装例です。

// NewFactorial(n, p int) returns 2 functions.
// 1. combination(x, r int) int: returns xCr modulo p (0 <= r <= x <= n)
// 2. permutation(x, r int) int: returns xPr modulo p (0 <= r <= x <= n)
// 3. factorial(x int) int: returns factorial x modulo p (0 <= x <= n)
func NewFactorial(n, p int) (combination func(int, int) int, permutation func(int, int) int, factorial func(int) int) {
        if n < 1 {
                panic("NewFactorial: n must be more than 0")
        }

        fact := make([]int, n+1)
        fact[0] = 1
        for i := 1; i <= n; i++ {
                fact[i] = mul(fact[i-1], i, p)
        }

        ifact := make([]int, n+1)
        ifact[n] = inv(fact[n], p)
        for i := n - 1; 0 <= i; i-- {
                ifact[i] = mul(ifact[i+1], i+1, p)
        }

        combination = func(n, r int) int {
                return mul3(fact[n], ifact[n-r], ifact[r], p)
        }
        permutation = func(n, r int) int {
                return mul(fact[n], ifact[n-r], p)
        }
        factorial = func(i int) int {
                return fact[i]
        }
        return
}

func mul(a, b, p int) int {
        return int(int64(a) * int64(b) % int64(p))
}

func mul3(a, b, c, p int) int {
        return int(int64(a) * int64(b) % int64(p) * int64(c) % int64(p))
}

func inv(a, p int) int {
        return exp(a, p-2, p)
}

func exp(n, e, p int) int {
        if e == 0 {
                return 1
        }
        if e&1 == 1 {
                return mul(n, exp(n, e-1, p), p)
        } else {
                return square(exp(n, e/2, p), p)
        }
}

next_permutation

next_permutation はたまに必要になりますが、 Go では自分で実装するしかないと思います。 Narayana Pandita のアルゴリズムによる next_permutation を C++ 風に使うのが楽な気がしています。 Heap のアルゴリズムやクロージャの実装例は過去リビジョンを参照してください。

下記は実装および利用例です。

func next_permutation(a []int) bool {
    return _permutation_pandita(a, func(x, y int) bool { return x < y })
}

func prev_permutation(a []int) bool {
    return _permutation_pandita(a, func(x, y int) bool { return y < x })
}

func _permutation_pandita(a []int, less func(x, y int) bool) bool {
    i := len(a) - 2
    // Find the right most incresing elements a[i] and a[i+1]
    for 0 <= i && !less(a[i], a[i+1]) {
        i--
    }
    if i == -1 {
        return false
    }
    j := i + 1
    // Find the smallest element that is greater than a[i] in a[i+1:]
    for k := j + 1; k < len(a); k++ {
        // a[i] < a[k] && a[k] <= a[j]
        if less(a[i], a[k]) && !less(a[j], a[k]) {
            j = k
        }
    }
    a[i], a[j] = a[j], a[i]
    for p, q := i+1, len(a)-1; p < q; p, q = p+1, q-1 {
        a[p], a[q] = a[q], a[p]
    }
    return true
}

func main() {
    a := make([]int, 4)
    for i := range a {
        a[i] = i + 1
    }

    for ok := true; ok; ok = next_permutation(a) {
        fmt.Println(a)
    }
}

Reverse, Rotate, Shuffle

Reverse は配列を逆順に入れ替える操作です。 sort.Slice には reflect.Swapper() が使われていますが、この reflect.Swapper() を使うとジェネリックな Reverse 関数を簡単に書くことができますので、余裕があれば用意しても良いでしょう。

func Reverse(slice interface{}) {
    rv := reflect.ValueOf(slice)
    swap := reflect.Swapper(slice)
    length := rv.Len()
    for i, j := 0, length-1; i < j; i, j = i+1, j-1 {
        swap(i, j)
    }
}

Rotate(指定した数だけ要素をローテーションさせる), Shuffle(配列の要素をシャッフルする。ランダムケースのテストデータ生成に便利) などの関数も同様です。

func Rotate(slice interface{}, n int) {
    rv := reflect.ValueOf(slice)
    swap := reflect.Swapper(slice)
    length := rv.Len()
    if length == 0 || n == 0 {
        return
    }
    n = (length + n) % length
    for i, j := 0, n-1; i < j; i, j = i+1, j-1 {
        swap(i, j)
    }
    for i, j := n, length-1; i < j; i, j = i+1, j-1 {
        swap(i, j)
    }
    for i, j := 0, length-1; i < j; i, j = i+1, j-1 {
        swap(i, j)
    }
}

func Shuffle(slice interface{}) {
    rv := reflect.ValueOf(slice)
    swap := reflect.Swapper(slice)
    length := rv.Len()

    for i := 0; i < length; i++ {
        j := rand.Int()%(length-i) + i
        swap(i, j)
    }
}

平衡二分探索木

最初から用意しておく必要はありませんが、余裕があれば平衡二分探索木も用意しておきましょう。先に書いたとおりビルトインの map は unordered_map なので、 lower_bound / upper_bound のような計算はもちろん、 range クエリのような操作もできないからです。

良い性質を持ち、且つ実装の軽い平衡二分探索木としてはLLRB Tree(Left-Leaning Red Black Tree, 左傾赤黒木)があります。LLRBは自分でも実装してみましたが、まずナイーブな二分探索木を書き、ノードのInsertとRemoveの一部を数十行書き換えるだけなので、比較的かんたんに実装はできます。ただし、二分探索木としてあったほうが良い各種操作をキッチリ実装すると、ナイーブな二分探索木の時点でそれなりに重たい実装になります。

私は実装したことがありませんが、 Treap は(さらに?)軽めの実装になるようです。

bit全探索

以下は bit全探索の実装例です。クロージャを渡すとビットパターンを引数にセットして呼び出してくれる形式です。

// DoWithExhaustiveBits(n, fn) calls fn with []bool for each n-bit true/false pattern
// e.g.
// DoWithExhaustiveBits(10, func(b []bool) { fmt.Println(b) }
// -> [false, false, false, false, false, false, false, false, false, false]
//    [true, false, false, false, false, false, false, false, false, false]
//    [false, true, false, false, false, false, false, false, false, false]
//    [true, true, false, false, false, false, false, false, false, false]
//    [false, false, true, false, false, false, false, false, false, false]
//    ...
func DoWithExhaustiveBits(n int, fn func([]bool)) {
    N := 1 << uint(n)
    a := make([]bool, n)
    for i := 0; i < N; i++ {
        for j := 0; j < n; j++ {
            k := n - j - 1
            if i&(1<<uint(j)) == 0 {
                a[k] = false
            } else {
                a[k] = true
            }
        }
        fn(a)
    }
}

慣れてくると、次のように 1 << uint(n) 回のループをそのまま書くほうが応用は効きます。

func main() {
        n := 4
        a := make([]int, n)
        for i := range a {
                a[i] = i + 1
        }

        N := 1 << uint(n)
        for i := 0; i < N; i++ {
                fmt.Printf("%2d:", i)
                for j := 0; j < n; j++ {
                        if i>>uint(j)&1 == 1 {
                                fmt.Printf(" %d", a[j])
                        } else {
                                fmt.Printf(" .")
                        }
                }
                fmt.Println()
        }
}

まとめ

普段自分で使っているGoで競技プログラミングするときのテクニックについて紹介しました。
使えそうなものがあれば参考にしてください。

内容の誤りや「もっといい方法があるよ」とかありましたらコメントで教えていただけると嬉しいです。

134
86
1

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
134
86