0Pricing
Coding Interview Prep · درس

فك الترميز واحتساب المسارات

حل decode-ways، أي ربط الأرقام بالحروف، باستخدام DP شبيه بـ Fibonacci، ثم احسب المسارات في سلم ذي أحجام خطوات متغيرة

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

مسألة Decode Ways

تربط مسألة Decode Ways (LeetCode 91) سلسلة من الأرقام بالمحارف: 'A'=1، و'B'=2، ...، و'Z'=26. بالنظر إلى سلسلة أرقام مُرمّزة، احسبوا عدد الطرق المختلفة لفك ترميزها. فمثلًا، يمكن فك ترميز '12' إلى 'AB' (1+2) أو 'L' (12)، مما يعطي طريقتين. ويمكن أن تكون '226' هي 'BZ' (2+26)، أو 'VF' (22+6)، أو 'BBF' (2+2+6)، مما يعطي 3 طرق. تجعل الأصفار البادئة بعض عمليات فك الترميز غير صالحة.

# Encoding: A=1, B=2, ..., Z=26
# '12' → 'AB' or 'L' → 2 ways
# '226' → 'BZ' or 'VF' or 'BBF' → 3 ways
# '06' → invalid (no letter for '0')
# '10' → 'J' only → 1 way (only valid as 10, not 1+0)

s = '226'
print('Decodings for', s, ':', 3)  # Expected: 3

صياغة البرمجة الديناميكية لمسألة فك الترميز

لتكن dp[i] مساوية لعدد طرق فك ترميز s[:i]. الحالات الأساسية: dp[0] = 1 (السلسلة الفارغة لها طريقة واحدة)، وdp[1] = 1 إذا كان s[0] != '0'، وإلا فقيمتها 0. في الانتقال: إذا كان s[i-1] != '0'، فأضف dp[i-1] (فك ترميز مكوّن من رقم واحد). وإذا كان 10 ≤ int(s[i-2:i]) ≤ 26، فأضف dp[i-2] (فك ترميز مكوّن من رقمين). وهذا في جوهره نمط فيبوناتشي مع فحوصات للصلاحية.

def num_decodings(s):
    n = len(s)
    dp = [0] * (n + 1)
    dp[0] = 1  # empty prefix
    dp[1] = 0 if s[0] == '0' else 1
    
    for i in range(2, n + 1):
        # Single digit decode
        if s[i-1] != '0':
            dp[i] += dp[i-1]
        # Two digit decode
        two_digit = int(s[i-2:i])
        if 10 <= two_digit <= 26:
            dp[i] += dp[i-2]
    return dp[n]

print(num_decodings('12'))   # 2
print(num_decodings('226'))  # 3
print(num_decodings('06'))   # 0

فخ الصفر البادئ

أصعب جزء في فك الترميز هو التعامل مع الأصفار. لا يمكن فك ترميز الصفر المنفرد '0' (فلا يوجد حرف يقابل 0)، لذلك إذا كان s[i-1] == '0'، فلا تضف dp[i-1]. أما '0' باعتباره الرقم الثاني فلا يكون صالحًا إلا إذا كان العدد المكوّن من رقمين هو 10 أو 20. أما '30' أو '40' (والأعداد الأكبر) فهي غير صالحة لأنها تتجاوز 26. احرص دائمًا على التحقق من 10 ≤ two_digit ≤ 26، وليس من two_digit ≤ 26 فقط.

def num_decodings(s):
    if not s or s[0] == '0': return 0
    n = len(s)
    dp = [0] * (n + 1)
    dp[0] = 1
    dp[1] = 1  # s[0] != '0' guaranteed by guard above
    for i in range(2, n + 1):
        one = int(s[i-1])
        two = int(s[i-2:i])
        if one != 0: dp[i] += dp[i-1]  # valid single digit
        if 10 <= two <= 26: dp[i] += dp[i-2]  # valid two digits
    return dp[n]

print(num_decodings('10'))   # 1 (only 'J')
print(num_decodings('30'))   # 0 (30 > 26, '0' alone invalid)
print(num_decodings('100'))  # 0 (dp[2]=1 then '00' invalid, single '0' invalid)

فك الترميز بمساحة محسّنة

مثل متتالية فيبوناتشي، لا تنظر علاقة تكرار عدد طرق فك الترميز إلا إلى موضعين سابقين، لذلك يمكنك تقليل المساحة من O(n) إلى O(1) باستخدام متغيرين. استخدم prev2 (القيمة التي تسبق بخطوتين) وprev1 (القيمة التي تسبق بخطوة واحدة). في كل خطوة، احسب curr اعتمادًا عليهما ثم أزح القيم. وهذا مطابق لتحسين فيبوناتشي باستخدام متغيرين.

