Closed76

アルゴ式をRubyで解く。【コンピュータサイエンス入門】

ハガユウキハガユウキ

貪欲法

アルゴリズムを設計するのに役立つ考え方を設計技法とよびます。 貪欲法は動的計画法と並んで、 主要なアルゴリズム設計技法の一つです。

「後先のことを考えず、その場その場最善の選択を繰り返していく」という考え方のことを、貪欲法と呼ぶ。

貪欲法は、全ステップの意思決定をとおして見たときに、 必ずしも最適解を導くとは限りませんが、 ある種の問題に対しては有効に機能します。

全体が見えた場合、必ずしも場面場面で選択していった結果が、必ずしも最適解ではないってこと。しかし、ある種の問題には有効的に機能する

ハガユウキハガユウキ
  • アルゴリズムを設計するのに役にたつ考え方を「設計技法」と呼ぶ。設計技法の1つに貪欲法や動的計画法などがある
  • アルゴリズムとは、解が定まっている「計算可能」な問題に対して、解を正しく求めるための手続き

アルゴリズム(英: algorithm[注 1])とは、解が定まっている「計算可能」問題に対して、その解を正しく求める手続きをさす

https://ja.wikipedia.org/wiki/アルゴリズム

ハガユウキハガユウキ

その場その場で最善な選択を繰り返していくってやっと意味がわかってきた。
その場その場のその場は1つのループだったり、するのか。

ハガユウキハガユウキ

最小で何枚の硬貨が必要って聞かれた時点で、貪欲法を用いてアルゴリズムを設計しろって話やね。

ハガユウキハガユウキ

決められた2数から最小値を代入し直すって場面で、Array#minは使えるかも

before

FIFTY_YEN = 50
TEN_YEN = 10
FIVE_YEN = 5
ONE_YEN = 1

begin
  lines = readlines.map(&:chomp)
  price = lines[0].to_i
  coin_counts = lines[1].split.map(&:to_i)

  coin_counter = Hash.new(0)
  [FIFTY_YEN, TEN_YEN, FIVE_YEN, ONE_YEN].each_with_index do |coin, i|
    coin_counter[coin] += price / coin
    coin_counter[coin] > coin_counts[i] && coin_counter[coin] = coin_counts[i]
    price -= coin * coin_counter[coin]
  end

  puts coin_counter.values.sum
rescue StandardError => e
  p "raise: error", e
end

after

FIFTY_YEN = 50
TEN_YEN = 10
FIVE_YEN = 5
ONE_YEN = 1

begin
  lines = readlines.map(&:chomp)
  price = lines[0].to_i
  coin_counts = lines[1].split.map(&:to_i)

  coin_counter = Hash.new(0)
  [FIFTY_YEN, TEN_YEN, FIVE_YEN, ONE_YEN].each_with_index do |coin, i|
    coin_counter[coin] = [price / coin, coin_counts[i]].min
    price -= coin * coin_counter[coin]
  end

  puts coin_counter.values.sum
rescue StandardError => e
  p "raise: error", e
end

ハガユウキハガユウキ

貪欲法って、ある2つの事柄をそれぞれ最大数(もしくは最小)までやって、それぞれの最適解を合わせて問題の最適解を求めるって考え方な気がする。

見方を変えて、貪欲法でも最適解が求まるようにする

整数 N が 1 になるまで、次のいずれかを繰り返す。最小回数を求めてください。

  • N から 1 を引く
  • (N が 3 の倍数ならば) N を 3 で割れるときは割る
ハガユウキハガユウキ

巡回セールスマン問題(貪欲法)

今いる地点を基準として、その地点から最小の距離の点に移動するを何回も繰り返すことで、無駄にぐるぐる回らなくて済むか。最小距離のルートで動ける気がする

ハガユウキハガユウキ

貪欲法では、必ずしも最適解を求められるわけではない。ただし、解に近い値を得ることができる。
効率よく最適解を求めることが困難な問題であっても、 まずは貪欲法でアプローチしていくことは有効といえるでしょう。

