アルゴリズムで1週間で結果出す

- アルゴ式をとく(アルゴリズムとデータ構造)
- leet codeのeasyからmediumを全部解けるような状態になっておく
- hackers rank
1週間で結果を出せなかったら、それはもう自分の責任

Product of Array Except Selfが計算量をo(n)にしてやるのがちょっとむずかった
計算量はこのキータがわかりやすかった。でかいステップ以外は無視するからあの計算量になるのか

String#each_charは文字列でループができる
String#each_char
String#split
正規表現にiを加えることで、大文字小文字を無視できる
String#scanは、パターンを繰り返しマッチさせて、マッチする文字列からなる配列を戻り値として返す
String#gsubは、文字列中でパターンにマッチする部分全てを指定した文字列に置き換えた文字列を生成する。ブロックを使うごとでループごとに特定の処理をしたりもできる
String#stripで、文字列の先頭と末尾から空白を取り除いた文字列を生成して返す
\s
掛け算の積のことをproductというそう

尺取り法
尺取り法は、右端と左端の2つのインデックスを保持し、条件に応じて片方のインデックスを動かすことで、計算ループの最適化を図るアルゴリズムです。。全探索の場合計算量は O(N2) ですが、尺取り法では O(N)
ですみます。
要は、2つのインデックスを使って、ループ効率を上げるのが尺取り法

配列から0を取り除いて、後ろに取り除いた0を追加するタイプの問題は、
2つのポインターを使って、そのポインターの幅が0の個数を表すようにすれば解ける。つまり、尺取り法の問題である。
# @param {Integer[]} nums
# @return {Void} Do not return anything, modify nums in-place instead.
def move_zeroes(nums)
# 2つのポインタを使って、その2つのポインタの幅が0の数だと考えれば良い
# a[right]が0だった場合、rigthに+1をして幅を広げる
left = 0
right = 0
while right < nums.length && left <= right
if nums[right].zero?
right += 1
next
end
# 0以外の時の処理
# 一番手前の0とrightが指し示す0以外の値を入れ替える
nums[left], nums[right] = nums[right], nums[left]
left += 1
# leftが先に行っちゃうパターンがあるから、rightも追いつかせる。
# leftとrightが同じ位置にいるのは問題なくてleftが先に行っちゃうのが問題
if right < left
right = left
end
end
nums
end

容器に入る水の最大値を求める問題で、two pointersを使ったのだが、こちらの記事が参考になった
# @param {Integer[]} height
# @return {Integer}
def max_area(height)
# l = 0, r = height.length - 1だとする
# a[l]とa[r]でたとえばa[l]のが小さい場合、
# rをheight.length - 1以外のどんな値にしても、a[l] * a[r]が一番でかい
# 最大値を求めるのが今回の問題だから、最小値をわざわざ調べなくても良い
# a[l]とa[r]のうち、小さい方をずらす。
# でかい方の高さを維持して、最大値の面積を探せば良い
l = 0
r = height.length - 1
s = 0
while (r - l).positive?
tmp = (r - l) * [height[l], height[r]].min
s = tmp if tmp > s
# こうもかける
s = [s, tmp].max
if height[l] >= height[r]
r -= 1
else
l += 1
end
end
s
end
# height = [1,8,6,2,5,4,8,3,7]
height =[1, 1]
p max_area(height)

整数配列 nums が与えられたとき、0 以外の要素の相対順序を維持したまま、すべての 0 を配列の末尾に移動する。
2つの文字列sとtが与えられたとき、sがtの部分列であれば真を、そうでなければ偽を返す。
文字列の部分列とは、元の文字列から、残りの文字の相対的な位置を乱すことなく、文字の一部(なくてもよい)を削除してできた新しい文字列のことである。(すなわち、"ace "は "abcde "の部分文字列であるが、"aec "はそうではない)。
長さnの整数配列heightが与えられ、i番目の線の2つの端点が(i, 0)と(i, height[i])となるようにn本の垂直線が引かれる。
x軸と一緒になって容器を形成し,その容器が最も多くの水を含むような2本の直線を求めよ.
調べる区間が決まっていて、その中で特定の2つのインデックスを操作して幅を広げたり狭くしたりしつつ、何か値を求める(最大値とか最小値が多いかも)系のアルゴリズムを書きたいなら、尺取り法が使える

スライディングウィンドウアルゴリズムとは
スライディングウィンドウアルゴリズムとは, 「配列内にある特定の範囲(ウィンドウサイズ)の配列に着目してアプローチを行い, アプローチ後に範囲をひとつずらして, またアプローチをする」という方法を繰り返していくアルゴリズムです。
なるほど、配列のある部分に着目してアプローチをして、アプローチ後に範囲を一つずらしてまたアプローチをするのが、スライディングウインドウアルゴリズムなのか

ウインドウとはシステム状態の一部のこと
システム状態が配列で表現されているなら、配列の一部がウインドウである。
スライディングとは、ウインドウの位置を1つずらすと、処理を適用できる別のウインドウが出現することを指している
ステップ数とは、命令が実行された回数
スライディング・ウィンドウ・アルゴリズムとは、計算のためのウィンドウが固定されているものを高速に計算するアルゴリズムであり、ネストされたループ(素朴なアプローチ)を使用するよりも最適化された方法で結果を取得することができる。このアルゴリズムの主な目的は、あるウィンドウの結果を再利用して次のウィンドウの結果を計算することである。
この説明がわかりやすい。任意の固定されたウインドウに対しての計算を高速化するアルゴリズムがスライディングウインドウアルゴリズム。このアルゴリズムの本質は、あるウインドウに対しての計算結果を再利用して次のウインドウの計算結果を計算でき、その結果、計算量をO(n)にすることができることである
たとえば、力ずくでk個のウインドウを1個処理するのに、kステップが必要(1ループ)で、配列内でウインドウがn子あるなら、このアルゴリズムを力ずくで完了させるためには、n * kステップ必要である。
しかし、スライディングウインドウアルゴリズムを適用すると、ズレるごとに変化する要素は2個なので、前ウインドウの計算結果の0番目の計算結果を消して、i + 1番目の計算結果を足せば、ウインドウの計算結果を求めることができる。そのため、計算量は、n個のウインドウがあるなら、k + 2 * (n - 1)である。kは1個目のウインドウの和をループで求めるときのステップ数で、2 * (n - 1)は、n - 1個のウインドウに対して、2つのステップ(前ウインドウの結果の0番目の値を消して、 i+ 1番目の結果を足す)を実施するので、このステップ数である。よって、2n - 2 + k(kは定数)の一番大きい項に着目すれば、2n、つまり、nであるから、計算量はnである。(kはnに比べると小さいと考えられるので、無視して良い)



スライディングウインドウで気をつけることは、次のウインドウを元の配列から生成するわけではないということ。それだと結構重めの処理が走る。

前のウインドウの計算結果を利用して次のウインドウの計算結果を求めるのがポイント

roundに引数を指定する
roundに引数を指定すると、その桁数で丸めてくれる
Numeric#fdiv
Numeric#fdivは、self を引数で割った商を Float で返します。

Array#countは、引数を何も指定しない場合、selfの要素数を数える(これはArray#lengthと一緒)
1文字を引数として指定すると、その1文字が何個配列に含まれているかをカウントしてくれる
さらにブロックを使うと、そのブロックの中で条件が真になるものをカウントしてくれる

指定されたk回反転しても良い
ある配列の中にいる連続する1の最大長を求めよ
この問題って、区間は動的。スライディングウインドウは区間は静的なので、スライディングウインドウではなさそう
この動的な区間の中で、k == 0になるまで区間を伸ばしていき、区間の最大長を求めれば良い。

最大値と最小値が決まっている区間の中で、動的な区間を作って、その動的な区間の中での最大値を求めよとか出たら、2pointersか、スライディングウインドウの問題かも

今やってたのは動的スライディングウインドウの問題
2pointersとスライディングウインドウ組み合わせる系の問題は結構やっかい

とりあえず出したやつ
# @param {Integer[]} nums
# @param {Integer} k
# @return {Integer}
def longest_ones(nums, k)
max_count = 0
i = 0
j = 0
c = k
while j <= nums.length - 1 && i <= j
if nums[j] == 1
j += 1
elsif nums[j].zero?
if c.positive?
j += 1
c -= 1
else
# これ以上区間を拡張できない時の処理
# jは動かず、iを動かす
# 本当にnumsが変わっているわけではなくて、cが減っているだけ
total_count = ((j - 1) - i) + 1
max_count = total_count if total_count >= max_count
c += 1 if nums[i].zero? && k > 0
i += 1
# i + 1番目に関しては、以前のウインドウでc -= 1しているはずだから、0かどうかは考慮しなくて良い
j = i if j <= i
end
end
if j == nums.length
total_count = ((j - 1) - i) + 1
max_count = total_count if total_count >= max_count
end
end
max_count
end
スライディングアルゴリズムと2 pointersで2重ループ使っている時点で微妙。本来の意図とずれてくる

