DSA Interview Prep · Oppitunti

Trapping Rain Water: pino ja kaksi osoitinta

Ratkaiskaa trapping-rain-water sekä monotonisella pinomenetelmällä, joka laskee vaakasuuntaiset kerrokset, että kahden osoittimen menetelmällä, joka laskee pystysuuntaiset sarakkeet.

Oppitunti 4/413 vaihetta

Trapping Rain Water: pino ja kaksi osoitinta on ilmainen DSA Interview Prep-oppitunti CoddyKitissä. Tämä on oppitunti 4/4. Voit lukea tästä oppimispolusta kokonaan mitkä tahansa 3 oppituntia ilmaiseksi — sen jälkeen CoddyKit PRO avaa kaikki oppitunnit sekä käytännön harjoittelun sisäänrakennetulla koodieditorilla ja ympäri vuorokauden toimivalla tekoälytuutorilla. Oppitunti kuuluu DSA Interview Prep-oppimispolkuun, ja edistymisesi synkronoituu verkon ja CoddyKit-sovelluksen välillä. DSA Interview Prep-kurssilla on yhteensä 4 oppituntia.

Ongelma: sadeveden kerääminen

Sadeveden kerääminen (LeetCode 42) on yksi tunnetuimmista haastatteluongelmista. Annettuna on n ei-negatiivista kokonaislukua, jotka kuvaavat korkeusprofiilia, jossa kunkin palkin leveys on 1. Tehtävänä on laskea, kuinka paljon vettä palkkien väliin voi jäädä sateen jälkeen. Vesi täyttää korkeampien palkkien väliin jäävät painanteet.

Jokaisessa kohdassa i vedenpinnan korkeus on min(max_left[i], max_right[i]) - height[i]. Jos tulos on negatiivinen, vettä ei jää, koska palkki on vähintään yhtä korkea kuin toinen rajoista. Mahdollisia menetelmiä on kolme: esilasketut taulukot O(n)/O(n), kaksi osoitinta O(n)/O(1) ja monotoninen pino 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)

Menetelmä 1: Esilasketut maksimiarvotaulukot

Suoraviivaisessa O(n)-aikaisessa ja O(n)-tilaa käyttävässä ratkaisussa lasketaan etukäteen kaksi taulukkoa: max_left[i] = suurin korkeus indeksien 0–i välillä ja max_right[i] = suurin korkeus indeksien i–n-1 välillä. Kohdassa i olevan veden määrä on max(0, min(max_left[i], max_right[i]) - height[i]).

max_left rakennetaan yhdellä vasemmalta oikealle etenevällä läpikäynnillä ja max_right oikealta vasemmalle etenevällä läpikäynnillä. Lopuksi veden määrä summataan vielä yhdellä läpikäynnillä. Menetelmä on selkeä ja helppo selittää, mutta se käyttää O(n) ylimääräistä tilaa.

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

Menetelmä 2: Kaksi osoitinta (O(1) lisämuisti)

Kahden osoittimen menetelmällä saavutetaan O(n)-aikavaativuus ja O(1) lisämuisti. Osoittimet asetetaan aluksi taulukon molempiin päihin. Ylläpidetään muuttujia max_left ja max_right, jotka sisältävät kummastakin suunnasta tähän mennessä nähdyn suurimman arvon.

Kullakin kierroksella käsitellään se puoli, jonka tähänastinen maksimiarvo on pienempi — koska tämä puoli rajoittaa veden korkeutta. Jos max_left < max_right, vasemman osoittimen kohdalla oleva vesimäärä on max_left - height[left] (oikea puoli on riittävän korkea). Vasen osoitin siirretään kohti keskustaa. Muussa tapauksessa käsitellään oikea osoitin vastaavalla tavalla. Esilaskettuja taulukoita ei tarvita.

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

Miksi kaksi osoitinta toimii: invariantti

Keskeinen havainto on seuraava: kun vasenta osoitinta käsitellään ehdon height[left] < height[right] vuoksi, tiedetään, että max_right >= height[right] > height[left]. Oikean puolen tehokas vesiraja on siis vähintään height[right], joka on jo suurempi kuin max_left. Näin ollen min(max_left, effective_max_right) = max_left, ja vesimäärän kaava yksinkertaistuu muotoon max_left - height[left].

Tarkkaa arvoa max_right ei tarvitse tietää — riittää, että tiedetään sen olevan vähintään height[right] > height[left]. Tällöin max_left voidaan valita vedenpinnan korkeudeksi. Tämä elegantti invariantti mahdollistaa O(1)-tilan käytön.

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

Lähestymistapa 3: monotoninen pino (vaakasuuntaiset kerrokset)

Monotonisen pinon lähestymistapa laskee veden vaakasuunnassa olevina kerroksina vierekkäisten pylväiden välissä. Ylläpidetään indeksien monotonisesti laskevaa pinoa. Kun pylväs i on pinon huipulla olevan pylvään j korkeampi, muodostuu syvennys: pohja on height[j], vasen seinämä on j:n poistamisen jälkeen height[stack[-1]] ja oikea seinämä on height[i]. Vesi täyttää syvennystä korkeuteen min(left_wall, right_wall) - floor asti, ja leveys on i - stack[-1] - 1.