ハガユウキハガユウキ

循環セールスマン問題は点視点で考える。点ごとに最適な意思決定をすれば良い。そこがようは貪欲法の考え方を用いている

ハガユウキハガユウキ

巡回セールスマン多分できるけど、なんか通らんから、暇な時やろう

ハガユウキハガユウキ

区間スケジューリング問題

区間スケジューリング問題とは、それぞれの区間が重複せず、それぞれの区間の数を最大化するためにはどうすればよいかを考える問題である。

この記事わかりやすかった。
これの問題結構バイトのシフト組む時とか役立ちそう。

https://qiita.com/uniTM/items/0dbd7ec962186c005c08

ハガユウキハガユウキ

区間スケジューリング問題を解いてみた

バブルソートの部分は「2次元配列 ソート ruby」で調べるといい感じの記事が出てきた

begin
  lines = readlines.map(&:chomp)
  event_count = lines[0].to_i
  event_times = lines[1..].map { |word| word.split.map(&:to_i) }

  sequential_events_count = 0

  # バブルソート
  # これで右端の値でソートしたevent_timesが出来上がる
  (0..(event_count - 1)).each do |i|
    (0..(event_count - (i + 2))).each do |j|
      if event_times[j][1] > event_times[j + 1][1]
        event_times[j], event_times[j + 1] = event_times[j + 1], event_times[j]
      end
    end
  end

  event_times.reduce do |result, event_time|
    if result[1] <= event_time[0]
      sequential_events_count += 1
      result = event_time
    end
    result
  end

  puts sequential_events_count + 1
rescue StandardError => e
  p "raise: error", e
end
ハガユウキハガユウキ

バブルソート

バブルソート以外とむずい。2数を比較して、一番でかい数を右端に追いやる。そして、その一番でかい数を除いた数たちで、また、どれがでかいかを決める。そしてsのでかい数をまた右端に追いやる。そうやっていくことで、数を昇順に並べることができる。

1ループで1回だけ入れ替えるか、入れ替えないか決まる
j = 1の時、 a[j-1]とa[j]を比較して、隣り合う2数を比較して(大 > 小)の関係なら、入れ替える。
(つまり、a[0], a[1] = a[1], a[0])。
大 > 小の関係ではないなら、入れ替えない。
そして次のループに行く。
j = 2の時、a[j - 1]はa1
そして内側のループが完了した際に、一番でかい数が一番右端に行っている。
次にその一番でかい数は無視して、残りの数でもう一度同じような流れをやる


  # バブルソート
  # これで右端の値でソートしたevent_timesが出来上がる
  (0..(event_count - 1)).each do |i|
    (0..(event_count - (i + 2))).each do |j|
      if event_times[j][1] > event_times[j + 1][1]
        event_times[j], event_times[j + 1] = event_times[j + 1], event_times[j]
      end
    end
  end
ハガユウキハガユウキ

二分探索

二分探索はその名の通り,「二分する」,つまりソートずみの候補を半分にしながら探索していくアルゴリズムである。

二分探索を使いたいときに重要なのは「質問」です.次のような質問を見つけることが出来たら,二分探索が使えます.

  • Yes / No で答えられる質問である.
  • あるところを境界として,そこより小さいところではずっと Yes だし,そこより大きいところではずっと No である.
  • または,あるところを境界として,そこより小さいところではずっと No だし,そこより大きいところではずっと Yes である.
ハガユウキハガユウキ

arrayの量を減らさなくても、arrayの始まりと終わりを自分でコントロールできれば、求めるインデックスは元のarrayを元にしているか。なるほどね。

ハガユウキハガユウキ

2択のうちどちらかを答えるので、一回の質問ごとに選択肢が 2^B → 2 ^B -1 → 2 ^ 1→ 1と減っていく
(Bは質問回数)
2 ^ Bは選択肢の総数だから、つまり、1以上N以下の整数をNとすると、N = 2^Bは成立するね。てなると、B = log2 N(2は底) が成立する。質問回数 = 計算回数だから、つまり、2分探索アルゴリズムの計算量はlog2Nってことか。

