Closed216

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

ハガユウキハガユウキ
  • アルゴ式をとく(アルゴリズムとデータ構造)
  • leet codeのeasyからmediumを全部解けるような状態になっておく
  • hackers rank

1週間で結果を出せなかったら、それはもう自分の責任

ハガユウキハガユウキ

gmailやdiscord、x.comなど、アルゴリズム以外のことは極力やらない。アルゴリズムを解くことに全集中する。それしかやらん。boomも見ない

ハガユウキハガユウキ

人と向き合う
自分自身と向き合う
アルゴリズムと向き合う
結果を妥協しない
結果を貪欲に追求する
結果に愚直に向き合う
自分のキャリと向き合う

この強みを伸ばしたい。
この会社なら実現できそうって会社に入る
サービスを伸ばすためならなんでもやるはちょっと違う。

ハガユウキハガユウキ

String#each_charは文字列でループができる
String#each_char
https://docs.ruby-lang.org/ja/latest/method/String/i/each_char.html

String#split
https://docs.ruby-lang.org/ja/latest/method/String/i/split.html

正規表現にiを加えることで、大文字小文字を無視できる
https://www.javadrive.jp/ruby/regex/index39.html

String#scanは、パターンを繰り返しマッチさせて、マッチする文字列からなる配列を戻り値として返す
https://docs.ruby-lang.org/ja/latest/method/String/i/scan.html

String#gsubは、文字列中でパターンにマッチする部分全てを指定した文字列に置き換えた文字列を生成する。ブロックを使うごとでループごとに特定の処理をしたりもできる

https://docs.ruby-lang.org/ja/latest/method/String/i/gsub.html

String#stripで、文字列の先頭と末尾から空白を取り除いた文字列を生成して返す

\s
https://docs.ruby-lang.org/ja/latest/doc/spec=2fregexp.html

掛け算の積のことをproductというそう
https://eikaiwa.dmm.com/uknow/questions/64518/
https://docs.ruby-lang.org/ja/latest/method/String/i/strip.html

ハガユウキハガユウキ

尺取り法

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

要は、2つのインデックスを使って、ループ効率を上げるのが尺取り法

https://www.kumilog.net/entry/two-pointers

ハガユウキハガユウキ

配列から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)

https://www.code-recipe.com/post/container-with-most-water

ハガユウキハガユウキ

整数配列 nums が与えられたとき、0 以外の要素の相対順序を維持したまま、すべての 0 を配列の末尾に移動する。

2つの文字列sとtが与えられたとき、sがtの部分列であれば真を、そうでなければ偽を返す。
文字列の部分列とは、元の文字列から、残りの文字の相対的な位置を乱すことなく、文字の一部(なくてもよい)を削除してできた新しい文字列のことである。(すなわち、"ace "は "abcde "の部分文字列であるが、"aec "はそうではない)。

長さnの整数配列heightが与えられ、i番目の線の2つの端点が(i, 0)と(i, height[i])となるようにn本の垂直線が引かれる。
x軸と一緒になって容器を形成し,その容器が最も多くの水を含むような2本の直線を求めよ.

調べる区間が決まっていて、その中で特定の2つのインデックスを操作して幅を広げたり狭くしたりしつつ、何か値を求める(最大値とか最小値が多いかも)系のアルゴリズムを書きたいなら、尺取り法が使える

ハガユウキハガユウキ

スライディングウィンドウアルゴリズムとは

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

なるほど、配列のある部分に着目してアプローチをして、アプローチ後に範囲を一つずらしてまたアプローチをするのが、スライディングウインドウアルゴリズムなのか
https://morizatta.com/algorithm-sliding-window/

ハガユウキハガユウキ

ウインドウとはシステム状態の一部のこと
システム状態が配列で表現されているなら、配列の一部がウインドウである。
スライディングとは、ウインドウの位置を1つずらすと、処理を適用できる別のウインドウが出現することを指している

ステップ数とは、命令が実行された回数
https://www.momoyama-usagi.com/entry/calc-order

スライディング・ウィンドウ・アルゴリズムとは、計算のためのウィンドウが固定されているものを高速に計算するアルゴリズムであり、ネストされたループ(素朴なアプローチ)を使用するよりも最適化された方法で結果を取得することができる。このアルゴリズムの主な目的は、あるウィンドウの結果を再利用して次のウィンドウの結果を計算することである。

この説明がわかりやすい。任意の固定されたウインドウに対しての計算を高速化するアルゴリズムがスライディングウインドウアルゴリズム。このアルゴリズムの本質は、あるウインドウに対しての計算結果を再利用して次のウインドウの計算結果を計算でき、その結果、計算量を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に比べると小さいと考えられるので、無視して良い)

ハガユウキハガユウキ

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

ハガユウキハガユウキ

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

