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

貪欲法
アルゴリズムを設計するのに役立つ考え方を設計技法とよびます。 貪欲法は動的計画法と並んで、 主要なアルゴリズム設計技法の一つです。
「後先のことを考えず、その場その場最善の選択を繰り返していく」という考え方のことを、貪欲法と呼ぶ。
貪欲法は、全ステップの意思決定をとおして見たときに、 必ずしも最適解を導くとは限りませんが、 ある種の問題に対しては有効に機能します。
全体が見えた場合、必ずしも場面場面で選択していった結果が、必ずしも最適解ではないってこと。しかし、ある種の問題には有効的に機能する

- アルゴリズムを設計するのに役にたつ考え方を「設計技法」と呼ぶ。設計技法の1つに貪欲法や動的計画法などがある
- アルゴリズムとは、解が定まっている「計算可能」な問題に対して、解を正しく求めるための手続き
アルゴリズム(英: algorithm[注 1])とは、解が定まっている「計算可能」問題に対して、その解を正しく求める手続きをさす

貪欲法に基づくアルゴリズムを設計してみる

この説明がわかりやすい
貪欲法とは、ある問題を部分問題に分割して、その部分問題の最適解をそれぞれ独立に求めて、評価値の高い順に取り込んでいくことで、問題の解を出す考え方のこと

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

rubyにインクリメントやデクリメント演算子はないので、代わりに+=や-=を使う

Integer#even?はselfが偶数なら、true、そうじゃないならfalseを返す。
Integer#odd?はselfが奇数なら、true、そうじゃないならfalseを返す。

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

決められた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

定数は大文字アンダースコア

Array#reverse_eachは各要素に対して、逆順にブロックを評価する

貪欲法って、ある2つの事柄をそれぞれ最大数(もしくは最小)までやって、それぞれの最適解を合わせて問題の最適解を求めるって考え方な気がする。
見方を変えて、貪欲法でも最適解が求まるようにする
整数 N が 1 になるまで、次のいずれかを繰り返す。最小回数を求めてください。
- N から 1 を引く
- (N が 3 の倍数ならば) N を 3 で割れるときは割る

巡回セールスマン問題(貪欲法)
今いる地点を基準として、その地点から最小の距離の点に移動するを何回も繰り返すことで、無駄にぐるぐる回らなくて済むか。最小距離のルートで動ける気がする

Array#firstで配列の最小の要素を取得できる。
String#firstとかはなかったきがするから、[0]を指定する

**を2つ書くことで、べき乗できる
Math.#sqrtで、正の平方根を求められる。Mathモジュールに用意されている。

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

区間スケジューリング問題
区間スケジューリング問題とは、それぞれの区間が重複せず、それぞれの区間の数を最大化するためにはどうすればよいかを考える問題である。
この記事わかりやすかった。
これの問題結構バイトのシフト組む時とか役立ちそう。

区間スケジューリング問題を解いてみた
バブルソートの部分は「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のインデックスはデカくても小さくても、どっちでもいいのか

バイナリサーチし目的要素のインデックスを取得する関数を作った
# @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項の交換は1ループで1回だけやる

隣り合う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

Array#find_indexは、引数で指定した要素が最初に出てくるインデックスを取得できる

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

挿入ソートは、与えられたいくつかの要素を、 所定の順序にしたがって並び替えるソートアルゴリズムです。 挿入ソートには次の特徴があります (要素の個数を N とします)。
与えられた配列以外の記憶領域が不要 (in-place であるといいます)
最悪時の計算量は O(N 2 )
平均時の計算量は O(N 2 )
最良時の計算量は O(N)

Numeric#positive?でselfが0より大きい時にtrueを返す

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

