Persediaan Temu Duga Pengaturcaraan · Pelajaran

Memerangkap Air Hujan: Tindanan dan Dua Penuding

Selesaikan masalah memerangkap air hujan menggunakan pendekatan tindanan monoton yang mengira lapisan mendatar dan pendekatan dua penuding yang mengira lajur menegak.

Pelajaran 4 daripada 413 langkah

Memerangkap Air Hujan: Tindanan dan Dua Penuding ialah pelajaran Persediaan Temu Duga Pengaturcaraan percuma di CoddyKit. Ini ialah pelajaran 4 daripada 4. Anda boleh membaca keseluruhan pelajaran di bawah secara percuma — kemudian berlatih secara praktikal dalam pelayar menggunakan penyunting kod terbina dalam dan tutor kecerdasan buatan 24/7. Pelajaran ini merupakan sebahagian daripada laluan pembelajaran Persediaan Temu Duga Pengaturcaraan, dan kemajuan anda disegerakkan merentas web serta aplikasi CoddyKit. Kursus Persediaan Temu Duga Pengaturcaraan merangkumi sejumlah 4 pelajaran.

Masalah: Memerangkap Air Hujan

Memerangkap Air Hujan (LeetCode 42) ialah salah satu masalah temu duga yang paling ikonik. Diberikan n integer bukan negatif yang mewakili peta ketinggian, dengan setiap batang mempunyai lebar 1, hitung jumlah air yang boleh terperangkap antara batang selepas hujan turun. Air memenuhi mana-mana lembah antara batang yang lebih tinggi pada kedua-dua sisi.

Bagi setiap kedudukan i, paras air ialah min(max_left[i], max_right[i]) - height[i]. Jika nilai ini negatif, tiada air yang terperangkap (batang itu lebih tinggi daripada sekurang-kurangnya satu sempadan). Terdapat tiga pendekatan: tatasusunan prapengiraan O(n)/O(n), dua penuding O(n)/O(1), dan tindanan monotonik 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: Tatasusunan Maksimum Prapengiraan

Penyelesaian mudah dengan masa O(n) dan ruang O(n) ini mengira terlebih dahulu dua tatasusunan: max_left[i] = ketinggian maksimum dari indeks 0 hingga i, dan max_right[i] = ketinggian maksimum dari indeks i hingga n-1. Air pada kedudukan i ialah max(0, min(max_left[i], max_right[i]) - height[i]).

Membina max_left memerlukan satu laluan dari kiri ke kanan; max_right memerlukan satu laluan dari kanan ke kiri. Laluan terakhir menjumlahkan air. Pendekatan ini kemas dan mudah diterangkan, 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 Penuding (Ruang O(1))

Pendekatan dua penuding mencapai masa O(n) dan ruang O(1). Gunakan penuding kiri dan kanan yang bermula pada kedua-dua hujung. Kekalkan max_left dan max_right sebagai nilai maksimum terkumpul yang dilihat setakat ini dari setiap sisi.

Pada setiap langkah, proses sisi dengan nilai maksimum terkumpul yang lebih kecil — kerana sisi itu ialah faktor pembatas. Jika max_left < max_right, air pada penuding kiri ialah max_left - height[left] (sisi kanan cukup tinggi). Gerakkan penuding kiri ke arah tengah. Jika tidak, proses penuding kanan secara simetri. Tiada tatasusunan prapengiraan diperlukan.

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 Penuding Berfungsi: Invarian

Wawasan utama ialah: apabila kita memproses penuding kiri kerana height[left] < height[right], kita tahu bahawa max_right >= height[right] > height[left]. Oleh itu, sempadan air efektif di sebelah kanan sekurang-kurangnya height[right], yang sudah lebih besar daripada max_left. Maka, min(max_left, effective_max_right) = max_left, dan formula air dipermudahkan menjadi max_left - height[left].

Kita tidak perlu mengetahui nilai tepat max_right — memadai untuk mengetahui bahawa nilainya sekurang-kurangnya height[right] > height[left] supaya max_left boleh digunakan sebagai paras air. Invarian yang elegan inilah yang membolehkan 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: Timbunan Monotonik (Lapisan Mendatar)

Pendekatan timbunan monotonik mengira air dalam lapisan mendatar antara batang bersebelahan. Kekalkan timbunan indeks yang menurun secara monotonik. Apabila batang i lebih tinggi daripada bahagian atas timbunan j, sebuah lembah terbentuk: dasarnya ialah height[j], dinding kiri ialah height[stack[-1]] selepas operasi pop pada j, dan dinding kanan ialah height[i]. Air memenuhi lembah sehingga min(left_wall, right_wall) - floor, dengan lebar i - stack[-1] - 1.

