雨水を溜める

生田 陸人
LuaGate エンジニア / 現役エンジニア
編集 LuaGate編集部

雨水を溜める

このレッスンで分かること

  • 「左の壁」「右の壁」「自分の高さ」の 3 つの値の関係 で水位が決まる、というのが Trapping Rain Water の核心
  • これは LeetCode Hard の常連であり、配列 + 双方向ポインタ のテクニックを最大限に活かせる名作問題です
  • height = [0,1,0,2,1,0,1,3,2,1,2,1] のとき、答えは 6 です

雨水を溜める とは

棒グラフ状の 高さ配列溜まる雨水量 を求める。左右の最大値配列 を使う O(n) 解と O(1) の双方向ポインタ解を学ぶ。

これは LeetCode Hard の常連であり、配列 + 双方向ポインタ のテクニックを最大限に活かせる名作問題です。整数配列 height棒グラフ の高さを表すとき、雨が降ったらこの地形にどれだけ が溜まるかを求めます。本レッスンでは段階的に O(n^2)O(n)O(1) 追加メモリ まで最適化します。

各位置に溜まる水は、その位置の 左側の最大高さ右側の最大高さ のうち 小さい方 から、自分の高さを引いた値だ。

直感をつかむ

height = [0,1,0,2,1,0,1,3,2,1,2,1] のとき、答えは 6 です。各 バー の上に水が溜まる量を可視化すると、バーが谷になっている部分 に水が貯まります。あるバーの上の水の高さは、左右の最も高い壁のうち低い方で決まります。

diagram (will load when visible)

解法 1: 左右の最大値配列 (O(n) 時間 / O(n) 空間)

leftMax[i]height[0..i] の最大、rightMax[i]height[i..n-1] の最大として 2 本の配列を事前計算します。あとは min(leftMax[i], rightMax[i]) - height[i] を全位置で合計するだけ。

Python

def trapWater(height): n = len(height) if n == 0: return 0 leftMax = [0] * n rightMax = [0] * n leftMax[0] = height[0] for i in range(1, n): leftMax[i] = max(leftMax[i - 1], height[i]) rightMax[n - 1] = height[n - 1] for i in range(n - 2, -1, -1): rightMax[i] = max(rightMax[i + 1], height[i]) total = 0 for i in range(n): total += min(leftMax[i], rightMax[i]) - height[i] return total

シンプルで分かりやすく、これだけでも O(n) で動きます。本問題はまずこの解を確実に書けることが目標です。

解法 2: 双方向ポインタ (O(n) 時間 / O(1) 空間)

左右から 2 本のポインタ で挟み込み、小さい方 のポインタを進めながら その時点の最大値との差 を加算していきます。leftMax / rightMax 配列を作らずに済むので 空間O(1) になります。

JavaScript

function trapWater(height) { let l = 0, r = height.length - 1; let leftMax = 0, rightMax = 0; let total = 0; while (l < r) { if (height[l] < height[r]) { if (height[l] >= leftMax) leftMax = height[l]; else total += leftMax - height[l]; l++; } else { if (height[r] >= rightMax) rightMax = height[r]; else total += rightMax - height[r]; r--; } } return total; }

双方向ポインタ解は最初は呑み込みにくい。まずは O(n) 空間 版を完全に書けるようにしてから、空間最適化の理由を後追いするのが近道。

なぜ min を使うか

左右いずれかの壁が低いと、水はその低い壁の高さまでしか溜まりません。バケツの理屈と同じです。min(leftMax, rightMax) < height[i] のときは溜まらない (= 自分が壁になっている) ので、max(0, ...) で安全側に倒します。今回の test_cases では負にならないので、min - height[i] だけで十分です。

「左の壁」「右の壁」「自分の高さ」の 3 つの値の関係 で水位が決まる、というのが Trapping Rain Water の核心。

よくある間違い

  • leftMax[i]height[0..i-1] の最大 (自分を含まない) と勘違いする。含む のが正解。
  • 空配列 を受け取った時の IndexError。最初に if n == 0: return 0 を必ず入れる。
  • 双方向ポインタ版で 小さい方のポインタを進める ルールを忘れる。両側からどちらを進めるかが本質。

やってみよう

trapWater(height) を実装してください。整数配列 height (各値は 0 以上) を受け取り、溜まる雨水量 の合計を整数で返します。空配列 のときは 0 を返します。O(n) で実装してください (空間は O(n) でも O(1) でも OK)。

よくある質問

Q. このトピックは実務でどう役立ちますか?

A. DB のクエリ最適化、API 設計、データ構造の選択など、設計判断の根拠になります。表面的にライブラリを使うだけでなく「なぜそれが速いのか」を理解できると、性能問題を未然に防げます。コーディング面接でも頻出のテーマです。

Q. 計算量はどう求めれば良いですか?

A. ループのネストごとに掛け算する、再帰なら漸化式から解く、というのが基本です。Big-O 表記は定数倍と低次の項を無視するため、n の指数(n²、n log n など)に注目してください。最悪・平均・最良の 3 つを意識すると説得力が増します。

Q. 覚えるべき定番アルゴリズムは何ですか?

A. 二分探索、クイックソート/マージソート、BFS/DFS、ダイクストラ、DP の基本問題(フィボナッチ・ナップサック)が必修です。これらを「白紙から書ける」状態にすると、応用問題が一気に解けるようになります。

次のレッスン

次は 最終総まとめクイズ で、これまで学んだアルゴリズムの総復習をします。

事前確認 — 進む前に次の 3 つができることを確認しましょう。

  1. 雨水を溜める の要点を自分の言葉で説明できる
  2. このレッスンの最小コード (または操作手順) を見ずに書ける
  3. 練習問題やクイズで間違えた箇所を読み直して理解した

理解度チェック (30 秒)

Q. 雨水を溜める とは何か、1 文で説明してください。

この章のポイント

A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。

関連レッスン

要件

  1. 関数 trapWater(height) を実装し、溜まる水量の合計を整数で返す
  2. 計算量は O(n) であること (leftMax / rightMax 配列方式または 双方向ポインタ 方式)
  3. 空配列 のときは 0 を返す

入出力例

test-cases.txt

trapWater([0,1,0,2,1,0,1,3,2,1,2,1])6 trapWater([4,2,0,3,2,5])9 trapWater([0,1,2,1,0])0 trapWater([3,0,3])3 trapWater([5,5,5,5])0 trapWater([1,2,3,4])0 trapWater([4,3,2,1])0

ヒント

main.py
main.py
学習モード

メモ

雨水を溜める

⌘S で保存