0Pricing
Coding Interview Prep · درس

تحسين المساحة في DP ثنائي الأبعاد

قلّل مساحة LCS ومسافة التحرير من O(mn) إلى O(min(m,n)) بالاحتفاظ بالصف الحالي والصف السابق فقط من جدول DP

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

أهمية المساحة في DP ثنائي الأبعاد

يتطلب جدول DP ثنائي الأبعاد لسلاسل طولها 1000 عددًا قدره 1000×1000 = 1,000,000 خلية، أي نحو 8 MB للأعداد الصحيحة ذات 64 بت. ويصبح ذلك غير عملي مع التسلسلات الأطول (مثل محاذاة DNA أو مقارنة النصوص الكبيرة). والملاحظة الأساسية هي أن معظم علاقات DP العودية ثنائية الأبعاد تنظر فقط إلى الصف الحالي والصف السابق، ولذلك يمكن ضغط الجدول بأكمله في مصفوفة أحادية أو مصفوفتين أحاديتي الأبعاد. وهذا هو جوهر تحسين مساحة DP ثنائي الأبعاد.

# Full 2D DP: O(mn) space
# LCS for 1000-char strings
m, n = 1000, 1000
dp_2d_size = m * n * 8  # bytes (64-bit ints)
print(f'2D table: {dp_2d_size:,} bytes = {dp_2d_size//1024} KB')

# 1D rolling array: O(n) space
dp_1d_size = n * 8
print(f'1D array: {dp_1d_size:,} bytes = {dp_1d_size} bytes')
print(f'Space saving: {dp_2d_size // dp_1d_size}x')

نمط المصفوفة المتدحرجة

يستبدل نمط المصفوفة المتدحرجة الجدول ثنائي الأبعاد الكامل بمصفوفة أحادية الأبعاد تمثل الصف السابق. عند حساب الصف i، تحدّث كل خلية j باستخدام القيمة الحالية dp[j] (التي لا تزال تحتوي على قيمة dp[i-1][j] للصف السابق) والقيمة المحدّثة للتو dp[j-1] (وهي dp[i][j-1]). يلتقط متغير diagonal قيمة dp[i-1][j-1] قبل الكتابة فوقها. وينطبق هذا النمط على LCS ومسافة التحرير ومعظم مسائل DP ثنائية الأبعاد.

# Rolling array template for 2D DP
# Before update: dp[j] holds dp[i-1][j] (previous row)
# After update: dp[j] holds dp[i][j] (current row)

def rolling_array_template(grid):
    m, n = len(grid), len(grid[0])
    dp = [0] * (n + 1)  # represents one row
    for i in range(1, m + 1):
        diag = 0  # stores dp[i-1][j-1] before overwrite
        for j in range(1, n + 1):
            temp = dp[j]  # save dp[i-1][j] before overwriting
            # compute dp[i][j] using dp[j] (above) and dp[j-1] (left) and diag
            dp[j] = diag + dp[j] + dp[j-1]  # placeholder logic
            diag = temp
    return dp[n]

‏LCS بمساحة O(min m,n)

بالنسبة إلى LCS، تأكد من أن text1 هي السلسلة الأقصر (حتى تكون n صغيرة). خصّص مصفوفة أحادية الأبعاد بحجم n+1. عالج الصفوف واحدًا تلو الآخر. في كل خلية: احفظ temp = dp[j] (وهذه هي dp[i-1][j]). ثم: إذا تطابقت الأحرف، dp[j] = diag + 1؛ وإلا dp[j] = max(dp[j], dp[j-1]). وأخيرًا عيّن diag = temp. بعد معالجة جميع الصفوف، تحتوي dp[n] على طول LCS.

def lcs_space_opt(text1, text2):
    # Ensure text2 is the shorter one
    if len(text1) < len(text2):
        text1, text2 = text2, text1
    m, n = len(text1), len(text2)
    dp = [0] * (n + 1)
    for i in range(1, m + 1):
        diag = 0
        for j in range(1, n + 1):
            temp = dp[j]  # dp[i-1][j]
            if text1[i-1] == text2[j-1]:
                dp[j] = diag + 1
            else:
                dp[j] = max(dp[j], dp[j-1])
            diag = temp
    return dp[n]