動的な区間を扱う時点で、パラメータが2つ必要だな。さらにその区間の長で最大値or最小値(または個数の最大最初)を求めるときは、尺取り方が使える
これが静的な区間になると、スライディングウインドウアルゴリズムが使える
尺取り法を使わずに普通にやると、n2の計算量がかかる。区間をずらしつつ、2重ループで全てのパターンを求めて、最後にそれらの値を比較する必要があるから。
尺取り方を使えば、値を満たすパターンごとにiとjがずれているだけだし、i <= jが常に成立するので、計算量は、n(他の項は省略しているよ)だと考えられる
尺取り法とスライディングアルゴリズム若干似てんのよなあ
尺取り法は2つのインデックスに関心がある。スライディングアルゴリズムは、静的な区間での処理結果に関心がある。
配列のコピーも要素数増えると、結構コスト高そうな処理だな

2区間をの端点をいじるなら、インデックスはlとrのがわかりやすいかも

ハッシュマップ
コンピューティングでは、ハッシュテーブル(ハッシュマップとも呼ばれる)は、辞書とも呼ばれる連想配列を実装したデータ構造であり、キーと値を対応付ける抽象データ型である[2]。ハッシュテーブルは、ハッシュ関数を使用して、バケットまたはスロットの配列へのインデックス(ハッシュコードとも呼ばれる)を計算し、そこから目的の値を見つけることができる。検索時にはキーがハッシュ化され、その結果のハッシュが対応する値が格納されている場所を示す。
理想的には、ハッシュ関数は各キーを一意のバケットに割り当てるが、ほとんどのハッシュテーブルの設計は不完全なハッシュ関数を採用しているため、ハッシュ関数が複数のキーに対して同じインデックスを生成するハッシュ衝突が発生する可能性がある。このような衝突は、通常何らかの方法で対処される。
十分な次元のハッシュ表では、各ルックアップの平均時間複雑度は、表に格納されている要素数に依存しない。また、多くのハッシュテーブルの設計では、操作ごとに償却された一定の平均コストで、キーと値のペアの任意の挿入と削除が可能です[3][4][5]。
ハッシュは時空間トレードオフの一例である。メモリが無限であれば、キー全体を直接インデックスとして使用し、1回のメモリアクセスでその値を見つけることができる。一方、無限の時間が利用可能であれば、キーに関係なく値を格納することができ、バイナリ検索や線形検索を使って要素を取り出すことができる[6]:458
多くの状況において、ハッシュ表はサーチツリーやその他の表検索構造よりも平均的に効率的であることが判明している。このため、ハッシュテーブルは多くのコンピュータ・ソフトウェア、特に連想配列、データベース・インデックス、キャッシュ、集合などで広く使われている。

なるほど、ハッシュテーブルはハッシュマップとも呼ばれるデータ構造であり、キーにハッシュ関数をかけることで、インデックスを取得して、そのインデックスがデータが格納されている配列のインデックス番号ってことか

ハッシュのデフォルト値をHash#default=で変更することができる。
# @param {Integer[]} nums1
# @param {Integer[]} nums2
# @return {Integer[][]}
def find_difference(nums1, nums2)
nums1_hash = nums1.tally
nums1_hash.default = 0
nums2_hash = nums2.tally
nums2_hash.default = 0
answers = [[], []]
nums1_hash.keys.each do |key|
if nums2_hash[key].zero?
answers[0].push(key)
end
end
nums2_hash.keys.each do |key|
if nums1_hash[key].zero?
answers[1].push(key)
end
end
answers
end

Array#uniqは重複を消したユニークな配列を返す

配列同士の引き算は、左の配列の要素から共通する要素を引く
右の配列が左配列にない要素を持っていても、無視される

rubyで配列がユニークかどうかを判定したいなら、元の配列 == 元の配列.uniqをすれば良いのか

uniq!はselfが元々ユニークだと、消す要素がなくてnilを返すので、
そこら辺を注意する

String#charsで、文字列の各文字を要素とする配列を作れたんかい
これってString#split("")とやっていること一緒やないかい

どんなインプットで、どんな結果が欲しいのかをアルゴリズムを解く際には意識した方が良いかも

ハッシュのメリットはデータ検索がO(1)

配列からハッシュを生成したいなら、Array#tarryを使えば良い。
tarryを使わなくても以下のように配列からハッシュを作ることができる。
h = Hash.new(0)
words.map { |word| h[word] += 1 }
つまり、tarryの計算量はおそらくO(n)だとわかる。
以下のような問題でハッシュの良さを考察する
文字列の集合を表す配列が与えられて、文字列を表すクエリが与えられる。
文字列の集合の中にクエリの文字列が何個含まれているのかを出力せよ
↓ ハッシュのデータ構造を採用して解いた場合
lines = readlines.map(&:chomp)
words = lines[1].split(" ")
querys = lines[3..]
word_hash = words.tally
word_hash.default = 0
until querys.empty?
query = querys.shift
puts word_hash[query]
end
データ構造にハッシュを採用した場合、配列からハッシュを作るまでの処理は、O(n)かかる。
しかし、それ以降はクエリのキーを用いて、ハッシュからキーに対応する個数を取得できるので、クエリごとの計算量はO(1)。O(1)はnに比べると限りなく小さいので、全体の計算量はO(n)である。
データ構造に配列を採用した場合、配列からクエリの文字列が何個配列に含まれているかをカウントするのに、クエリごとに、配列のn個の要素がクエリの文字列と等しいかどうかチェックする必要がある。これだと、クエリごとに計算量がO(n)なので、全体の計算量はO(k * n)である。これはクエリの個数がめちゃくちゃ増加すると、それに比例してとんでもなく大きな計算量になってしまう。
# TLEになっちゃうアルゴリズム(TLE: Time Limit Exceeded)
# データ構造の選定に失敗している。(配列を採用した)
lines = readlines.map(&:chomp)
words = lines[1].split(" ")
querys = lines[3..]
until querys.empty?
query = querys.shift
puts words.count(query)
end

行と列を逆にしたいなら、Array#transposeが使える

String#each_charで1文字ずつループできるんか

Array#firstやArray#lastは破壊的変更をせずに要素を出力できる


もっと短く書けそう
def asteroid_collision(asteroids)
# この配列をスタックのデータ構造とする
asteroid_stack = []
asteroid_stack.push(asteroids[0])
asteroids[1..].each do |asteroid|
poped_asteroid = asteroid_stack.pop
if poped_asteroid.nil?
asteroid_stack.push(asteroid)
next
end
if (poped_asteroid.positive? && asteroid.positive?) \
|| (poped_asteroid.negative? && asteroid.negative?) \
|| (poped_asteroid.negative? && asteroid.positive?)
asteroid_stack.push(poped_asteroid, asteroid)
else
while asteroid_stack.length >= 0
if (poped_asteroid.positive? && asteroid.positive?) \
|| (poped_asteroid.negative? && asteroid.negative?) \
|| (poped_asteroid.negative? && asteroid.positive?)
asteroid_stack.push(poped_asteroid, asteroid)
break
end
if poped_asteroid.abs > asteroid.abs
asteroid_stack.push(poped_asteroid)
break
elsif poped_asteroid.abs == asteroid.abs
break
else
# asteroidの方がでかい場合
poped_asteroid = asteroid_stack.pop
# もし、asteroid_stackが空になってしまった場合
if poped_asteroid.nil?
asteroid_stack.push(asteroid)
break
end
end
end
end
end
asteroid_stack
end

# @param {Integer[]} asteroids
# @return {Integer[]}
def asteroid_collision(asteroids)
# この配列をスタックのデータ構造とする
asteroid_stack = []
asteroids.each do |asteroid|
# 衝突するパターンだけ、whileで処理する
# 衝突するパターンが終了するまで、ループする
while !asteroid_stack.empty? && asteroid.negative? && asteroid_stack.last.positive?
diff = asteroid + asteroid_stack.last
if diff.negative? # アステロイドの方がでかい
asteroid_stack.pop
elsif diff.positive? # stack.lastの方がでかい
# アステロイドが消えたことを表したいので、0を使う
asteroid = 0
else # 同じ大きさの惑星の時
asteroid = 0
asteroid_stack.pop
end
end
asteroid_stack.push(asteroid) if asteroid != 0
end
asteroid_stack
end
できた
応急処置的なコード書きたくねーなー
綺麗にしたい

操作から規則性を見つける

スタックとキューもデータ構造の一つ
配列や連結リストを使えば簡単に実現できるので、そんな特別なデータ構造でもない

リンクリストは、データ(ここでは例として整数値)と次のノードへのリンクという2つのフィールドを含むノードのシーケンスである。最後のノードは、リストの終わりを示すターミネーターにリンクされている。
リンクドリストは、一連のノードで構成されるデータ構造である。各ノードはポインターを持っていて、次のノードを指し示している
リンクドリストの順序はメモリ上の物理的な配置に依存しない
連結リストのメリットは、データの追加、削除が簡単。連結リストのデータの前後の連結リストデータを変更すれば良いので楽。デメリットは、目的のデータを見つけるのに1個1個連結リストのデータを辿らないといけないので、めんどくさい
配列のメリットは、データの探索が楽。O(1)
デメリットは、データの追加、削除でずらさないといけないので、結構重めな計算が発生すること