def num_decodings_o1(s):
    if not s or s[0] == '0': return 0
    prev2 = 1  # dp[0]
    prev1 = 1  # dp[1]
    for i in range(2, len(s) + 1):
        curr = 0
        if s[i-1] != '0':
            curr += prev1
        two = int(s[i-2:i])
        if 10 <= two <= 26:
            curr += prev2
        prev2, prev1 = prev1, curr
    return prev1

print(num_decodings_o1('226'))   # 3
print(num_decodings_o1('12'))    # 2
print(num_decodings_o1('0'))     # 0

عدّ مسارات الدرج

تسأل مسألة تسلّق الدرج (LeetCode 70): كم طريقة يمكن بها صعود n درجة إذا كان مسموحًا بأخذ درجة واحدة أو درجتين في كل مرة؟ هذه هي بالضبط متتالية فيبوناتشي: ways(n) = ways(n-1) + ways(n-2). ‏ways(1)=1، وways(2)=2، وways(3)=3، وways(4)=5. وتُعمَّم الفكرة عندما يكون مسموحًا بأخذ ما يصل إلى k درجات: ways(n) = sum(ways(n-1), ..., ways(n-k)).

def climb_stairs(n):
    if n <= 2: return n
    prev2, prev1 = 1, 2
    for _ in range(3, n + 1):
        prev2, prev1 = prev1, prev1 + prev2
    return prev1

for i in range(1, 8):
    print(f'climb_stairs({i}) = {climb_stairs(i)}')
# 1, 2, 3, 5, 8, 13, 21 — Fibonacci!

تسلّق الدرج بخطوات متغيرة

عندما يكون مسموحًا بأخذ أي عدد من الخطوات من مجموعة محددة (مثل {1, 3, 5})، تصبح علاقة التكرار dp[i] = sum(dp[i-k] for k in steps if i-k >= 0). استخدم نافذة منزلقة بحجم max(steps) لتحسين كفاءة الذاكرة. وهذه هي نسخة العدّ من مسألة حقيبة الظهر غير المحدودة، إذ يمكن استخدام كل حجم خطوة أي عدد من المرات.

def count_ways(n, steps):
    dp = [0] * (n + 1)
    dp[0] = 1  # one way to stay at ground
    for i in range(1, n + 1):
        for step in steps:
            if i >= step:
                dp[i] += dp[i - step]
    return dp[n]

# Steps of 1 or 2 (classic climbing stairs)
print(count_ways(5, [1, 2]))    # 8
# Steps of 1, 3, or 5
print(count_ways(5, [1, 3, 5])) # 5
# Steps of 2 or 3
print(count_ways(6, [2, 3]))    # 3 (2+2+2, 3+3, 2+4-invalid, 2+2+2, 3+3, 3+2+1-no...)

أقل تكلفة لصعود الدرج

تربط مسألة أقل تكلفة لصعود الدرج (LeetCode 746) تكلفة بكل درجة، وتطلب حساب أقل تكلفة للوصول إلى القمة. من الدرجة i يمكنك القفز إلى i+1 أو i+2. علاقة التكرار هي dp[i] = cost[i] + min(dp[i-1], dp[i-2]). يمكنك البدء من الدرجة 0 أو الدرجة 1. والإجابة هي min(dp[n-1], dp[n-2]).

def min_cost_climbing(cost):
    n = len(cost)
    if n == 1: return cost[0]
    dp = [0] * n
    dp[0] = cost[0]
    dp[1] = cost[1]
    for i in range(2, n):
        dp[i] = cost[i] + min(dp[i-1], dp[i-2])
    return min(dp[-1], dp[-2])  # can start from step 0 or 1

print(min_cost_climbing([10, 15, 20]))      # 15
print(min_cost_climbing([1, 100, 1, 1, 1, 100, 1, 1, 100, 1]))  # 6

فك الترميز II: رقم البدل

تضيف مسألة فك الترميز II (LeetCode 639) محرف بدل '*' يمكن أن يمثّل أي رقم من 1 إلى 9. وهذا يزيد عدد طرق فك الترميز الصالحة زيادة كبيرة. يساهم '*' المفرد في 9 طرق، إذ يمكن أن يمثّل أي رقم من 1 إلى 9. ويمكن لرمزي '*' معًا تكوين 9×9 تركيبة من رقمين، لكن التركيبات التي ≤ 26 فقط تكون صالحة (من 11 إلى 19 = 9 طرق، ومن 21 إلى 26 = 6 طرق، أي 15 طريقة لـ '**'). ويتطلب ذلك تحليلًا دقيقًا للحالات.

