13
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

[AtCoder] ABC042 - Elixir による二項係数(組み合わせ数)の高速計算 [Elixir Livebook]

Last updated at Posted at 2023-07-05

はじめに

AtCoder の Beginners Selection コンテスト ABC042 問題を Elixir Livebook で回答しました

他の人の(他の言語の)実装を参考にしながらですが、初めて D 問題を解きました

その中で組み合わせ数の高速計算が必要になったので、 Elixir で実装しています

ABC042A - 和風いろはちゃんイージー

ABC042A 問題

ABC042A 実装したノートブック

ABC042A 入出力例

"""
5 5 7
"""
|> Main.solve()
|> then(&(&1 == "YES"))
"""
7 7 5
"""
|> Main.solve()
|> then(&(&1 == "NO"))

ABC042A 回答1

入力を小さいい順に並べ替えて 5 5 7 になれば YES を出力します

defmodule Main do
  def main do
    :stdio
    |> IO.read(:all)
    |> solve()
    |> IO.puts()
  end

  defp split_words(words) do
    String.split(words, " ")
  end

  def solve(input) do
    input
    |> String.trim()
    |> split_words()
    |> Enum.sort()
    |> case do
      ["5", "5", "7"] -> "YES"
      _ -> "NO"
    end
  end
end

実行結果は 327 ms でした

ABC042A 回答2

5 が2つ、 7 が1つのパターンを網羅しました

defmodule Main do
  def main do
    :stdio
    |> IO.read(:all)
    |> solve()
    |> IO.puts()
  end

  def solve(input) do
    input
    |> String.trim()
    |> case do
      "5 5 7" -> "YES"
      "5 7 5" -> "YES"
      "7 5 5" -> "YES"
      _ -> "NO"
    end
  end
end

実行結果は 333 ms でした

ABC042B - 文字列大好きいろはちゃんイージー

ABC042B 問題

ABC042B 実装したノートブック

ABC042B 入出力例

"""
3 3
dxx
axx
cxx
"""
|> Main.solve()
|> then(&(&1 == "axxcxxdxx"))

ABC042B 回答

先頭行以外を小さい順に並べ替えて結合します

defmodule Main do
  def main do
    :stdio
    |> IO.read(:all)
    |> solve()
    |> IO.puts()
  end

  defp split_lines(lines) do
    lines
    |> String.trim()
    |> String.split("\n")
  end

  def solve(input) do
    input
    |> split_lines()
    |> tl()
    |> Enum.sort()
    |> Enum.join()
  end
end

実行結果は 343 ms でした

ABC042C - こだわり者いろはちゃん

ABC042C 問題

ABC042C 実装したノートブック

ABC042C 入出力例

"""
1000 8
1 3 4 5 6 7 8 9
"""
|> Main.solve()
|> then(&(&1 == 2000))
"""
9999 1
0
"""
|> Main.solve()
|> then(&(&1 == 9999))

ABC042C 回答1

支払い金額の候補(代金〜9,999円)のうち、全ての桁が嫌いな数字ではない値を探します

Enum.find で最初に該当するもの = 最小値が求められます

defmodule Main do
  def main do
    :stdio
    |> IO.read(:all)
    |> solve()
    |> IO.puts()
  end

  defp split_lines(lines) do
    lines
    |> String.trim()
    |> String.split("\n")
  end

  defp split_words(words) do
    String.split(words, " ")
  end

  def solve(input) do
    [[n, _] | [d]] =
      input
      |> split_lines()
      |> Enum.map(fn line ->
        line
        |> split_words()
        |> Enum.map(&String.to_integer/1)
      end)

    n..99999
    |> Enum.find(fn payment ->
      all_digits =
        payment
        |> Integer.digits()
        |> Enum.uniq()
        |> Enum.concat(d)

      all_digits
      |> Enum.uniq()
      |> Enum.count()
      |> Kernel.==(Enum.count(all_digits))
    end)
  end
end

実行結果は 445 ms でした

ABC042C 回答2

まず、嫌いな数字ではない数字 = 支払い金額に含んで良い数字 pay_d を求めます

0 が嫌いな数字だったとしても、 10 の位以上は 0 があり得るため、 0 も含む pay_d_up を求めます

1の位が pay_d 、10の位以上が pay_d_up の組み合わせでできる数字のうち、代金以上の数字を求めます

0 が嫌いな数字だった場合、改めて支払い金額に 0 が含まれていれば候補から除外します

該当する支払い金額のうち、最小の値を取得します

defmodule Main do
  def main do
    :stdio
    |> IO.read(:all)
    |> solve()
    |> IO.puts()
  end

  defp split_lines(lines) do
    lines
    |> String.trim()
    |> String.split("\n")
  end

  defp split_words(words) do
    String.split(words, " ")
  end

  def solve(input) do
    [[n, _] | [d]] =
      input
      |> split_lines()
      |> Enum.map(fn line ->
        line
        |> split_words()
        |> Enum.map(&String.to_integer/1)
      end)

    pay_d = Enum.filter(0..9, &(not Enum.member?(d, &1)))
    pay_d_up = Enum.uniq([0 | pay_d])

    for d_1 <- pay_d,
        d_10 <- pay_d_up,
        d_100 <- pay_d_up,
        d_1000 <- pay_d_up,
        d_10000 <- pay_d_up,
        payment = Integer.undigits([d_10000, d_1000, d_100, d_10, d_1]),
        payment >= n do
      payment
    end
    |> Enum.filter(fn payment ->
      if Enum.member?(d, 0) do
        payment |> Integer.digits() |> Enum.member?(0) |> Kernel.not()
      else
        true
      end
    end)
    |> Enum.min()
  end
