0Pricing
Coding Interview Prep · درس

المجاميع البادئة والإجماليات التراكمية

أنشئ مصفوفات المجاميع البادئة للإجابة عن استعلامات مجموع النطاق في O(1)، وطبّق التقنية على مسائل المصفوفات الجزئية مثل المصفوفة الجزئية ذات المجموع الأقصى

المجاميع البادئة والإجماليات التراكمية درس مجاني في Coding Interview Prep على CoddyKit. هذا هو الدرس 2 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في Coding Interview Prep، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة Coding Interview Prep 4 دروس في المجموع.

مسألة مجموع النطاق

مع إعطائك مصفوفة nums، عليك الإجابة عن العديد من الاستعلامات من الشكل التالي: ما مجموع العناصر من الفهرس i إلى الفهرس j؟ يستغرق حساب كل استعلام بالطريقة الساذجة زمن O(n)، ولذلك تستغرق k استعلامات O(n×k). باستخدام مصفوفة المجموع السابق، تحسب مسبقًا مجموعًا تراكميًا في O(n)، ثم تجيب عن كل استعلام في O(1). وهذه إحدى أكثر تقنيات الحساب المسبق استخدامًا في مقابلات البرمجة.

# Naive: O(n) per query
def range_sum_naive(nums, i, j):
    return sum(nums[i:j+1])

nums = [1, 3, 5, 7, 9]
print(range_sum_naive(nums, 1, 3))  # 3+5+7 = 15
print(range_sum_naive(nums, 0, 4))  # 1+3+5+7+9 = 25
# For 1000 queries, this takes 5000 operations

بناء مصفوفة المجموع السابق

عرّف prefix[i] على أنه مجموع nums[0] حتى nums[i-1] (مع خانة إضافية؛ إذ يجعل الإزاح بمقدار 1 مع الفهرسة التي تبدأ من الصفر معالجة الحالات الحدية أبسط). ابنِها في O(n) بمرور واحد: prefix[i] = prefix[i-1] + nums[i-1]. بعد ذلك يصبح استعلام النطاق sum(i, j) هو prefix[j+1] - prefix[i]، أي عملية طرح واحدة بتكلفة O(1).

def build_prefix(nums):
    n = len(nums)
    prefix = [0] * (n + 1)
    for i in range(n):
        prefix[i+1] = prefix[i] + nums[i]
    return prefix

def range_sum(prefix, i, j):
    return prefix[j+1] - prefix[i]  # O(1)

nums = [1, 3, 5, 7, 9]
pre = build_prefix(nums)
print(pre)                    # [0, 1, 4, 9, 16, 25]
print(range_sum(pre, 1, 3))  # 9 - 1 = 8? Wait: 3+5+7=15
# Hmm: prefix[4]-prefix[1] = 16-1 = 15  correct
print(range_sum(pre, 1, 3))  # 15

مجموع المصفوفة الجزئية يساوي K

يُعد العثور على عدد المصفوفات الجزئية التي يساوي مجموعها k مسألة كلاسيكية تجمع بين جدول التجزئة والمجموع السابق. والفكرة الأساسية هي أن مجموع المصفوفة الجزئية من i إلى j يساوي prefix[j] - prefix[i-1]. وإذا أردنا أن يساوي هذا المجموع k، فإن prefix[i-1] = prefix[j] - k. وأثناء المسح من اليسار إلى اليمين مع الحفاظ على مجموع سابق تراكمي، نبحث عن عدد مرات ظهور current_sum - k سابقًا، وبذلك نعدّ جميع المصفوفات الجزئية الصحيحة في زمن إجمالي O(n).

from collections import defaultdict

def subarray_sum_k(nums, k):
    count = 0
    current = 0
    freq = defaultdict(int)
    freq[0] = 1  # empty prefix
    for n in nums:
        current += n
        count += freq[current - k]  # how many prior sums give diff=k
        freq[current] += 1
    return count

print(subarray_sum_k([1, 1, 1], 2))    # 2
print(subarray_sum_k([1, 2, 3], 3))    # 2  ([1,2] and [3])

