diff --git a/0084.Largest-Rectangle-in-Histogram/memo.md b/0084.Largest-Rectangle-in-Histogram/memo.md new file mode 100644 index 0000000..60bcc17 --- /dev/null +++ b/0084.Largest-Rectangle-in-Histogram/memo.md @@ -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)$ で行うための発展形。データが本当に必要になるまで下位ノードへの更新をサボる(遅延させる)テクニック。 diff --git a/0084.Largest-Rectangle-in-Histogram/step1.py b/0084.Largest-Rectangle-in-Histogram/step1.py new file mode 100644 index 0000000..50e2494 --- /dev/null +++ b/0084.Largest-Rectangle-in-Histogram/step1.py @@ -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 diff --git a/0084.Largest-Rectangle-in-Histogram/step2.py b/0084.Largest-Rectangle-in-Histogram/step2.py new file mode 100644 index 0000000..cb00444 --- /dev/null +++ b/0084.Largest-Rectangle-in-Histogram/step2.py @@ -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), + ) + num_greater_right += num_greater_left + 1 + + increasing_stack.append((height, num_greater_right)) + + return largest_area diff --git a/0084.Largest-Rectangle-in-Histogram/step2_index_stack.py b/0084.Largest-Rectangle-in-Histogram/step2_index_stack.py new file mode 100644 index 0000000..7c9d1fb --- /dev/null +++ b/0084.Largest-Rectangle-in-Histogram/step2_index_stack.py @@ -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 diff --git a/0084.Largest-Rectangle-in-Histogram/step2_revised.py b/0084.Largest-Rectangle-in-Histogram/step2_revised.py new file mode 100644 index 0000000..1c34dcd --- /dev/null +++ b/0084.Largest-Rectangle-in-Histogram/step2_revised.py @@ -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 diff --git a/0084.Largest-Rectangle-in-Histogram/step3_segmenttree.py b/0084.Largest-Rectangle-in-Histogram/step3_segmenttree.py new file mode 100644 index 0000000..a32ec27 --- /dev/null +++ b/0084.Largest-Rectangle-in-Histogram/step3_segmenttree.py @@ -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))