Jokainen syvennys lasketaan, kun kohdataan korkeampi pylväs. Näin vesi käsitellään rajattuina suorakulmaisina alueina, mikä on hyödyllistä silloin, kun on lisäksi seurattava, mitkä pylväät vaikuttavat veden korkeuteen.

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

Monotonisen pinon jäljittäminen

Tarkastellaan pinomenetelmällä tapausta [0,1,0,2,1,0,1,3,...]. Kun kohdataan pylväs 3 (h=2) kohdassa i=3: pinon huipulla on i=2 (h=0), joten se poistetaan. Vasen seinämä on i=1 (h=1) ja oikea seinämä on h=2. Veden korkeus = min(1,2)-0=1, leveys=3-1-1=1 ja pinta-ala=1. Jatketaan: pinon huipulla oleva i=1 (h=1) ei ole pienempi kuin 2, joten lopetetaan. Lisätään 3 pinoon.

Pinomenetelmä on toteutukseltaan monimutkaisempi kuin kahden osoittimen menetelmä, mutta se paljastaa, mitkä yksittäiset pylväät muodostavat kunkin vesialueen. Tästä on hyötyä jatkokysymyksissä, joissa pyydetään rekonstruoimaan veden asettelu tai laskemaan erillisten syvennysten määrä.

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)

Kaikkien kolmen lähestymistavan vertailu

Yhteenveto kolmesta sadeveden keräämisen lähestymistavasta:

  • Prefiksitaulukot: O(n) aikaa, O(n) tilaa. Helpoin ymmärtää ja varmentaa. Paras työhaastatteluissa, joissa selkeys on tilankäytön tehokkuutta tärkeämpää.
  • Kaksi osoitinta: O(n) aikaa, O(1) tilaa. Optimaalinen sekä ajan että tilan suhteen. Paras jatkokysymyksiin, joissa kysytään, voiko ratkaisun toteuttaa O(1) tilassa.
  • Monotoninen pino: O(n) aikaa, O(n) tilaa. Käsittelee veden vaakasuuntaisina kerroksina. Paras, kun on tiedettävä, mitkä pylväät vaikuttavat veden määrään tai kun ongelma esiintyy suuremman pinopohjaisen algoritmin osana.
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}')

Eniten vettä sisältävä säiliö

Eniten vettä sisältävä säiliö (LeetCode 11) sekoitetaan usein sadeveden keräämiseen. Tässä valitaan täsmälleen kaksi pylvästä, ja vesi rajautuu vain näiden kahden pylvään väliin; sisäpuolisilla pylväillä ei ole merkitystä. Pinta-ala maksimoidaan kaavalla min(height[l], height[r]) × (r - l).

Kaksi osoitinta ratkaisee ongelman ahneesti: aloitetaan molemmista päistä, jolloin leveys on suurin. Lyhyempää osoitinta siirretään sisäänpäin — korkeamman siirtäminen voi vain pienentää pinta-alaa. Aikavaativuus on O(n) ja tilavaativuus O(1), joten menetelmä on sadeveden keräämisen kahden osoittimen menetelmää yksinkertaisempi, koska juoksevaa maksimia ei tarvita.

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

Edistynyt aihe: Trapping Rain Water II (3D)

Sadeveden kerääminen II (LeetCode 407) laajentaa ongelman kaksiulotteiseen korkeusmatriisiin. Vesi voi virrata kaikkiin neljään suuntaan, ja sen on päästävä pois reunan yli. Ratkaisussa käytetään minimikekoa: kekoon lisätään aluksi kaikki reunasolut, minkä jälkeen suoritetaan BFS-tyyppinen laajennus. Käsitellään korkeudeltaan pienin solu — jokaisen sitä matalamman naapurin on pidätettävä vettä vähintään nykyisen solun korkeudelle asti.

Tämä on perustavanlaatuisesti erilainen algoritmi kuin yksiulotteisessa tapauksessa, ja siinä testataan sekä kekotoimintojen että BFS-läpikäynnin hallintaa. Yksiulotteinen kahden osoittimen menetelmä ei yleisty kaksiulotteiseksi, mutta kekomenetelmä yleistyy.

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

Milloin kutakin menetelmää käytetään työhaastatteluissa

Päätösohje sadeveden keräämistä käsittelevään työhaastattelutehtävään:

  • Aloittakaa seuraavasti: prefiksitaulukot — helppo selittää, visuaalisesti havainnollinen ja selvästi oikeaksi osoitettava
  • Jatkokysymys O(1)-tilasta: kaksi osoitinta — selittäkää invariantti, jonka mukaan pienempi puoli toimii pullonkaulana
  • Jos haastattelija kysyy toisesta lähestymistavasta: monotoninen pino — selittäkää veden laskeminen vaakasuuntaisina kerroksina

Määritelkää aina ensin selkeästi, mikä määrää veden korkeuden kussakin kohdassa: kummankin puolen korkeimpien pylväiden korkeuksista pienempi. Näin osoitatte ymmärtävänne ongelman ja teette ratkaisun selittämisestä helpompaa.

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