クイックソート
基準値を決めて、その基準値より大きいグループと、小さいグループを作る
その後、そのグループでも同じことを繰り返す。各グループの配列の要素数が1になったら、終了
クイックソートには次の特徴があります (要素の個数を N とします)。
- 部分問題を再帰的に解くアルゴリズムである (分割統治法)
- 最悪時の計算量は O(N 2 )
- 最良時と平均時の計算量は O(NlogN)
分割統治法とは、、
分割統治法(ぶんかつとうちほう、英: divide-and-conquer method)は、そのままでは解決できない大きな問題を小さな問題に分割し、その全てを解決することで、最終的に最初の問題全体を解決する、という問題解決の手法である。
小さな問題を全て解決することで、最終的に最初の問題全体も解決する

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

クイックソートはバブルソートなどと比べ煩雑ですが、平均計算量は 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つだと、後ろの数を含めない(未満になる)
こんなあっさりかけるのは知らなかった
# 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)]

整数 /整数ならデフォルトで切り捨てられる。

Array#any?はひとつでも条件を満たす要素があれば、trueを返す。全ての要素が条件を満たさなかったら、falseを返す。

クイックソートとマージソートは若干似ている。
クイックソートは、何もソートしていない配列から真ん中の要素を取り出して、その真ん中の要素を基準として、基準より大きいか小さかいで配列全体をグルーピングして、2つのグループを作る。そしてグループに対してクイックソートを再起的に繰り返して、クイックソートされたグループを作って、最後に合体させたら、ソートずみの配列が作れる
マージソートは、何もソートしていない配列に対して、真ん中のインデックスを基準にして、その基準よりインデックスが大きいか小さいかでグルーピングして、2つのグループを作る。そしてグループに対して、マージソートを再起的に繰り返して、ソートされたグループを、右側のグループをリバースさせて合体させる(小 ~ 大 ~ 小のレイヤーに配列がなっている)。その後、その合体させた配列の左右を比較していって、小さい方を先に別の配列に入れて、合体させた配列から小さい方を削除する。それを繰り返すと、ソートされた配列が完成する。
こうしてみると、クイックソートは、はじめに何もソートしていない配列の真ん中の要素を基準として、小さいか大きいかで2グループ作っている。
マージソートは、はじめに配列の真ん中のインデックスを基準として、インデックスより大きいか小さいかで2グループ作っている(つまり、要素の大きさはここのフェーズでは関係ないことがわかる)

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

ヒープソート

二分木
隣接してる2つの点に対して、より根に近い方を親、遠い方を子と呼びます。表記法としては、「xxxはyyyの親/子である」と表現します。
隣接している2点っていうのがポイント
根付き木の中でもそれぞれの点における子の数が m 個以下である根付き木のことを
m 分木と呼びます。
なるほど、てことは、2分木は、それぞれの点における子の数が2個以下の根付き木ってことか
m分木の中でも葉以外のすべての点が m個ずつ子をもつ根付き木のことを完全 m 分木と呼びます。
なるほど、ちなみに、葉とは、根付き木の一番下の頂点のこと


ヒープ
順序木とは、どのノードを親として見たときに「必ず 親 ≧ 子 が成立する木構造のことを表します。順序木のことをヒープと呼ぶこともあります。
なるほど、どの頂点に対しても、必ず親の値 >= 子の値が成立する木構造のことを、ヒープだったり、ヒープ木と呼ぶのか。別に二分木である必要はない感じだね
ヒープソート
ヒープソートは、データを「順序木*3」と呼ばれる木構造に構成し、最大値(最小値)を取り出して整列済みにしていくことでソートを行うアルゴリズムです。
なるほど、ヒープソートは、データをヒープ木にして、その最大値を取りだしていって、整列していくアルゴリズムのことなのか

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

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

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

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

根付き木の各頂点の深さの最大値を、木の高さとよびます
ヒープ木それぞれの要素は、木の高さ - 要素の高さ回交換されることが直感的にわかる

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

ヒープソートの手順
- 配列をヒープ構造(全ての親の値 >= 子の値)にしておく。ヒープ構造にしたことで、一番先頭の値がデカくて、一番後ろの値がちっちゃいって状態になっている
- 配列の先頭と一番後ろのノードを交換する。そしていちばん後ろのノードは切り離しておく
- 残ったノードで1 ~ 2を繰り返す

添字を上手く使えば、記憶容量は一個で済むか