ハガユウキハガユウキ

2分探索って配列の要素が偶数の時どうするんやろうか。あ、でもcenterのインデックスが仮に小さくても、centerの数よりでかいか小さいかで判断して、探索領域をずらすから、centerのインデックスはデカくても小さくても、どっちでもいいのか

ハガユウキハガユウキ

https://qiita.com/ryosuketter/items/2798b09330e7102b6cfe

バイナリサーチし目的要素のインデックスを取得する関数を作った

# @param [Integer] target
# @param [Array<Integer>] sorted_array
# return [Integer]
def binary_search_index(target, sorted_array)
  start_index = 0
  end_index = sorted_array.length - 1

  while start_index <= end_index
    center_index = (start_index + end_index) / 2
    if target == sorted_array[center_index]
      return center_index
    elsif target > sorted_array[center_index]
      start_index = center_index + 1
    else
      end_index = center_index - 1
    end
  end

  - 1
end
ハガユウキハガユウキ
irb(main):009:0> puts 1e-4
0.0001

1e-4は、10のマイナス4乗を表している

ハガユウキハガユウキ

なんか泥沼化が予想される問題は一旦飛ばそう。このコード書いて何故かできなかった

begin
  number = gets.chomp.to_i

  start_number = 0
  end_number = 100
  while (end_number - start_number) > 1e-4
    center_number = (start_number + end_number) / 2
    result_number = center_number * (center_number * (center_number + 1) + 2) + 3
    if result_number < number
      start_number = center_number
    else
      end_number = center_number
    end
  end

  solution = start_number
  puts solution
rescue StandardError => e
  p "raise: error", e
end
ハガユウキハガユウキ

バブルソートやるか

バブルソートとは

バブルソートは、与えられたいくつかの要素を、 所定の順序にしたがって並び替えるソートアルゴリズムです。 バブルソートには次の特徴があります (要素の個数を N とします)。

与えられた配列以外の記憶領域が不要 (in-place であるといいます)
最悪時の計算量は O(N 2 )
平均時の計算量は O(N 2)
最良時の計算量は O(N)

ハガユウキハガユウキ

バブルソートこの前やったな。前と後ろをどんどん入れ替えて、でかい数を後ろに追いやっていくやつだった気がする。次のループではでかい数を除いた数で比較をしていく

ハガユウキハガユウキ

隣り合う2項をどんどん比較しているし、交換したら、次のループでもまた比較対象になるから、てことはでかい数はやっぱ一番左側に向かうね。

ハガユウキハガユウキ

バブルソートはO ^ 2だから結構重めのソート方法。最初に学ぶソートでもある

# バブルソート
# バブルソートは与えられた配列以外の記憶領域が不要

# @params [Array<Integer>] numbers
def bubble_sort(numbers:)
  (0..numbers.length - 1).each do |i|
    exchange_count = 0

    (1..(numbers.length - (i + 1))).each do |j|
      if numbers[j - 1] > numbers[j]
        numbers[j - 1], numbers[j] = numbers[j], numbers[j - 1]
        exchange_count += 1
      end
    end

    # 上のループの隣り合う2項の比較で、一度も要素が交換されなかったら、処理を終了する
    break if exchange_count.zero?

    puts numbers.join(" ")
  end

  numbers
end

begin
  lines = readlines.map(&:chomp)
  # numbers_count = lines[0].to_i
  numbers = lines[1].split.map(&:to_i)

  bubble_sort(numbers: numbers)
  # p sorted_numbers
rescue StandardError => e
  p "raise error:", e.message
end

ハガユウキハガユウキ

選択ソート

選択ソートは、与えられたいくつかの要素を、 所定の順序にしたがって並び替えるソートアルゴリズムです。 選択ソートには次の特徴があります (要素の個数を N とします)。

与えられた配列以外の記憶領域が不要 (in-place であるといいます)
計算量は O(N 2 ) (最悪時、平均時、最良時すべてについて)

ハガユウキハガユウキ

