أكبر مستطيل في المدرج التكراري
استخدم مكدسًا رتيبًا لتتبّع الحدود اليسرى وحساب مساحة أكبر مستطيل يمكن أن يتسع له المدرج التكراري في مرور واحد.
أكبر مستطيل في المدرج التكراري درس مجاني في Coding Interview Prep على CoddyKit. هذا هو الدرس 2 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في Coding Interview Prep، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة Coding Interview Prep 4 دروس في المجموع.
المسألة: أكبر مستطيل في مدرج بياني
تعطي مسألة أكبر مستطيل في مدرج بياني (LeetCode 84) مصفوفة من الأعداد الصحيحة غير السالبة تمثل ارتفاعات أعمدة في مدرج بياني، بحيث يكون عرض كل عمود 1. أوجدوا مساحة أكبر مستطيل يمكن تشكيله داخل المدرج البياني. يجب أن يمتد المستطيل عبر أعمدة متجاورة، ويحد ارتفاعه أقصر عمود يغطيه.
نهج القوة الغاشمة: لكل زوج (i, j)، احسبوا الارتفاع الأدنى في [i, j] واضربوه في (j - i + 1). هذا التعقيد هو O(n³)، أو O(n²) عند حساب القيم الدنيا مسبقًا، وهو بطيء جدًا. أما حل المكدس الرتيب فيعمل بتعقيد O(n).
# Example: heights = [2, 1, 5, 6, 2, 3]
# Rectangles:
# width=1, height=6 at index 3 => area=6
# width=2, height=5 at indices 2-3 => area=10 (maximum!)
# width=6, height=1 across all => area=6
# width=3, height=2 at indices 2-4 => area=6
heights = [2, 1, 5, 6, 2, 3]
print('Heights:', heights)
print('Expected max area: 10 (bars of height 5 and 6, width 2)')
# Brute force for small inputs:
def brute_force(heights):
n = len(heights)
max_area = 0
for i in range(n):
min_h = heights[i]
for j in range(i, n):
min_h = min(min_h, heights[j])
max_area = max(max_area, min_h * (j - i + 1))
return max_area
print('Brute force answer:', brute_force(heights)) # 10الفكرة الأساسية: ما الذي يحد مستطيل كل عمود؟
لكل عمود i ارتفاعه h، يمتد أكبر مستطيل يمكن أن يكون فيه هذا العمود هو العنصر الأدنى إلى اليسار حتى أول عمود أقصر من h، وإلى اليمين حتى أول عمود أقصر من h. العرض هو right_boundary - left_boundary - 1، والمساحة هي h × width.
تعيد هذه الصياغة تشكيل المسألة: لكل عمود، أوجدوا العنصر الأصغر السابق (PSE) والعنصر الأصغر التالي (NSE). وهذا بالضبط ما يحسبه مكدس تصاعدي رتيب. في اللحظة التي نزيل فيها العمود i (لأننا عثرنا على عمود أقصر)، يكون العمود الحالي هو NSE الخاص به، وتكون قمة المكدس بعد الإزالة هي PSE الخاص به.
heights = [2, 1, 5, 6, 2, 3]
n = len(heights)
# Find PSE and NSE for each bar
pse = [-1] * n # index of previous smaller element
nse = [n] * n # index of next smaller element (default: beyond array)
# PSE
stack = []
for i in range(n):
while stack and heights[stack[-1]] >= heights[i]:
stack.pop()
pse[i] = stack[-1] if stack else -1
stack.append(i)
# NSE
stack = []
for i in range(n - 1, -1, -1):
while stack and heights[stack[-1]] >= heights[i]:
stack.pop()
nse[i] = stack[-1] if stack else n
stack.append(i)
max_area = 0
for i in range(n):
width = nse[i] - pse[i] - 1
area = heights[i] * width
print(f'Bar {i} (h={heights[i]}): PSE={pse[i]}, NSE={nse[i]}, width={width}, area={area}')
max_area = max(max_area, area)
print('Max area:', max_area)حل بمرور واحد باستخدام مكدس رتيب
يعمل النهج ذو المرورين المذكور أعلاه، لكن يمكن دمجه في مرور واحد. عالجوا الأعمدة من اليسار إلى اليمين باستخدام مكدس تصاعدي رتيب. عندما يكون العمود i أقصر من قمة المكدس، أزيلوا قمة المكدس — يكون ارتفاع العمود المُزال هو ارتفاع مستطيل، وحدّه الأيمن هو i، وحدّه الأيسر هو قمة المكدس الجديدة + 1.
حيلة شائعة: أضيفوا قيمة حارسة 0 في نهاية heights. يضمن ذلك إزالة جميع الأعمدة من المكدس في النهاية، حتى إذا لم يظهر عمود أقصر بصورة طبيعية. ومن دون القيمة الحارسة، تحتاجون إلى مرحلة تنظيف بعد الحلقة للعناصر المتبقية في المكدس.
def largest_rectangle(heights):
stack = [] # monotonic increasing: indices of bars
max_area = 0
heights = heights + [0] # sentinel: forces all bars to be popped
for i, h in enumerate(heights):
while stack and heights[stack[-1]] > h:
height = heights[stack.pop()] # height of the rectangle
width = i if not stack else i - stack[-1] - 1 # left boundary
max_area = max(max_area, height * width)
stack.append(i)
return max_area
print(largest_rectangle([2, 1, 5, 6, 2, 3])) # 10
print(largest_rectangle([2, 4])) # 4
print(largest_rectangle([1, 1])) # 2
print(largest_rectangle([0, 9])) # 9
print(largest_rectangle([6, 7, 5, 2, 4, 5, 9, 3])) # 16تتبّع خوارزمية المرور الواحد
لنتتبّع [2, 1, 5, 6, 2, 3, 0] (مع قيمة حارسة) خطوةً خطوة:
- i=0, h=2: أضف 0 إلى المكدس. المكدس: [0]
- i=1, h=1: أزل 0 (h=2، العرض=1، المساحة=2). المكدس فارغ، فأضف 1. المكدس: [1]
- i=2, h=5: 5>1، أضف 2. المكدس: [1,2]
- i=3, h=6: 6>5، أضف 3. المكدس: [1,2,3]
- i=4, h=2: أزل 3 (h=6,width=4-2-1=1,area=6)، وأزل 2 (h=5,width=4-1-1=2,area=10★)، ثم توقف لأن 2>1. أضف 4. المكدس: [1,4]
- i=5, h=3: 3>2، أضف 5. المكدس: [1,4,5]
- i=6, sentinel h=0: أزل جميع العناصر، مع حساب المساحات...
def largest_rectangle_trace(heights):
stack = []
max_area = 0
hs = heights + [0]
for i, h in enumerate(hs):
while stack and hs[stack[-1]] > h:
top = stack.pop()
w = i if not stack else i - stack[-1] - 1
area = hs[top] * w
print(f' Pop bar {top} (h={hs[top]}): width={w}, area={area}', end='')
if area > max_area:
max_area = area
print(' *** NEW MAX ***', end='')
print()
print(f'i={i} h={h}: push {i}, stack={[hs[s] for s in stack + [i]]}')
stack.append(i)
print(f'Max area: {max_area}')
return max_area
largest_rectangle_trace([2, 1, 5, 6, 2, 3])حساب العرض: لماذا i - stack[-1] - 1؟
عندما نزيل العمود j من المكدس، نعرف أن: الحد الأيمن لمستطيل j هو i (أول عمود أقصر من j إلى اليمين). أما الحد الأيسر فهو العمود الموجود أسفل j مباشرةً في المكدس بعد الإزالة — ولنسَمِّه k. لذلك يكون العرض i - k - 1 (الأعمدة من k+1 إلى i-1، شاملًا الطرفين).
إذا كان المكدس فارغًا بعد الإزالة، يمتد مستطيل j حتى الحافة اليسرى تمامًا (الفهرس 0). ويكون العرض ببساطة i (الفهارس من 0 إلى i-1، وجميعها بارتفاع لا يقل عن heights[j]). وهذه هي الحالة الخاصة width = i if not stack else i - stack[-1] - 1.
# Illustrating left/right boundary logic
heights = [1, 3, 5, 2]
# After processing with stack:
# When we pop bar 2 (h=5) at i=3 (h=2):
# stack after pop = [0, 1] => left boundary = 1+1=2, right=3-1=2 => width=1
# When we pop bar 1 (h=3) at i=3 (h=2):
# stack after pop = [0] => left boundary = 0+1=1, right=3-1=2 => width=2
# etc.
def compute_boundaries(heights):
hs = heights + [0]
stack = []
for i, h in enumerate(hs):
while stack and hs[stack[-1]] > h:
top = stack.pop()
if stack:
left = stack[-1] + 1
width = i - stack[-1] - 1
else:
left = 0
width = i
print(f'Bar {top} (h={hs[top]}): extends from {left} to {i-1}, width={width}')
stack.append(i)
compute_boundaries([2, 1, 5, 6, 2, 3])أكبر مستطيل في مصفوفة ثنائية
توسّع مسألة أكبر مستطيل (LeetCode 85) مسألة المدرج البياني إلى مصفوفة ثنائية الأبعاد. لكل صف، احسبوا ارتفاع الواحدات المتتالية فوق كل خلية. ينشئ ذلك مدرجًا بيانيًا لذلك الصف. طبّقوا خوارزمية أكبر مستطيل في مدرج بياني على مدرج كل صف. وتكون القيمة العظمى بين جميع الصفوف هي الإجابة.
يحوّل هذا مسألة ثنائية الأبعاد إلى n مسائل متكررة أحادية الأبعاد للمدرجات البيانية. ويكون التعقيد الزمني O(m × n) لمصفوفة مكوّنة من m صفوف وn أعمدة — مرور مدرج بياني واحد لكل صف، ويستغرق كل مرور O(n).
def maximal_rectangle(matrix):
if not matrix or not matrix[0]:
return 0
n = len(matrix[0])
heights = [0] * n
max_area = 0
def hist_max_area(h):
stack, area = [], 0
for i, hh in enumerate(h + [0]):
while stack and h[stack[-1]] > hh:
top = stack.pop()
w = i if not stack else i - stack[-1] - 1
area = max(area, h[top] * w)
stack.append(i)
return area
for row in matrix:
for j in range(n):
heights[j] = heights[j] + 1 if row[j] == '1' else 0
max_area = max(max_area, hist_max_area(heights[:]))
return max_area
matrix = [['1','0','1','0','0'],
['1','0','1','1','1'],
['1','1','1','1','1'],
['1','0','0','1','0']]
print(maximal_rectangle(matrix)) # 6الحالات الحدية في مسائل المدرجات البيانية
حالات حدية مهمة يجب التعامل معها:
- جميع الارتفاعات متساوية: تشكل المصفوفة بأكملها مستطيلًا واحدًا؛ الإجابة = n × height
- ترتيب تصاعدي رتيب: لا تحدث أي إزالة حتى القيمة الحارسة؛ مساحة العمود الأخير هي الأكبر
- عمود واحد: الإجابة = height[0]
- أعمدة بارتفاع 0: تعمل كقيم حارسة طبيعية، فتقسم المدرج البياني إلى مقاطع مستقلة
تتعامل القيمة الحارسة (إضافة 0) في النهاية مع الحالة ذات الترتيب التصاعدي الرتيب، إذ تجبر جميع الأعمدة المتبقية على الإزالة في النهاية. ومن دونها، تحتاجون إلى حلقة تنظيف منفصلة بعد التكرار الرئيسي.
def largest_rectangle(heights):
stack = []
max_area = 0
heights = heights + [0]
for i, h in enumerate(heights):
while stack and heights[stack[-1]] > h:
top = stack.pop()
w = i if not stack else i - stack[-1] - 1
max_area = max(max_area, heights[top] * w)
stack.append(i)
return max_area
# Edge cases
print(largest_rectangle([5, 5, 5, 5])) # 20 (all same)
print(largest_rectangle([1, 2, 3, 4, 5])) # 9 (increasing: 3*3)
print(largest_rectangle([5, 4, 3, 2, 1])) # 9 (decreasing: 3*3)
print(largest_rectangle([5])) # 5 (single bar)
print(largest_rectangle([0, 0, 0])) # 0 (all zero)
print(largest_rectangle([3, 0, 3])) # 3 (zero splits)بديل التقسيم والغزو
يمكن أيضًا حل مسألة المدرج البياني باستخدام التقسيم والغزو: قسّموا عند العمود ذي الارتفاع الأدنى، وحلّوا كل نصف بصورة عودية، ثم قارنوا ذلك بالمستطيل الممتد عبر العرض الكامل باستخدام الارتفاع الأدنى. يعطي هذا تعقيدًا متوسطه O(n log n)، لكنه يصل إلى O(n²) في أسوأ حالة عند استخدام مدخلات مرتبة.
يُعد نهج المكدس الرتيب أفضل منه على نحو صارم، إذ يحقق O(n) في أسوأ الحالات. ومع ذلك، فإن فهم نهج التقسيم والغزو يعمّق الحدس تجاه المسألة، ويوضح لماذا يكون العمود ذو الارتفاع الأدنى في أي مقطع دائمًا العامل المحدد للمستطيلات ذات العرض الكامل.
def largest_rectangle_dc(heights, lo=0, hi=None):
if hi is None:
hi = len(heights) - 1
if lo > hi:
return 0
# Find the index of the minimum height in [lo, hi]
min_idx = lo
for i in range(lo, hi + 1):
if heights[i] < heights[min_idx]:
min_idx = i
# Three options:
# 1. Max rect entirely in left half
# 2. Max rect entirely in right half
# 3. Max rect spanning entire [lo, hi] with height = min
full_width_area = heights[min_idx] * (hi - lo + 1)
left_area = largest_rectangle_dc(heights, lo, min_idx - 1)
right_area = largest_rectangle_dc(heights, min_idx + 1, hi)
return max(full_width_area, left_area, right_area)
print(largest_rectangle_dc([2, 1, 5, 6, 2, 3])) # 10نمط المدرج البياني: عدد المصفوفات الجزئية
مسألة ذات صلة تستخدم تقنية المكدس نفسها: احسبوا عدد المصفوفات الجزئية في مدرج بياني التي يساوي عنصرها الأدنى قيمة مستهدفة. تُجاب هذه المسألة بحساب PSE وNSE لكل عمود، ثم استخدام الصيغة (i - pse[i]) × (nse[i] - i)، التي تحسب عدد المدرجات الفرعية التي يكون فيها العمود i هو العنصر الأدنى.
تظهر تقنية «عدد العناصر إلى اليسار × عدد العناصر إلى اليمين» في عدة مسائل من LeetCode، منها مجموع القيم الدنيا للمصفوفات الجزئية (907)، وعدّ السلاسل الفرعية التي تحتوي على محارف فريدة تمامًا، ومسائل تقنية المساهمة. يحسب المكدس الرتيب PSE وNSE في O(n)، مما يتيح حساب مساهمة كل عنصر في O(1).
def sum_of_subarray_minimums(arr):
n = len(arr)
pse = [-1] * n # previous strictly smaller element
nse = [n] * n # next smaller or equal element
stack = []
for i in range(n):
while stack and arr[stack[-1]] >= arr[i]:
stack.pop()
pse[i] = stack[-1] if stack else -1
stack.append(i)
stack = []
for i in range(n - 1, -1, -1):
while stack and arr[stack[-1]] > arr[i]:
stack.pop()
nse[i] = stack[-1] if stack else n
stack.append(i)
MOD = 10**9 + 7
total = 0
for i in range(n):
left_count = i - pse[i] # subarrays where i is leftmost min
right_count = nse[i] - i # subarrays where i is the min
total += arr[i] * left_count * right_count
return total % MOD
print(sum_of_subarray_minimums([3, 1, 2, 4])) # 17
print(sum_of_subarray_minimums([11, 81, 94, 43, 3])) # 444نصائح عملية لمقابلات العمل
عند مواجهة مسألة مخطط أعمدة في مقابلة عمل، اتبع قائمة التحقق التالية:
- وضّح المعطيات: هل يمكن أن تكون الارتفاعات 0؟ وما المخرج المطلوب — المساحة أم الفهارس أم العدد؟
- ابدأ بالحل بالقوة الغاشمة واذكر أن تعقيده O(n²) أو O(n³)
- اذكر أن مساهمة كل عمود تعتمد على امتداده إلى اليسار واليمين حتى أقرب عمود أقصر
- قدّم PSE/NSE ← المكدس الرتيب ← حل O(n)
- تعامل مع حيلة العنصر الحارس (إلحاق 0) لتبسيط الشيفرة
- تتبّع مثالًا صغيرًا على اللوح الأبيض
سؤال متابعة شائع: توسيع الحل إلى بُعدَين (المستطيل الأكبر). وضّح أنه يمكن اختزاله إلى n من مسائل مخطط الأعمدة، يستغرق حل كل منها O(n)، ليكون التعقيد الإجمالي O(m×n).
# Final clean solution for interview
def largest_rectangle_in_histogram(heights):
stack = []
max_area = 0
for i, h in enumerate(heights + [0]): # sentinel forces final pops
while stack and heights[stack[-1]] > h:
height = heights[stack.pop()]
width = i if not stack else i - stack[-1] - 1
max_area = max(max_area, height * width)
stack.append(i)
return max_area
# Verify all test cases from earlier
test_cases = [
([2, 1, 5, 6, 2, 3], 10),
([6, 7, 5, 2, 4, 5, 9, 3], 16),
([1], 1),
([2, 0, 2], 2),
([], 0),
]
for heights, expected in test_cases:
if not heights:
result = 0
else:
result = largest_rectangle_in_histogram(heights)
status = 'PASS' if result == expected else 'FAIL'
print(f'{status}: {heights} => {result} (expected {expected})')مجموع نطاقات المقاطع الفرعية ومتغيرات مشابهة
تُعمّم تقنية PSE/NSE على عدة مسائل في LeetCode. تسأل مسألة Sum of Subarray Ranges (2104) عن مجموع (max - min) عبر جميع المقاطع الفرعية. ويساوي ذلك (مجموع القيم العظمى للمقاطع الفرعية) ناقصًا (مجموع القيم الصغرى للمقاطع الفرعية)، مع حساب كل منهما باستخدام مكدس رتيب في O(n). تستخدم مسألة Number of Visible People in a Queue (1944) مكدسًا تنازليًا، حيث تُحصى شخصية مرئية مع كل عملية إخراج. ويمكن التعرّف على هذه الفئة من المسائل من خلال ملاحظة العبارة: «لكل عنصر، إلى أي مدى يمكنه الهيمنة؟» — والإجابة دائمًا هي PSE/NSE باستخدام مكدس رتيب.
def sum_subarray_ranges(nums):
n = len(nums)
# Sum of subarray max - sum of subarray min
def contrib(arr, is_max):
# Count contribution of each element as max (or min)
n = len(arr)
left = [0]*n; right = [0]*n
stack = []
for i in range(n):
while stack and (arr[stack[-1]] < arr[i] if is_max else arr[stack[-1]] > arr[i]):
stack.pop()
left[i] = i - (stack[-1] if stack else -1)
stack.append(i)
stack = []
for i in range(n-1, -1, -1):
while stack and (arr[stack[-1]] <= arr[i] if is_max else arr[stack[-1]] >= arr[i]):
stack.pop()
right[i] = (stack[-1] if stack else n) - i
stack.append(i)
return sum(arr[i] * left[i] * right[i] for i in range(n))
return contrib(nums, True) - contrib(nums, False)
print(sum_subarray_ranges([1, 2, 3])) # 4
print(sum_subarray_ranges([1, 3, 3])) # 4
print(sum_subarray_ranges([4, -2, -3, 4, 1])) # 59اختبار سريع
اختبر مدى فهمك لمفاهيم Data Structures & Algorithms — Coding Interview Prep التي تناولها هذا الدرس.
مراجعة الدرس
في هذا الدرس تعلّمت: أن حدود أكبر مستطيل يمر عبر كل عمود يحددها أقرب عمود أقصر على كل جانب (PSE وNSE)، وأن مكدسًا رتيبًا متزايدًا يحسب جميع حدود PSE/NSE في مرور واحد بتعقيد O(n)، من خلال العثور على الحدود عند إخراج الأعمدة، وأن إلحاق عنصر حارس بقيمة 0 يضمن إخراج جميع الأعمدة من المكدس، مما يبسّط الشيفرة إلى حلقة واحدة. بعد ذلك سنستخدم deque رتيبًا لحل مسألة الحد الأقصى للنافذة المنزلقة في O(n).
الأسئلة الشائعة
هل درس «أكبر مستطيل في المدرج التكراري» مجاني؟
نعم — نص درس «أكبر مستطيل في المدرج التكراري» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة Coding Interview Prep، انتقل إلى CoddyKit PRO. تتضمن دورة Coding Interview Prep 4 دروس في المجموع.
ماذا ستتعلم في «أكبر مستطيل في المدرج التكراري»؟
استخدم مكدسًا رتيبًا لتتبّع الحدود اليسرى وحساب مساحة أكبر مستطيل يمكن أن يتسع له المدرج التكراري في مرور واحد. تتمرن على Coding Interview Prep مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.
هل أحتاج إلى خبرة سابقة لأبدأ Coding Interview Prep؟
لا تُشترط خبرة سابقة. Coding Interview Prep على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 2 من أصل 4.
كم من الوقت يستغرق درس «أكبر مستطيل في المدرج التكراري»؟
معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.
هل يمكنني كتابة وتشغيل أكواد في درس Coding Interview Prep هذا؟
نعم. كل درس في Coding Interview Prep يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.
جميع الدروس في هذه الدورة
- المكدس الرتيب: تصاعدي مقابل تنازلي
- أكبر مستطيل في المدرج التكراري
- أقصى قيمة في النافذة المنزلقة باستخدام deque رتيب
- احتجاز مياه الأمطار: المكدس والمؤشران