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
119 changes: 119 additions & 0 deletions 0084.Largest-Rectangle-in-Histogram/memo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# 84. Largest Rectangle in Histogram

## step1
考えたこと:

15mぐらい紙に書いて考えたが分からない -> (Hintがないので)Topicを見る -> monotonic stack というキーワードを見つける -> ある長方形を高さとする場合、左と右の、それよりも低い長方形の位置が分かれば解けることに気が付く -> monotonic stackを使えばこの操作を実現できることに気が付く

合計で 40m ほど。

「自分より大きい(or 小さい)最初の要素」を探したいときに単調スタックを使う。
以前にも見たはずだが身についていない。

欲しい条件とは逆のスタックを用いる。今回は次に小さい値が欲しいので、スタックは単調増加。

## step2
少し変更


## 類題

練習として以下を解き直す。

https://leetcode.com/problems/next-greater-element-i/description/


```python
class Solution:
def nextGreaterElement(self, nums1: list[int], nums2: list[int]) -> list[int]:
num_to_next_greater = {}
decreasing_stack = []

for n in nums2:
while decreasing_stack and decreasing_stack[-1] < n:
previous_smaller = decreasing_stack.pop()
num_to_next_greater[previous_smaller] = n
decreasing_stack.append(n)

return [num_to_next_greater.get(n, -1) for n in nums1]

```


## 他の人のコード

- https://github.com/shining-ai/leetcode/pull/68

> 最小値をsegment treeで管理して効率化

区間の最小値を求めれば良いからSegment Treeを用いることができる

時間計算量 O(NlogN)

SegmentTreeは頭で知っているけど自分で書けないデータの一つなので練習する必要がある


> 一応、入力を破壊的に変更すると、呼んだ人がびっくりする可能性があるので、それを指摘されたら考えましょう。

> 用途によっては何もしないのも一つです。
> あるいは、こう書かないのも一つです。
> 入力をコピーしちゃうのは一つです。コピーのコストについて聞かれたら、Python であることなどいろいろ勘案してコストは小さいと見積もるということです。
> 使い終わったら pop して元に戻すのも一つです。ただし、その場合は、スレッド並列性との兼ね合いがあります。
> マルチスレッドで起きる諸々といえば、
> The Deadlock Empire
> https://deadlockempire.github.io/#menu

マルチスレッドでは、関数のローカル変数は共有されないが、グローバル変数、クラス変数、(インスタンスを共有する場合の)インスタンス変数が共有される場合、スレッドセーフではなくなる

入力の破壊は意識していたが、マルチスレッドで問題が生じるという意識はなかった。

The Deadlock Empire、バグらせるゲームなのか。時間があるときに見てみても良いかも。

- https://github.com/yamashita-ki/codingTest/pull/12
- https://github.com/Exzrgs/LeetCode/pull/39

## step3
単調スタック、セグメントツリーの解法を書く

TODO: 遅延評価の実装と応用問題


以下LLM:

## セグメントツリー(Segment Tree)

## 基本構造と親子関係
* **ルートノードのインデックスは `1` から始める**
* `0` から始めるよりも、親子関係のインデックス計算が非常にシンプルになります。
* **ノード間の移動(親子関係)の計算規則**
* **親ノードへの移動:** `i // 2` (またはビット演算で `i >> 1`)
* **左の子ノードへの移動:** `i * 2` (またはビット演算で `i << 1`)
* **右の子ノードへの移動:** `i * 2 + 1` (またはビット演算で `(i << 1) | 1`)

## 計算量(Time & Space Complexity)
* **構築(Build):** $O(N)$
* 葉ノードから親ノードへ向かってボトムアップに1回走査するだけで構築可能です。
* **クエリ(Query)/ 更新(Update):** $O(\log N)$
* 木の高さ(深さ)に比例したステップ数だけで、ピンポイントの更新や区間内の検索が完了します。