選択ソートは、ソート順序が確定していないデータ(最初は全部)から、最小値を見つけて、その最小値を一番左に置く。次にその確定済みのデータを除いた数の中で最小値を見つけて、考慮対象のデータの一番左におく。以降繰り返すと、ソートされる。計算量はO(N ^ 2)で安定している。

ハガユウキハガユウキ

選択ソートのサンプルコード

# 選択ソート

# @params [Array<Integer>] numbers
def selection_sort(numbers:)
  (0..numbers.length - 2).each do |i|
    minimum_number = numbers[i..].min
    minimum_number_index = numbers[i..].find_index(minimum_number)
    numbers[i], numbers[minimum_number_index + i] = numbers[minimum_number_index + i], numbers[i]
    puts numbers.join(" ")
  end
  numbers
end

begin
  lines = readlines.map(&:chomp)
  # numbers_count = lines[0].to_i
  numbers = lines[1].split.map(&:to_i)

  selection_sort(numbers: numbers)
  # p sorted_numbers
rescue StandardError => e
  p "raise error:", e.message
end
ハガユウキハガユウキ

挿入ソート

挿入ソートとは、データを整列済と未整列の2つに分類し、未整列データの先頭の要素を、整列済みデータの正しい場所に挿入していくことで、ソートしていく方法のこと

ハガユウキハガユウキ

挿入ソートは、与えられたいくつかの要素を、 所定の順序にしたがって並び替えるソートアルゴリズムです。 挿入ソートには次の特徴があります (要素の個数を N とします)。

与えられた配列以外の記憶領域が不要 (in-place であるといいます)
最悪時の計算量は O(N 2 )
平均時の計算量は O(N 2 )
最良時の計算量は O(N)

ハガユウキハガユウキ

バブルソート、選択ソート、挿入ソートは、基本ソートアルゴリズムと呼ばれているそう
この3つの基本的なソートアルゴリズムより、クイックソート、マージソート、シェルソート、ヒープソートの方が実用的で高速だそう。

ハガユウキハガユウキ

クイックソート

基準値を決めて、その基準値より大きいグループと、小さいグループを作る
その後、そのグループでも同じことを繰り返す。各グループの配列の要素数が1になったら、終了

クイックソートには次の特徴があります (要素の個数を N とします)。

  • 部分問題を再帰的に解くアルゴリズムである (分割統治法)
  • 最悪時の計算量は O(N 2 )
  • 最良時と平均時の計算量は O(NlogN)

分割統治法とは、、

分割統治法(ぶんかつとうちほう、英: divide-and-conquer method)は、そのままでは解決できない大きな問題を小さな問題に分割し、その全てを解決することで、最終的に最初の問題全体を解決する、という問題解決の手法である。

https://ja.wikipedia.org/wiki/分割統治法#:~:text=分割統治法(ぶんかつ,解決の手法である。

小さな問題を全て解決することで、最終的に最初の問題全体も解決する

ハガユウキハガユウキ

クイックソートは基準の値がもし最小値だった場合、基準の値より小さい値の集合の配列が空の配列となる。そして、基準値より大きい値を集合させた配列に対して再帰的にクイックソートを実施するから、結果的に、ソートされる。だから基準値で最小値を選んでも大丈夫

ハガユウキハガユウキ

クイックソートはバブルソートなどと比べ煩雑ですが、平均計算量は O(NlogN) と効率的です。

クイックソートは早いな。今までの基本ソートはO(N^2)だから。クイックソートのO(NlogN)は早い

ハガユウキハガユウキ

クイックソートのアルゴリズム

# @param [Array<Integer>]
# return [Array<Integer>]
def quick_sort(numbers)
  return numbers if numbers.length == 1
  return numbers if numbers.empty?

  standard_number = (numbers.length / 2).floor
  left_numbers = numbers.filter.with_index { |number, i| i != standard_number && number < numbers[standard_number] }
  right_numbers = numbers.filter.with_index { |number, i| i != standard_number && number >= numbers[standard_number] }

  sorted_left_numbers = quick_sort(left_numbers)
  sorted_right_numbers = quick_sort(right_numbers)

  sorted_left_numbers.push(numbers[standard_number]).concat(sorted_right_numbers)