Reunatapaukset ja yleiset virheet

Yleisiä virheitä sadeveden keräämisessä:

  • Minimiarvon unohtaminen: veden korkeus on min(max_left, max_right), ei vain jompikumpi arvoista. Pylvään molemmilla puolilla on oltava korkeat seinämät.
  • Negatiivinen vesimäärä: negatiiviset arvot rajataan nollaan käyttämällä max(0, ...), kun kohdan korkeus ylittää veden korkeuden.
  • Reunakohdat: vasemman- ja oikeanpuoleisimmat pylväät eivät voi koskaan pidättää vettä, koska toisella puolella ei ole seinämää. Prefiksitaulukkomenetelmä käsittelee tämän luonnostaan, sillä max_left[0] = height[0] tekee veden määräksi aina 0 indeksissä 0.
  • Tyhjät tai hyvin pienet taulukot: palautetaan 0 taulukoille, joissa on alle 3 alkiota.
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

Pikatarkistus

Testatkaa tämän oppitunnin Data Structures & Algorithms — Coding Interview Prep -käsitteiden ymmärtämistä.

Oppitunnin kertaus

Tässä oppitunnissa opittiin, että sadeveden kerääminen ratkaistaan etsimällä kussakin kohdassa vasemman ja oikean puolen korkeimpien seinämien pienempi korkeus, että O(1)-tilaa käyttävä kahden osoittimen lähestymistapa toimii, koska pienemmän puolen juokseva maksimi on aina rajoittava tekijä ja että monotoninen pinomenetelmä laskee veden vaakasuuntaisina kerroksina, mikä on hyödyllistä yhdistettynä muuhun pinopohjaiseen logiikkaan. Seuraavaksi siirrytään järjestelmäsuunnittelun käsitteisiin ja aloitetaan jäsenneltyihin haastatteluvastauksiin tarkoitetusta RADIO-kehyksestä.

Aloita maksutta

Opi Python tekoälytuutorin avulla — ilmaiseksi

Kirjoita ja suorita oikeaa koodia selaimessa, saa välitöntä apua tekoälytuutorilta ympäri vuorokauden ja jatka siitä, mihin jäit, verkossa tai sovelluksessa.

Kurssit
30
Oppitunnit
120

Usein kysytyt kysymykset

Onko oppitunti ”Trapping Rain Water: pino ja kaksi osoitinta” ilmainen?

Kyllä — voit lukea täällä verkossa kokonaan ilmaiseksi mitkä tahansa DSA Interview Prep-oppimispolun 3 oppituntia, myös oppitunnin “Trapping Rain Water: pino ja kaksi osoitinta”. Sen jälkeen CoddyKit PRO avaa kaikki oppitunnit sekä interaktiiviset harjoitukset sisäänrakennetulla koodieditorilla ja ympäri vuorokauden toimivalla tekoälytuutorilla. DSA Interview Prep-kurssilla on yhteensä 4 oppituntia.

Mitä opin oppitunnilla ”Trapping Rain Water: pino ja kaksi osoitinta”?

Ratkaiskaa trapping-rain-water sekä monotonisella pinomenetelmällä, joka laskee vaakasuuntaiset kerrokset, että kahden osoittimen menetelmällä, joka laskee pystysuuntaiset sarakkeet. Harjoittelet DSA Interview Prep-aihetta koodilla, jonka suoritat suoraan selaimessa. Ympäri vuorokauden käytettävissä oleva tekoälytuutori vastaa kysymyksiisi oppitunnin aikana.

Tarvitsenko kokemusta aloittaakseni DSA Interview Prep-opiskelun?

Aiempi kokemus ei ole tarpeen. CoddyKitin DSA Interview Prep-oppimispolku sopii vasta-alkajista edistyneisiin, joten voit aloittaa tästä tai alusta ja edetä omaan tahtiisi. Tämä on oppitunti 4/4.

Kuinka kauan ”Trapping Rain Water: pino ja kaksi osoitinta”-oppitunnin suorittaminen kestää?

Useimmat CoddyKitin oppitunnit kestävät noin 5–10 minuuttia. Jokainen oppitunti on lyhyt ja interaktiivinen, joten edistyt tasaisesti ja voit jatkaa siitä, mihin jäit – sekä verkossa että sovelluksessa.

Voinko kirjoittaa ja suorittaa koodia tällä DSA Interview Prep-oppitunnilla?

Kyllä. Jokainen DSA Interview Prep-oppitunti sisältää sisäänrakennetun koodieditorin, joten voit kirjoittaa ja suorittaa oikeaa koodia suoraan selaimessa ja saada välitöntä palautetta tekoälyltä – paikallista asennusta ei tarvita.

Kaikki tämän kurssin oppitunnit

  1. Monotoninen pino: kasvava ja vähenevä
  2. Histogrammin suurin suorakulmio
  3. Liukuikkunan maksimi monotonisella dequella
  4. Trapping Rain Water: pino ja kaksi osoitinta
← Takaisin: DSA Interview Prep