0Pricing
Coding Interview Prep · درس

المسارات الفريدة وأقل مجموع مسار في الشبكات

املأ جدول DP ثنائي الأبعاد للمسارات الفريدة مع العوائق وبدونها، ثم عدّله لتقليل مجموع القيم على طول المسار

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

المسارات الفريدة على شبكة

تسأل مسألة المسارات الفريدة (LeetCode 62): في شبكة m×n، كم عدد المسارات المختلفة من الزاوية العلوية اليسرى إلى الزاوية السفلية اليمنى إذا كان مسموحًا بالحركة إلى اليمين أو إلى الأسفل فقط؟ في شبكة بحجم 3×7، تكون الإجابة 28. والفكرة الأساسية هي أن كل مسار يصل إلى الخلية (i,j) لا بد أن يأتي إما من (i-1,j) (أعلى الخلية) أو من (i,j-1) (يسارها)، مما يعطي صياغة طبيعية لـ DP ثنائي الأبعاد.

# 3x7 grid: robot starts at (0,0), goes to (2,6)
# Must make exactly 2 down-moves and 6 right-moves
# Total moves = 8, choose 2 for down = C(8,2) = 28
import math
print('Unique paths 3x7:', math.comb(3+7-2, 3-1))  # 28
print('Unique paths 3x3:', math.comb(3+3-2, 3-1))  # 6
print('Unique paths 2x2:', math.comb(2+2-2, 2-1))  # 2

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

عرّف dp[i][j] بأنه عدد المسارات التي تصل إلى الخلية (i,j). تكون جميع قيم الصف الأول والعمود الأول مساوية لـ 1، إذ لا توجد إلا طريقة واحدة للوصول إلى أي خلية في الصف العلوي أو العمود الأيسر. أما الخلايا الأخرى فتعتمد على العلاقة: dp[i][j] = dp[i-1][j] + dp[i][j-1]. املأ الجدول صفًا بعد صف، وتكون الإجابة dp[m-1][n-1]. التعقيد الزمني هو O(m×n)، وتعقيد المساحة هو O(m×n)، ويمكن اختزاله إلى O(n).

def unique_paths(m, n):
    dp = [[1] * n for _ in range(m)]
    # First row and column stay as 1s (base cases)
    for i in range(1, m):
        for j in range(1, n):
            dp[i][j] = dp[i-1][j] + dp[i][j-1]
    return dp[m-1][n-1]

print(unique_paths(3, 7))  # 28
print(unique_paths(3, 3))  # 6
print(unique_paths(1, 1))  # 1 (already at destination)

تحسين المساحة إلى O(n)

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

def unique_paths_1d(m, n):
    dp = [1] * n  # initial row: all 1s
    for i in range(1, m):
        for j in range(1, n):
            dp[j] += dp[j-1]  # dp[j] was dp[i-1][j], dp[j-1] is dp[i][j-1]
    return dp[n-1]

print(unique_paths_1d(3, 7))  # 28
print(unique_paths_1d(3, 3))  # 6

# Or use math for O(1)
import math
print(math.comb(3+7-2, 3-1))  # 28

المسارات الفريدة II: العوائق

تضيف مسألة المسارات الفريدة II (LeetCode 63) عوائق (خلايا تحمل القيمة 1) إلى الشبكة. ويكون أي مسار يمر عبر عائق غير صالح، لذا تكون dp[i][j] = 0 إذا كانت obstacle[i][j] == 1. وإلا فتبقى علاقة التكرار كما هي: dp[i][j] = dp[i-1][j] + dp[i][j-1]. وإذا كانت نقطة البداية أو النهاية محجوبة، تكون الإجابة 0 مباشرةً. هيّئ الحالات الأساسية بعناية؛ فبمجرد ظهور 1 في الصف الأول أو العمود الأول، تصبح جميع الخلايا اللاحقة في ذلك الصف أو العمود مساوية لـ 0.