end

begin
  lines = readlines.map(&:chomp)
  # numbers_count = lines[0].to_i
  numbers = lines[1].split.map(&:to_i)

  sorted_numbers = quick_sort(numbers)
  puts sorted_numbers.join(" ")
rescue StandardError => e
  p "raise error:", e.message
end
ハガユウキハガユウキ

マージソート

マージソートは、配列を再起的に半分にして、要素数が1個になるまで分割していき、そっからソートしながら、マージして、最終的にソートするソート方法

ハガユウキハガユウキ

マージ部分は作業用配列を用意する
左半分のデータを作業用配列にコピーして、右半分のデータは逆順にして作業用配列にコピーする。
すると、作業用配列の両端が小さい値に、真ん中が大きい値となる。
あとは、両端の「先頭」と「最後尾」のどちらか小さい値の方を空の配列に値を入れていく操作を作業用配列のすべてのデータがコピーされるまで続ければマージ完了である。

作業用配列の両端を比較して、それを空の配列に格納して、最終的にソートされた状態でマージが完了するのか

ハガユウキハガユウキ

マージソートはバブルソートなどと比べ煩雑ですが、平均計算量は O(NlogN) と効率的です。

マージソートのアルゴリズム

# マージソート

# @param [Array<Integer>]
# return [Array<Integer>]
def merge_sort(numbers)
  return numbers if numbers.length <= 1

  # 整数 / 整数なら、デフォルトで切り捨て羅れる
  middle_index = numbers.length / 2
  left_numbers = numbers[0...middle_index]
  right_numbers = numbers[middle_index..(numbers.length - 1)]

  sorted_left_numbers = merge_sort(left_numbers)
  sorted_right_numbers = merge_sort(right_numbers)
  merge_numbers = sorted_left_numbers.concat(sorted_right_numbers.reverse)

  sorted_merge_numbers = []
  until merge_numbers.empty?
    if merge_numbers.first <= merge_numbers.last
      sorted_merge_numbers.push(merge_numbers.shift)
    else
      sorted_merge_numbers.push(merge_numbers.pop)
    end
  end

  sorted_merge_numbers
end

begin
  lines = readlines.map(&:chomp)
  # numbers_count = lines[0].to_i
  numbers = lines[1].split.map(&:to_i)

  sorted_numbers = merge_sort(numbers)
  puts sorted_numbers.join(" ")
rescue StandardError => e
  p "raise error:", e.message
end

ハガユウキハガユウキ

空になるまで続けるとか、〇〇になるまで続けるって出てきたら、whileやuntilの使い所かもしれんな。

ハガユウキハガユウキ

分割統治法をする際に、再帰的な考え方が役に立つな。別の対象が独自に意思決定をする感じ

ハガユウキハガユウキ

Rangeなので、点3つだと、後ろの数を含めない(未満になる)

https://docs.ruby-lang.org/ja/latest/class/Range.html

こんなあっさりかけるのは知らなかった

  # left_numbers = numbers.filter.with_index { |_, i| i < middle_index }
  # right_numbers = numbers.filter.with_index { |_, i| i >= middle_index }

  left_numbers = numbers[0...middle_index]
  right_numbers = numbers[middle_index..(numbers.length - 1)]
ハガユウキハガユウキ

クイックソートとマージソートは若干似ている。
クイックソートは、何もソートしていない配列から真ん中の要素を取り出して、その真ん中の要素を基準として、基準より大きいか小さかいで配列全体をグルーピングして、2つのグループを作る。そしてグループに対してクイックソートを再起的に繰り返して、クイックソートされたグループを作って、最後に合体させたら、ソートずみの配列が作れる