Setiap 'lembah' dikira apabila batang yang lebih tinggi ditemui. Ini memproses air dalam segmen segi empat yang terbatas, dan berguna apabila anda juga perlu menjejaki batang yang menyumbang kepada aras 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

Menjejaki Timbunan Monotonik

Marilah kita jejaki [0,1,0,2,1,0,1,3,...] menggunakan pendekatan timbunan. Apabila kita menemui batang 3 (h=2) pada i=3: bahagian atas timbunan ialah i=2 (h=0), lalu lakukan pop terhadapnya. Dinding kiri ialah i=1 (h=1), dinding kanan ialah h=2. Ketinggian air = min(1,2)-0=1, lebar=3-1-1=1, luas=1. Teruskan: bahagian atas timbunan i=1 (h=1) tidak kurang daripada 2, jadi berhenti. Tolak 3 ke dalam timbunan.

Kaedah timbunan lebih rumit untuk dilaksanakan berbanding dua penuding, tetapi menunjukkan batang tertentu yang membentuk setiap sel air. Wawasan ini berguna dalam soalan susulan tentang membina semula susun atur air atau mengira lembah yang berbeza.

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-tiga Pendekatan

Ringkasan tiga pendekatan memerangkap air hujan:

  • Tatasusunan awalan: masa O(n), ruang O(n). Paling mudah difahami dan disahkan. Terbaik untuk temu duga apabila kejelasan lebih diutamakan berbanding kecekapan ruang.
  • Dua penuding: masa O(n), ruang O(1). Optimum dari segi masa dan ruang. Terbaik untuk soalan susulan seperti 'bolehkah anda menggunakan ruang O(1)?'.
  • Timbunan monotonik: masa O(n), ruang O(n). Memproses air dalam lapisan mendatar. Terbaik apabila anda perlu mengetahui batang yang menyumbang atau apabila masalah ini muncul sebagai submasalah dalam algoritma berasaskan timbunan 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}')

Bekas yang Menampung Air Paling Banyak

Bekas yang Menampung Air Paling Banyak (LeetCode 11) sering dikelirukan dengan memerangkap air hujan. Di sini, anda memilih tepat dua batang dan air dibatasi hanya oleh kedua-dua batang itu (batang dalaman tidak penting). Maksimumkan luas min(height[l], height[r]) × (r - l).

Dua penuding menyelesaikannya secara tamak: mulakan pada kedua-dua hujung (lebar maksimum). Gerakkan penuding yang lebih pendek ke arah dalam — menggerakkan penuding yang lebih tinggi hanya boleh mengurangkan luas. Ini mengambil masa O(n) dan ruang O(1), lebih mudah berbanding pendekatan dua penuding untuk memerangkap air hujan kerana maksimum berjalan tidak diperlukan.

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: Memerangkap Air Hujan II (3D)

Memerangkap Air Hujan II (LeetCode 407) mengembangkan masalah ini kepada matriks ketinggian 2D. Air boleh mengalir ke semua empat arah dan mesti keluar melalui sempadan. Penyelesaiannya menggunakan timbunan minimum: mulakan timbunan dengan semua sel sempadan, kemudian lakukan pengembangan seperti BFS. Proses sel yang mempunyai ketinggian paling rendah — mana-mana jiran yang lebih rendah mesti menakung air sekurang-kurangnya pada aras sel semasa.

Ini ialah algoritma yang pada asasnya berbeza daripada kes 1D dan menguji kedua-dua operasi timbunan serta rentasan BFS. Helah dua penuding 1D tidak boleh digeneralisasikan kepada 2D; pendekatan timbunan boleh.

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

Bila Hendak Menggunakan Setiap Kaedah dalam Temu Duga

Panduan membuat keputusan untuk temu duga memerangkap air hujan:

  • Mulakan dengan: tatasusunan awalan — mudah diterangkan, intuitif secara visual, dan jelas kebenarannya
  • Soalan susulan 'ruang O(1)?': dua penuding — terangkan invarian bahawa sisi yang lebih kecil ialah kekangan utama
  • Jika penemu duga bertanya 'pendekatan lain?': timbunan monotonik — terangkan pengiraan lapisan mendatar

Sentiasa mulakan dengan mentakrifkan dengan jelas perkara yang menentukan aras air pada setiap kedudukan (nilai minimum batang tertinggi pada setiap sisi) sebelum beralih kepada kod. Ini menunjukkan pemahaman terhadap masalah dan memudahkan penyelesaian diterangkan.

# 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()

Kes Tepi dan Kesilapan Lazim