## 実装のアプローチ(2つの手法)
* **再帰型(トップダウン)**
* ルートから目的の区間に向かって条件分岐しながら掘り下げていく。
* **メリット:** 構造の直感性が高く、後に「遅延評価」などの高度な機能を拡張しやすい。
* **非再帰型(ボトムアップ)**
* 最下層の「葉」から、区間の両端を挟み込むように親へと登っていく。
* **メリット:** 関数の呼び出しオーバーヘッドがないため、実行速度が最速かつ省メモリ(空間計算量 $O(1)$)。
* **注意点:** 奇数偶数のインデックス判定(`begin % 2 == 1` / `end % 2 == 1`)のトリックが必要。

## バグを防ぎ、プロっぽく仕上げる実践ポイント
* **配列サイズは「元の要素数の4倍($4N$)」確保する**
* べき乗への拡張を行わない場合でも、サイズを $4N$ 確保しておけば、境界値のバグによる `IndexError` を確実に防ぐことができます。
* **ビット演算を活用して高速化する**
* `// 2` や `* 2`、`% 2 == 1` の代わりに、`>> 1`、`<< 1`、`& 1` などのビット演算を使うと実行速度が向上し、低レイヤーに強いクリーンなコードになります。
* **問題の目的に応じて「単位元(初期値)」を正しく設計する**
* **最小値 (RMQ):** `float("inf")` (無限大)
* **最大値:** `-float("inf")` または `0`
* **総和 (Sum):** `0`
* **総積 (Product):** `1`
* **さらなる発展:遅延評価(Lazy Propagation)**
* 「区間の一斉更新」を $O(\log N)$ で行うための発展形。データが本当に必要になるまで下位ノードへの更新をサボる(遅延させる)テクニック。
23 changes: 23 additions & 0 deletions 0084.Largest-Rectangle-in-Histogram/step1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
class Solution:
def largestRectangleArea(self, heights: list[int]) -> int:
increasing_stack: tuple[int, int] = [] # list of (height, num_greater_left)
largest_area = 0
heights.append(0) # sentinel

for height in heights:
if not increasing_stack or increasing_stack[-1][0] <= height:
increasing_stack.append((height, 0))
continue

num_greater_right = 0
while increasing_stack and increasing_stack[-1][0] > height:
height_greater, num_greater_left = increasing_stack.pop()
largest_area = max(
largest_area,
height_greater * (num_greater_left + 1 + num_greater_right),
)
num_greater_right += 1 + num_greater_left

increasing_stack.append((height, num_greater_right))

return largest_area
21 changes: 21 additions & 0 deletions 0084.Largest-Rectangle-in-Histogram/step2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
class Solution:
def largestRectangleArea(self, heights: list[int]) -> int:
increasing_stack: list[
tuple[int, int]
] = [] # list of (height, num_greater_left)
largest_area = 0
heights.append(0) # sentinel

for height in heights:
num_greater_right = 0
while increasing_stack and height <= increasing_stack[-1][0]:
height_of_rectangle, num_greater_left = increasing_stack.pop()
largest_area = max(
largest_area,
height_of_rectangle * (num_greater_left + 1 + num_greater_right),

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

Choose a reason for hiding this comment

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

参考:

class Solution {
public:
    int largestRectangleArea(vector<int>& heights) {
        heights.push_back(0);
        stack<int> st;
        st.push(-1);
        int max_area = 0;
        for (int i = 0; i < heights.size(); ++i) {
            while (st.top() != -1 && heights[st.top()] >= heights[i]) {
                int h = heights[st.top()];
                st.pop();
                max_area = max(max_area, h * (i - st.top() - 1));
            }
            st.push(i);
        }
        return max_area;
    }
};

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.

indexだけを入れておいても幅を計算できるのですね。最初に-1を入れておけば条件分岐もなくなりますね。

)
num_greater_right += num_greater_left + 1

increasing_stack.append((height, num_greater_right))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