print(lcs_space_opt('ABCBDAB', 'BDCABA'))  # 4
print(lcs_space_opt('AGGTAB', 'GXTXAYB')) # 4

مسافة التحرير بمساحة O(n)

تستخدم مسافة التحرير النمط المتدحرج نفسه. تمثل المصفوفة الأحادية الأولية الصف 0: dp[j] = j (إدراج j من الأحرف). ولكل صف i، عيّن dp[0] = i (حذف i من الأحرف) واحفظ diag = dp[0] قبل التحديث. في الحلقة الداخلية، احفظ temp = dp[j]، واحسب القيمة الجديدة من الإدراج (dp[j-1]+1) والحذف (dp[j]+1) والاستبدال (diag + cost)، ثم عيّن diag = temp.

def edit_dist_opt(s, t):
    m, n = len(s), len(t)
    dp = list(range(n + 1))   # row 0: dp[0][j] = j
    for i in range(1, m + 1):
        diag = dp[0]           # dp[i-1][0] before dp[0] update
        dp[0] = i              # dp[i][0] = i
        for j in range(1, n + 1):
            temp = dp[j]       # dp[i-1][j]
            cost = 0 if s[i-1] == t[j-1] else 1
            dp[j] = min(
                dp[j-1] + 1,  # insert
                dp[j] + 1,    # delete
                diag + cost   # replace or match
            )
            diag = temp
    return dp[n]

print(edit_dist_opt('horse', 'ros'))  # 3
print(edit_dist_opt('intention', 'execution'))  # 5

أقل مجموع لمسار باستخدام مساحة O(n)

في مسألة أقل مجموع لمسار على شبكة، تبدأ المصفوفة المتحركة أحادية البعد بمجاميع البادئات للصف الأول (إذ توجد طريقة واحدة فقط للوصول إلى كل خلية في الصف الأول). لكل صف لاحق، يُحدَّث الصف من اليسار إلى اليمين: تكون قيمة dp[j] قبل تحديثها هي قيمة الصف السابق (dp[i-1][j])، بينما تكون dp[j-1] التي حُدِّثت للتو هي قيمة الخلية اليسرى. لا نحتاج إلى الخلية القطرية هنا، لأن مسألة أقل مجموع لمسار لا تتطلبها.

def min_path_sum_opt(grid):
    m, n = len(grid), len(grid[0])
    dp = [float('inf')] * n
    dp[0] = 0
    for i in range(m):
        # Update first column (only from above)
        dp[0] += grid[i][0]
        for j in range(1, n):
            # min of above (dp[j] = old) and left (dp[j-1] = updated)
            dp[j] = grid[i][j] + min(dp[j], dp[j-1])
    return dp[n-1]

grid = [[1,3,1],[1,5,1],[4,2,1]]
print(min_path_sum_opt(grid))  # 7

متى يلزم الوصول إلى الخلية القطرية

لا يمكن ضغط جميع مسائل DP ثنائية الأبعاد باستخدام مصفوفة متحركة بسيطة، لأن بعضها يحتاج إلى العنصر القطري dp[i-1][j-1] بعد استبدال قيمة dp[j]. والحل دائمًا واحد: احفظ temp = dp[j] قبل تحديثها، واستخدمها بصفتها diag عند حساب العمود التالي. يتعامل هذا الحفظ المسبق لقيمة خلية واحدة مع علاقات التكرار ذات الاتجاهات الثلاثة، مثل LCS ومسافة التحرير، بطريقة واضحة.

# Recap: the diagonal save pattern
# Without it: dp[j-1] updated (left) and dp[j] about to be overwritten
# With it:

def show_diagonal_pattern(s1, s2):
    n = len(s2)
    dp = [0] * (n + 1)
    for ch1 in s1:
        diag = 0  # was dp[i-1][0] = 0 for LCS
        for j, ch2 in enumerate(s2, 1):
            temp = dp[j]  # SAVE before overwrite
            if ch1 == ch2:
                dp[j] = diag + 1  # use saved diagonal
            else:
                dp[j] = max(dp[j], dp[j-1])
            diag = temp  # advance diagonal
    return dp[n]

print(show_diagonal_pattern('ABCBDAB', 'BDCABA'))  # 4

تحسين استخدام الذاكرة في حقيبة الظهر ثنائية الأبعاد