أكبر مجموع لمصفوفة جزئية باستخدام المجموع السابق

يمكن صياغة أكبر مجموع لمصفوفة جزئية بوصفه مسألة مجموع سابق: لكل فهرس j، نريد تعظيم prefix[j] - prefix[i] لجميع قيم i الأصغر من j. وتكون قيمة i المثلى عند كل j هي أصغر مجموع سابق رأيناه حتى تلك اللحظة. يتيح المسح من اليسار إلى اليمين مع تتبّع min_prefix الوصول إلى زمن O(n). وهذا مكافئ لخوارزمية Kadane ولكن من منظور المجاميع السابقة.

def max_subarray_prefix(nums):
    max_sum  = float('-inf')
    min_pre  = 0  # prefix[0] = 0
    current  = 0
    for n in nums:
        current += n
        max_sum = max(max_sum, current - min_pre)
        min_pre = min(min_pre, current)
    return max_sum

print(max_subarray_prefix([-2,1,-3,4,-1,2,1,-5,4]))
# 6  (same as Kadane's)
print(max_subarray_prefix([-1,-2,-3]))
# -1

المجاميع السابقة ثنائية الأبعاد لاستعلامات الشبكة

تمتد المجاميع السابقة إلى الشبكات ثنائية الأبعاد. عرّف P[i][j] على أنه مجموع جميع العناصر في المستطيل من (0,0) إلى (i-1,j-1). ابنِه باستخدام صيغة مبدأ الاشتمال والاستبعاد: P[i][j] = P[i-1][j] + P[i][j-1] - P[i-1][j-1] + grid[i-1][j-1]. بعد ذلك يمكن الإجابة عن أي استعلام لمجموع المستطيل من (r1,c1) إلى (r2,c2) في O(1) باستخدام أربع عمليات بحث.

def build_2d_prefix(grid):
    R, C = len(grid), len(grid[0])
    P = [[0]*(C+1) for _ in range(R+1)]
    for r in range(1, R+1):
        for c in range(1, C+1):
            P[r][c] = (P[r-1][c] + P[r][c-1]
                       - P[r-1][c-1] + grid[r-1][c-1])
    return P

def rect_sum(P, r1, c1, r2, c2):
    return P[r2+1][c2+1] - P[r1][c2+1] - P[r2+1][c1] + P[r1][c1]

grid = [[3,0,1,4],[5,6,3,2],[1,2,0,1]]
P = build_2d_prefix(grid)
print(rect_sum(P, 0, 0, 1, 1))  # 3+0+5+6 = 14

المجموع التراكمي لفهرس التوازن

فهرس التوازن هو الموضع الذي يساوي فيه مجموع العناصر إلى اليسار مجموع العناصر إلى اليمين. احسب المجموع الكلي مسبقًا، ثم امسح المصفوفة من اليسار مع الحفاظ على مجموع يساري تراكمي. ويكون المجموع اليميني هو total - left_sum - nums[i]. تحقّق من التساوي في O(1) عند كل فهرس، لتحصل على O(n) إجمالًا. يوضح هذا كيف يحل المجموع التراكمي محل مصفوفتين منفصلتين من المجاميع السابقة.

def find_pivot_index(nums):
    total = sum(nums)
    left_sum = 0
    for i, n in enumerate(nums):
        # right_sum = total - left_sum - nums[i]
        if left_sum == total - left_sum - n:
            return i
        left_sum += n
    return -1

print(find_pivot_index([1, 7, 3, 6, 5, 6]))  # 3
print(find_pivot_index([1, 2, 3]))             # -1

مصفوفة حاصل ضرب العناصر ما عدا العنصر نفسه

مع إعطائك مصفوفة، أعد مصفوفة يكون كل عنصر فيها حاصل ضرب جميع العناصر الأخرى. لا يُسمح باستخدام القسمة. استخدم حاصل الضرب السابق وحاصل الضرب اللاحق: result[i] = (حاصل ضرب جميع العناصر قبل i) × (حاصل ضرب جميع العناصر بعد i). ابنِ حواصل الضرب السابقة بمرور من اليسار إلى اليمين، ثم اضرب في حواصل الضرب اللاحقة بمرور من اليمين إلى اليسار باستخدام متغير تراكمي، من دون الحاجة إلى مصفوفة إضافية للحواصل اللاحقة.