ハガユウキハガユウキ

指定された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]。ハッシュテーブルは、ハッシュ関数を使用して、バケットまたはスロットの配列へのインデックス(ハッシュコードとも呼ばれる)を計算し、そこから目的の値を見つけることができる。検索時にはキーがハッシュ化され、その結果のハッシュが対応する値が格納されている場所を示す。

理想的には、ハッシュ関数は各キーを一意のバケットに割り当てるが、ほとんどのハッシュテーブルの設計は不完全なハッシュ関数を採用しているため、ハッシュ関数が複数のキーに対して同じインデックスを生成するハッシュ衝突が発生する可能性がある。このような衝突は、通常何らかの方法で対処される。

十分な次元のハッシュ表では、各ルックアップの平均時間複雑度は、表に格納されている要素数に依存しない。また、多くのハッシュテーブルの設計では、操作ごとに償却された一定の平均コストで、キーと値のペアの任意の挿入と削除が可能です[3][4][5]。

ハッシュは時空間トレードオフの一例である。メモリが無限であれば、キー全体を直接インデックスとして使用し、1回のメモリアクセスでその値を見つけることができる。一方、無限の時間が利用可能であれば、キーに関係なく値を格納することができ、バイナリ検索や線形検索を使って要素を取り出すことができる[6]:458

多くの状況において、ハッシュ表はサーチツリーやその他の表検索構造よりも平均的に効率的であることが判明している。このため、ハッシュテーブルは多くのコンピュータ・ソフトウェア、特に連想配列、データベース・インデックス、キャッシュ、集合などで広く使われている。

https://en.wikipedia.org/wiki/Hash_table

ハガユウキハガユウキ

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

ハガユウキハガユウキ

ハッシュのデフォルト値を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

https://docs.ruby-lang.org/ja/latest/method/Hash/i/default=3d.html

ハガユウキハガユウキ

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

ハガユウキハガユウキ

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

ハガユウキハガユウキ

配列からハッシュを生成したいなら、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#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つのフィールドを含むノードのシーケンスである。最後のノードは、リストの終わりを示すターミネーターにリンクされている。

リンクドリストは、一連のノードで構成されるデータ構造である。各ノードはポインターを持っていて、次のノードを指し示している

リンクドリストの順序はメモリ上の物理的な配置に依存しない

https://en.wikipedia.org/wiki/Linked_list

連結リストのメリットは、データの追加、削除が簡単。連結リストのデータの前後の連結リストデータを変更すれば良いので楽。デメリットは、目的のデータを見つけるのに1個1個連結リストのデータを辿らないといけないので、めんどくさい

配列のメリットは、データの探索が楽。O(1)
デメリットは、データの追加、削除でずらさないといけないので、結構重めな計算が発生すること

ハガユウキハガユウキ

リンクドリストの真ん中の要素を見つけたいなら、それぞれのリンクドリストを配列に入れて、真ん中の要素を見つけば良い。ただし、これは、時間計算量がO(n)で空間計算量がO(n)である

空間計算量はアルゴリズムで必要とするメモリの使用量のこと

https://qiita.com/besaklish/items/17c3c246fb172364b80e
https://the-simple.jp/what-is-spatial-complexity-algorithm-memory-usage-metrics#:~:text=空間計算量とは、アルゴリズムが必要とする,を与えることがあります。

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)の時間複雑度で解かなければならない。

ハガユウキハガユウキ

連結リストを逆転させるのは、この記事が役に立った

https://zenn.dev/ike_pon/articles/ec970cc20e7ff05c850b

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分探索木

二分木とは、すべての頂点に対して子の個数が 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

ハガユウキハガユウキ

二分探索木から特定のノードを削除する問題
削除対象のノードが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。

ハガユウキハガユウキ

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より良くない

https://zenn.dev/convers39/articles/1c315cd96a991f#dfsとbfsの比較

ハガユウキハガユウキ

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

https://find-best-practice.com/2023/05/23/【leetcode】-1448-count-good-nodes-in-binary-tree-解答・解説【python】/

https://leetcode.jp/leetcode-1448-count-good-nodes-in-binary-tree-解题思路分析/

ハガユウキハガユウキ

BFS

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

https://zenn.dev/kueharx/articles/6e0362c9c28a65#bfs

ハガユウキハガユウキ

キューが空になるまで処理を続ける
キューを使うことで、BFSを実装できる。つまり、深さごとにnodeを調査できる。今までは深さを優先して探索していたが。

ハガユウキハガユウキ

優先度付きキュー

優先度キューでは、高い優先度を持つ要素は、低い優先度を持つ要素よりも先に提供されます。

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

https://lets-csharp.com/binary-heap/
https://en.wikipedia.org/wiki/Priority_queue

ハガユウキハガユウキ