end

発想はシンプルですが、 0 の扱いのため複雑になってしまいました

実行結果は 386 ms でした

ABC042D - いろはちゃんとマス目

ABC042D 問題

ABC042D 実装したノートブック

ABC042D 入出力例

"""
2 3 1 1
"""
|> Main.solve()
|> then(&(&1 == 2))
"""
10 7 3 4
"""
|> Main.solve()
|> then(&(&1 == 3570))
"""
100000 100000 99999 99999
"""
|> Main.solve()
|> then(&(&1 == 1))
"""
100000 100000 44444 55555
"""
|> Main.solve()
|> then(&(&1 == 738_162_020))

ABC042D 回答

自力では解けなかったので、以下の記事を参考に回答しました

W * H の格子を左上から右下に移動する場合、移動する回数は以下のようになります

  • 横方向の移動回数: W - 1
  • 縦方向の移動回数: H - 1
  • 合計: W - 1 + H - 1 = W + H - 2

全ての移動のうち、どのタイミングで縦方向(下)に移動するか、によって移動経路が変わります

従って、移動経路の数 = 全移動回 W + H - 2 から縦方向の移動回数 H - 1 を選択する組み合わせの数

$ {}_{(W+H-2)} \mathrm{C} _{(H-1)} $

になります

特定の範囲を通らない場合については参考記事に解説してある通りです

ここからが大変でした

値の取りうる範囲が 1 ≦ H, W ≦ 100,000 と大きいため、普通に以下の公式を使って計算すると、オーバーフローしたり、メモリがパンクしたり、計算時間が膨大になったりします

${}_n \mathrm{C}_r = \frac{n!}{r!(n-r)!}$

そこで、問題文にある以下の条件を使います

なお、答えは非常に大きくなることがあるので、答えは 10^9+7 で割ったあまりを出力してください。

もう一つの参考記事から、逆元による組み合わせ数の計算を実装します

また、計算結果に高速アクセスするため、 @zacky1972 さんにオススメ頂いた ETS を使います

Livebook で試行錯誤するため、テーブルが既にある場合は削除するようにしています

組み合わせ数計算は他でも使えそうなので、 Combination モジュールとして Main とは別に定義しておき、 Combination.init で必要な全ての階乗、逆元を計算しておき、 Combination.combination で以下の計算を実行します

${}_n \mathrm{C}_r = n! * r!^{-1} * ((n-r)!)^{-1}$

defmodule Combination do
  @mod 1000000007
  @max_index 200000

  defp renew_table(table_name, initial_objects) do
    if :ets.info(table_name) != :undefined do
      :ets.delete(table_name)
    end
    :ets.new(table_name, [:set, :protected, :named_table])
    :ets.insert(table_name, initial_objects)
  end

  defp lookup(table_name, key) do
    [{_, value}] = :ets.lookup(table_name, key)
    value
  end

  def init do
    renew_table(:factorial, [{0, 1}, {1, 1}])
    renew_table(:inverse, [{1, 1}])
    renew_table(:factorial_inverse, [{0, 1}, {1, 1}])

    2..@max_index
    |> Enum.reduce(1, fn index, acc ->
      factorial = rem(acc * index, @mod)

      mod_inverse = lookup(:inverse, rem(@mod, index))
      inverse = @mod - rem(mod_inverse * div(@mod, index), @mod)

      pre_factorial_inverse = lookup(:factorial_inverse, index - 1)  
      factorial_inverse = rem(pre_factorial_inverse * inverse, @mod)

      :ets.insert(:factorial, {index, factorial})
      :ets.insert(:inverse, {index, inverse})
      :ets.insert(:factorial_inverse, {index, factorial_inverse})

      factorial
    end)
  end

  def combination(n, r) do
    factorial_n = lookup(:factorial, n)
    factorial_inverse_nr = lookup(:factorial_inverse, n - r)
    factorial_inverse_r = lookup(:factorial_inverse, r)

    (factorial_inverse_nr * factorial_inverse_r)
    |> rem(@mod)
    |> Kernel.*(factorial_n)
    |> rem(@mod)
  end
end

defmodule Main do
  def main do
    :stdio
    |> IO.read(:all)
    |> solve()
    |> IO.puts()
  end

  defp split_words(words) do
    String.split(words, " ")
  end

  def solve(input) do
    [h, w, a, b] =
      input
      |> String.trim()
      |> split_words()
      |> Enum.map(&String.to_integer/1)

    Combination.init()

    0..(h - a - 1)
    |> Enum.reduce(0, fn h_i, acc ->
      l = Combination.combination(h_i + b - 1, h_i)
      r = Combination.combination(h + w - h_i - b - 2, h - 1 - h_i)
      acc + l * r
    end)
    |> rem(1_000_000_007)
  end
end

逆元による組み合わせ数の計算と、 ETS の利用によって制限時間をクリアできました

実行結果は 908 ms でした

13
0
2

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
13
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?