def product_except_self(nums):
    n = len(nums)
    result = [1] * n
    # Left pass: result[i] = product of nums[:i]
    prefix = 1
    for i in range(n):
        result[i] = prefix
        prefix *= nums[i]
    # Right pass: multiply in product of nums[i+1:]
    suffix = 1
    for i in range(n-1, -1, -1):
        result[i] *= suffix
        suffix *= nums[i]
    return result

print(product_except_self([1, 2, 3, 4]))
# [24, 12, 8, 6]   O(n) time, O(1) extra space

المجموع السابق مع باقي القسمة

تطلب بعض المسائل حساب عدد المصفوفات الجزئية التي يقبل مجموعها القسمة على k. باستخدام المجاميع السابقة مع باقي القسمة على k: إذا كان prefix[j] % k == prefix[i] % k، فإن sum(i+1..j) يقبل القسمة على k. ويتيح جدول تجزئة يعدّ كل قيمة من قيم الباقي أثناء المسح الوصول إلى زمن O(n). والتهيئة الأساسية هي freq[0] = 1 لمعالجة المصفوفات الجزئية التي تبدأ عند الفهرس 0.

from collections import defaultdict

def subarray_div_by_k(nums, k):
    freq = defaultdict(int)
    freq[0] = 1
    current = 0
    count = 0
    for n in nums:
        current = (current + n) % k
        count += freq[current]
        freq[current] += 1
    return count

print(subarray_div_by_k([4, 5, 0, -2, -3, 1], 5))
# 7  (seven subarrays divisible by 5)

مصفوفة الفروق لتحديثات النطاق

مصفوفة الفروق هي معكوس المجموع السابق. مع إعطائك مصفوفة، احسب مسبقًا diff[i] = nums[i] - nums[i-1]. لإضافة x إلى نطاق [l, r]، لا تحتاج إلا إلى عمليتي O(1) على مصفوفة الفروق: diff[l] += x وdiff[r+1] -= x. بعد تنفيذ جميع التحديثات، أعد بناء مصفوفة الناتج بمرور واحد لحساب المجموع السابق. وبهذا تتحول k من تحديثات النطاق من O(n×k) إلى O(n + k).

def apply_range_updates(n, updates):
    # updates: list of (l, r, val)
    diff = [0] * (n + 1)
    for l, r, val in updates:
        diff[l]   += val
        diff[r+1] -= val
    # Reconstruct with prefix sum
    result = []
    running = 0
    for i in range(n):
        running += diff[i]
        result.append(running)
    return result

# Add 3 to [1,3], add 1 to [0,2]
print(apply_range_updates(5, [(1,3,3),(0,2,1)]))
# [1, 4, 4, 3, 0]

المجموع السابق في مسائل المقابلات

تظهر المجاميع السابقة في فئات عديدة من المسائل:

  • استعلامات النطاق — مجموع المصفوفة الجزئية، ومجموع المستطيل
  • عدّ المصفوفات الجزئية — مجموع يساوي k، أو يقبل القسمة على k
  • مسائل حاصل الضرب — حاصل ضرب العناصر ما عدا العنصر نفسه
  • التوازن — العثور على فهرس المحور
  • تحديثات النطاق — مصفوفة الفروق
عندما ترى مسألة تتضمن مجاميع تراكمية أو تجميعات تعتمد على النطاقات، فكّر أولًا في المجموع السابق. فهذا يفتح غالبًا طريقًا إلى حل O(n) بدلًا من القوة الغاشمة الساذجة ذات التعقيد O(n²).

# Template: prefix sum + hash map for subarray problems
from collections import defaultdict