def num_decodings_ii(s):
    MOD = 10**9 + 7
    prev2, prev1 = 1, 9 if s[0] == '*' else (0 if s[0] == '0' else 1)
    for i in range(1, len(s)):
        curr = 0
        c, p = s[i], s[i-1]
        # Single digit
        if c == '*': curr += 9 * prev1
        elif c != '0': curr += prev1
        # Two digits
        if p == '*' and c == '*': curr += 15 * prev2  # 11-19(9) + 21-26(6)
        elif p == '*': curr += (2 if c <= '6' else 1) * prev2
        elif c == '*': curr += (9 if p == '1' else (6 if p == '2' else 0)) * prev2
        else:
            two = int(p + c)
            if 10 <= two <= 26: curr += prev2
        prev2, prev1 = prev1, curr % MOD
    return prev1 % MOD

print(num_decodings_ii('*'))   # 9
print(num_decodings_ii('1*'))  # 18

الصلة بمتتالية فيبوناتشي

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

# Fibonacci family: dp[i] = f(dp[i-1], dp[i-2])
# Fibonacci itself:        dp[i] = dp[i-1] + dp[i-2]
# Climbing stairs:         dp[i] = dp[i-1] + dp[i-2]
# Decode ways:             dp[i] = (dp[i-1] if one_valid) + (dp[i-2] if two_valid)
# Min cost stairs:         dp[i] = cost[i] + min(dp[i-1], dp[i-2])
# House robber:            dp[i] = max(dp[i-1], nums[i] + dp[i-2])

# All solved with 2 rolling variables:
prev2, prev1 = 0, 1
for _ in range(10):
    prev2, prev1 = prev1, prev1 + prev2
print('Fibonacci F(10):', prev1)  # 89

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

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

def unique_paths(m, n):
    dp = [[1] * n for _ in range(m)]
    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]

# Or use math for O(1) solution
import math
def unique_paths_math(m, n):
    return math.comb(m + n - 2, m - 1)

print(unique_paths(3, 7))         # 28
print(unique_paths_math(3, 7))    # 28
print(unique_paths(3, 3))         # 6

ملخص أخطاء المقابلات الشائعة

من الأخطاء الشائعة في فك الترميز: (1) نسيان أن '0' وحده غير صالح، لذا تحقّق دائمًا من s[i-1] != '0' قبل إضافة dp[i-1]. (2) استخدام two_digit <= 26 دون التحقق من two_digit >= 10، إذ لا ينبغي فك '07' إلى 'G'. (3) إرجاع dp[n-1] بدلًا من dp[n]، فالجدول مفهرس بدءًا من 1، ولذلك يمثّل dp[n] السلسلة كاملة. تحقّق دائمًا من فهارس المصفوفة عندما يحتوي جدول DP على عنصر واحد أكثر من المدخل.

# Common bug: checking two_digit <= 26 without >= 10
def buggy_decode(s):
    dp = [0] * (len(s) + 1)
    dp[0] = dp[1] = 1
    for i in range(2, len(s) + 1):
        if s[i-1] != '0': dp[i] += dp[i-1]
        two = int(s[i-2:i])
        # BUG: '07' gives two=7, and 7 <= 26 would add dp[i-2]
        # Fix: require two >= 10
        if 10 <= two <= 26: dp[i] += dp[i-2]  # CORRECT
    return dp[len(s)]

print(buggy_decode('06'))   # 0 (correct, '0' alone invalid)
print(buggy_decode('07'))   # 0 (correct, '07' not valid, '0' alone invalid)
print(buggy_decode('27'))   # 1 (only 'BG', 27>26 so no two-digit)

اختبار سريع

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

مراجعة الدرس

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

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

هل درس «فك الترميز واحتساب المسارات» مجاني؟

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

ماذا ستتعلم في «فك الترميز واحتساب المسارات»؟

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

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

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

كم من الوقت يستغرق درس «فك الترميز واحتساب المسارات»؟

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

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

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

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

  1. لص المنازل: علاقة تكرار الأخذ أو التخطي
  2. المصفوفة الجزئية العظمى والمصفوفة الجزئية ذات حاصل الضرب الأقصى
  3. تقسيم الكلمات وتقسيم السلسلة
  4. فك الترميز واحتساب المسارات
← العودة إلى Coding Interview Prep