キューみたいに入れたものを必ず先に実行しないで、優先度の高いものから実行していくので、現実世界に合うようなデータ構造だな

ハガユウキハガユウキ

そうか、優先度付きキューって言っているくらいだから、popしたら優先度が高いのが出るのか。てことは厳密にはヒープとは違うね。ヒープを用いて実装されるってくらいがちょうど良い表現だね。

ハガユウキハガユウキ

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

1 = n / 2^xとなる。これを変形すると、x = logn。探索回数は計算回数でもあるので、よって二分探索の計算量はlogn
https://yottagin.com/?p=8030

ハガユウキハガユウキ

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

ハガユウキハガユウキ

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

ハガユウキハガユウキ

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

ハガユウキハガユウキ

二分探索はある値より大きい要素の個数を求めよでも使える

https://www.momoyama-usagi.com/entry/heap-sort#1_1

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

https://docs.ruby-lang.org/ja/latest/method/Array/i/count.html

ハガユウキハガユウキ

2分探索は数が決まっているなら、右と左のインデックスをずらしていくのがポイント
rubyなら、使えそうなら、Array#bsearchやArray#bsearch_indexをガンガン使っていく

ハガユウキハガユウキ

Set

Setは数学における集合をデータ構造として表したもので、順序はなく、変更可能で、重複した要素を持たない(配列からSetを作るが、配列が重複した要素を持っていた場合、勝手に削除される)。Setは「集合」の英語
要素の挿入、削除、検索(include?)を高速に行うことができる

https://algo-logic.info/set/

Setの使い所

要素を入れつつ、常に重複を除外して要素を格納するときは、HashやSetを使う
(もっとざっくりいうと要素を重複させない要素の集合を持ちたい場合に、Setを使う)
→Arrayで常にユニークにしたいなら、uniq!を使って、毎回全要素を調べO(n)なので、ループごとにO(n)かかってしまう。毎回のループごとに同じ配列に何回もループするのは変だし。
→最後にユニークにするなら、Setで常にユニークにするより、配列に要素を追加していって、最後にuniq!をした方が速そう

https://zenn.dev/universato/articles/20201210-z-ruby

データのソートやinclude?はやっぱArrayよりSetの方が早そう

https://muramurasan.hatenablog.jp/entry/2017/02/13/120000

https://techracho.bpsinc.jp/hachi8833/2017_03_27/37659

https://docs.ruby-lang.org/ja/latest/class/Set.html#I_LENGTH
https://qiita.com/namitop/items/238ebc4dfc1a367f87a8

ハガユウキハガユウキ

持つデータの順序に関心がなくて、ユニークな値を常に持ちたい状況でSetは使えそう

ハガユウキハガユウキ

動的計画法

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

動的計画法とは

  1. 与えられた問題をうまく部分問題にして、
  2. 部分問題の解をメモ化しながら、
  3. 小さな部分問題からより大きな部分問題へと順に解を求めていく方法
ハガユウキハガユウキ
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)

同じ関数を計算しないように、事前に処理結果をメモしておくことをメモ化と呼ぶ。
動的計画法では結構大事だそう

ハガユウキハガユウキ
カメのアルルは N 段の階段をのぼろうとしています。
それぞれの段には、次の 2 通りの方法でのぼることができます。
(階段を降りることはできません。)

1 つ前の段からのぼる。
(存在する場合のみ) 2 つ前の段からのぼる。
今、アルルは階段の 0 段目にいます。
アルルが階段の N 段目までのぼる方法は全部で何通りありますか。

このタイプの問題、昔paizaで解いたことあるけど、まさか動的計画法だったとは

ハガユウキハガユウキ

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

ハガユウキハガユウキ

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

ハガユウキハガユウキ

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

ハガユウキハガユウキ

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

ハガユウキハガユウキ

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

ハガユウキハガユウキ

このように、もとの問題を一連の部分問題へと分解し、それぞれの部分問題の解をメモ化しながら、小さな部分問題からより大きな部分問題の解を順に求めていく手法を総称して、動的計画法とよびます。

このとき、より大きな部分問題の解を求めるために、小さな部分問題に対してメモ化された値を再利用していることがポイントとなります。

わかりやすい。
動的計画法は、複数の問題に分割して、それぞれの問題を解くことに全力を尽くした方が良いね。てか、この問題を見つける力と複数の問題を分割する力が動的計画法には大事。
(これを満たしている必要があるなってやつを見つける。小さな問題を解いていくことをイメージする)

ハガユウキハガユウキ

グラフ理論の用語を整理する

グラフとは、対象物の関係性を表すもの。
点は頂点、頂点と頂点をつなぐものは辺と呼ばれる

有向グラフ辺 e=(u,v) に対して、頂点 u を辺 e の始点とよび、 頂点 v を辺 e の終点と呼びます。