def unique_paths_with_obstacles(obstacle_grid):
    m, n = len(obstacle_grid), len(obstacle_grid[0])
    dp = [[0] * n for _ in range(m)]
    # First row
    for j in range(n):
        if obstacle_grid[0][j] == 1: break
        dp[0][j] = 1
    # First column
    for i in range(m):
        if obstacle_grid[i][0] == 1: break
        dp[i][0] = 1
    for i in range(1, m):
        for j in range(1, n):
            if obstacle_grid[i][j] == 0:
                dp[i][j] = dp[i-1][j] + dp[i][j-1]
    return dp[m-1][n-1]

grid = [[0,0,0],[0,1,0],[0,0,0]]
print(unique_paths_with_obstacles(grid))  # 2

مسألة مجموع المسار الأدنى

تسأل مسألة مجموع المسار الأدنى (LeetCode 64): إذا كانت لديك شبكة m×n مملوءة بأعداد غير سالبة، فما المسار من الزاوية العلوية اليسرى إلى الزاوية السفلية اليمنى الذي يقلّل مجموع جميع الأعداد على المسار، مع السماح بالحركة إلى اليمين أو إلى الأسفل فقط؟ على سبيل المثال، في [[1,3,1],[1,5,1],[4,2,1]] يعطي المسار 1→3→1→1→1 المجموع 7. وتكون حالة DP مماثلة لحالة المسارات الفريدة، لكن علاقة التكرار تستخدم القيمة الدنيا بدلًا من الجمع.

grid = [[1, 3, 1],
        [1, 5, 1],
        [4, 2, 1]]
# Optimal path: (0,0)→(0,1)→(0,2)→(1,2)→(2,2)
# Values:        1  +  3  +  1  +  1  +  1  = 7
print('Expected minimum path sum:', 7)

تنفيذ DP لمجموع المسار الأدنى

عرّف dp[i][j] بأنه أقل تكلفة للوصول إلى الخلية (i,j). الحالة الأساسية: dp[0][0] = grid[0][0]. الصف الأول: dp[0][j] = dp[0][j-1] + grid[0][j] (الطريقة الوحيدة للوصول هي من اليسار). العمود الأول: dp[i][0] = dp[i-1][0] + grid[i][0] (الطريقة الوحيدة للوصول هي من الأعلى). الحالة العامة: dp[i][j] = grid[i][j] + min(dp[i-1][j], dp[i][j-1]). وهذا تطبيق مباشر لمبدأ الأمثلية.

def min_path_sum(grid):
    m, n = len(grid), len(grid[0])
    dp = [[0]*n for _ in range(m)]
    dp[0][0] = grid[0][0]
    for j in range(1, n):  # first row
        dp[0][j] = dp[0][j-1] + grid[0][j]
    for i in range(1, m):  # first column
        dp[i][0] = dp[i-1][0] + grid[i][0]
    for i in range(1, m):
        for j in range(1, n):
            dp[i][j] = grid[i][j] + min(dp[i-1][j], dp[i][j-1])
    return dp[m-1][n-1]

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

مجموع المسار الأدنى في مكانه

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

def min_path_sum_inplace(grid):
    m, n = len(grid), len(grid[0])
    # Mutate in place
    for i in range(m):
        for j in range(n):
            if i == 0 and j == 0: continue
            if i == 0:
                grid[i][j] += grid[i][j-1]
            elif j == 0:
                grid[i][j] += grid[i-1][j]
            else:
                grid[i][j] += min(grid[i-1][j], grid[i][j-1])
    return grid[m-1][n-1]

import copy
grid = [[1,3,1],[1,5,1],[4,2,1]]
print(min_path_sum_inplace(copy.deepcopy(grid)))  # 7

مجموع المسار الأدنى في مثلث

تطلب مسألة المثلث (LeetCode 120) حساب أقل مجموع لمسار من أعلى المثلث إلى أسفله في مصفوفة مثلثية، حيث تنتقل كل خطوة إلى عدد مجاور في الصف التالي. ويُعد DP من الأسفل إلى الأعلى أبسط حل: ابدأ من الصف قبل الأخير، ولكل خلية أضف أصغر قيمة من الخليتين الموجودتين أسفلها مباشرةً. وتجنّب هذه الطريقة تتبّع فهارس البداية، كما ترفع الإجابة طبيعيًا إلى قمة المثلث.