連結リストってリンクドリストって呼ぶのか

リンクドリストの真ん中の要素を見つけたいなら、それぞれのリンクドリストを配列に入れて、真ん中の要素を見つけば良い。ただし、これは、時間計算量がO(n)で空間計算量がO(n)である
空間計算量はアルゴリズムで必要とするメモリの使用量のこと
O(1)の空間計算量でどうにかしたい
時間計算量は変わらないが、空間計算量が変わるのか。メモリ効率的にだいぶ良いね
そういう時は、遅いポインタと速いポインタを使えば良い
遅いポインターは一度に1つのノードを動かす。速いポインターは一度に2つのノードを動かす。速いポインタがリストの最後に来たとき、遅いポインタはリストの真ん中に来る。

さて、1ターン後、遅いポインターはノードを1つ動かし、速いポインターは2つ動かす。

ここで、fast変数とslow変数が変化し続けるループを作りたい。fast'がnullでない限り、また次のノードがnullでない限り、変化し続けるようにしたい。fastがnullであるか、次のノードがnullであれば、fastはリストの末尾にあり、slowは真ん中のノードにあることがわかる。whileループの中でslowとfastを変更する。slowはslow.nextと等しく設定され、fastはfast.next.nextと等しく設定されます。
Open in DeepL.com


上のやり方だと、リンクドリストのノードが偶数個あった時に、半分に割った右半分の要素が真ん中になる。(おそらく多くのリンクドリストの問題でそうだから大丈夫)

速いポインターが遅いポインターの2倍のスピードで進むんだから、速いポインターが一番最後のノードを指し示したときは、遅いポインターは真ん中のノードを指し示しているのは普通やな

単一リンクリストの先頭が与えられたら、奇数のインデックスを持つノードをすべてグループ化し、その後に偶数のインデックスを持つノードをグループ化し、並べ替えたリストを返す。
最初のノードは奇数とみなされ、2番目のノードは偶数とみなされる。
偶数グループと奇数グループの内部での相対的な順序は、入力時のままでなければならないことに注意してください。
あなたはこの問題をO(1)の余分な空間複雑度とO(n)の時間複雑度で解かなければならない。

主に境界値周りの検証を行うためのテストケースを「エッジケース」という。
境界値周りのテストケースがエッジケースと呼ぶのか
なるほど

連結リストを逆転させるのは、この記事が役に立った
def reverse_list(head)
prev_current_node = nil
while head
current_node = head
head = current_node.next
current_node.next = prev_current_node
prev_current_node = current_node
end
prev_current_node
end
current変数にheadを入れた後に、headをnextにずらしてheadに再代入すればよかったのか。
そうすると、headの始まりがズレるので、headが前のノードへの関心がなくなる

2つの和は一旦飛ばそう。
空間計算量がO(n)になるのがなんかなあと

2分探索木
二分木とは、すべての頂点に対して子の個数が 2 以下であるような根付き木のことです。
二分探索木とは、親の左側のノードの値が必ず親より小さく、右側のノードの値が必ず親より大きい2分木のことです。
2分探索木では、どの親に対しても値が必ず左の子<親<右の子となっています。
2分探索木では、それぞれのノードに対し、「左部分木のそれぞれのノード<基準ノード<右部分木のそれぞれのノード」が成り立つので、データの探索を簡単に行うことができます。
目標データと基準の大小を比べて基準を動かす際に、移動するノードが見つからなければ、探索失敗です。


2分探索木ってようは2分木の子供を左の子 < 基準 < 右の子にした、データ構造か。

2分探索木はデータの探索のためのデータ構造

2分探索法は、アルゴリズム。
2分探索木はデータ構造

二分探索木から、特定のデータを持つノードを取得するアルゴリズム
↓ 再帰的に探索した場合
def search_bst(root, val)
# 2分探索木からwhileでノードを探索する場合
# この2分探索木の問題の場合、whileのがシンプルに探索できるな
while root
break if root.val == val
root = val > root.val ? root.right : root.left
end
root
end
↓ whileで探索した場合
def search_bst(root, val)
# 2分探索木から再起的にノードを探索する場合
return nil if root.nil?
current_val = root.val
result_node = nil
if current_val == val
result_node = root
elsif val > current_val
# データ構造が2分探索木なので、current_valがvalより小さいってことは、
# 右を探索しに行った方が良い
result_node = search_bst(root.right, val)
elsif val < current_val
result_node = search_bst(root.left, val)
end
result_node
end

BSTってBinary Search Tree(2分探索木)の略か

2分探索木からノードを削除する際にこの記事が役に立った

二分探索木から特定のノードを削除する問題
削除対象のノードが0 ~ 2個の子を持つかで、場合分けが必要で、そこがめんどかった。
最小の値を持つノードを右部分木から取得して、入れ替えた。
def find_min_node(node)
while node.left
node = node.left
end
node
end
# 特定ノード削除後のルートノードを返してくれる
def delete_node(root, key)
return nil if root.nil?
if key == root.val
# 子を持たない場合
if root.left.nil? && root.right.nil?
root = nil
# 子を一つ持つ場合
# 上のif文を書かないと、この条件文に子を持たない場合が内包される
elsif root.left.nil? || root.right.nil?
root = root.left || root.right
# 子を2つ持つ場合
# 右ツリーが持つ最小値をrootのvalueにする
else
min_right_node = find_min_node(root.right)
root.val = min_right_node.val
root.right = delete_node(root.right, min_right_node.val)
end
elsif key > root.val
root.right = delete_node(root.right, key)
elsif key < root.val
root.left = delete_node(root.left, key)
end
root
end

DFS/BFS
木構造にある各ノードを1度だけ訪問することを、探索と呼ぶ
全探索の仕方を大まかに分けると、
全探索の仕方をおおまかに分けると、
深さ優先探索(DFS)
幅優先探索(BFS)
の2つとなります。
さらに、深さ優先探索はさらに
行きがけ順(先行順 / 前順 / preorder)
通りがけ順(中間順 / 間順 / inorder)
帰りがけ順(後行順 / 後順 / postorder)
の3つに分けることができます*1。

breadthは幅って意味
つまり、BFSは、Breadth First Searchの略
depthは深いって意味
つまり、DFSは、Depth First Searchの略

バイナリツリーは2分木って意味

バイナリサーチツリーは2分探索木のこと

DFSには、行きがけ順、帰りがけ順、通りがけ順の3種類がある

DFS
再帰関数を使うことで短く、簡潔に書ける
BFS
最短経路を求めたい時に使う。
なるほど

DFSは再帰関数を用いて、可能な限り遠くに行くので、木から葉までの一番遠くまでの距離を求めたりする際に使えそう

行きがけ順は、左に向かってどんどんループを始めていって、移動できるノードが存在しないなら、前のノードに戻って右側に行く。右側に行った後も左があるなら左に向かってループを始めるし、ないなら、右側に行く。そのように探索するので、、ノードを訪問した順に反時計回りで出力できたりする。
# @param {TreeNode} root
# @return {Integer}
def max_depth(root)
return 0 if root.nil?
unless root.nil?
left_depth = max_depth(root.left)
right_depth = max_depth(root.right)
end
1 + [left_depth, right_depth].max
end

bfsとdfsはこの記事のまとめがわかりやすい
やっぱdfsは深さと関わる問題に適しているのか。遠くのノードを見つけたいときに使える
bfsは最短距離か
bfsは階層毎に進めるので、ノードの間の距離は常に最短、ただキューとかハッシュセットとかがあるためメモリ使用量がdfsより高い。
dfsは効率的に遠くにあるノードを見つけることができる、つまり深さと関わる問題に適している。最短も求められますが、全ての回答を見つけてその中からまた最短を絞り出すことになり、効率がbfsより良くない


木構造のパスでリーフがそのパスで最大値かどうかをチェックするの地味に苦戦した。
nodeとmax_valueを引数に持つ関数を使えばよかったのか
max valueをメモ化して、それを用いて再起的に処理をすれば良い

BFS
BFSは、深さが同じ要素を順番に巡回し、それらの探索が住んだらもう一つ深い部分の要素へと探索対象を移して巡回を進めます。

timesはインデックス0の分を含まないから注意

Integer#timesはself回だけ繰り返す。selfが正の整数ではない場合、何もしない

優先度付きキュー
優先度キューでは、高い優先度を持つ要素は、低い優先度を持つ要素よりも先に提供されます。
なるほど、優先度付きキューは優先度が高い要素が先に出されるようなデータ構造のことか。この優先度付きキューはヒープを用いて実装されることが多いそう。

二分探索の計算量はこの記事がわかりやすかった。
ある要素数nの配列があって、その配列は一度の探索で個数が半分になる。配列の個数が1になるまで探索を続けるので、探索回数をxとおくと、
1 = n / 2^xとなる。これを変形すると、x = logn。探索回数は計算回数でもあるので、よって二分探索の計算量はlogn

2分探索はあらかじめソートしてあるやつに対して実行する

2分探索はこの記事がわかりやすい
Rubyに既にArray#bsearchが用意されている
当たり前だが、selfはソートされていないとダメ

