Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions 703/step1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# step1 何も見ずに解く

Nをスコアの長さとする。Mをadd操作の回数とする。
最初のスコアをソートしておくと、k番目のスコアはO(1)で取り出せる。
新しいスコアを追加する時は二分探索を使うとO(logN)で入れる場所を探せる。
ただ、配列の途中に挿入するときにO(N + M)の計算量がかかるので、最終的な時間計算量は
M * (log(M + N) + (M + N))となり、O(M * (M + N))となり、M,Nの最大値が10^4なので間に合わない気がする。

```ruby
class KthLargest

=begin
:type k: Integer
:type nums: Integer[]
=end
def initialize(k, nums)
raise ArgumentError, "k must be positive integer" if k <= 0
@k = k
@sorted_nums = nums.sort.reverse
end


=begin
:type val: Integer
:rtype: Integer
=end
def add(val)
return @sorted_nums[@k - 1] if @sorted_nums.size >= @k && @sorted_nums[@k - 1] >= val

index = @sorted_nums.bsearch_index { |num| num <= val }
if index.nil?
@sorted_nums << val
else
@sorted_nums.insert(index, val)
end
@sorted_nums[@k - 1]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

この問題では入力の制約上無いですが、仮にこの時点で配列サイズがkより小さい場合範囲外参照になりnilが返りますかね。

end
end
```

なぜか間に合った。テストケースに厳しいものが少なかったのかも。

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

native で走る部分は速いということかと思います。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

コメントありがとうございます。
Array#insertはRuby VMからC関数にすぐに制御が渡って結果を返すが、Rubyで書いたループはVMからCの制御を何回も行き来するためオーバーヘッドが大きくなるということですね。
このあたり意識できていませんでした。

試しにinsertのような処理をRubyレベルで実行して比較したところArray#insertに比べて300倍近くの差がありましたね。

require "benchmark"

def native_insert(ary, num)
  ary.insert(50, num)
end

def ruby_insert(ary, num)
  ary << 0
  (ary.size - 1).downto(50) do |i|
    ary[i + 1] = ary[i]
  end
  ary[50] = num
end

BASE = Array.new(10_000, 0)
N = 10 ** 3
Benchmark.bmbm(10) do |x|
  x.report("insert (native Array#insert)") do
    arr = BASE.dup
    N.times do
      native_insert(arr, 99)
    end
  end
  x.report("ruby_array_inset") do
    arr = BASE.dup
    N.times do
      ruby_insert(arr, 99)
    end
  end