def subarray_count_template(nums, target):
    """
    Count subarrays with property involving prefix sums.
    Adapt 'target' and lookup condition for each problem.
    """
    freq = defaultdict(int)
    freq[0] = 1          # empty prefix at sum=0
    current = 0
    count = 0
    for n in nums:
        current += n
        count += freq[current - target]  # adjust per problem
        freq[current] += 1
    return count

print(subarray_count_template([1,2,3,2,1], 3))  # 3

المجموع التراكمي والقيمة العظمى التراكمية

بالإضافة إلى المجاميع السابقة، تستخدم مسائل كثيرة قيمة عظمى تراكمية أو قيمة صغرى تراكمية تُحافَظ عليها باستخدام متغير واحد. تستخدم مسألة أفضل وقت لشراء الأسهم وبيعها أقل سعر تراكمي، بينما يستخدم حبس مياه الأمطار من اليسار أكبر ارتفاع يساري تراكمي. لا تتطلب هذه الأنماط سوى مسح واحد ومساحة إضافية O(1)، ما يجعلها معيارًا مثاليًا لكفاءة الزمن والمساحة معًا.

def max_profit(prices):
    # Running minimum buy price
    min_price = float('inf')
    max_prof  = 0
    for price in prices:
        if price < min_price:
            min_price = price
        elif price - min_price > max_prof:
            max_prof = price - min_price
    return max_prof

def left_max_array(heights):
    # Running max from left for trapping rain water
    n = len(heights)
    left_max = [0] * n
    left_max[0] = heights[0]
    for i in range(1, n):
        left_max[i] = max(left_max[i-1], heights[i])
    return left_max

print(max_profit([7,1,5,3,6,4]))  # 5

اختبار سريع

اختبر مدى فهمك لمفاهيم Data Structures & Algorithms — Coding Interview Prep الواردة في هذا الدرس.

مراجعة الدرس

تعلّمت في هذا الدرس أن: المجاميع السابقة تحوّل استعلامات النطاق ذات التعقيد O(n) إلى عمليات بحث O(1) عبر حساب المجاميع التراكمية مسبقًا في مرور واحد بتعقيد O(n)، وأن الجمع بين المجاميع السابقة وجدول التجزئة يتيح حلول O(n) لعدّ المصفوفات الجزئية ذات مجموع معين أو خاصية قابلية معينة للقسمة، وأن مصفوفات الفروق هي المعكوس؛ إذ تتيح تحديثات نطاق بتكلفة O(1) مع مرور واحد لإعادة بناء المجموع السابق في النهاية. بعد ذلك سنتناول تقنية المؤشرين، بدءًا بالمؤشرين عند الطرفين المتقابلين.

الأسئلة الشائعة

هل درس «المجاميع البادئة والإجماليات التراكمية» مجاني؟

نعم — نص درس «المجاميع البادئة والإجماليات التراكمية» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة Coding Interview Prep، انتقل إلى CoddyKit PRO. تتضمن دورة Coding Interview Prep 4 دروس في المجموع.

ماذا ستتعلم في «المجاميع البادئة والإجماليات التراكمية»؟

أنشئ مصفوفات المجاميع البادئة للإجابة عن استعلامات مجموع النطاق في O(1)، وطبّق التقنية على مسائل المصفوفات الجزئية مثل المصفوفة الجزئية ذات المجموع الأقصى تتمرن على Coding Interview Prep مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.

هل أحتاج إلى خبرة سابقة لأبدأ Coding Interview Prep؟

لا تُشترط خبرة سابقة. Coding Interview Prep على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 2 من أصل 4.

كم من الوقت يستغرق درس «المجاميع البادئة والإجماليات التراكمية»؟

معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.

هل يمكنني كتابة وتشغيل أكواد في درس Coding Interview Prep هذا؟

نعم. كل درس في Coding Interview Prep يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.

جميع الدروس في هذه الدورة

  1. أساسيات المصفوفات والعمليات في مكانها
  2. المجاميع البادئة والإجماليات التراكمية
  3. مؤشران: الطرفان المتقابلان
  4. مؤشران: البطيء والسريع
← العودة إلى Coding Interview Prep