YesとNoの2境界で数をグルーピングできるなら、2分探索が使える可能性がある

二分探索(バイナリーサーチ)はソート済みの配列において、検索する間隔を半分に分割しながらデータを探し出すアルゴリズム
リニアサーチより高速であるがデータを必ずソートしなければならない


ループごとに同じソート済み配列を使うなら、ループごとにソートするんじゃなくて、ループ処理に入る前にソートしておく。配列の要素が半端ないやつだと、ループごとにソートしてたら大変なことになる

二分探索はある値より大きい要素の個数を求めよでも使える
二分探索はソート済みの配列に対して使うから、目的の値が見つかったら、その値以降はその目的の値以上の数だということがわかる。なので、目的の値のインデックスさえゲットできれば、配列からその数以上の数の個数を求めよは簡単に求められる。countのように、全ての要素に対して関数を実行するとO(n)だが、二分探索でのサーチはインデックスが見つかっちゃえばあとは、要素数 - 1 - インデックス + 1で個数がわかるので、計算量は、O(logN)である。

Set
Setは数学における集合をデータ構造として表したもので、順序はなく、変更可能で、重複した要素を持たない(配列からSetを作るが、配列が重複した要素を持っていた場合、勝手に削除される)。Setは「集合」の英語
要素の挿入、削除、検索(include?)を高速に行うことができる
Setの使い所
要素を入れつつ、常に重複を除外して要素を格納するときは、HashやSetを使う
(もっとざっくりいうと要素を重複させない要素の集合を持ちたい場合に、Setを使う)
→Arrayで常にユニークにしたいなら、uniq!を使って、毎回全要素を調べO(n)なので、ループごとにO(n)かかってしまう。毎回のループごとに同じ配列に何回もループするのは変だし。
→最後にユニークにするなら、Setで常にユニークにするより、配列に要素を追加していって、最後にuniq!をした方が速そう
データのソートやinclude?はやっぱArrayよりSetの方が早そう

動的計画法
動的計画法はアルゴリズム設計技法の一つです。一言で言えば「与えられた問題全体を一連の部分問題に上手に分解し、各部分問題に対する解をメモ化しながら、小さな部分問題からより大きな部分問題へと順に解を求めていく手法」
動的計画法とは
- 与えられた問題をうまく部分問題にして、
- 部分問題の解をメモ化しながら、
- 小さな部分問題からより大きな部分問題へと順に解を求めていく方法

引数でメモ化する問題を昔やったことあるけど、それに似ているかも

n, x, y = gets.chomp.split(" ").map(&:to_i)
index = n - 1
# param [Integer] n
# param [Integer] x
# param [Integer] y
def main(n, x, y)
num_array = Array.new(n, 0)
sum_nums(n, x, y, num_array)
end
def sum_nums(n, x, y, num_array)
return x if n == 0
return y if n == 1
left_num = if num_array[n - 1] > 0
num_array[n - 1]
else
num = sum_nums(n - 1, x, y, num_array)
num_array[n - 1] = num
num
end
right_num = if num_array[n - 2] > 0
num_array[n - 2]
else
num = sum_nums(n - 2, x, y, num_array)
num_array[n - 2] = num
num
end
(left_num + right_num) % 100
end
p main(index, x, y)

上のだと、もし配列がめちゃくちゃサイズでかいと引数にコピーするのにすごいコストがかかる(おそらく値渡しなので、とんでもないコストがかかる)
もっとシンプルにかける
n, x, y = gets.chomp.split(" ").map(&:to_i)
# param [Integer] n
# param [Integer] x
# param [Integer] y
def sum_nums(n, x, y)
num_array = Array.new(n, 0)
num_array[0] = x
num_array[1] = y
(2..n - 1).each do|i|
num_array[i] = (num_array[i - 1] + num_array[i -2]) % 100
end
num_array[n - 1]
end
p sum_nums(n, x, y)
同じ関数を計算しないように、事前に処理結果をメモしておくことをメモ化と呼ぶ。
動的計画法では結構大事だそう

このwikiがわかりやすい

カメのアルルは N 段の階段をのぼろうとしています。
それぞれの段には、次の 2 通りの方法でのぼることができます。
(階段を降りることはできません。)
1 つ前の段からのぼる。
(存在する場合のみ) 2 つ前の段からのぼる。
今、アルルは階段の 0 段目にいます。
アルルが階段の N 段目までのぼる方法は全部で何通りありますか。
このタイプの問題、昔paizaで解いたことあるけど、まさか動的計画法だったとは

動的計画法は初項と第二項がわかっていて、漸化式が出せそうなパターンだと使える設計技法かも

タイルの敷き詰め方も階段でイメージするとわかりやすいかも
動的計画法は最後の行動で場合わけをする

トリボナッチ数列というものがあるそう

動的計画法ではi番目の選択肢の個数を配列でメモ化しておく方が良い気がする。頻繁に同じ値を計算するから。i番目の何の値をメモ化したいのかは常に考える

視点をずらして同じような処理をするって時に、再帰関数は使えるかも。あと大きな問題を部分問題に分けて、部分問題を解決したい時

問題の視点を変えて、別の設定の問題だと読み替える力が大事かもしれん

このように、もとの問題を一連の部分問題へと分解し、それぞれの部分問題の解をメモ化しながら、小さな部分問題からより大きな部分問題の解を順に求めていく手法を総称して、動的計画法とよびます。
このとき、より大きな部分問題の解を求めるために、小さな部分問題に対してメモ化された値を再利用していることがポイントとなります。
わかりやすい。
動的計画法は、複数の問題に分割して、それぞれの問題を解くことに全力を尽くした方が良いね。てか、この問題を見つける力と複数の問題を分割する力が動的計画法には大事。
(これを満たしている必要があるなってやつを見つける。小さな問題を解いていくことをイメージする)

グラフ理論の用語を整理する
グラフとは、対象物の関係性を表すもの。
点は頂点、頂点と頂点をつなぐものは辺と呼ばれる
有向グラフ辺 e=(u,v) に対して、頂点 u を辺 e の始点とよび、 頂点 v を辺 e の終点と呼びます。
ほー

ある頂点に隣接している点は、配列で管理すれば良い。そうすればグラフをコンピュータ上で表現できる

隣接
辺 e=(u,v) に対して、頂点 u,v は、互いに隣接していると呼ぶ

辺ってエッジともいうのか
グラフをどのようなデータ構造で表現するか
隣接リストとエッジリストが使えそう。
隣接リスト
graph = {
a: ["b", "c"] ,
b: ["a", "d", "e"],
c: ["a", "f"],
d: ["b"],
e: ["b", "f"],
f: ["c", "e"],
}
エッジリスト
graph = [
["a", "b"],
["a", "c"] ,
["b", "d"] ,
["b", "e"] ,
["c", "f"] ,
["e", "f"] ,
]
隣接リストは、隣接ノードの取得やエッジの操作が高頻度でで発生する場面で向いている
エッジの存在確認とかは遅い
エッジリストは、疎グラフやシンプルな表現が望ましい場面で向いているそう
デメリットは、エッジの存在確認が遅い、隣接ノードの取得が遅い(てかこのデータ構造だと向いとらんよね)、エッジの追加、削除が遅い

グラフはノードとエッジという2つのパラメータを持つ点が、グラフ構造に関するアルゴリズムの検討に影響する。ノードとエッジの数をそれぞれV,Eとすると、EがVに比べ比較的少ないグラフを疎グラフ(sparse graph)、EがVに比べ比較的多いグラフを密グラフ(dense graph)と呼ぶ。
なるほど、エッジがノードに比べて少ないグラフを疎グラフ
エッジがノードに比べて多いグラフを密グラフと呼ぶのか

頂点に具体的な値が設定されていたとしても、頂点をそれぞれ0, 1,2 ...とすることで頂点集合を単純化することができる

そうか、エッジリストは、全要素を探索しないと、ある頂点とある頂点に関係があるかわからないな。計算量がO(n)だ。隣接リストだと、ある頂点を見つけ出すのにO(1)かかって、あとはその頂点から、特定の頂点を見つけ出せば良いから、こっちのが早い。特定の頂点をSetで管理していたら、より早い。
隣接リストを作るのに計算量がO(n ^ 2)かかりそうで、そこがデメリットでもあるか

グラフでも順序が意外と関係しているなら、グラフ全体をハッシュじゃなくて、配列で持った方が良いかもインデックスが、頂点を表すって感じで。

隣接リストでグラフ全体を配列にすると、使ってない余計な容量を作っちゃうからそこが微妙やね

フォロー関係をグラフ化する場合、グラフは有向グラフになる(AさんはBさんをフォローしているがB三はAさんをフォローしているとは限らないため)
友人関係とかは(Aさんが友達ならBさんも友達なので、無向グラフである)

頂点 v を根とする部分木に含まれる頂点のうち、v 以外の頂点を v の子孫とよびます
子孫って子と孫って意味だから理解できた

