0Pricing
DSA Interview Prep · Pelajaran

Menampung Air Hujan: Tumpukan dan Dua Penunjuk

Selesaikan masalah trapping-rain-water menggunakan pendekatan tumpukan monotonik yang menghitung lapisan horizontal dan pendekatan dua penunjuk yang menghitung kolom vertikal

Menampung Air Hujan: Tumpukan dan Dua Penunjuk adalah pelajaran DSA Interview Prep gratis di CoddyKit. Ini adalah pelajaran 4 dari 4. Kamu bisa membaca pelajaran lengkapnya di bawah secara gratis — lalu praktikkan langsung di browser dengan editor kode bawaan dan tutor AI 24/7. Ini adalah bagian dari jalur belajar DSA Interview Prep, dan progresmu tersinkronisasi di web dan aplikasi CoddyKit. Kursus DSA Interview Prep mencakup 4 pelajaran total.

Soal: Menampung Air Hujan

Menampung Air Hujan (LeetCode 42) adalah salah satu soal wawancara paling ikonis. Diberikan n bilangan bulat nonnegatif yang merepresentasikan peta ketinggian, dengan setiap batang memiliki lebar 1, hitung banyak air yang dapat ditampung di antara batang setelah hujan turun. Air mengisi lembah di antara batang yang lebih tinggi di kedua sisinya.

Untuk setiap posisi i, tinggi airnya adalah min(max_left[i], max_right[i]) - height[i]. Jika hasilnya negatif, tidak ada air yang tertampung (batang tersebut lebih tinggi daripada setidaknya salah satu batas). Tiga pendekatan tersedia: larik yang telah dihitung sebelumnya O(n)/O(n), dua penunjuk O(n)/O(1), dan tumpukan monoton O(n)/O(n).

height = [0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1]
# Water trapped at each position:
# pos 2: min(1,3)-0=1
# pos 4: min(2,3)-1=1
# pos 5: min(2,3)-0=2
# pos 6: min(2,3)-1=1
# pos 9: min(3,2)-1=1
# Total = 6
print('height:', height)
print('Expected trapped water: 6')

# Visualise
max_h = max(height)
for row in range(max_h, 0, -1):
    line = ''
    for h in height:
        line += '#' if h >= row else ' '
    print(line)

Pendekatan 1: Larik Maksimum yang Telah Dihitung Sebelumnya

Solusi langsung dengan waktu O(n) dan ruang O(n) menghitung sebelumnya dua larik: max_left[i] = tinggi maksimum dari indeks 0 hingga i, dan max_right[i] = tinggi maksimum dari indeks i hingga n-1. Air pada posisi i adalah max(0, min(max_left[i], max_right[i]) - height[i]).

Membangun max_left memerlukan satu lintasan dari kiri ke kanan; max_right memerlukan satu lintasan dari kanan ke kiri. Lintasan terakhir menjumlahkan air. Pendekatan ini rapi dan mudah dijelaskan, tetapi menggunakan ruang tambahan O(n).

def trap_prefix(height):
    n = len(height)
    if n < 3:
        return 0

    max_left = [0] * n
    max_right = [0] * n

    max_left[0] = height[0]
    for i in range(1, n):
        max_left[i] = max(max_left[i-1], height[i])

    max_right[-1] = height[-1]
    for i in range(n-2, -1, -1):
        max_right[i] = max(max_right[i+1], height[i])

    water = 0
    for i in range(n):
        water += max(0, min(max_left[i], max_right[i]) - height[i])
    return water

print(trap_prefix([0,1,0,2,1,0,1,3,2,1,2,1]))  # 6
print(trap_prefix([4,2,0,3,2,5]))                # 9

Pendekatan 2: Dua Penunjuk (Ruang O(1))

Pendekatan dua penunjuk mencapai waktu O(n) dan ruang O(1). Gunakan penunjuk kiri dan kanan yang dimulai dari kedua ujung. Pertahankan max_left dan max_right sebagai nilai maksimum berjalan yang sejauh ini terlihat dari masing-masing sisi.

Pada setiap langkah, proses sisi dengan maksimum berjalan yang lebih kecil—karena sisi itu merupakan faktor pembatas. Jika max_left < max_right, air pada penunjuk kiri adalah max_left - height[left] (sisi kanan cukup tinggi). Geser penunjuk kiri ke dalam. Jika tidak, proses penunjuk kanan secara simetris. Tidak diperlukan larik yang telah dihitung sebelumnya.

