احتجاز مياه الأمطار: المكدس والمؤشران
حلّ مسألة trapping-rain-water باستخدام نهج المكدس الرتيب الذي يحسب الطبقات الأفقية، ونهج المؤشرين الذي يحسب الأعمدة الرأسية.
احتجاز مياه الأمطار: المكدس والمؤشران درس مجاني في DSA Interview Prep على CoddyKit. هذا هو الدرس 4 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في DSA Interview Prep، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة DSA Interview Prep 4 دروس في المجموع.
المسألة: احتجاز مياه الأمطار
تُعد مسألة Trapping Rain Water (LeetCode 42) من أكثر مسائل المقابلات شهرة. مع إعطائك n من الأعداد الصحيحة غير السالبة التي تمثل خريطة ارتفاعات، حيث يبلغ عرض كل عمود 1، احسب كمية المياه التي يمكن احتجازها بين الأعمدة بعد هطول المطر. تمتلئ أي منطقة منخفضة بين عمودين أعلى منها على الجانبين بالماء.
لكل موضع i، يكون مستوى الماء هو min(max_left[i], max_right[i]) - height[i]. وإذا كانت هذه القيمة سالبة، فلا تُحتجز أي مياه (فالعمود أعلى من أحد الحدّين على الأقل). توجد ثلاث طرق: مصفوفات القيم العظمى المحسوبة مسبقًا O(n)/O(n)، والمؤشران O(n)/O(1)، والمكدس الرتيب 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)الطريقة 1: مصفوفات القيم العظمى المحسوبة مسبقًا
تحسب الطريقة المباشرة، ذات التعقيد الزمني O(n) والمساحة O(n)، مصفوفتين مسبقًا: max_left[i] = أكبر ارتفاع من الفهرس 0 إلى i، وmax_right[i] = أكبر ارتفاع من الفهرس i إلى n-1. وتكون كمية الماء في الموضع i هي max(0, min(max_left[i], max_right[i]) - height[i]).
يتطلب إنشاء max_left مرورًا واحدًا من اليسار إلى اليمين، بينما يتطلب إنشاء max_right مرورًا من اليمين إلى اليسار. ثم يجمع مرور نهائي كمية الماء. هذه الطريقة واضحة وسهلة الشرح، لكنها تستخدم مساحة إضافية قدرها 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الطريقة 2: المؤشران (مساحة O(1))
تحقق طريقة المؤشرين تعقيدًا زمنيًا قدره O(n) ومساحة O(1). استخدم مؤشرين يسارًا ويمينًا يبدأ كل منهما من أحد الطرفين. وحافظ على max_left وmax_right بوصفهما القيمتين العظميين المتراكمتين اللتين شوهدتا حتى الآن من كل جانب.
في كل خطوة، عالج الجانب الذي يملك القيمة العظمى المتراكمة الأصغر — لأن هذا الجانب هو العامل المحدِّد. إذا كان max_left < max_right، فإن كمية الماء عند المؤشر الأيسر هي max_left - height[left] (فالجانب الأيمن مرتفع بما يكفي). حرّك المؤشر الأيسر إلى الداخل. وإلا فعالج المؤشر الأيمن بالطريقة المتماثلة. ولا حاجة إلى مصفوفات محسوبة مسبقًا.
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لماذا تنجح طريقة المؤشرين: الثابت
الفكرة الأساسية هي: عندما نعالج المؤشر الأيسر لأن height[left] < height[right]، نعلم أن max_right >= height[right] > height[left]. ولذلك فإن الحد الفعلي للماء على اليمين لا يقل عن height[right]، وهو أكبر أصلًا من max_left. ومن ثم فإن min(max_left, effective_max_right) = max_left، وتتبسّط صيغة الماء إلى max_left - height[left].
لا نحتاج إلى معرفة قيمة max_right الدقيقة؛ فمجرد معرفة أنها لا تقل عن height[right] > height[left] يكفي لاستخدام max_left مستوىً للماء. وهذا هو الثابت الأنيق الذي يجعل استخدام مساحة 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)النهج 3: المكدس الرتيب (الطبقات الأفقية)
يحسب نهج المكدس الرتيب الماء على طبقات أفقية بين الأعمدة المتجاورة. حافظ على مكدس رتيب تنازلي للفهرسة. عندما يكون العمود i أطول من قمة المكدس j، يتكوّن منخفض: يكون القاع height[j]، ويكون الجدار الأيسر height[stack[-1]] بعد إخراج j، والجدار الأيمن height[i]. يملأ الماء المنخفض حتى min(left_wall, right_wall) - floor، بعرض i - stack[-1] - 1.
يُحسَب كل «منخفض» عند مصادفة عمود أطول. يعالج هذا الماء ضمن مقاطع مستطيلة محدودة، وهو أمر مفيد عندما تحتاج أيضًا إلى تتبّع الأعمدة التي تسهم في مستوى الماء.
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تتبّع المكدس الرتيب
لنتتبّع [0,1,0,2,1,0,1,3,...] باستخدام نهج المكدس. عندما نصل إلى العمود 3 (h=2) عند i=3: تكون قمة المكدس هي i=2 (h=0)، فنُخرجه. الجدار الأيسر هو i=1 (h=1)، والجدار الأيمن هو h=2. ارتفاع الماء = min(1,2)-0=1، والعرض=3-1-1=1، والمساحة=1. نتابع: قمة المكدس i=1 (h=1) ليست أقل من 2، لذا نتوقف. نضيف 3 إلى المكدس.
طريقة المكدس أكثر تعقيدًا في التنفيذ من المؤشرين، لكنها توضّح أي أعمدة محددة تشكّل كل خلية مائية. وتفيد هذه الرؤية في الأسئلة اللاحقة التي تتناول إعادة بناء توزيع الماء أو عدّ المنخفضات المختلفة.
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)مقارنة الأساليب الثلاثة
ملخّص أساليب احتجاز مياه الأمطار الثلاثة:
- مصفوفات البادئات: زمن O(n)، ومساحة O(n). الأسهل فهمًا والتحقق من صحته. الأفضل في المقابلات التي تُقدّر وضوح الحل أكثر من كفاءة استخدام المساحة.
- المؤشران: زمن O(n)، ومساحة O(1). الحل الأمثل من حيث الزمن والمساحة. الأفضل للأسئلة اللاحقة من نوع «هل يمكنك تنفيذ ذلك بمساحة O(1)؟».
- المكدس الرتيب: زمن O(n)، ومساحة O(n). يعالج الماء ضمن طبقات أفقية. الأفضل عندما تحتاج إلى معرفة الأعمدة التي تسهم في الماء، أو عندما تظهر هذه المسألة كمسألة فرعية ضمن خوارزمية أكبر تعتمد على المكدس.
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}')الحاوية ذات أكبر كمية من الماء
Container With Most Water (LeetCode 11) كثيرًا ما يختلط أمرها بمسألة احتجاز مياه الأمطار. هنا تختار عمودين بالضبط، ويُحصر الماء بين هذين العمودين فقط، ولا تؤثر الأعمدة الداخلية. احسب أكبر مساحة وفقًا للمعادلة min(height[l], height[r]) × (r - l).
يحل المؤشران المسألة بطريقة جشعة: ابدأ من الطرفين، حيث يكون العرض في أقصى قيمة، ثم حرّك المؤشر الأقصر إلى الداخل؛ فتحريك المؤشر الأطول لا يمكنه إلا تقليل المساحة. يستغرق ذلك زمن O(n) ومساحة O(1)، وهو أبسط من أسلوب المؤشرين في مسألة احتجاز مياه الأمطار، لأنه لا يحتاج إلى قيمة عظمى متراكمة.
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متقدم: احتجاز مياه الأمطار II (ثلاثي الأبعاد)
توسّع مسألة Trapping Rain Water II (LeetCode 407) المسألة لتشمل مصفوفة ارتفاعات ثنائية الأبعاد. يمكن للماء أن يتدفق في الاتجاهات الأربعة، ويجب أن يخرج عبر الحدود. يستخدم الحل كومة دنيا: نهيّئ الكومة بجميع الخلايا الحدودية، ثم ننفّذ توسعًا شبيهًا بالبحث بعرض أول. نعالج الخلية ذات الارتفاع الأصغر؛ فأي جار أقل ارتفاعًا منها سيحتجز ماءً لا يقل مستواه عن مستوى الخلية الحالية.
هذه خوارزمية مختلفة جوهريًا عن حالة البعد الواحد، وتختبر عمليات الكومة واجتياز البحث بعرض أول معًا. ولا يمكن تعميم حيلة المؤشرين ذات البعد الواحد على بعدين، بينما يمكن تعميم نهج الكومة.
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متى تستخدم كل أسلوب في المقابلات
دليل اتخاذ القرار في مقابلة احتجاز مياه الأمطار:
- ابدأ بـ: مصفوفات البادئات — سهلة الشرح، وبديهية بصريًا، وصحتها واضحة
- السؤال اللاحق «مساحة O(1)؟»: المؤشران — اشرح الثابت الذي ينص على أن الجانب الأقصر هو عنق الزجاجة
- إذا سأل المحاور «هل يوجد نهج آخر؟»: المكدس الرتيب — اشرح حساب الماء ضمن طبقات أفقية
ابدأ دائمًا بتحديد ما يحدد مستوى الماء عند كل موضع بوضوح، وهو الحد الأدنى من أعلى عمود على كل جانب، قبل الانتقال إلى الشيفرة. فهذا يبيّن فهمك للمسألة ويجعل شرح الحل أسهل.
# 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()الحالات الحدّية والأخطاء الشائعة
الأخطاء الشائعة في مسألة احتجاز مياه الأمطار:
- نسيان min: مستوى الماء هو
min(max_left, max_right)، وليس أحدهما فقط. يحتاج العمود إلى جدارين مرتفعين على كلا الجانبين. - ماء سالب: استخدم
max(0, ...)لحصر القيم السالبة عند 0 عندما يتجاوز ارتفاع الموضع مستوى الماء. - المواضع الطرفية: لا يمكن للعمود الموجود في أقصى اليسار أو أقصى اليمين احتجاز الماء مطلقًا، إذ لا يوجد جدار على أحد الجانبين. يتعامل نهج مصفوفة البادئات مع ذلك طبيعيًا، لأن
max_left[0] = height[0]يجعل الماء يساوي 0 دائمًا عند الفهرس 0. - المصفوفات الفارغة أو الصغيرة جدًا: أعد 0 للمصفوفات التي تحتوي على أقل من 3 عناصر.
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اختبار سريع
اختبر فهمك لمفاهيم Data Structures & Algorithms — Coding Interview Prep من هذا الدرس.
مراجعة الدرس
تعلّمت في هذا الدرس أن: حل مسألة احتجاز مياه الأمطار يعتمد على إيجاد الحد الأدنى من أعلى جدار على اليسار وعلى اليمين عند كل موضع، وأن نهج المؤشرين ذي المساحة O(1) يعمل لأن القيمة العظمى المتراكمة في الجانب الأقصر تكون دائمًا القيد المحدِّد، وأن نهج المكدس الرتيب يحسب الماء ضمن طبقات أفقية، وهو مفيد عند دمجه مع منطق آخر يعتمد على المكدس. ننتقل بعد ذلك إلى مفاهيم تصميم الأنظمة، بدءًا بإطار RADIO للإجابات المنظّمة في المقابلات.
الأسئلة الشائعة
هل درس «احتجاز مياه الأمطار: المكدس والمؤشران» مجاني؟
نعم — نص درس «احتجاز مياه الأمطار: المكدس والمؤشران» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة DSA Interview Prep، انتقل إلى CoddyKit PRO. تتضمن دورة DSA Interview Prep 4 دروس في المجموع.
ماذا ستتعلم في «احتجاز مياه الأمطار: المكدس والمؤشران»؟
حلّ مسألة trapping-rain-water باستخدام نهج المكدس الرتيب الذي يحسب الطبقات الأفقية، ونهج المؤشرين الذي يحسب الأعمدة الرأسية. تتمرن على DSA Interview Prep مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.
هل أحتاج إلى خبرة سابقة لأبدأ DSA Interview Prep؟
لا تُشترط خبرة سابقة. DSA Interview Prep على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 4 من أصل 4.
كم من الوقت يستغرق درس «احتجاز مياه الأمطار: المكدس والمؤشران»؟
معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.
هل يمكنني كتابة وتشغيل أكواد في درس DSA Interview Prep هذا؟
نعم. كل درس في DSA Interview Prep يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.
جميع الدروس في هذه الدورة
- المكدس الرتيب: تصاعدي مقابل تنازلي
- أكبر مستطيل في المدرج التكراري
- أقصى قيمة في النافذة المنزلقة باستخدام deque رتيب
- احتجاز مياه الأمطار: المكدس والمؤشران