グラフにおける幅優先探索 (BFS)
マス目もグラフとして捉えることができる(グリッドグラフと呼ばれる)
幅優先探索は「出発点に近いところから順に探索する」という探索アルゴリズムです。
グラフにおいてスタートを意味する点を始点と呼ぶ
「まだ探索していない頂点」がなくなるまで繰り返すものです。 幅優先探索後には、各頂点が 1 手の頂点、2 手の頂点、......というように層分けされるイメージです。
なるほど、グラフにおける幅優先探索は、まだ探索していない頂点が無くなるまで、繰り返す。始点から1手目、始点から2手目...で層分けされる
幅優先探索の計算量は、頂点数 N、辺数 M として、O(M) と評価できます。 つまり、グラフの入力を受け取るのと同等の計算時間で、幅優先探索を実行できます。
これ後で証明したい
ここまでの実装でも十分、幅優先探索を実現できているのですが、より洗練した実装法を紹介します。 幅優先探索は通常、キュー (queue) を用いて実装します。
木のときもキューでやったな。
BFSは辿れるところまで辿っちゃおうぜってスタンスの探索方法だったな。

ハッシュで Setや配列をdefault valueにしたいときはブロックを使う。じゃないと、キー間でvalueを共有してしまう。0とかのプリミティブならそんなことはないが
graph = Hash.new { |key, hash| hash[key] = Set.new([]) }
その他参考にした記事

グラフでBFSをやってみた。
木構造のやつと比べて、与えらえられたデータからグラフをまずは作らないといけないから、そこがめんどい
## 方針1
# まず与えられたデータから隣接リストを作る。その隣接リストをハッシュでもつ
# そのハッシュに対してループをかけて、そのノードまでの最短経路を求める
## 方針1の実装手順
# 1. 標準入力を受け取る。友達関係の配列をedgesとする
# 2. edgesからグラフを作成して、変数graphに代入する
# 3. 変数queue = []を用意する
# 4. 0からの距離を表すd = 1を用意する。
# 5. 既に会ったことのある友人をまとめるmet_friends = Set.new([])を定義する
# 5. graph[0]をqueueに入れる
# 6. distances = Array.new(n - 1, 0)を定義する
# 7. queueが空になるまでループを続ける
# 8. ループ内では、queueの長さを取得して、その長さだけループをする。ループ2とする
# 9. ループ2の中では、
# - queueからnodeを取り出す。そのnodeがもしmet_friendsに含まれていたら次のループに行く
# - met_friendsにnodeをaddする
# - disatnces[node] = dを実行する
# - queueにnodeの連結リストをpushする
# 10. これらの一連の処理が終わったら、distances.maxを出力して終了
require "set"
n, m = gets.chomp.split(" ").map(&:to_i)
edges = readlines.map { |line| line.chomp.split(" ").map(&:to_i) }
graph = Hash.new { |hash, key| hash[key] = Set.new([]) }
edges.each do |edge|
graph[edge[0]].add(edge[1])
graph[edge[1]].add(edge[0])
end
# 主要な変数を定義する
queue = []
d = 1
met_friends = Set.new([0])
distances = Array.new(n - 1, 0)
queue.push(*graph[0])
until queue.empty?
range = queue.length
range.times do
node = queue.shift
next if met_friends.include?(node)
met_friends.add(node)
distances[node - 1] = d
queue.push(*graph[node])
end
d += 1
end
p distances.max

グリッドグラフをデータとして作ってみた
require "set"
grid_graph = Hash.new { |hash, key| hash[key] = Set.new([]) }
# グリッドの行数と列数を定義
rows = 3
cols = 3
# グリッドの各セルをハッシュに追加
rows.times do |i|
cols.times do |j|
cell = [i, j]
grid_graph[cell].add([cell[0] + 1, cell[1]]) if i < rows - 1 # 下
grid_graph[cell].add([cell[0], cell[1] + 1]) if j < cols - 1 # 右
grid_graph[cell].add([cell[0] - 1, cell[1]]) if i > 0 # 上
grid_graph[cell].add([cell[0], cell[1] - 1]) if j > 0 # 左
end
end
p grid_graph

上下左右でマスの動く距離をsteps = [[-1, 0], [1, 0], [0, -1], [0, 1]]として、1マスに対して、stepsでループをかければもうちょい簡単に解けるかも
# 方針1
# graph = Hash.new { |hash, key| hahs[key] = Set.new([]) }を用意する
# S_iを解析して、黒いマス(i, j)をblack_spacesとして保持する
# グリッドグラフをデータに変換して、変数grid_graphに代入する。
# データに変換する際に、頂点がblack_spacesに含まれていたら、その点を含めないようにする
# あとは、キューを用意して、BFSを目的地までの実行して最短距離を求めて出力する
# 方針1の実装手順
# 1. 標準入力を受け取る。入力を変数h, w, start, goalに代入する
# sの集合は、変数space_statesで保持する
# 2. space_statesから、黒の点の集合を作って、変数balock_spacesに代入する
# 3. h, mから、グリッドグラフを頂点と辺で構成されたグラフに変換する。そのグラフを変数grid_graphに代入する
# グラフに変換する際に、頂点がblack_spacesに含まれていたら、その点を含めないようにする
# 4. あとは、キューを用意してBFSを実施する。そして、目的地までの最短距離の手数を出力する
require "set"
# メインの処理
def main()
h, w = gets.chomp.split(" ").map(&:to_i)
start_and_goal = gets.chomp.split(" ").map(&:to_i)
start = start_and_goal[0..1]
goal = start_and_goal[2..3]
black_spaces = Set.new([])
readlines.each.with_index do |line, i|
line.each_char.with_index do |c, j|
if c == "B"
black_spaces.add([i, j])
end
end
end
grid_graph = create_grid_graph(h, w, black_spaces: black_spaces)
# BFS
passed_space = Set.new([start])
queue = []
queue.push(*grid_graph[start])
d = 1
until queue.empty?
range = queue.length
range.times do
space = queue.shift
next if passed_space.include?(space)
if space == goal
puts d
return
end
passed_space.add(space)
queue.push(*grid_graph[space])
end
d += 1
end
end
# @param [Integer] h
# @param [Integer] w
# `param [Array<Set<Integer>> black_spaces]
def create_grid_graph(h, w, black_spaces: [])
grid_graph = Hash.new { |hash, key| hash[key] = Set.new([]) }
# グリッドの行数と列数を定義
rows = h
cols = w
# グリッドの各セルをハッシュに追加
rows.times do |i|
cols.times do |j|
cell = [i, j]
next if black_spaces.include?(cell)
bottom_cell = [cell[0] + 1, cell[1]]
right_cell = [cell[0], cell[1] + 1]
left_cell = [cell[0] - 1, cell[1]]
top_cell = [cell[0], cell[1] - 1]
grid_graph[cell].add(bottom_cell) if i < rows - 1 && !black_spaces.include?(bottom_cell)
grid_graph[cell].add(right_cell) if j < cols - 1 && !black_spaces.include?(right_cell)
grid_graph[cell].add(left_cell) if i > 0 && !black_spaces.include?(left_cell)
grid_graph[cell].add(top_cell) if j > 0 && !black_spaces.include?(top_cell)
end
end
grid_graph
end
main()
↑以前といたやつ

グリッドグラフでBFSやるならこっちのが綺麗かも。ある点の周りをどんどん広げていって探すスタイル。点をずらしたらその点まで何歩でいけるかを配列でカウントする。 元いた場所+ 1でカウントする。カウントした結果、つながっている点でずっとこの連鎖が続くから、最終的にゴールまで何歩でいけるかがわかる。
## 方針1
# 2マス間の上下右左の距離をdistancesとして定義する
# queueを用意して、スタート地点をqueueに入れる
# 手数を表すd = 1を用意する。
# キューが空になるまでループする
# ループ内で、distancesでループをかける。ループカウンタをdとする
# - スタート地点(x_0, y_0)にdをたす。この点をcurrent_spaceとする
# - dが範囲外ではないかつ黒いスペースではないなら、その地点に進むと仮定する
# - falseならnextを実行する
# - もし、goalと一致するマスなら、putsを実行する。その後、returnを実行する
# - current_spaceをqueueに入れて、次のdistanceのループに行く。
# distancesのループが終了した後に、d += 1を実行する
## 方針1の実装
# 1 標準入力を受け取る。その入力を変数h, w, start, goal, space_conditionsに入れる
# 2. queue = [start]を用意する。
# 3. d = 1を用意する, distancesも定義する。passed_spacesも定義する
# 4. キューが空になるまでループする
# 5. ループ内でdisntancesでループをかける。ループカウンタをdとする
# 6. 一連のBFSの処理をする
# 7. goalと一致するますがあったらuptsを実行して、returnする
# 8. distancesのループが終了した後に、d += 1を実行する
require "matrix"
require "set"
h, w = gets.chomp.split(" ").map(&:to_i)
start_and_goal = gets.chomp.split(" ").map(&:to_i)
start = start_and_goal[0..1]
goal = start_and_goal[2..3]
space_conditions = readlines.map(&:chomp).map(&:chars)
queue = [start]
move_counts = Array.new(h).map { Array.new(w, -1) }
move_counts[start[0]][start[1]] = 0
# 上下右左
distances = [[-1, 0], [1, 0], [0, 1], [0, -1]]
passed_spaces = Set.new([start])
until queue.empty?
space = queue.shift
distances.each do |d|
current_space = (Vector.elements(space) + Vector.elements(d)).to_a
next if current_space[0] < 0 || current_space[0] > h - 1
next if current_space[1] < 0 || current_space[1] > w - 1
next if space_conditions[current_space[0]][current_space[1]] == "B"
next if passed_spaces.include?(current_space)
passed_spaces.add(current_space)
move_counts[current_space[0]][current_space[1]] = move_counts[space[0]][space[1]] + 1
queue.push(current_space)
end
end
puts move_counts[goal[0]][goal[1]]