def minimum_total(triangle):
    # Bottom-up: start from second-to-last row
    dp = triangle[-1][:]  # copy of bottom row
    for row in range(len(triangle) - 2, -1, -1):
        for col in range(len(triangle[row])):
            dp[col] = triangle[row][col] + min(dp[col], dp[col+1])
    return dp[0]

triangle = [
    [2],
    [3, 4],
    [6, 5, 7],
    [4, 1, 8, 3]
]
print(minimum_total(triangle))  # 11 (2+3+5+1)

البرمجة الديناميكية على شبكة الزنزانة

تطلب مسألة لعبة الزنزانة (LeetCode 174) حساب أقل صحة ابتدائية لازمة لإنقاذ أميرة في الزاوية السفلية اليمنى لشبكة تحتوي على خلايا سالبة (ضرر) وموجبة (شفاء). يجب التحرك إلى اليمين أو إلى الأسفل. والحيلة هي ملء جدول DP بالعكس (من الزاوية السفلية اليمنى إلى الزاوية العلوية اليسرى)، وحساب الحد الأدنى للصحة اللازمة في كل خلية. في كل خلية: dp[i][j] = max(1, min(dp[i+1][j], dp[i][j+1]) - dungeon[i][j]). ويجب أن تبقى الصحة دائمًا 1 على الأقل.

def calculate_minimum_hp(dungeon):
    m, n = len(dungeon), len(dungeon[0])
    dp = [[0]*n for _ in range(m)]
    # Fill from bottom-right
    dp[m-1][n-1] = max(1, 1 - dungeon[m-1][n-1])
    for i in range(m-2, -1, -1):  # last column
        dp[i][n-1] = max(1, dp[i+1][n-1] - dungeon[i][n-1])
    for j in range(n-2, -1, -1):  # last row
        dp[m-1][j] = max(1, dp[m-1][j+1] - dungeon[m-1][j])
    for i in range(m-2, -1, -1):
        for j in range(n-2, -1, -1):
            need = min(dp[i+1][j], dp[i][j+1])
            dp[i][j] = max(1, need - dungeon[i][j])
    return dp[0][0]

dungeon = [[-2,-3,3],[-5,-10,1],[10,30,-5]]
print(calculate_minimum_hp(dungeon))  # 7

مقارنة مسائل DP على الشبكات

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

# Summary: Grid DP Patterns
#
# Problem          Fill Dir   Transition
# Unique Paths     top-left   dp[i][j] = dp[i-1][j] + dp[i][j-1]
# Unique Paths II  top-left   same but 0 if obstacle
# Min Path Sum     top-left   dp[i][j] = grid[i][j] + min(above, left)
# Triangle         bottom-up  dp[col] = row[col] + min(dp[col], dp[col+1])
# Dungeon          bottom-right max(1, min(right, down) - cell)

# Recognise the pattern, write the transition, verify with examples
print('Grid DP summary complete')

ملخص التعقيد لمسائل DP على الشبكات

تعمل جميع مسائل DP على الشبكات هنا في زمن O(m×n). وتتراوح المساحة من O(m×n) عند استخدام جدول كامل، إلى O(n) باستخدام مصفوفة متدحرجة أحادية البعد، وإلى O(1) كمساحة إضافية عندما يمكن تعديل الشبكة في مكانها. في المقابلات، اذكر تحسين المساحة إلى O(n) بعد عرض حل O(m×n)، فهذا يبيّن إدراكك للمفاضلات. وفكّر أيضًا في جميع المسائل فيما إذا كان هناك اختصار جشع ممكن، مثل الصيغة الرياضية للمسارات الفريدة.

# O(n) space version of Min Path Sum
def min_path_sum_1d(grid):
    m, n = len(grid), len(grid[0])
    dp = [float('inf')] * n
    dp[0] = 0
    for i in range(m):
        dp[0] += grid[i][0]  # first column: only from above
        for j in range(1, n):
            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_1d(grid))  # 7

اختبار سريع

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

مراجعة الدرس

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

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

هل درس «المسارات الفريدة وأقل مجموع مسار في الشبكات» مجاني؟

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

ماذا ستتعلم في «المسارات الفريدة وأقل مجموع مسار في الشبكات»؟

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

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

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

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

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

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

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

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

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