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.
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])) # 9Menetelmä 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])) # 3Miksi 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])) # 9Monotonisen 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 mapEdistynyt 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)) # 4Milloin 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 waterPikatarkistus
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ä.
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
- Monotoninen pino: kasvava ja vähenevä
- Histogrammin suurin suorakulmio
- Liukuikkunan maksimi monotonisella dequella
- Trapping Rain Water: pino ja kaksi osoitinta