ほー

ハガユウキハガユウキ

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

ハガユウキハガユウキ

辺ってエッジともいうのか

https://ja.wikipedia.org/wiki/グラフ理論

グラフをどのようなデータ構造で表現するか

隣接リストとエッジリストが使えそう。

隣接リスト

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)と呼ぶ。

なるほど、エッジがノードに比べて少ないグラフを疎グラフ
エッジがノードに比べて多いグラフを密グラフと呼ぶのか

ハガユウキハガユウキ

そうか、エッジリストは、全要素を探索しないと、ある頂点とある頂点に関係があるかわからないな。計算量が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は辿れるところまで辿っちゃおうぜってスタンスの探索方法だったな。

ハガユウキハガユウキ

https://docs.ruby-lang.org/ja/latest/method/Hash/s/new.html

ハッシュで Setや配列をdefault valueにしたいときはブロックを使う。じゃないと、キー間でvalueを共有してしまう。0とかのプリミティブならそんなことはないが

graph = Hash.new { |key, hash| hash[key] = Set.new([]) }

その他参考にした記事
https://docs.ruby-lang.org/ja/latest/method/Array/s/new.html
https://docs.ruby-lang.org/ja/latest/class/Set.html#I_EACH
https://docs.ruby-lang.org/ja/latest/method/Set/i/each.html

ハガユウキハガユウキ

グラフで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)}

https://qiita.com/dddisk/items/8ad2d5570edf3c577eea

ハガユウキハガユウキ

DFS

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

ハガユウキハガユウキ

https://qiita.com/namitop/items/238ebc4dfc1a367f87a8

↓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 は、配列に対して、順に各マスの値を決定していく手法である

やっぱそうだったか

ハガユウキハガユウキ

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

緩和というのか

ハガユウキハガユウキ

動的計画法、BFS、DFS、バイナリサーチの使う場面や特徴をまとめる

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

https://jabba.cloud/20161020172918

ハガユウキハガユウキ

1次元で扱うと横への広がりがなくなる

ハガユウキハガユウキ

動的計画法は部分問題の解の集合を表に記録するって何かの記事で書いてたけど、確かにそうだな。

ハガユウキハガユウキ

部分和問題
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
ハガユウキハガユウキ

ナップサック問題

ナップサック問題とは、それぞれ重さと値をもつアイテムの集合から、重さの合計がある重さ以下となるような組み合わせで、値の合計ができるだけ大きくなるようにするにはどうすれば良いかを考える問題である。

https://en.wikipedia.org/wiki/Knapsack_problem

https://www.msi.co.jp/solution/nuopt/docs/examples/html/02-05-00.html#:~:text=この問題は,組合せ最適,に応用されています.

# 方針
# 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
ハガユウキハガユウキ

グラフの連結

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

ハガユウキハガユウキ

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

ハガユウキハガユウキ

サイクル、パス

パス:隣接する頂点をたどっていくことで、始点から終点へと到達できる部分グラフ
サイクル:隣接する頂点をたどっていくことで、元の頂点に戻ってこれる部分グラフ

https://algo-method.com/descriptions/105#definition

連結な部分グラフ」:グラフ内の一部で、その部分自体も連結である部分。
「極大で連結な部分グラフ」:その部分グラフをさらに拡張することはできない部分グラフ。

ハガユウキハガユウキ

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

ハガユウキハガユウキ

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部グラフ

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

重要な着眼点として、 1 つの頂点を白色に決め打ちしたときに、 その頂点を含む連結成分に含まれるすべての頂点の色が自動的に決まることに着目します。 このとき両端点が同色になるような辺を含むならば、二部グラフではありません。

なるほど

ハガユウキハガユウキ

連結成分

連結成分とは「部分グラフのうち、極大で連結なもの」と定義されるグラフの用語です。 直感的には、「辺をいくつか辿ってたどりつける関係にある頂点の集合」を指します。

https://algo-method.com/tasks/431

ハガユウキハガユウキ

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)

ハガユウキハガユウキ

サイクルがないってことは、一方向に進んだら元の始点に戻るってことはないってこと
(逆流しないと元の点に戻れない)

ハガユウキハガユウキ

サイクルがあるってことは、一方向に進んだら元の点に戻れちゃうってこと

ハガユウキハガユウキ

連結グラフは、連結を保ちたいなら最低でも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
ハガユウキハガユウキ

木の直径を簡単に求める

木の直径は以下の手順で簡単に求められる。

  1. 木の適当な頂点 r を 1 つ選び、頂点 r から最も距離が遠い頂点 s を求める
  2. 頂点 s から最も遠い頂点 t を求める
  3. 頂点 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頂点は辺で結ばれていないということである。

ハガユウキハガユウキ

累積和を求める関数



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
このスクラップは2023/11/08にクローズされました