Container With Most Water
非負整数のリスト height が与えられます。線 i は、位置 i に立つ高さ height[i] の垂直な壁です。任意の2本の線は地面とともに容器を形成し、その容器が保持できる水の量は、短いほうの線の高さに2本の線の間の距離を掛けた値です。ほかの線は邪魔になりません。1組の線で保持できる水の最大量を返してください。
関数
- heightinteger-array
- 位置 0、1、2 などにある行の高さ
- 戻り値integer
- 2本の線に入れられる水の最大量
制約
2 ≤ height.length ≤ 1040 ≤ height[i] ≤ 104- 答えは最大でも 108 なので、32 ビット整数に収まります。
例
- 入力
- height = [3, 7, 2, 5, 4, 7, 3, 6]
- 出力
- 36
- 説明
- 位置1と7の線の高さは7と6で、間隔は6なので、6 × 6 = 36を保持します。最も高い2本の線、位置1と5の7の線が保持できるのは7 × 4 = 28だけで、外側のペアは3 × 7 = 21を保持します。
- 入力
- height = [4, 4]
- 出力
- 4
- 説明
- 2本の線でちょうど1つのコンテナができ、高さは4、幅は1なので、4を入れられます。
提出時に隠しテスト+15件
発展問題
ここでは、選んだ2本の線の間にある線は無視されます。すべての線が代わりに塊状の棒だったとしたら、それらすべての間にどれだけの水がたまるでしょうか?これも O(n) で計算できますか?
ヒント
1つずつ開いてください。開くたびに少しずつ答えに近づきます。
まず、両端の2本の線から始めます。これらが最も幅の広いコンテナを作ります。どちらかの端を内側に動かすと、幅が1単位減ります。その減少を補える可能性があるのは、どちらの線でしょうか?
水位は短い方の線によって制限されます。より高い方の線を内側に動かすと、その水位の上限は変わらず幅だけが狭くなるため、決して有利にはなりません。短い方の線を置き換える場合にのみ、改善の可能性があります。
両端にポインターを置きます。その間の水の量を測り、最良の値を保持してから、短い線の側のポインターを内側に1つ移動します。ポインターが出会ったら終了します。
解説
線のペアはおよそ n²/2 個あるため、10^4 本の線をすべて調べると、5 × 10^7 回の積の計算が必要です。解決の鍵は、水の量はペアのうち短い方の線だけに左右されるということです。ある線が、その線で作れる最も幅の広い容器の短い側だと分かった時点で、その線を使う、より幅の狭い容器でより多くの水を入れることはできません。2つのポインターを使えば、この事実に基づいて両端から1回走査できます。
すべてのペアを確認してください
正しいが、最大のテストでは終わらない
考え方
すべての容器は、位置 i < j のペアです。水位は低い方の壁を越えてあふれるまで上がり、壁の間の底面の幅は j - i なので、このペアが保持できる水の量は min(height[i], height[j]) × (j - i) です。すべてのペアを試し、最大値を保持すれば、定義上それが答えになります。
問題はペアの数です。n 本の線からは n(n-1)/2 個のペアができます。線が 10^4 本の場合は約 5 × 10^7 個で、リストの長さが 2 倍になるたびにその数は 4 倍になります。コンパイル言語なら一瞬で処理できますが、Python、Ruby、R では何秒もかかり、n が 10^5 に達すると、どの言語でもペアの数が増えすぎてしまいます。
アルゴリズム
bestを 0 に設定します。- 各
iについて、それより後の各jについて、min(height[i], height[j]) × (j - i)を計算します。 bestとその値のうち、大きい方を保持します。bestを返します。
def maxArea(height):
n = len(height)
best = 0
for i in range(n):
for j in range(i + 1, n):
water = min(height[i], height[j]) * (j - i)
best = max(best, water)
return best最も高い行から先に
考え方
容器を、短い方の辺の側から見てみましょう。辺 i が短い方の辺なら、水の量は height[i] と距離の積になり、相手の辺は少なくとも同じ高さであればどれでも構いません。したがって、i が短い方の辺となる容器のうち最良のものは、少なくとも同じ高さの辺の中で最も遠い辺と組み合わせたものです。
そのような相手をすばやく見つけるには、辺を高い順から低い順に並べます。辺 i の番になると、それより前に並べられた辺はすべて少なくとも同じ高さであり、その中で最も遠い辺は、配置済みのインデックスの最左端または最右端にあります。この2つのインデックス lo と hi を記録しておけば、辺 i が保持できる水の量は最大で height[i] × max(i - lo, hi - i) です。最良の容器は短い方の辺が処理されるときに数えられるため、答えはこれらの値の最大値です。
最初の例では、位置1と5にある2つの7が最初に処理され、28の水を保持します。次に位置7の6が処理され、このとき lo = 1、hi = 5 で、6 × 6 = 36の水を保持します。これを上回る短い辺はありません。同じ高さの辺はどの順番で処理しても構いません。同じ高さの2つの辺のうち、後に処理される方は、先に処理された方を相手として見ることになります。
ソートには O(n log n)、走査には O(n) かかり、十分高速です。ただし順序を保持するために O(n) のメモリが必要で、次の方法ではソートもメモリも不要になります。
アルゴリズム
- インデックスを高さの高い順に並べ替えます。
loとhiをその順序の最初のインデックスに設定し、bestを 0 に設定します。- 次の各インデックス
iについて、height[i]にi - loとhi - iの大きい方を掛け、その最大値を保持します。 loとhiを更新してiを含めます。bestを返します。
def maxArea(height):
# Indices from the tallest line to the shortest.
order = sorted(range(len(height)), key=lambda i: height[i], reverse=True)
lo = hi = order[0] # leftmost and rightmost index among the lines placed so far
best = 0
for i in order[1:]:
# Every placed line is at least as tall as line i, so line i is the
# shorter side, and its best partner is the placed line farthest away.
best = max(best, height[i] * max(i - lo, hi - i))
lo = min(lo, i)
hi = max(hi, i)
return best両端からの2つのポインター
考え方
最も幅の広い容器から始めます。left = 0、right = n-1として、その容量を測ります。ここで2本の線のうち1本を取り除けますが、選択肢は決まっています。短いほうを取り除きます。height[left] ≤ height[right]だとします。線leftを使うほかのどの容器も、rightより近くにある線と組み合わせるため、幅が狭く、高さもheight[left]以下です。測定した水の量を上回るものはないので、線leftの役目は終わり、leftを1つ右へ進めます。背の高いほうの線を動かすと、高さの上限はそのままに幅だけが狭くなるため、結果は悪くなる一方です。2本の線の高さが等しい場合は、どちらの線も役目を終えているので、どちらを動かしてもかまいません。
各ステップで1本の線を永久に候補から外すため、n-1ステップ後にはポインターが合流します。最適なペアが見落とされることはありません。その2本の線のどちらかが初めて取り除かれるとき、その時点で測定した容器には、少なくとも同量の水が入ります。
[3, 7, 2, 5, 4, 7, 3, 6]では、位置0と7が3 × 7 = 21を保持します。3のほうが低いため、leftを1へ進めます。位置1と7は6 × 6 = 36を保持し、今度は6のほうが低いため、rightを6へ進めます。その後の容器が保持する水の量は15、28、12、10、2なので、答えは36のままです。
アルゴリズム
left = 0、right = n-1、best = 0を設定します。left < rightの間、min(height[left], height[right]) × (right - left)を計算し、最大値を保持します。height[left] < height[right]の場合、leftを右に1つ進めます。それ以外の場合は、rightを左に1つ進めます。- ポインターが一致したら、
bestを返します。
def maxArea(height):
left, right = 0, len(height) - 1
best = 0
while left < right:
water = min(height[left], height[right]) * (right - left)
best = max(best, water)
# The shorter line cannot do better with any line closer in, so drop it.
if height[left] < height[right]:
left += 1
else:
right -= 1
return best
落とし穴と境界ケース
2ポインターループは短いため、間違いは細部にあります。
- 高い方の線を動かす。最初の例で36ではなく21が返るのは、位置7の6が最初のペアの高い方の線であり、位置1の7と出会う前に離れてしまうからです。
- 高い方の線、または2本の平均を高さとして使う。水は低い方の壁を越えてあふれるため、高さは小さい方です。
- 幅を1つずらしてしまう。位置
iとjの線の間隔はj - iであり、j - i + 1ではありません。そのため、隣り合う2本が保持できる水の量は、低い方の高さに1を掛けた値です。 - 答えは最も高い線、または両端のペアを使うと思い込む。最初の例では、2本の7は28を、両端のペアは21を保持しますが、答えは36です。
- 制約が大きい場合のオーバーフロー。ここでは水の量は10^8未満に収まりますが、高さと長さが10^5に近い場合、積は2^31を超えるため、64ビット整数が必要です。
よくある質問4
Container With Most Water の時間計算量はどれくらいですか?
2ポインター解法は、時間計算量がO(n)、追加の空間計算量がO(1)です。各ステップでポインターの一方を内側に1つ移動するため、ステップ数は最大でn-1です。すべてのペアを確認するとO(n²)かかり、高さで線をソートするとO(n log n)かかります。
短い方の線でポインターを動かすのはなぜですか?
水位は短い方の線によって制限されます。その線を保つほかの容器は、より内側にある相方の線を持つため、幅が狭く、しかも短い方の線より高くはありません。どの容器も測定した容器を上回ることはできないので、答えを失うことなく短い方の線を候補から外せます。
「最大の水を入れるコンテナ」は貪欲法の問題ですか?
はい。各ステップでは、後戻りすることのない局所的な選択を行い、短い方の線を除外します。この選択が安全なのは、そのステップで候補から外されるコンテナが、すでに測定したコンテナより良くないからです。そのため、この問題は貪欲法と二ポインター法の両方に分類されています。
Container With Most Water と Trapping Rain Water はどのように異なりますか?
ここでは選んだ2本の線だけが重要で、その間の線は無視されるため、答えは1つの長方形になります。Trapping Rain Waterではすべての棒が塗りつぶされており、各棒の上には、その両側にある最も高い棒のうち低い方の高さまで水がたまるため、答えはすべての位置での合計になります。どちらにもO(n)の2ポインター解法がありますが、ポインターの規則と合計する対象は異なります。
似た問題
同じ考え方を使う問題です。2〜3問解くとパターンが身につきます。
Python
def maxArea(height):
# ここにコードを書いてくださいケース1
ケース2
入力
height = [3, 7, 2, 5, 4, 7, 3, 6]
期待値
36