تستفيد مسألة حقيبة الظهر 0/1 أيضًا من تحسين استخدام الذاكرة. أبعاد الجدول الكامل ثنائي الأبعاد هي (n_items+1) × (capacity+1). وتختزل المصفوفة المتحركة ذلك إلى O(capacity). والفرق المهم عن LCS ومسافة التحرير هو: التكرار على بُعد السعة بالعكس (من القيمة العليا إلى الدنيا). يضمن ذلك احتساب كل عنصر مرة واحدة على الأكثر — أما التكرار إلى الأمام فيسمح باختيار العنصر نفسه عدة مرات.

def knapsack_01(weights, values, capacity):
    dp = [0] * (capacity + 1)
    for w, v in zip(weights, values):
        # Reverse order: prevents using the same item twice
        for c in range(capacity, w - 1, -1):
            dp[c] = max(dp[c], dp[c - w] + v)
    return dp[capacity]

weights = [1, 3, 4, 5]
values  = [1, 4, 5, 7]
cap = 7
print(knapsack_01(weights, values, cap))  # 9 (items 3+4: weight 3+4=7, value 4+5=9)

التكرار إلى الأمام مقابل التكرار العكسي

من الضروري معرفة الاتجاه الذي يجب اتباعه في الحلقة الداخلية: العكسي في حقيبة الظهر 0/1 (يُستخدم كل عنصر مرة واحدة على الأكثر — فالاعتماد على الحالات السابقة يمنع إعادة استخدامه). وإلى الأمام في حقيبة الظهر غير المحدودة (يمكن إعادة استخدام كل عنصر — فالاعتماد على الحالات التي حُدِّثت بالفعل يسمح باستخدامه عدة مرات). يؤدي اختيار الاتجاه الخطأ بصمت إلى تحويل المسألة من 0/1 إلى غير محدودة أو العكس. يجب دائمًا التحقق من القيد قبل اختيار الاتجاه.

# 0/1 Knapsack: each item used AT MOST ONCE → iterate reverse
def knapsack_01_demo(weights, values, cap):
    dp = [0] * (cap + 1)
    for w, v in zip(weights, values):
        for c in range(cap, w-1, -1):  # REVERSE
            dp[c] = max(dp[c], dp[c-w] + v)
    return dp[cap]

# Unbounded Knapsack: items can be reused → iterate forward
def knapsack_unbounded(weights, values, cap):
    dp = [0] * (cap + 1)
    for c in range(1, cap + 1):
        for w, v in zip(weights, values):
            if c >= w:
                dp[c] = max(dp[c], dp[c-w] + v)  # FORWARD
    return dp[cap]

print(knapsack_01_demo([2,3],[3,4],5))     # 7
print(knapsack_unbounded([2,3],[3,4],5))   # 8 (use weight-2 twice: 3+3=6? or 4+... )

المسارات الفريدة باستخدام مساحة O(n)

في مسألة المسارات الفريدة، يمكن استبدال الجدول بأكمله بصف واحد. تُهيَّأ جميع الخلايا بالقيمة 1 (وهو الصف الأول). في كل صف لاحق، يُحدَّث الصف من اليسار إلى اليمين: dp[j] += dp[j-1]. لا نحتاج إلى الخلية القطرية، لأن علاقة التكرار تستخدم فقط الخلية الموجودة أعلى الخلية الحالية (dp[j]، وقيمتها الحالية قبل التحديث) والخلية الموجودة إلى اليسار (dp[j-1]، التي حُدِّثت بالفعل). وهذا أبسط ضغط من 2D إلى 1D.

def unique_paths_opt(m, n):
    dp = [1] * n  # first row: all 1s
    for i in range(1, m):
        for j in range(1, n):
            dp[j] += dp[j-1]  # above (dp[j]) + left (dp[j-1])
    return dp[n-1]

# With obstacles
def unique_paths_obstacles_opt(grid):
    m, n = len(grid), len(grid[0])
    dp = [0] * n
    dp[0] = 1
    for i in range(m):
        if grid[i][0] == 1: dp[0] = 0  # blocked column
        for j in range(1, n):
            if grid[i][j] == 1: dp[j] = 0  # blocked
            else: dp[j] += dp[j-1]
    return dp[n-1]