有向グラフにおいて、ある頂点vを終点とする辺の本数を頂点vの入次数と呼ぶ
有向グラフにおいて入次数が0であるような頂点をソースと呼ぶ
多次元配列を定義するときはmap使わないとあかんな。
ary = Array.new(3).map{Array.new(3,0)}

DFS
DFSで再帰関数の処理が始まっていく順番のことを行きがけ順と呼ぶ
再帰関数の処理が終わっていく順番のことを帰りがけ順と呼ぶ

↓Set同士で差を実行することで、差集合を求めることができる。
Set.new((0..n - 1).to_a) - arrival_nodes
↓ 有向グラフを求める関数
# @param [Array<Array<Integer>>] edges
def create_directed_graph(edges)
graph = Hash.new { |hash, key| hash[key] = Set.new([]) }
edges.each do |edge|
graph[edge[0]].add(edge[1])
end
graph
end
↓ DFSを実装した再帰関数
# @param [Integer] n
# @param [Hash<Integer, Set<Integer>>] graph
# @param [Set<Integer>] arrival_nodes
def rec(n, graph, arrival_nodes)
arrival_nodes.add(n)
graph[n].to_a.sort.each do |node|
next if arrival_nodes.include?(node)
rec(node, graph, arrival_nodes)
end
end

2次元の動的計画法
2次元の動的計画法やるか

2次元の動的計画法はマス目で考えるとうまくいきやすい
グラフィカルな方がパッとイメージできる気がする。自分は。
# 方針1
# i日目の最大の報酬を
# 方針1の修正
# - n日間のそれぞれの最大報酬を持つ配列と、n日間で何の仕事を実施したのかを管理する配列day_tasks必要かも
def main
n = gets.chomp.to_i
rewards = readlines.map { |line| line.chomp.split(" ").map(&:to_i) }
day_max_rewards = Array.new(n).map { Array.new(3, -1)}
day_max_rewards[0] = rewards[0]
(1..n - 1).each do |i|
(0..2).each do |j|
prev_day_rewards = []
if j == 0
prev_day_rewards.push(day_max_rewards[i - 1][1], day_max_rewards[i - 1][2])
end
if j == 1
prev_day_rewards.push(day_max_rewards[i - 1][0], day_max_rewards[i - 1][2])
end
if j == 2
prev_day_rewards.push(day_max_rewards[i - 1][0], day_max_rewards[i - 1][1])
end
day_max_rewards[i][j] = prev_day_rewards.max + rewards[i][j]
end
end
puts day_max_rewards[n - 1].max
end
main
これ、横のマス目が一定だからこうシンプルにかける。
# 方針1
# i日目の最大の報酬を
# 方針1の修正
# - n日間のそれぞれの最大報酬を持つ配列と、n日間で何の仕事を実施したのかを管理する配列day_tasks必要かも
def main
n = gets.chomp.to_i
rewards = readlines.map { |line| line.chomp.split(" ").map(&:to_i) }
day_max_rewards = Array.new(n).map { Array.new(3, -1)}
day_max_rewards[0] = rewards[0]
(1..n - 1).each do |i|
day_max_rewards[i][0] = [day_max_rewards[i - 1][1], day_max_rewards[i - 1][2]].max + rewards[i][0]
day_max_rewards[i][1] = [day_max_rewards[i - 1][0], day_max_rewards[i - 1][2]].max + rewards[i][1]
day_max_rewards[i][2] = [day_max_rewards[i - 1][0], day_max_rewards[i - 1][1]].max + rewards[i][2]
end
puts day_max_rewards[n - 1].max
end
main

DP は、配列に対して、順に各マスの値を決定していく手法である
やっぱそうだったか

DPでメモ化するのは目的の値

辺の始点の情報を活用して、辺の終点に関する情報を更新することを緩和
緩和というのか

グラフの最短経路求める感じ結構好きだな

動的計画法、BFS、DFS、バイナリサーチの使う場面や特徴をまとめる
- 動的計画法
- 最適化問題で使える(最適化問題とは、ある条件下での結果を最大(または最小)にする解を求める問題)。
- ある解を求める問題が、その問題と類似した部分問題の集合で構成されてると判断できた時に、動的計画法が使える。
- 動的計画法はトップダウンに解を求めていくやり方(再帰関数の計算結果をメモ化して、再帰関数でループさせる) or ボトムアップに解を求めていくやり方(解をメモ化してループする)がある。どちらの方法を採用しても、漸化式は作る
- 2次元の動的計画法はマス目に目的の解を入れていくって考えるとうまくいきやすい
- i = 0 ~ N -1の中で、iを固定した際に、どんな目的の値が欲しいかを考えてそれを配列でメモ化する
- BFS
- 最短経路が存在しているかを確認したり、最短手数を求めたい時に使える。
- ある点の周辺からどんどん広げていって探索していく。ゆえ最短経路が存在しているかや最短手数を求められる
- キューを使って実装する
- DFS
- グラフをくまなく探索したいときや、グラフ内の特定のパスを探す場合に使える。
- グラフ内の深い部分を優先的に探索していく。先にどんどん奥に進んでいく、
- 再帰を使って実装するのが一般的
- バイナリサーチ
- バイナリサーチは、ソートされた莫大なデータから、特定の値を探し出す際に使える。普通に検索するとO(n)でデータ量に比例して計算量が大きくなるが、バイナリサーチはO(logN)なので、nが増えても緩やかに計算量が増加する

部分和問題
部分和問題とは、与えられた数値の要素の集合から、部分集合を作って、その部分集合の和が与えられた数と等しくなるようにできるかを判定する問題である。
部分集合は包含関係がある

部分和問題
N個の整数のうち、「最初の1個のみ」に関する部分問題を解き
次にその解を用いて、N個の整数のうちの「最初の2個のみ」に関する部分問題を解き、
次にその解を用いて、N個の整数のうちの「最初の3個のみ」に関する部分問題を解き、
....
最後に、N個の整数全体に関する問題を解く
と考えると、部分和問題でも、部分問題の解を順に求めていることが分かる。

解けた。結構むずい
# 方針
# 箱に入れられる最大の重さをjとして、0 <= j <= m + 1 と考える。
# ボールの重さの集合をball_weightsとして、インデックスをiとする(0 <= i <= n - 1)
# jの時の箱が0からi個のボールを含められるかを、is_box_includable[i][j]とする
# (配列定義時に初期値は全部falseにしておく)
# 0 <= i <= n - 1と0 <= j <= m + 1の範囲で2重ループをかける。
# - j == 0ならnextを実行
# - j == ball_weigths[i]なら、is_box_includable[i][j]にtrueを代入する
# - i >= 1 && is_box_includable[i - 1][j] == trueなら、is_box_includable[i][j]はtrue
# - j >= 1 && is_box_includable[i - 1][j - ball_values[i][j]] == trueなら、is_box_includable[i][j]はtrue
# 以上のループをかけた後で、is_box_includable[n - 1][m]を実行すれば、
# 重さの合計がMとなるようなボールの選び方が存在するかがわかる。(何通りの選び方が存在するかに関心がなくて、あるかないかに関心がある)
def main
n, m = gets.chomp.split(" ").map(&:to_i)
ball_weights = gets.chomp.split(" ").map(&:to_i)
is_box_includable = Array.new(n).map { Array.new(m + 1, false) }
(0..n - 1).each do |i|
(0..m).each do |j|
# 箱に入れられる最大の重さが0なら次のループに行く。
next if j.zero?
# i番目のボールの重さが、箱に入れられる最大の重さと同じなら、
# 当然ボールは入れられるからtrue
if j == ball_weights[i]
is_box_includable[i][j] = true
next
end
# 条件1に関しては、0 ~ i - 1個のボールを使って、is_box_includedable[i - 1][j]を満たすなら、
# 0 ~ i個のボールを使った場合、0 ~ i ~ 1個のボールを使えば、is_box_includedable[i][j]を満たす
# 条件2に関しては、0 ~ i - 1個のボールを使って、is_box_includedable[i - 1][j - ball_weights[i]]を満たすなら、
# 0 ~ i個のボールを使った場合、ボールiが使えるので、is_box_includedable[i - 1][j - ball_weights[i]]にボールiのball_weight[i]を追加すれば、
# is_box_includalbe[i][j]は満たす
if (i >= 1 && is_box_includable[i - 1][j]) || (j >= 1 && is_box_includable[i - 1][j - ball_weights[i]])
is_box_includable[i][j] = true
next
end
end
end
puts is_box_includable[n - 1][m] ? "Yes" : "No"
end
main