def trap_two_pointer(height):
    left, right = 0, len(height) - 1
    max_left = max_right = 0
    water = 0

    while left < right:
        if height[left] < height[right]:
            if height[left] >= max_left:
                max_left = height[left]    # new max on the left
            else:
                water += max_left - height[left]  # trapped by max_left
            left += 1
        else:
            if height[right] >= max_right:
                max_right = height[right]
            else:
                water += max_right - height[right]
            right -= 1
    return water

print(trap_two_pointer([0,1,0,2,1,0,1,3,2,1,2,1]))  # 6
print(trap_two_pointer([4,2,0,3,2,5]))                # 9
print(trap_two_pointer([3,0,3]))                      # 3

Mengapa Dua Penunjuk Berhasil: Invariannya

Gagasan utamanya: saat kita memproses penunjuk kiri karena height[left] < height[right], kita tahu bahwa max_right >= height[right] > height[left]. Oleh karena itu, batas air efektif di kanan setidaknya sebesar height[right], yang sudah lebih besar daripada max_left. Jadi min(max_left, effective_max_right) = max_left, dan rumus air menyederhana menjadi max_left - height[left].

Kita tidak perlu mengetahui max_right yang tepat—cukup mengetahui bahwa nilainya setidaknya sebesar height[right] > height[left] untuk menggunakan max_left sebagai tinggi air. Invarian elegan inilah yang memungkinkan penggunaan ruang O(1).

# Trace two-pointer on [4, 2, 0, 3, 2, 5]
height = [4, 2, 0, 3, 2, 5]
left, right = 0, len(height) - 1
max_l = max_r = water = 0
print('height:', height)
print(f'{'Step':5} {'L':3} {'R':3} {'maxL':5} {'maxR':5} {'water':6} {'total':6}')
step = 0
while left < right:
    side = 'L' if height[left] < height[right] else 'R'
    if side == 'L':
        if height[left] >= max_l: max_l = height[left]
        else:
            w = max_l - height[left]; water += w
        left += 1
    else:
        if height[right] >= max_r: max_r = height[right]
        else:
            w = max_r - height[right]; water += w
        right -= 1
    step += 1
    print(f'{step:5} {left:3} {right:3} {max_l:5} {max_r:5} {water:6}')
print('Total trapped:', water)

Pendekatan 3: Tumpukan Monotonik (Lapisan Horizontal)

Pendekatan tumpukan monotonik menghitung air dalam lapisan horizontal di antara batang-batang yang berdekatan. Pertahankan tumpukan indeks yang menurun secara monotonik. Ketika batang i lebih tinggi daripada bagian teratas tumpukan j, terbentuk cekungan: dasarnya adalah height[j], dinding kirinya adalah height[stack[-1]] setelah j dikeluarkan, dan dinding kanannya adalah height[i]. Air mengisi cekungan hingga min(left_wall, right_wall) - floor, dengan lebar i - stack[-1] - 1.

Setiap “cekungan” dihitung ketika batang yang lebih tinggi ditemukan. Cara ini memproses air dalam segmen persegi panjang yang dibatasi, sehingga berguna ketika Anda juga perlu melacak batang mana yang berkontribusi terhadap ketinggian air.

def trap_stack(height):
    stack = []   # monotonic decreasing indices
    water = 0

    for i in range(len(height)):
        while stack and height[stack[-1]] < height[i]:
            bottom_idx = stack.pop()        # the floor of the valley
            if not stack:
                break                       # no left wall, no water
            left_idx = stack[-1]
            floor = height[bottom_idx]
            water_height = min(height[left_idx], height[i]) - floor
            width = i - left_idx - 1
            water += water_height * width
        stack.append(i)
    return water

print(trap_stack([0,1,0,2,1,0,1,3,2,1,2,1]))  # 6
print(trap_stack([4,2,0,3,2,5]))                # 9

Menelusuri Tumpukan Monotonik

Mari kita telusuri [0,1,0,2,1,0,1,3,...] dengan pendekatan tumpukan. Ketika menemukan batang 3 (h=2) pada i=3: bagian teratas tumpukan adalah i=2 (h=0), lalu pop elemen tersebut. Dinding kiri adalah i=1 (h=1), sedangkan dinding kanan adalah h=2. Tinggi air = min(1,2)-0=1, lebar=3-1-1=1, luas=1. Selanjutnya, bagian teratas tumpukan i=1 (h=1) tidak lebih kecil daripada 2, jadi berhenti. Masukkan 3 ke tumpukan.