end
Rehearsal ----------------------------------------------------------------
insert (native Array#insert)   0.001143   0.000012   0.001155 (  0.001167)
ruby_array_inset               0.354955   0.001944   0.356899 (  0.357692)
------------------------------------------------------- total: 0.358054sec

                                   user     system      total        real
insert (native Array#insert)   0.001125   0.000012   0.001137 (  0.001135)
ruby_array_inset               0.351911   0.001223   0.353134 (  0.353635)

一応Rubyのinsertメソッドの実装を見たが、やはりO(N)かかるっぽい。まあそりゃそうか。

今のコードでは、scoreは降順に並んでいて、@sorted_nums[@k - 1]より小さい値、つまり配列の後にくる値の挿入をスキップしている。
配列の挿入は後ろであればあるほどコストが小さいはずなので昇順にして、前の方に挿入する時にスキップした方がマシだったかもしれない。
130 changes: 130 additions & 0 deletions 703/step2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# step2 他の方の解答を見る

- https://github.com/Ryotaro25/leetcode_first60/pull/9

MinHeapで実装する。
小さい順に上位k個数の値を保持しておけば良い。
Heapに値を追加する時の時間計算量はO(logN)
一番上の値を取り出した後にHeapの順を直す時の時間計算量もO(logN)
なのでnumsの長さをN, addが呼ばれる回数をMとすると、
最初にnumを入れるのにO(N*logN)でaddのクエリは合計で(M * log(k))になる。
なのでO(NlogN)
空間計算量はO(M + N)
Comment on lines +9 to +12

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
なのでnumsの長さをN, addが呼ばれる回数をMとすると、
最初にnumを入れるのにO(N*logN)でaddのクエリは合計で(M * log(k))になる
なのでO(NlogN)
空間計算量はO(M + N)
なのでnumsの長さをN, addが呼ばれる回数をMとすると、
addは1回O(log k)なので、初期化O(N logk)、クエリの処理O(M logk)
よって時間計算量はO(N logk + M logk)=O((N+M)logk)。
空間計算量はO(k)。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

コメントありがとうございます。
最初のheapのビルドでO(N * logK)かかっていて、その後のaddのクエリでO(M * logK)かかるので合計でO((M + N) * logk)ですね。
あとヒープの中は常に要素がkなので空間計算量はO(k)でしたね。


```ruby
class MinHeap
def initialize
@heap = []
end

def peek
@heap.first
end

def push(val)
@heap << val
sift_up(@heap.size - 1)
end

def pop
return nil if @heap.empty?

swap(0, @heap.size - 1)
min = @heap.pop
sift_down(0)
min
end

def size
@heap.size
end

private

def parent_index(index)
(index - 1) / 2
end

def left_child_index(index)
2 * index + 1
end

def right_child_index(index)
2 * index + 2
end

def sift_up(child)
parent = parent_index(child)
return if parent < 0 || @heap[parent] <= @heap[child]

swap(child, parent)
sift_up(parent)
end

def sift_down(parent)
left = left_child_index(parent)
right = right_child_index(parent)
smallest = parent
smallest = left if left < @heap.size && @heap[smallest] > @heap[left]
smallest = right if right < @heap.size && @heap[smallest] > @heap[right]
Comment on lines +65 to +69

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

微かにトリッキーさを感じました。
left_child_index が呼ばれたときには、まだ left child があるかどうかまだ分からないことが理由でしょうか。
left, right のスコープが広く見えるとかですかね。まあ、でもベターな案はないです。

        smallest = parent
        left = left_child_index(parent)
        smallest = left if left < @heap.size && @heap[smallest] > @heap[left]
        right = right_child_index(parent)
        smallest = right if right < @heap.size && @heap[smallest] > @heap[right]

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

確かに、smallestを更新する直前にleft/rightをそれぞれ変数に入れる方が少しわかりやすいかもしれないです。

ちょっと冗長かもしれませんがhas_left_child?メソッドを作るとかですかね。

        if has_left_child?(smallest)
            left = left_child_index(smallest)
            smallest = left if @heap[left] < @heap[smallest]
        end

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://discord.com/channels/1084280443945353267/1192736784354918470/1194613857046503444
私が昔書いた実装です。これはホワイトボードの前で何も見ずに書いたものです。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ありがとうございます。
smaller_childをメソッド化してparentの値と比較する方が書く方も読む方も認知負荷が低くて良いですね。

class MinHeap
    def initialize
        @heap = []
    end

    def peek
        @heap.first
    end

    def push(val)
        @heap << val
        sift_up(@heap.size - 1)
    end

    def pop
        return nil if @heap.empty?

        swap(0, @heap.size - 1)
        min = @heap.pop
        sift_down(0)
        min
    end

    def size
        @heap.size
    end

    private

    def parent_index(index)
        (index - 1) / 2
    end

    def left_child_index(index)
        2 * index + 1
    end

    def right_child_index(index)
        2 * index + 2
    end

    def has_left_child?(index)
        left_child_index(index) < @heap.size
    end

    def has_right_child?(index)
        right_child_index(index) < @heap.size
    end

    def has_child?(index)
        has_left_child?(index)
    end

    def smaller_child_index(index)
        smaller_child_index = left_child_index(index)
        if has_right_child?(index) && @heap[left_child_index(index)] > @heap[right_child_index(index)]
            smaller_child_index = right_child_index(index)
        end
        smaller_child_index
    end

    def sift_up(child)
        parent = parent_index(child)
        return if parent < 0 || @heap[parent] <= @heap[child]

        swap(child, parent)
        sift_up(parent)
    end

    def sift_down(parent)
        return unless has_child?(parent)
        smaller_child = smaller_child_index(parent)
        return if @heap[parent] <= @heap[smaller_child]

        swap(parent, smaller_child)
        sift_down(smaller_child)
    end

    def swap(index1, index2)
        @heap[index1], @heap[index2] = @heap[index2], @heap[index1]
    end
end

return if smallest == parent

swap(parent, smallest)
sift_down(smallest)
end

def swap(index1, index2)
@heap[index1], @heap[index2] = @heap[index2], @heap[index1]
end
end



class KthLargest

=begin
:type k: Integer
:type nums: Integer[]
=end
def initialize(k, nums)
raise ArgumentError, "k must be positive integer" if k <= 0
raise ArgumentError, "Initial array size is too small to determine the top #{k}" if k > nums.size + 1
Comment on lines +90 to +91

@naoto-iwase naoto-iwase Oct 12, 2025

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

確かにエラーメッセージには実際の値を埋め込むと、デバッグ時に原因を特定しやすくなると思います。
たとえば以下のようにすると、より親切だと思います。

Suggested change
raise ArgumentError, "k must be positive integer" if k <= 0
raise ArgumentError, "Initial array size is too small to determine the top #{k}" if k > nums.size + 1
raise ArgumentError, "k '#{k}' must be positive integer" if k <= 0
raise ArgumentError, "Initial array size '#{nums.size}' is too small to determine the top k '#{k}'" if k > nums.size

また、2個目のエラーは不要だと思いました。

初期配列がkより小さくてもaddメソッドがガードして正常に機能するためです。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

初期配列がkより小さくてもaddメソッドがガードして正常に機能するためです。

機能はしますが、以下のようにtop 3が欲しいのにそうではない結果を返すことが良いかは要件次第かなと思います。
個人的には上から3番目を教えて欲しい時に要素が2個しかないから二番目を返してもらうと困ると思いました。

k = KthLargest.new(3, [])
k.add(3) #=> 3を返す

@k = k
@top_k = MinHeap.new
nums.each { |num| add(num) }
end


=begin
:type val: Integer
:rtype: Integer
=end
def add(val)
if @top_k.size < @k
@top_k.push(val)
elsif @top_k.peek < val
@top_k.pop
@top_k.push(val)
end
@top_k.peek
end
end
```

https://github.com/shintaroyoshida20/leetcode/pull/13/files/db18f87a811ced86f26a72e4ee842f1b99d71b15#r2052161956

LeetCode環境でRubyは`Module: Algorithms`が使えるらしい。初めて知った。
https://www.rubydoc.info/github/kanwei/algorithms/Algorithms


https://github.com/fhiyo/leetcode/pull/10/files/0ee4b594d9657627d07ef9810b8f695611e366ac#r1605950261

メソッド単位でmutexを取る前提だと、topで値を返さないと

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

マルチスレッド前提だとこのような問題が起こる可能性があるのですね。勉強になりました。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

スレッドセーフではない外部ライブラリを使う時などは、呼び出し側がmutexをとって競合が起きないようにする、という方法もあり得ると思います。

- スレッドA
- 1. top
- 3. pop
スレッドB
- 2. pop

上の順番で処理が走ると不整合が起きる。

99 changes: 99 additions & 0 deletions 703/step3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# step3 3回続けて10分以内に書いてエラーを出さなければOKとする

```ruby
class MinHeap
def initialize
@heap = []
end

def size
@heap.size
end

def push(val)
@heap << val
sift_up(@heap.size - 1)
end

def pop
swap(0, @heap.size - 1)
min_val = @heap.pop
Comment on lines +19 to +20

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

「先頭の要素を末尾に持っていき、それをpopすると同時にmin_valとして格納する」
ということなのですが個人的にはややトリッキーに感じました。
ヒープというデータ構造が先頭が最小(or最大)という性質しか保証していないので、先頭以外から取ってくるのが違和感があるかもしれません。

一行長くなっても

  • 先頭の値をmin_valに格納する
  • 先頭と末尾をswap
  • 末尾をpop

となっているほうが個人的には違和感がない形かなと思います。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

確かに、そっちの方がわかりやすいですね。
コメントありがとうございます!

sift_down(0)
min_val
Comment on lines +18 to +22

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

step2 -> step3でreturn nil if @heap.empty?を削ったと思うですが、賛成です。

addメソッドでチェックがあるので、低レベルにもチェックがあると冗長になりますね。

もしpopメソッドでチェックを含める設計にする場合は、設計の一貫性からpeekメソッドなどにもチェックを含めて欲しくなりますね。

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

個人的にはMinHeapを実装するのであればKthLargestなど他のクラスに依存せずMinHeap単体でも使えるようになっていてほしいので、emptyチェックがあるほうが安心ではあるなと思いました。
(この問題を解くだけなら確かに冗長なのですが。)

もしpopメソッドでチェックを含める設計にする場合は、設計の一貫性からpeekメソッドなどにもチェックを含めて欲しくなりますね。

こちらに関しては賛成です。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

個人的にはMinHeapを実装するのであればKthLargestなど他のクラスに依存せずMinHeap単体でも使えるようになっていてほしい

自分もこの意見ですね。step3では単純にempty?のチェックが漏れていました。

end

def peek
@heap.first
end

private

def parent_index(index)
(index - 1) / 2
end

def left_child_index(index)
index * 2 + 1
end

def right_child_index(index)
index * 2 + 2
end

def swap(index1, index2)
@heap[index1], @heap[index2] = @heap[index2], @heap[index1]
end

def sift_up(child)
parent = parent_index(child)
return if parent < 0 || @heap[parent] <= @heap[child]

swap(child, parent)
sift_up(parent)
end

def sift_down(parent)
left = left_child_index(parent)
right = right_child_index(parent)
smallest = parent
smallest = left if left < @heap.size && @heap[left] < @heap[smallest]
smallest = right if right < @heap.size && @heap[right] < @heap[smallest]
return if smallest == parent

swap(parent, smallest)
sift_down(smallest)
end
end



class KthLargest

=begin
:type k: Integer
:type nums: Integer[]
=end
def initialize(k, nums)
raise ArgumentError, "k must be positive integer" if k <= 0
raise ArgumentError, "Initial array size is too small to determine the top #{k}" if k > nums.size + 1
@k = k
@top_k = MinHeap.new
nums.each { |num| add(num) }
end


=begin
:type val: Integer
:rtype: Integer
=end
def add(val)
if @top_k.size < @k
@top_k.push(val)
elsif @top_k.peek < val
@top_k.pop
@top_k.push(val)
end
@top_k.peek
end
end
```
1 change: 1 addition & 0 deletions 703/step4.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
## step4 レビューを受けて解答を修正