マージソートは、何もソートしていない配列に対して、真ん中のインデックスを基準にして、その基準よりインデックスが大きいか小さいかでグルーピングして、2つのグループを作る。そしてグループに対して、マージソートを再起的に繰り返して、ソートされたグループを、右側のグループをリバースさせて合体させる(小 ~ 大 ~ 小のレイヤーに配列がなっている)。その後、その合体させた配列の左右を比較していって、小さい方を先に別の配列に入れて、合体させた配列から小さい方を削除する。それを繰り返すと、ソートされた配列が完成する。

こうしてみると、クイックソートは、はじめに何もソートしていない配列の真ん中の要素を基準として、小さいか大きいかで2グループ作っている。
マージソートは、はじめに配列の真ん中のインデックスを基準として、インデックスより大きいか小さいかで2グループ作っている(つまり、要素の大きさはここのフェーズでは関係ないことがわかる)

ハガユウキハガユウキ

真ん中のインデックスって言っているけど、配列の要素数が奇数だろうが偶数だろうが切り捨て前提ある。どちらにせよソートできるので。

ハガユウキハガユウキ

ヒープソート

ハガユウキハガユウキ

二分木

隣接してる2つの点に対して、より根に近い方を親、遠い方を子と呼びます。表記法としては、「xxxはyyyの親/子である」と表現します。

隣接している2点っていうのがポイント

根付き木の中でもそれぞれの点における子の数が m 個以下である根付き木のことを
m 分木と呼びます。

なるほど、てことは、2分木は、それぞれの点における子の数が2個以下の根付き木ってことか

m分木の中でも葉以外のすべての点が m個ずつ子をもつ根付き木のことを完全 m 分木と呼びます。

なるほど、ちなみに、葉とは、根付き木の一番下の頂点のこと

https://www.momoyama-usagi.com/entry/math-risan11

ハガユウキハガユウキ

ヒープ

順序木とは、どのノードを親として見たときに「必ず 親 ≧ 子 が成立する木構造のことを表します。順序木のことをヒープと呼ぶこともあります。

なるほど、どの頂点に対しても、必ず親の値 >= 子の値が成立する木構造のことを、ヒープだったり、ヒープ木と呼ぶのか。別に二分木である必要はない感じだね

ヒープソート

ヒープソートは、データを「順序木*3」と呼ばれる木構造に構成し、最大値(最小値)を取り出して整列済みにしていくことでソートを行うアルゴリズムです。

なるほど、ヒープソートは、データをヒープ木にして、その最大値を取りだしていって、整列していくアルゴリズムのことなのか

ハガユウキハガユウキ

ヒープかどうかをチェックする必要があるのは、子ノードが存在する親ノードだけ。

ハガユウキハガユウキ

入れ替えた相手のノードに子ノードが繋がれている場合、入れ替えた相手のノードがヒープになっているかの確認が必要になります。

ハガユウキハガユウキ

入れ替えたら相手に子ノードが存在するなら、その子ノードとヒープになっているか確認する必要があるのかん。なるほどなあ

ハガユウキハガユウキ

2分木においては、左の子と右の子の位置関係とかどうでも良いな。
親の値 >= 子の値になっていれば2分木だから。

ハガユウキハガユウキ

根付き木の各頂点の深さの最大値を、木の高さとよびます

ヒープ木それぞれの要素は、木の高さ - 要素の高さ回交換されることが直感的にわかる

ハガユウキハガユウキ

ヒープソートの特徴

  • ヒープと呼ばれるデータ構造を用いて、最大値から決定していってソートする
  • 与えられた配列以外の記憶領域が不要 (in-place であるといいます)
  • 最良、最悪、平均計算量はすべて O(NlogN)
ハガユウキハガユウキ

ヒープソートの手順

  1. 配列をヒープ構造(全ての親の値 >= 子の値)にしておく。ヒープ構造にしたことで、一番先頭の値がデカくて、一番後ろの値がちっちゃいって状態になっている
  2. 配列の先頭と一番後ろのノードを交換する。そしていちばん後ろのノードは切り離しておく
  3. 残ったノードで1 ~ 2を繰り返す
このスクラップは2023/11/08にクローズされました