😸
AtCoderをElixirでやってみる
はじめに
- Elixir楽しんでいますか!
-
AtCoderはご存知でしょうか。
- 世界最高峰の競技プログラミングサイトです
- だいたい毎週土曜や日曜の21時すぎにコンテストが行われているようです
- 出題された問題の答えを出力するプログラムを書いて提出することで自動的に採点されます
- 実行時間が長かったり、メモリの使用量が多いとパスできません
- 競技プログラミングというもの自体に私は馴染みがなかったのですが、最近はじめました
- もちろん、Elixirでも参加できます
- いまのところバージョンは1.10.2です
- 私はいまのところA問題、B問題は自力で解けて、C問題は解けたり解けなかったりといった程度のレベルですがElixirで解くうえでのノウハウが溜まったような気がするので、Zenn.dev初投稿で書いておきます
- AtCoderはコンテストごとに難易度が異なっているようでして、私が練習していたAtCoder Beginner Contest(ABC)でのA〜C問題のレベルの話です
- 前半はElixirを触ったことがあるが、けれどもAtCoderははじめてだよという方を対象に書き進めています
- 後半にElixirをインストールからだという方向けにも少しだけ情報を載せております
- ElixirもAtCoderも猛者の方には物足りないとおもいますが、この関数を使ったほうが良いよといった情報がありましたら、ぜひコメントをください
- 私が言いたいことはひとつです
- Elixirを楽しみましょう!
提出の形式
-
Main
モジュールにMain.main/0
関数を作ります -
Main.main/0
から実行されます - 入力を読み込んで、答えを計算して、出力をするという形でプログラムを提出をします
defmodule Main do
def main do
IO.read(:line)
...
solve()
|> IO.puts()
end
def solve(n) do
...
end
end
入力の読み取りあれこれ
1つの整数を読み取る
入力例
1
プログラム例
n = IO.read(:line) |> String.trim() |> String.to_integer()
- IO.read/2した値には末尾に改行があります
- String.trim/1を使って改行を取り除きます
- おなじみのpipe operator(|>)を使って、気持ちよく書けます
スペースで区切られた整数を読み取る(一行)
入力例①
2 3
プログラム例①
[n, m] =
IO.read(:line) |> String.trim() |> String.split(" ") |> Enum.map(&String.to_integer/1)
-
String.split/3には、第2引数のpatternに
" "
を指定したほうが速く処理してくれるようです - 私の経験上、
" "
の有り無しでパスしたり不合格となったりする問題がありました
入力例②
5 4 3 2 1 0 7 7 6 6
- 200,000個以上並んでいることがあります
プログラム例②
list =
IO.read(:line) |> String.trim() |> String.split(" ") |> Enum.map(&String.to_integer/1)
スペースで区切られた整数を読み取る(複数行)
- ここまでの組み合わせともいえます
- list_of_listsの形になります
入力例
3
1 3 3 100
1 2 2 10
2 3 2 10
- 3行分スペースで区切られた数字のリストが続いている例です
プログラム例
q = IO.read(:line) |> String.trim() |> String.to_integer()
for _ <- 1..q do
IO.read(:line)
|> String.trim()
|> String.split(" ")
|> Enum.map(&String.to_integer/1)
end
# [[1, 3, 3, 100], [1, 2, 2, 10], [2, 3, 2, 10]]
リストは末尾に追加していくとパフォーマンスが悪くなりますそのかわり先頭に追加するのはいつも速い(一定時間)↑ということが、Listモジュールの先頭のほうに書いてありましてこれを利用しています- 以前はEnum.reduce/3を使う例を書いていましたが、for/1を使ったほうが20msほどAtcoderの実行環境において短縮できることがわかりましたので書き直しました
出力
- IO.puts/2は1回だけの呼び出しにするとよいです
- たとえば複数行出力する場合にはEnum.join/2を使って改行で文字列にしてから、IO.puts/2を呼び出すとよいです
NG例
list
|> Enum.each(&IO.puts/1)
- 解答数が少ないときは上記でも通るときがありますが、200,000行の回答を出力するような場合には間に合わないことがあります
OK例
list
|> Enum.join("\n")
|> IO.puts()
解く
- 入力、出力の仕方はだいたいわかったのであとは問題を解いていきます
- A問題は入力をもとになにかに変換するという感じの問題が多いように思います
- B問題は多少複雑になりますが入力数が200個程度ですので、そこまでパフォーマンスを意識した書き方をしなくても通る場合が多いように思います
- 割り算は使わず、式を変形して掛け算で大小判定するようなことをしたほうがいい場合があります
- C問題はある法則のようなものをみつけて、その上で計算量を減らす工夫ができるとパスできるように思います
- 以下の関数をうまくつかうと解けることが多いような気がします
-
Enum.at/3をあちこちで使うプログラム(Enum.map等ループしている処理の中でさらにEnum.atを使う)を書いてしまうと答えは合っているかもしれませんが制限時間の中で収まらない傾向にあるようにおもいます
- どうしてもそういう形でしか解けない問題はリストではなく、マップを使うとうまくいったことがあったようななかったようなという感じです
- 感覚的なものですが前から処理するのではなく、後ろからみていったらどうなるだろうかとか考えるとうまいやり方をおもいつくことがあります
- D問題以降は私にはまだちょっと早くて何もいえないです
- 以下、B問題くらいまではなにかしら役に立つことがあるかもしれないので紹介しておきます
combination
- オートレース、競馬、競輪、競艇の3連複です
- 順番関係なしに組み合わせるものです
- Elixirには標準ではない機能です
defmodule Awesome do
def combination(_, 0), do: [[]]
def combination([], _), do: []
def combination([x | xs], n) do
for(y <- combination(xs, n - 1), do: [x | y]) ++ combination(xs, n)
end
end
iex> Awesome.combination([1, 2, 3, 4, 5, 6], 3)
[
[1, 2, 3],
[1, 2, 4],
[1, 2, 5],
[1, 2, 6],
[1, 3, 4],
[1, 3, 5],
[1, 3, 6],
[1, 4, 5],
[1, 4, 6],
[1, 5, 6],
[2, 3, 4],
[2, 3, 5],
[2, 3, 6],
[2, 4, 5],
[2, 4, 6],
[2, 5, 6],
[3, 4, 5],
[3, 4, 6],
[3, 5, 6],
[4, 5, 6]
]
permutation
- オートレース、競馬、競輪、競艇の2連単です
- Elixirには標準ではない機能です
defmodule Awesome do
def permutation(_, 0), do: [[]]
def permutation(list, n) do
for x <- list, rest <- permutation(list -- [x], n - 1), do: [x | rest]
end
end
iex> Awesome.permutation([1, 2, 3, 4, 5, 6], 2)
[
[1, 2],
[1, 3],
[1, 4],
[1, 5],
[1, 6],
[2, 1],
[2, 3],
[2, 4],
[2, 5],
[2, 6],
[3, 1],
[3, 2],
[3, 4],
[3, 5],
[3, 6],
[4, 1],
[4, 2],
[4, 3],
[4, 5],
[4, 6],
[5, 1],
[5, 2],
[5, 3],
[5, 4],
[5, 6],
[6, 1],
[6, 2],
[6, 3],
[6, 4],
[6, 5]
]
1000000007で割った余りを求めなさい
- 初見だとなんのことやらさっぱりわからないとおもいます
- この数字はなんなのだろう? -> 素数なのだそうです
- 私はちゃんと説明できませんのでリンクを紹介しておきます
- 足し算、引き算、掛け算は、計算の途中過程で積極的に余りをとってよいということがあるようです
- 詳しくは「1000000007 で割ったあまり」の求め方を総特集! 〜 逆元から離散対数まで 〜をご参照ください
- Elixirも計算途中で負になった場合はC++と同じ仲間ですので、2-2. 引き算のところをよくご確認ください
- 競技プログラミング界では常識のことのようですが、なぜそれで正しい答えがでるのか、そんなの学生時代にでてきたっけ? と結局ちゃんとはわかっておらず、おっかなびっくりで使うわけですがとりあえずは丸暗記でもこれは知っておいたほうがよいです
-
ElixirでABC178のC Ubiquityを解いてみるを参考にしてみてください
- 私が解けなかった問題を他の方が解いてくださいました
試しにやってみましょう!
- ここまでいろいろ書いてきましたがとりあえず簡単な問題を一題やってみましょう
- PracticeA - Welcome to AtCode
- 問題は上記リンクをご参照ください
$ mix new at_coder
- とおもむろにプロジェクトの雛形を作ります
-
lib/practice_1.ex
を作ります - まずは解答のキモになる部分だけを作ります
defmodule Practice1 do
@doc ~S"""
https://atcoder.jp/contests/abs/tasks/practice_1
## Examples
iex> Practice1.solve(1, 2, 3, "test")
"6 test"
iex> Practice1.solve(72, 128, 256, "myonmyon")
"456 myonmyon"
"""
def solve(a, b, c, s), do: "#{a + b + c} #{s}"
end
-
解答のキモになる部分をDoctestsで書きます
- 詳しくはExUnit.DocTestをご参照ください
-
test/at_coder_test.exs
に設定を追加します
defmodule AtCoderTest do
use ExUnit.Case
doctest AtCoder
doctest Practice1
- テストの実行ができます!
$ mix test
Compiling 1 file (.ex)
....
Finished in 0.1 seconds
3 doctests, 1 test, 0 failures
🎉🎉🎉
- あとはlib/practice_1.exに入力と出力の
main/0
関数を追加します
defmodule Practice1 do
def main do
a = IO.read(:line) |> String.trim() |> String.to_integer()
[b, c] =
IO.read(:line) |> String.trim() |> String.split(" ") |> Enum.map(&String.to_integer/1)
s = IO.read(:line) |> String.trim()
solve(a, b, c, s)
|> IO.puts()
end
@doc ~S"""
https://atcoder.jp/contests/abs/tasks/practice_1
## Examples
iex> Practice1.solve(1, 2, 3, "test")
"6 test"
iex> Practice1.solve(72, 128, 256, "myonmyon")
"456 myonmyon"
"""
def solve(a, b, c, s), do: "#{a + b + c} #{s}"
end
- 提出前に手元のiexで動かしてみます
$ iex -S mix
iex> Practice1.main
72
128 256
myonmyon
456 myonmyon
:ok
- 正しそうです
-
提出の際にはモジュール名は
Main
にしておきましょう - パスしています
- 🎉🎉🎉
過去問精選10問
- AtCoder Beginners Selectionにありますので取り組んでみるとなんとなく雰囲気をつかめるとおもいます
-
Elixirでの解答例は、【doctestつき】AtCoder に登録したら解くべき精選過去問 10 問を"Elixir"で解いてみたにあります
- ありがとうございます!
- 私の回答です
AtCoderをやることで得られるもの
インストール
- 手前味噌ですがインストールを参考にしてください
Wrapping Up
- Enjoy Elixir !!! 🚀🚀🚀
Discussion