print(unique_paths_opt(3, 7))  # 28
print(unique_paths_obstacles_opt([[0,0,0],[0,1,0],[0,0,0]]))  # 2

مخزن مؤقت من صفين لعلاقات التكرار المعقدة

عندما تحتاج علاقة التكرار إلى خلايا من صفين سابقين أو أكثر (مثل بعض تنويعات DP الخاصة بالفترات أو اختزالات DP ثلاثية الأبعاد)، يُستخدم مخزن مؤقت من صفين: تُحافَظ على مصفوفتَي prev وcurr، ثم تُبدَّلان بعد كل صف. يوفر ذلك مساحة مقدارها O(2n) = O(n). وإذا كانت علاقة التكرار تعتمد على k صفوف سابقة، فتُحافَظ على k مصفوفات في مخزن دائري. وهذا تعميم لنمط المصفوفة المتحركة ذات الصف الواحد.

def lcs_two_row_buffer(s1, s2):
    m, n = len(s1), len(s2)
    prev = [0] * (n + 1)  # dp[i-1]
    curr = [0] * (n + 1)  # dp[i]
    for i in range(1, m + 1):
        curr[0] = 0
        for j in range(1, n + 1):
            if s1[i-1] == s2[j-1]:
                curr[j] = prev[j-1] + 1
            else:
                curr[j] = max(prev[j], curr[j-1])
        prev, curr = curr, prev  # swap (curr becomes prev)
    return prev[n]  # after swap, prev holds the last computed row

print(lcs_two_row_buffer('ABCBDAB', 'BDCABA'))  # 4

متى لا يكون تحسين استخدام الذاكرة ممكنًا

لا يكون تحسين استخدام الذاكرة ممكنًا دائمًا. فإذا كنتم بحاجة إلى إعادة بناء الحل الأمثل (وليس قيمته فقط)، فستحتاجون عمومًا إلى الجدول الكامل لإجراء التتبع العكسي. وتشمل الحلول البديلة: (1) تخزين جدول قرارات منفصل بالحجم نفسه. (2) استخدام خوارزمية Hirschberg، التي تحسب LCS في زمن O(mn) وبمساحة O(min(m,n))، بما في ذلك إعادة البناء، وذلك بتقسيم المسألة تكراريًا عند نقطة المنتصف. (3) قبول استخدام مساحة O(mn) عند الحاجة إلى إعادة البناء.

# When reconstruction needed: must keep full table or use Hirschberg
# Hirschberg's idea: compute LCS length in O(n) space at midpoint of s1,
# recurse on left and right halves. O(mn) time, O(n) space + reconstruction.

# For interview: mention the trade-off
# 'I can reduce to O(n) space if only the value is needed.
#  To also reconstruct the sequence, I need the full O(mn) table
#  or a more complex divide-and-conquer approach.'

print('Space opt: O(n) for length only')
print('Full table: O(mn) needed for reconstruction')

تحقق سريع

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

مراجعة الدرس

في هذا الدرس تعلّمتم: يمكن ضغط جداول DP ثنائية الأبعاد إلى مساحة O(n) باستخدام مصفوفة أحادية البعد متحركة عندما لا تكون هناك حاجة إلا إلى الصف السابق، ويتعامل نمط متغير القطر (حفظ temp قبل الكتابة فوق القيمة) مع علاقات التكرار التي تحتاج إلى dp[i-1][j-1]، ويُجرى التكرار على السعة بالعكس في حقيبة الظهر 0/1، بينما يُجرى إلى الأمام في حقيبة الظهر غير المحدودة. بعد ذلك سندرس قالب التراجع: Choose، Explore، Unchoose — وهو أساس خوارزميات البحث الشامل.

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

هل درس «تحسين المساحة في DP ثنائي الأبعاد» مجاني؟

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

ماذا ستتعلم في «تحسين المساحة في DP ثنائي الأبعاد»؟

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

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

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

كم من الوقت يستغرق درس «تحسين المساحة في DP ثنائي الأبعاد»؟

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

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

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

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

  1. المسارات الفريدة وأقل مجموع مسار في الشبكات
  2. أطول تتابع مشترك
  3. مسافة التحرير (Levenshtein)
  4. تحسين المساحة في DP ثنائي الأبعاد
← العودة إلى Coding Interview Prep