Metode tumpukan lebih rumit untuk diterapkan daripada metode dua penunjuk, tetapi menunjukkan batang-batang tertentu yang membentuk setiap sel air. Wawasan ini berguna untuk pertanyaan lanjutan tentang merekonstruksi susunan air atau menghitung cekungan yang berbeda.

def trap_stack_trace(height):
    stack = []
    water = 0
    for i in range(len(height)):
        print(f'i={i} h={height[i]}: stack={[height[s] for s in stack]}')
        while stack and height[stack[-1]] < height[i]:
            bot = stack.pop()
            if not stack:
                print(f'  Pop {height[bot]}: no left wall, skip')
                break
            left = stack[-1]
            h = min(height[left], height[i]) - height[bot]
            w = i - left - 1
            water += h * w
            print(f'  Pop {height[bot]}: floor={height[bot]}, left_wall={height[left]}, right_wall={height[i]}, h={h}, w={w}, +{h*w}')
        stack.append(i)
    return water

result = trap_stack_trace([0,1,0,2,1,0,1,3,2,1,2,1])
print('Total:', result)

Membandingkan Ketiga Pendekatan

Ringkasan tiga pendekatan penampungan air hujan:

  • Larik prefiks: waktu O(n), ruang O(n). Paling mudah dipahami dan diverifikasi. Cocok untuk wawancara yang mengutamakan kejelasan daripada efisiensi ruang.
  • Dua penunjuk: waktu O(n), ruang O(1). Optimal dalam waktu dan ruang. Cocok untuk pertanyaan lanjutan “dapatkah Anda menggunakan ruang O(1)?”.
  • Tumpukan monotonik: waktu O(n), ruang O(n). Memproses air dalam lapisan horizontal. Cocok ketika Anda perlu mengetahui batang mana yang berkontribusi atau ketika masalah ini muncul sebagai submasalah dalam algoritma berbasis tumpukan yang lebih besar.
height = [0,1,0,2,1,0,1,3,2,1,2,1]

# All three methods — verify they agree
def trap_prefix(h):
    n = len(h)
    ml = [0]*n; mr = [0]*n; ml[0]=h[0]; mr[-1]=h[-1]
    for i in range(1,n): ml[i]=max(ml[i-1],h[i])
    for i in range(n-2,-1,-1): mr[i]=max(mr[i+1],h[i])
    return sum(max(0,min(ml[i],mr[i])-h[i]) for i in range(n))

def trap_two_ptr(h):
    l,r,ml,mr,w = 0,len(h)-1,0,0,0
    while l<r:
        if h[l]<h[r]:
            ml=max(ml,h[l]); w+=ml-h[l]; l+=1
        else:
            mr=max(mr,h[r]); w+=mr-h[r]; r-=1
    return w

def trap_stk(h):
    stk,w = [],[]
    for i in range(len(h)):
        while stk and h[stk[-1]]<h[i]:
            b=stk.pop()
            if not stk: break
            w.append(max(0,min(h[stk[-1]],h[i])-h[b])*(i-stk[-1]-1))
        stk.append(i)
    return sum(w)

for h in [height, [4,2,0,3,2,5], [3,0,3], [1,0,1]]:
    p=trap_prefix(h); t=trap_two_ptr(h); s=trap_stk(h)
    print(f'{h}: prefix={p}, two-ptr={t}, stack={s}, match={p==t==s}')

Wadah dengan Air Terbanyak

Wadah dengan Air Terbanyak (LeetCode 11) sering tertukar dengan penampungan air hujan. Di sini, Anda memilih tepat dua batang dan air hanya dibatasi oleh kedua batang tersebut (batang di antaranya tidak berpengaruh). Maksimalkan luas min(height[l], height[r]) × (r - l).

Dua penunjuk menyelesaikannya dengan pendekatan tamak: mulai dari kedua ujung (lebar maksimum). Gerakkan penunjuk yang lebih pendek ke dalam—menggerakkan penunjuk yang lebih tinggi hanya dapat mengurangi luas. Cara ini membutuhkan waktu O(n) dan ruang O(1), serta lebih sederhana daripada metode dua penunjuk untuk penampungan air hujan karena tidak memerlukan nilai maksimum berjalan.

def max_water_container(height):
    left, right = 0, len(height) - 1
    max_area = 0

    while left < right:
        area = min(height[left], height[right]) * (right - left)
        max_area = max(max_area, area)
        # Move the shorter bar: moving taller bar can only reduce min
        if height[left] < height[right]:
            left += 1
        else:
            right -= 1
    return max_area

print(max_water_container([1,8,6,2,5,4,8,3,7]))  # 49: bars 8 and 7
print(max_water_container([1,1]))                  # 1
print(max_water_container([4,3,2,1,4]))            # 16