increasing_stack: tuple[int, int] = []  # list of (height, num_greater_left)

とありますが、pushしているのは、num_greater_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.

コメントを日本語にして修正しました。型ヒントも間違っていましたね。


return largest_area
15 changes: 15 additions & 0 deletions 0084.Largest-Rectangle-in-Histogram/step2_index_stack.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
class Solution:
def largestRectangleArea(self, heights: list[int]) -> int:
heights.append(0)
stack = [-1]
max_area = 0

for i in range(len(heights)):
while stack[-1] != -1 and heights[stack[-1]] >= heights[i]:
h = heights[stack.pop()]
width = i - stack[-1] - 1
max_area = max(max_area, h * width)

stack.append(i)

return max_area
19 changes: 19 additions & 0 deletions 0084.Largest-Rectangle-in-Histogram/step2_revised.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
class Solution:
def largestRectangleArea(self, heights: list[int]) -> int:
increasing_stack: list[tuple[int, int]] = [] # (バーの高さ, 左側に広がる自分以上の合計幅)のリスト
largest_area = 0
heights.append(0) # sentinel

for height in heights:
num_greater_right = 0 # ポップされるバーから見て右側に広がる自分以上の合計幅
while increasing_stack and height <= increasing_stack[-1][0]:
height_of_rectangle, num_greater_left = increasing_stack.pop()
largest_area = max(
largest_area,
height_of_rectangle * (num_greater_left + 1 + num_greater_right),
)
num_greater_right += num_greater_left + 1

increasing_stack.append((height, num_greater_right)) # 回収した右の幅がこのバーの左の幅

return largest_area
69 changes: 69 additions & 0 deletions 0084.Largest-Rectangle-in-Histogram/step3_segmenttree.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# rootのindexは1
# 親ノード: i // 2
# 子ノード: (i * 2), (i * 2 + 1)
class SegmentTree:
def __init__(self, nums):
self.size = 1
while self.size < len(nums):
self.size *= 2
self.tree = [(float("inf"), 0)] * (self.size * 2)
# 葉に値をセット
for i, num in enumerate(nums):
self.tree[self.size + i] = (num, i)
# ノードを更新
for i in range(self.size - 1, 0, -1):
self.tree[i] = min(self.tree[i * 2], self.tree[i * 2 + 1])

# トップダウン
def query_recursive(self, begin, end, node=1, node_begin=0, node_end=-1):
if node_end == -1:
node_end = self.size
if node_end <= begin or end <= node_begin:
return (float("inf"), 0)
if begin <= node_begin and node_end <= end:
return self.tree[node]

node_middle = (node_begin + node_end) // 2
left_min = self.query_recursive(begin, end, node * 2, node_begin, node_middle)
right_min = self.query_recursive(
begin, end, node * 2 + 1, node_middle, node_end
)
return min(left_min, right_min)

# ボトムアップ
def query(self, left, right):
left += self.size
right += self.size
min_node = (float("inf"), 0)
while left < right:
if left % 2 == 1:
# 親ノードは範囲外なので右に移動
min_node = min(min_node, self.tree[left])
left += 1
if right % 2 == 1:
# 親ノードは範囲外なので左に移動
right -= 1
min_node = min(min_node, self.tree[right])
left //= 2
right //= 2
return min_node


class Solution:
def largestRectangleArea(self, heights: list[int]) -> int:
segment_tree = SegmentTree(heights)

def calculate_area_with_minimum_height_between(begin, end):
if begin >= end:
return 0
# min_height, min_index = segment_tree.query(begin, end)
min_height, min_index = segment_tree.query_recursive(begin, end)
area = min_height * (end - begin)
max_area = max(
area,
calculate_area_with_minimum_height_between(begin, min_index),
calculate_area_with_minimum_height_between(min_index + 1, end),
)
return max_area

return calculate_area_with_minimum_height_between(0, len(heights))