再帰関数はデバッグしづらいから、動的計画法で使うのは微妙かも

というか再帰関数自体がデバッグしづらいのかも。
何回目の関数呼び出しに問題があるのかがわかりづらい

部分和問題はこの図がわかりやすかった

貰うDP
# 方針
# 0 <= i <= N - 1, 0 <= j <= M - 1の時にマス(i, j)に到達した際の最大得点を
# max_scores[i][j]とする。
# Aの集合をline_directions
# Bの集合をline_pointsとする
# したがって、以下のような漸化式が成立する
# max_scores[i][j] = [
# max_scores[i - 1][j - line_directions[i - 1]] + line_points[i - 1]
# max_scores[i - 1][j]
# ].max
# これを0 <= i <= N - 1, 0 <= j <= M - 1の範囲で2重ループすれば、
# ある点(i, j)に行った際の最大得点が分かる。
# その点にいかなかった際は -1を出力する。デフォルトで - 1を全点に設定しておけばよい
def main
n, m = gets.chomp.split(" ").map(&:to_i)
line_directions = gets.chomp.split(" ").map(&:to_i)
line_points = gets.chomp.split(" ").map(&:to_i)
max_scores = Array.new(n).map { Array.new(m, -1)}
# 初期値を設定
# スタート地点では0点と考える
max_scores[0][0] = 0
(1..n - 1).each do |i|
(0..m - 1).each do |j|
top_value = max_scores[i - 1][j]
left_corner_value = j - line_directions[i - 1] >= 0 && max_scores[i - 1][j - line_directions[i - 1]] >= 0 ? max_scores[i - 1][j - line_directions[i - 1]] + line_points[i - 1] : -1
max_scores[i][j] = [top_value, left_corner_value].max
end
end
p max_scores[n - 1][m - 1]
end
main
配るDP
# 方針
# 配るDPで解いてみる
def main
n, m = gets.chomp.split(" ").map(&:to_i)
line_directions = gets.chomp.split(" ").map(&:to_i)
line_points = gets.chomp.split(" ").map(&:to_i)
max_scores = Array.new(n).map { Array.new(m, -1)}
# 初期値を設定
# スタート地点では0点と考える
max_scores[0][0] = 0
(0..n - 2).each do |i|
(0..m - 1).each do |j|
next if max_scores[i][j].negative?
if (j + line_directions[i] >= 0 && j + line_directions[i] <= m - 1) && max_scores[i][j] + line_points[i] > max_scores[i + 1][j + line_directions[i]]
max_scores[i + 1][j + line_directions[i]] = max_scores[i][j] + line_points[i]
end
if max_scores[i][j] > max_scores[i + 1][j]
max_scores[i + 1][j] = max_scores[i][j]
end
end
end
p max_scores[n - 1][m - 1]
end
main

ナップサック問題
ナップサック問題とは、それぞれ重さと値をもつアイテムの集合から、重さの合計がある重さ以下となるような組み合わせで、値の合計ができるだけ大きくなるようにするにはどうすれば良いかを考える問題である。
# 方針
# 0 <= i <= n - 1, 0 <= j <= mとする
# ボールを0 ~ i個使った時、重さj以下で最大の価値をmax_values[i][j]とする
# 0 <= i <= N - 1, 0 <= j <= Mの範囲で2重ループをかけて、
# max_values[i][j]の値を可能な限り埋めていく。
# 最後に、max_values[n -1][m]を出力することで、
# 重さの合計がM以下の時の、箱の中に入っているボールの合計最大価値を出力できる
def main
n, m = gets.chomp.split(" ").map(&:to_i)
ball_weights = gets.chomp.split(" ").map(&:to_i)
ball_values = gets.chomp.split(" ").map(&:to_i)
max_values = Array.new(n).map { Array.new(m + 1, 0) }
(0..n - 2).each do |i|
(0..m).each do |j|
next if j.zero?
if j >= ball_weights[i] && ball_values[i] > max_values[i][j]
max_values[i][j] = ball_values[i]
end
if i + 1 <= n - 1
if max_values[i][j] > max_values[i + 1][j]
max_values[i + 1][j] = max_values[i][j]
end
if j + ball_weights[i + 1] <= m && max_values[i][j] > max_values[i + 1][j + ball_weights[i + 1]]
max_values[i + 1][j + ball_weights[i + 1]] = max_values[i][j] + ball_values[i + 1]
end
end
end
end
puts max_values[n - 1][m]
end
main

strptimeとstrftime
fはformat
p はparse

行列の部分和めちゃくちゃむずいな。

グラフの連結
グラフが連結であれば、頂点 0 から他すべての頂点にたどり着くことができる。

逆にグラフが連結でなければ、頂点 0 からたどり着けない頂点が存在することも示せるため、 グラフが連結であることと頂点 0 から全ての頂点にたどり着けることは同値である

サイクル、パス
パス:隣接する頂点をたどっていくことで、始点から終点へと到達できる部分グラフ
サイクル:隣接する頂点をたどっていくことで、元の頂点に戻ってこれる部分グラフ
連結な部分グラフ」:グラフ内の一部で、その部分自体も連結である部分。
「極大で連結な部分グラフ」:その部分グラフをさらに拡張することはできない部分グラフ。

DFSはグラフが連結かどうかをチェックするのに一瞬でチェックできるから良いな。点0から遠くまで行けるってことは繋がっているってことだし

連結していないのもグラフではあるのか。連結していないグラフって感じ。

連結していないグラフの中で、極大で連結な部分グラフをDFSで調べて、まだ調べていない点に対してDFSを実行すれば、連結していないグラフの中で、極大で連結な部分グラフが何個あるかを確認できる

5 1
3 1
このパターンは、0 ~ 4のノードがあるけど、3 -1しか連結していないってこと。
つまり、このグラフは、1- 3, 0, 4, 2で構成されたグラフってこと

元のグラフの一部であるようなグラフを部分グラフとよびます。

有向グラフを作る関数
require "set"
# param [Array<Array<integer>>]
# return [Hash<Integer, Set<Integer>>]
def create_directed_graph(edges)
graph = Hash.new { |hash, key| hash[key] = Set.new([]) }
edges.each do |edge|
graph[edge[0]].add(edge[1])
end
graph
end

無向グラフを作る関数
require "set"
# param [Array<Array<integer>>]
# return [Hash<Integer, Set<Integer>>]
def create_indirected_graph(edges)
graph = Hash.new { |hash, key| hash[key] = Set.new([]) }
edges.each do |edge|
graph[edge[0]].add(edge[1])
graph[edge[1]].add(edge[0])
end
graph
end

あるところまでのパスが存在するかを調べたいなら、絶対DFSのが早いな。
DFSはパスの有無に関してとか、連結とかパス関係に強い気がする

動的計画法、BFS、DFS、バイナリサーチの使う場面や特徴をまとめる
- 動的計画法
- 最適化問題で使える(最適化問題とは、ある条件下での結果を最大(または最小)にする解を求める問題)。
- ある解を求める問題が、その問題と類似した部分問題の集合で構成されてると判断できた時に、動的計画法が使える。
- 動的計画法はトップダウンに解を求めていくやり方(再帰関数の計算結果をメモ化して、再帰関数でループさせる) or ボトムアップに解を求めていくやり方(解をメモ化してループする)がある。どちらの方法を採用しても、漸化式は作る
- 2次元の動的計画法はマス目に目的の解を入れていくって考えるとうまくいきやすい
- i = 0 ~ N -1の中で、iを固定した際に、どんな目的の値が欲しいかを考えてそれを配列でメモ化する
- BFS
- 最短経路が存在しているかを確認したり、最短手数を求めたい時に使える。
- ある点の周辺からどんどん広げていって探索していく。ゆえ最短経路が存在しているかや最短手数を求められる
- キューを使って実装する
- DFS
- グラフをくまなく探索したいときや、グラフ内の特定のパスを探す場合に使える。
- 連結しているかどうかを探すときも使える。パスを探すので。
- 連結の定義は、頂点 0 から他すべての頂点にたどり着くことができること。これはDFSで通る点をくまなく探して、グラフの全ての点と通った点が一致していれば、連結していると言える。(DFSは点Oからスタートするので)
- グラフ内の深い部分を優先的に探索していく。先にどんどん奥に進んでいく、
- 再帰を使って実装するのが一般的
- バイナリサーチ
- バイナリサーチは、ソートされた莫大なデータから、特定の値を探し出す際に使える。普通に検索するとO(n)でデータ量に比例して一次関数的に計算量が大きくなるが、バイナリサーチはO(logN)なので、nが増えても緩やかに計算量が増加する

DFSはスタックで解いてきたけど、パスがあるかを特定する問題ならスタックでも行けるんだけど(通った点だけ覚えておけばよくて、どんな順番で来たかを覚えなくて良いため)、パスの値を全部だせってなると、再帰の方が良いな。覚えないといけないパスの数が多すぎるから。関数ごとに覚えさせたい