# Key difference from trapping rain water:
# Container: choose 2 bars, water fills freely between them (no internal barriers)
# Trapping:  water fills ALL valleys in the full elevation map

Lanjutan: Penampungan Air Hujan II (3D)

Penampungan Air Hujan II (LeetCode 407) memperluas masalah ini menjadi matriks tinggi 2D. Air dapat mengalir ke keempat arah dan harus keluar melalui tepi. Solusinya menggunakan tumpukan minimum: inisialisasi tumpukan dengan semua sel tepi, lalu lakukan perluasan mirip BFS. Proses sel dengan tinggi terkecil—setiap tetangga yang lebih rendah harus menampung air setidaknya setinggi level sel saat ini.

Ini adalah algoritma yang secara mendasar berbeda dari kasus 1D dan menguji operasi tumpukan serta penelusuran BFS. Trik dua penunjuk untuk kasus 1D tidak dapat digeneralisasi ke 2D, sedangkan pendekatan tumpukan dapat.

import heapq

def trap_rain_water_2d(heightMap):
    if not heightMap or not heightMap[0]:
        return 0
    m, n = len(heightMap), len(heightMap[0])
    visited = [[False]*n for _ in range(m)]
    heap = []  # (height, row, col)

    # Add all border cells to the heap
    for i in range(m):
        for j in [0, n-1]:
            heapq.heappush(heap, (heightMap[i][j], i, j))
            visited[i][j] = True
    for j in range(n):
        for i in [0, m-1]:
            if not visited[i][j]:
                heapq.heappush(heap, (heightMap[i][j], i, j))
                visited[i][j] = True

    total = 0
    max_h = 0
    while heap:
        h, r, c = heapq.heappop(heap)
        max_h = max(max_h, h)
        for dr, dc in [(-1,0),(1,0),(0,-1),(0,1)]:
            nr, nc = r+dr, c+dc
            if 0<=nr<m and 0<=nc<n and not visited[nr][nc]:
                visited[nr][nc] = True
                total += max(0, max_h - heightMap[nr][nc])
                heapq.heappush(heap, (max(max_h, heightMap[nr][nc]), nr, nc))
    return total

map2d = [[1,4,3,1,3,2],[3,2,1,3,2,4],[2,3,3,2,3,1]]
print(trap_rain_water_2d(map2d))  # 4

Kapan Menggunakan Setiap Metode dalam Wawancara

Panduan pengambilan keputusan untuk wawancara penampungan air hujan:

  • Mulai dengan: larik prefiks—mudah dijelaskan, intuitif secara visual, dan jelas kebenarannya
  • Pertanyaan lanjutan “ruang O(1)?”: dua penunjuk—jelaskan invarian bahwa sisi yang lebih rendah adalah titik hambatnya
  • Jika pewawancara bertanya “pendekatan lain?”: tumpukan monotonik—jelaskan perhitungan lapisan horizontal

Selalu mulai dengan mendefinisikan secara jelas hal yang menentukan ketinggian air di setiap posisi (nilai minimum dari batang tertinggi di setiap sisi) sebelum beralih ke kode. Ini menunjukkan pemahaman terhadap masalah dan membuat solusi lebih mudah dijelaskan.

# Quick summary of all three approaches
approaches = [
    {
        'name': 'Prefix max arrays',
        'time': 'O(n)', 'space': 'O(n)',
        'description': '3 passes: build max_left, max_right, sum water column-by-column',
    },
    {
        'name': 'Two pointers',
        'time': 'O(n)', 'space': 'O(1)',
        'description': 'Process smaller side: its max is the limiting wall, no array needed',
    },
    {
        'name': 'Monotonic stack',
        'time': 'O(n)', 'space': 'O(n)',
        'description': 'Compute water in horizontal layers when a taller bar is encountered',
    },
]
for a in approaches:
    print(f'{a["name"]} [{a["time"]} / {a["space"]}]')
    print(f'  {a["description"]}')
    print()

Kasus Tepi dan Kesalahan Umum

Kesalahan umum dalam penampungan air hujan:

  • Lupa menggunakan min: ketinggian air adalah min(max_left, max_right), bukan hanya salah satunya. Sebuah batang membutuhkan dinding tinggi di kedua sisinya.
  • Air bernilai negatif: gunakan max(0, ...) untuk membatasi nilai negatif menjadi 0 ketika tinggi suatu posisi melebihi ketinggian air.
  • Posisi tepi: batang paling kiri dan paling kanan tidak pernah dapat menampung air (tidak memiliki dinding di salah satu sisi). Pendekatan larik prefiks menangani hal ini secara alami karena max_left[0] = height[0] membuat air selalu 0 pada indeks 0.
  • Larik kosong atau sangat kecil: kembalikan 0 untuk larik yang memiliki kurang dari 3 elemen.