Kesilapan lazim ketika memerangkap air hujan:

  • Lupa mengambil nilai minimum: aras air ialah min(max_left, max_right), bukannya hanya salah satu daripadanya. Batang memerlukan dinding yang tinggi pada kedua-dua sisi.
  • Air negatif: gunakan max(0, ...) untuk mengehadkan nilai negatif kepada 0 apabila ketinggian sesuatu kedudukan melebihi aras air.
  • Kedudukan tepi: batang paling kiri dan paling kanan tidak boleh menakung air (tiada dinding pada satu sisi). Pendekatan tatasusunan awalan mengendalikan perkara ini secara semula jadi kerana max_left[0] = height[0] menjadikan air sentiasa 0 pada indeks 0.
  • Tatasusunan kosong atau terlalu kecil: kembalikan 0 untuk tatasusunan yang mempunyai kurang daripada 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

Semakan Pantas

Uji pemahaman anda tentang konsep Struktur Data & Algoritma — Persediaan Temu Duga Pengekodan daripada pelajaran ini.

Ringkasan Pelajaran

Dalam pelajaran ini, anda mempelajari bahawa: memerangkap air hujan diselesaikan dengan mencari nilai minimum dinding kiri dan kanan yang tertinggi pada setiap kedudukan, pendekatan dua penuding dengan ruang O(1) berfungsi kerana maksimum berjalan pada sisi yang lebih rendah sentiasa menjadi kekangan penentu, dan pendekatan timbunan monotonik mengira air dalam lapisan mendatar, yang berguna apabila digabungkan dengan logik berasaskan timbunan yang lain. Seterusnya, kita beralih kepada konsep reka bentuk sistem, bermula dengan rangka kerja RADIO untuk jawapan temu duga yang tersusun.

Percuma untuk bermula

Pelajari Persediaan Temu Duga Pengaturcaraan dengan tutor kecerdasan buatan — percuma

Tulis dan jalankan kod sebenar dalam pelayar anda, dapatkan bantuan segera daripada tutor kecerdasan buatan yang tersedia 24/7, dan sambung semula dari tempat anda berhenti di web atau dalam aplikasi.

Kursus
90
Pelajaran
360

Soalan Lazim

Adakah pelajaran “Memerangkap Air Hujan: Tindanan dan Dua Penuding” percuma?

Ya — teks penuh “Memerangkap Air Hujan: Tindanan dan Dua Penuding” boleh dibaca secara percuma di web ini. Untuk berlatih secara interaktif menggunakan penyunting kod terbina dalam dan tutor kecerdasan buatan 24/7, serta membuka kunci baki kursus Persediaan Temu Duga Pengaturcaraan, tingkat taraf kepada CoddyKit PRO. Kursus Persediaan Temu Duga Pengaturcaraan merangkumi sejumlah 4 pelajaran.

Apakah yang akan saya pelajari dalam “Memerangkap Air Hujan: Tindanan dan Dua Penuding”?

Selesaikan masalah memerangkap air hujan menggunakan pendekatan tindanan monoton yang mengira lapisan mendatar dan pendekatan dua penuding yang mengira lajur menegak. Anda berlatih Persediaan Temu Duga Pengaturcaraan menggunakan kod praktikal yang dijalankan terus dalam pelayar, manakala tutor kecerdasan buatan 24/7 menjawab soalan anda semasa anda mengikuti pelajaran.

Adakah saya memerlukan pengalaman untuk memulakan Persediaan Temu Duga Pengaturcaraan?

Tiada pengalaman terdahulu diperlukan. Pembelajaran Persediaan Temu Duga Pengaturcaraan di CoddyKit disusun untuk pelajar daripada peringkat pemula hingga lanjutan, jadi anda boleh bermula di sini atau dari awal dan belajar mengikut kadar anda sendiri. Ini ialah pelajaran 4 daripada 4.

Berapa lamakah pelajaran “Memerangkap Air Hujan: Tindanan dan Dua Penuding” diambil?

Kebanyakan pelajaran CoddyKit mengambil masa kira-kira 5–10 minit. Setiap pelajaran ringkas dan interaktif, jadi anda boleh membuat kemajuan secara berterusan dan menyambung tepat dari tempat anda berhenti di web atau aplikasi.

Bolehkah saya menulis dan menjalankan kod dalam pelajaran Persediaan Temu Duga Pengaturcaraan ini?

Ya. Setiap pelajaran Persediaan Temu Duga Pengaturcaraan menyertakan penyunting kod terbina dalam, jadi anda boleh menulis dan menjalankan kod sebenar terus dalam pelayar serta menerima maklum balas kecerdasan buatan serta-merta — tanpa memerlukan persediaan setempat.

Semua pelajaran dalam kursus ini

  1. Tindanan Monoton: Menaik berbanding Menurun
  2. Segi Empat Terbesar dalam Histogram
  3. Maksimum Tetingkap Gelongsor dengan Dek Monoton
  4. Memerangkap Air Hujan: Tindanan dan Dua Penuding
← Kembali ke Persediaan Temu Duga Pengaturcaraan