前の設定覚えておかないといけない系のDFSだと、再起のがやりやすいね

s-tパスの存在範囲
DFSで解いたけど、BFSでも解ける
そりゃそうか
DFSをスタックで実装している
require "set"
# param [Array<Array<integer>>]
# return [Hash<Integer, Set<Integer>>]
def directed_graph(edges)
graph = Hash.new { |hash, key| hash[key] = Set.new([]) }
edges.each do |edge|
graph[edge[0]].add(edge[1])
end
graph
end
def main
n, m, start, goal = gets.chomp.split(" ").map(&:to_i)
edges = readlines.map { |line| line.chomp.split(" ").map(&:to_i) }
graph = directed_graph(edges)
stack = [*graph[start]]
visited = Set.new([start])
while !stack.empty?
node = stack.pop
if !visited.include?(node)
visited.add(node)
graph[node].reverse_each do |n|
stack.push(n) unless visited.include?(n)
end
end
end
if visited.include?(goal)
puts "Yes"
else
puts "No"
end
end
main

スタックで後ろがけ順とかむずい気がする

パスを復元するためには「どの頂点から来たか」という情報が必要になります。

2部グラフ
- 両端点が白色で塗られているような辺は存在しない
- 両端点が黒色で塗られているような辺は存在しない
なお、この条件を満たすグラフのことを二部グラフと呼びます。

DFS再帰
def puts_path(s, t, graph, current:, path_nodes: [], visited: Set.new([]))
return if visited.include?(current)
return if graph[current].empty?
path_nodes.push(current)
visited.add(current)
if current == t
puts path_nodes.length
puts path_nodes.join(" ")
end
graph[current].reverse_each do |n|
puts_path(s, t, graph, current: n, path_nodes: path_nodes, visited: visited)
end
end

根について

input
一列の2つの値を取得
def get_two_values_by_input_line
n, m = gets.chomp.split(" ").map(&:to_i)
end

一列の一つの値を取得
def get_value_by_input_line
n = gets.chomp.to_i
end
複数列の2つの値を取得
def get_two_values_by_lines
readlines.map { |line| line.chomp.split(" ").map(&:to_i) }
end

グリッドグラフ
上下左右に隣接した部分に辺を結んだグラフが与えられます (グリッドグラフと呼びます)。

連結であるってことは、始点Oから全ての点まで辿っていける

N 頂点の連結グラフにおける辺数の最小値は (N−1)
変数の最大値も(N - 1)

木に対し、特定の 1 つの頂点を特別扱いして根とよぶことがあります。 根をもつ木のことを根付き木と呼びます。

根なし木のある頂点を根と便宜的に指定して、根付き木にすることもある。

根付き木の高さとは
頂点 r を根とする根付き木において、根付き木の高さとは
各辺の長さを 1 としたときに、頂点 r から最も遠い頂点までの距離
のことである。
遠いが大事

無向グラフの木から最大の高さを求める関数
木の高さを求めるときは、有向かはどうでも良いので、無向グラフを採用する
def get_max_height_by_graph(graph, start_node: 0)
visited = Set.new([start_node])
queue = [*graph[start_node]]
max_height = 0
while !queue.empty?
range = queue.length
range.times do
node = queue.shift
if !visited.include?(node)
visited.add(node)
graph[node].each do |n|
queue.push(n) unless visited.include?(n)
end
end
end
max_height += 1
end
max_height
end

木の直径
無向グラフにおいて、2 頂点間の最短距離の最大値を、グラフの直径と呼ぶ

あるノードから最大の高さを持つノードを取得する
def get_max_height_node_by_graph(graph, start_node: 0)
visited = [start_node]
queue = [*graph[start_node]]
while !queue.empty?
range = queue.length
range.times do
node = queue.shift
if !visited.include?(node)
visited.push(node)
graph[node].each do |n|
queue.push(n) unless visited.include?(n)
end
end
end
end
visited.last
end

木の直径を簡単に求める
木の直径は以下の手順で簡単に求められる。
- 木の適当な頂点 r を 1 つ選び、頂点 r から最も距離が遠い頂点 s を求める
- 頂点 s から最も遠い頂点 t を求める
- 頂点 s,t 間の距離が求める直径である

あるノードからの高さを取得する関数
高さって、結局根からの最大の距離のことだから、最小の高さとか言われたら、根は自分で指定してよくて、最終的に出した高さの集合の中から最小値を選べってことか
def get_tree_height_by_graph(graph, start_node: 0)
visited = Set.new([start_node])
queue = [*graph[start_node]]
height = 0
while !queue.empty?
range = queue.length
range.times do
node = queue.shift
if !visited.include?(node)
visited.add(node)
graph[node].each do |n|
queue.push(n) unless visited.include?(n)
end
end
end
height += 1
end
height
end

任意の点を選んだ時の木の高さをそれぞれ出した際の、木の高さの最小値は、直径を /2して切り上げしたやつだそう

任意の点を選んだ時の木の高さをそれぞれ出した際の、木の高さの最小値
def get_value_by_input_line
n = gets.chomp.to_i
end
def get_two_values_by_lines
readlines.map { |line| line.chomp.split(" ").map(&:to_i) }
end
require "set"
# param [Array<Array<integer>>]
# return [Hash<Integer, Set<Integer>>]
def indirected_graph(edges)
graph = Hash.new { |hash, key| hash[key] = Set.new([]) }
edges.each do |edge|
graph[edge[0]].add(edge[1])
graph[edge[1]].add(edge[0])
end
graph
end
def get_max_height_by_graph(graph, start_node: 0)
visited = Set.new([start_node])
queue = [*graph[start_node]]
max_height = 0
while !queue.empty?
range = queue.length
range.times do
node = queue.shift
if !visited.include?(node)
visited.add(node)
graph[node].each do |n|
queue.push(n) unless visited.include?(n)
end
end
end
max_height += 1
end
max_height
end
def get_max_height_node_by_graph(graph, start_node: 0)
visited = [start_node]
queue = [*graph[start_node]]
while !queue.empty?
range = queue.length
range.times do
node = queue.shift
if !visited.include?(node)
visited.push(node)
graph[node].each do |n|
queue.push(n) unless visited.include?(n)
end
end
end
end
visited.last
end
def main
n = get_value_by_input_line
edges = get_two_values_by_lines
graph = indirected_graph(edges)
s_node = get_max_height_node_by_graph(graph)
max_height = get_max_height_by_graph(graph, start_node: s_node)
puts (max_height / 2.0).ceil
end
main

安定集合
一般に無向グラフにおいて、頂点からなる集合のうち、どの 2 頂点も辺で結ばれていないようなものを安定集合と呼びます。
安定集合の中から任意の2頂点を取ってきても、その2頂点は辺で結ばれていないということである。

葉
根を除く頂点のうち、その頂点に接続している辺が 1 本しかないものを葉とよびます。
なるほど

累積和を求める関数
sales = [10, 20, 15, 20]
# @param [Array<Integer>] nums
def calc_acc_sums(nums)
sums = [0]
total = 0
i = 0
while i <= nums.length - 1
total += nums[i]
sums.push(total)
i += 1
end
sums
end

累積和の計算例
S_nは要素がn個の時の累積和
s_0 = 0
s_1 = a_1
s_n = a_0 + .... + a_n - 1
irb(main):017:0> sales
=> [10, 20, 15, 20]
irb(main):018:0> calc_acc_sums(sales)
=> [0, 10, 30, 45, 65]
irb(main):019:0> calc_acc_sums(sales).bsearch { |n| n >= 30 }
=> 30
irb(main):020:0> calc_acc_sums(sales).bsearch { |n| n >= 45 }
=> 45
irb(main):021:0> calc_acc_sums(sales).bsearch { |n| n >= 5 }
=> 10
irb(main):022:0> calc_acc_sums(sales).bsearch { |n| n >= 2 }
=> 10
irb(main):023:0> s =calc_acc_sums(sales)
irb(main):024:0> s
=> [0, 10, 30, 45, 65]
irb(main):025:0> s_from_3 = s[4] - s[2] # a2 ~ a3までの和
irb(main):026:0> s_from_3
=> 35
irb(main):027:0> s[4]
=> 65
irb(main):028:0> s[2]
=> 30
irb(main):029:0>
sの配列とaの配列がごっちゃになるとわかりづらい
sの配列はaの要素より1要素(s_0)多い


dfsの関数
def dfs(graph, start_node: 0)
stack = [*graph[start_node]]
visited = [start_node]
while !stack.empty?
node = stack.pop
if !visited.include?(node)
visited.push(node)
graph[node].reverse_each do |n|
stack.push(n) unless visited.include?(n)
end
end
end
visited
end

bfsの関数
def bfs(graph, start_node: 0)
queue = [*graph[start_node]]
visited = [start_node]
while !queue.empty?
range = queue.length
range.times do
node = queue.shift
if !visited.include?(node)
visited.push(node)
graph[node].each do |n|
queue.push(n) unless visited.include?(n)
end
end
end
end
visited
end

前の状態を元にどうするか決めたいなら再帰のが良い気がしてきた

グラフ系のアルゴリズムやってないから、今度やろう