def trap(height):
    n = len(height)
    if n < 3:
        return 0   # need at least 3 bars to trap anything

    left, right = 0, n - 1
    max_l = max_r = water = 0
    while left < right:
        if height[left] <= height[right]:
            if height[left] >= max_l:
                max_l = height[left]
            else:
                water += max_l - height[left]  # never negative: max_l > height[left]
            left += 1
        else:
            if height[right] >= max_r:
                max_r = height[right]
            else:
                water += max_r - height[right]
            right -= 1
    return water

# Edge cases
print(trap([]))          # 0: empty
print(trap([1]))         # 0: single bar
print(trap([1,2]))       # 0: two bars
print(trap([3,0,3]))     # 3: simple valley
print(trap([3,3,3]))     # 0: flat top, no water

Pemeriksaan Singkat

Uji pemahaman Anda tentang konsep Struktur Data & Algoritma — Persiapan Wawancara Pemrograman dari pelajaran ini.

Rangkuman Pelajaran

Dalam pelajaran ini Anda mempelajari bahwa: penampungan air hujan diselesaikan dengan menemukan nilai minimum dari dinding kiri dan kanan tertinggi di setiap posisi, pendekatan dua penunjuk dengan ruang O(1) berhasil karena nilai maksimum berjalan di sisi yang lebih rendah selalu menjadi batasan penentu, dan pendekatan tumpukan monotonik menghitung air dalam lapisan horizontal, sehingga berguna ketika digabungkan dengan logika berbasis tumpukan lainnya. Selanjutnya kita beralih ke konsep desain sistem, dimulai dengan kerangka kerja RADIO untuk jawaban wawancara yang terstruktur.

Pertanyaan yang Sering Diajukan

Apakah pelajaran “Menampung Air Hujan: Tumpukan dan Dua Penunjuk” gratis?

Ya — teks lengkap “Menampung Air Hujan: Tumpukan dan Dua Penunjuk” gratis dibaca di sini di web. Untuk praktiknya secara interaktif (editor kode bawaan dan tutor AI 24/7) dan buka sisa kursus DSA Interview Prep, upgrade ke CoddyKit PRO. Kursus DSA Interview Prep mencakup 4 pelajaran total.

Apa yang akan aku pelajari di “Menampung Air Hujan: Tumpukan dan Dua Penunjuk”?

Selesaikan masalah trapping-rain-water menggunakan pendekatan tumpukan monotonik yang menghitung lapisan horizontal dan pendekatan dua penunjuk yang menghitung kolom vertikal Kamu berlatih DSA Interview Prep dengan kode praktik yang langsung kamu jalankan di browser, dan tutor AI 24/7 menjawab pertanyaanmu saat kamu mengerjakan pelajaran ini.

Apakah aku perlu pengalaman untuk memulai DSA Interview Prep?

Tidak diperlukan pengalaman sebelumnya. DSA Interview Prep di CoddyKit dirancang untuk pemula hingga pelajar tingkat lanjut, jadi kamu bisa memulai di sini atau dari awal dan belajar sesuai kecepatan kamu sendiri. Ini adalah pelajaran 4 dari 4.

Berapa lama pelajaran “Menampung Air Hujan: Tumpukan dan Dua Penunjuk” memakan waktu?

Sebagian besar pelajaran CoddyKit memakan waktu sekitar 5–10 menit. Setiap pelajaran ringkas dan interaktif, jadi kamu membuat kemajuan stabil dan melanjutkan dari tempat kamu tinggalkan di web dan aplikasi.

Bisakah aku menulis dan menjalankan kode dalam pelajaran DSA Interview Prep ini?

Ya. Setiap pelajaran DSA Interview Prep menyertakan editor kode bawaan, jadi kamu menulis dan menjalankan kode nyata langsung di browser dan mendapatkan umpan balik AI instan — tidak diperlukan penyiapan lokal.

Semua pelajaran dalam kursus ini

  1. Tumpukan Monotonik: Menaik vs Menurun
  2. Persegi Panjang Terbesar dalam Histogram
  3. Maksimum Jendela Geser dengan Deque Monotonik
  4. Menampung Air Hujan: Tumpukan dan Dua Penunjuk
← Kembali ke DSA Interview Prep