0Pricing
DSA Interview Prep · درس

التبديلات والتوافيق

عدّد جميع تبديلات قائمة، مع وجود عناصر مكررة ومن دونها، وولّد جميع التوافيق ذات الحجم k ومتغيرات مجموع التوافيق.

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

التباديل مقابل التوافيق

التباديل هي ترتيبات يكون فيها الترتيب مهمًا: فـ [1,2,3] و[3,2,1] مختلفان. وعدد تباديل n من العناصر هو n!. أمّا التوافيق فهي اختيارات يكون فيها الترتيب غير مهم: فاختيار {1,2} يساوي اختيار {2,1}. وعدد التوافيق ذات الحجم k من بين n من العناصر هو C(n,k) = n! / (k! × (n-k)!). ويُعدّ كلا النمطين أساسيًا في مسائل المقابلات المتعلقة بالعدّ والتعداد والاختيار.

import math

# Permutations
n = 4
print(f'Permutations of {n} items: {math.factorial(n)}')
# 4! = 24

# Combinations
for k in range(n+1):
    print(f'C({n},{k}) = {math.comb(n,k)}')
# C(4,0)=1, C(4,1)=4, C(4,2)=6, C(4,3)=4, C(4,4)=1
# Sum = 2^4 = 16 (total subsets)

إنشاء جميع التباديل

استخدموا مصفوفة منطقية used لتتبّع العناصر الموجودة في المسار الحالي. في كل خطوة، جرّبوا كل عنصر غير مستخدم. وبعد استكشافه، أعيدوا تعيينه إلى غير مستخدم. بخلاف المجموعات الجزئية، لا يوجد فهرس start، لأن التباديل تستخدم العناصر بأي ترتيب. ينتهي الاستدعاء التعاودي عندما يتحقق len(path) == n.

def permutations(nums):
    result = []
    used = [False] * len(nums)
    def backtrack(path):
        if len(path) == len(nums):
            result.append(list(path))
            return
        for i, num in enumerate(nums):
            if not used[i]:
                used[i] = True         # CHOOSE
                path.append(num)
                backtrack(path)        # EXPLORE
                path.pop()             # UNCHOOSE
                used[i] = False
    backtrack([])
    return result

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

التباديل المعتمدة على التبديل

هناك بديل آخر: بدّلوا العنصر الموجود في الموضع start مع كل عنصر من start إلى n-1، ثم نفّذوا الاستدعاء التعاودي، وبعد ذلك أعيدوا التبديل. يعدّل هذا الأسلوب المصفوفة في مكانها دون استخدام مصفوفة used. والفكرة الأساسية هي أن كل ما يقع إلى يسار start يكون ثابتًا في كل مستوى، ونختار العنصر الذي سيوضع في الموضع start. هذا الأسلوب أكثر كفاءة قليلًا من حيث الذاكرة، وهو أساس خوارزمية Heap's algorithm.

def permutations_swap(nums):
    result = []
    def backtrack(start):
        if start == len(nums):
            result.append(list(nums))
            return
        for i in range(start, len(nums)):
            nums[start], nums[i] = nums[i], nums[start]  # CHOOSE (swap)
            backtrack(start + 1)                          # EXPLORE
            nums[start], nums[i] = nums[i], nums[start]  # UNCHOOSE (swap back)
    backtrack(0)
    return result

print(permutations_swap([1, 2, 3]))
# Same 6 permutations, different order

التباديل II: التعامل مع القيم المكررة

عندما تحتوي المدخلات على قيم مكررة، مثل [1, 1, 2]، ينشئ أسلوب مصفوفة used تباديل مكررة. والحل هو ترتيب المصفوفة، ثم تخطّي القيمة المكررة إذا لم تكن القيمة المطابقة السابقة مستخدمة في هذا الاستدعاء التعاودي. والشرط هو: if i > 0 and nums[i] == nums[i-1] and not used[i-1]: continue. ويضمن ذلك اختيار القيم المكررة دائمًا من اليسار إلى اليمين.

def permutations_unique(nums):
    nums.sort()
    result = []
    used = [False] * len(nums)
    def backtrack(path):
        if len(path) == len(nums):
            result.append(list(path))
            return
        for i in range(len(nums)):
            if used[i]: continue
            # Skip if this num is a duplicate and the previous dup was not used
            if i > 0 and nums[i] == nums[i-1] and not used[i-1]:
                continue
            used[i] = True
            path.append(nums[i])
            backtrack(path)
            path.pop()
            used[i] = False
    backtrack([])
    return result

print(permutations_unique([1, 1, 2]))
# [[1,1,2],[1,2,1],[2,1,1]] — 3, not 6

التبديل التالي (بالترتيب المعجمي)

تحوّل خوارزمية التبديل التالي (LeetCode 31) المصفوفة إلى التبديل الأكبر التالي معجميًا، مباشرةً داخل المصفوفة. الخوارزمية: (1) ابحثوا عن أقصى فهرس من اليمين i بحيث nums[i] < nums[i+1]. (2) ابحثوا عن أقصى فهرس من اليمين j بحيث nums[j] > nums[i]. (3) بدّلوا بين nums[i] وnums[j]. (4) اعكسوا الجزء اللاحق للفهرس i. إذا لم يوجد مثل هذا الفهرس i، فاعكسوا المصفوفة بأكملها، وبذلك تنتقل إلى أصغر تبديل.

def next_permutation(nums):
    n = len(nums)
    # Step 1: find rightmost i where nums[i] < nums[i+1]
    i = n - 2
    while i >= 0 and nums[i] >= nums[i+1]:
        i -= 1
    if i >= 0:
        # Step 2: find rightmost j where nums[j] > nums[i]
        j = n - 1
        while nums[j] <= nums[i]:
            j -= 1
        # Step 3: swap
        nums[i], nums[j] = nums[j], nums[i]
    # Step 4: reverse suffix after i
    nums[i+1:] = nums[i+1:][::-1]
    return nums

print(next_permutation([1, 2, 3]))  # [1,3,2]
print(next_permutation([3, 2, 1]))  # [1,2,3] (wraps)
print(next_permutation([1, 1, 5]))  # [1,5,1]

التراجع لتوليد التوافيق ذات الحجم k

أنشئوا جميع التوافيق المكوّنة من k من العناصر من بين n من العناصر (LeetCode 77). استخدموا فهرس بداية، كما في المجموعات الجزئية، لتجنب إعادة زيارة العناصر والحفاظ على الترتيب. نفّذوا التقليم عندما يتبقى عدد من العناصر أقل من k - len(path): if len(nums) - i + 1 < k - len(path): break. وهذا مكافئ للدالة combine(n, k) السابقة، لكنه يعمل على مصفوفة فعلية.

def combinations(nums, k):
    result = []
    def backtrack(start, path):
        if len(path) == k:
            result.append(list(path))
            return
        for i in range(start, len(nums)):
            # Pruning: not enough elements left
            if len(nums) - i < k - len(path):
                break
            path.append(nums[i])
            backtrack(i + 1, path)
            path.pop()
    backtrack(0, [])
    return result

print(combinations([1,2,3,4,5], 3))
# 10 combinations: C(5,3)
import math
print(math.comb(5,3))  # 10

مجموع التوافيق: إعادة استخدام غير محدودة

تسمح مسألة مجموع التوافيق (LeetCode 39) باستخدام كل عدد عددًا غير محدود من المرات. والفرق عن التوافيق العادية هو أنه بدلًا من نقل start إلى i+1، نمرّر i (الفهرس نفسه) للسماح بإعادة استخدام العنصر الحالي. أمّا التقليم فيكون كما يلي: إذا أصبح الهدف المتبقي 0، فسجّلوا المسار؛ وإذا أصبح سالبًا، فتوقّفوا. ويتيح ترتيب القيم الإنهاء المبكر عندما تتجاوز جميع العناصر المرشحة المتبقية الهدف المتبقي.

def combination_sum(candidates, target):
    candidates.sort()
    result = []
    def backtrack(start, path, remaining):
        if remaining == 0:
            result.append(list(path))
            return
        for i in range(start, len(candidates)):
            c = candidates[i]
            if c > remaining: break  # all remaining are too big
            path.append(c)
            backtrack(i, path, remaining - c)  # reuse allowed: pass i, not i+1
            path.pop()
    backtrack(0, [], target)
    return result

print(combination_sum([2, 3, 6, 7], 7))
# [[2,2,3],[7]]

مجموع التوافيق II: دون إعادة استخدام، مع قيم مكررة

تستخدم مسألة مجموع التوافيق II (LeetCode 40) كل عدد مرة واحدة على الأكثر، لكن قد تحتوي المدخلات على قيم مكررة. ويتطلب الحل الجمع بين تقنيتين: نقل start إلى i+1 (لمنع إعادة الاستخدام)، وتخطّي القيم المكررة في المستوى نفسه (if i > start and nums[i] == nums[i-1]: continue) بعد الترتيب. وهذا يجمع بين طريقة التعامل مع القيم المكررة من المجموعات الجزئية II وقيد منع إعادة الاستخدام من التوافيق.

def combination_sum_ii(candidates, target):
    candidates.sort()
    result = []
    def backtrack(start, path, remaining):
        if remaining == 0:
            result.append(list(path))
            return
        for i in range(start, len(candidates)):
            if candidates[i] > remaining: break
            # Skip duplicates at same level
            if i > start and candidates[i] == candidates[i-1]:
                continue
            path.append(candidates[i])
            backtrack(i + 1, path, remaining - candidates[i])  # no reuse: i+1
            path.pop()
    backtrack(0, [], target)
    return result

print(combination_sum_ii([10,1,2,7,6,1,5], 8))
# [[1,1,6],[1,2,5],[1,7],[2,6]]

تركيبات أحرف رقم الهاتف

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

def letter_combinations(digits):
    if not digits: return []
    phone = {
        '2': 'abc', '3': 'def', '4': 'ghi', '5': 'jkl',
        '6': 'mno', '7': 'pqrs', '8': 'tuv', '9': 'wxyz'
    }
    result = []
    def backtrack(index, path):
        if index == len(digits):
            result.append(''.join(path))
            return
        for letter in phone[digits[index]]:
            path.append(letter)
            backtrack(index + 1, path)
            path.pop()
    backtrack(0, [])
    return result

print(letter_combinations('23'))
# ['ad','ae','af','bd','be','bf','cd','ce','cf']

مقارنة التباديل والتوافيق

الفروق البنيوية الأساسية هي: التباديل — لا تستخدم فهرس بداية، بل تستخدم مصفوفة used أو التبديل لتجنب إعادة الاستخدام، وتوجد n اختيارات في كل مستوى، مع n! من الأوراق إجمالًا. التوافيق — تستخدم فهرس بداية لفرض الترتيب، مع C(n,k) من الأوراق. مجموع التوافيق — لا تقدّم فهرس البداية عند السماح بإعادة الاستخدام، وتنفّذ التقليم اعتمادًا على الهدف. إن مطابقة أي مسألة جديدة مع أحد هذه الأشكال الثلاثة تمنحكم القالب الصحيح مباشرةً.

# Pattern summary:
# Permutations: for i in range(n); if not used[i]; no start advancement
# Combinations: for i in range(start, n); advance start → i+1
# Combo Sum (reuse): for i in range(start, n); advance start → i (same)

# Quick reference:
import math
n = 5
print(f'Perm({n})   = n! = {math.factorial(n)}')
print(f'Comb({n},2) = C(n,k) = {math.comb(n,2)}')
print(f'Comb({n},3) = {math.comb(n,3)}')
# Also: subsets = sum(C(n,k) for k=0..n) = 2^n
print(f'Subsets({n}) = 2^n = {2**n}')

التعقيد ونصائح المقابلات

التعقيد الزمني للتعداد هو: التباديل O(n × n!)، والتوافيق O(k × C(n,k))، ومجموع التوافيق O(n^(T/min_val)). أمّا التعقيد المكاني فهو O(n) لعمق الاستدعاء التعاودي، إضافةً إلى O(output) لتخزين النتائج. نصائح أساسية: (1) وضّحوا دائمًا ما إذا كان الترتيب مهمًا (تباديل أم توافيق). (2) اذكروا طريقة التعامل مع القيم المكررة قبل أن يُطلب ذلك. (3) صرّحوا دائمًا بشرط التقليم بوضوح. (4) عند كِبَر n، أشيروا إلى أن المخرجات نفسها أُسّية الحجم، ولذلك تكون الخوارزمية مثلى لهذه المهمة.

import math

# Complexity for n=10
n = 10
print(f'Permutations(10): {math.factorial(n):,} results')
print(f'Combinations(10,5): {math.comb(n,5):,} results')
print(f'Subsets(10): {2**n:,} results')

# For interview: state which pattern
# 'This is a combinations problem because order doesnt matter'
# 'I will use a start index to avoid revisiting elements'
# 'Pruning: when sum exceeds target, break (after sorting)'

اختبار سريع

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

مراجعة الدرس

تعلّمتم في هذا الدرس أن: التباديل تستخدم مصفوفة used ولا تستخدم فهرس بداية، وتنشئ n! من الترتيبات، وأن التوافيق تستخدم فهرس بداية يتقدم لتجنب إعادة الاستخدام، وتنشئ C(n,k) من الاختيارات، وأن القيم المكررة في كلتا المسألتين تُعالج بترتيب القيم وتخطّي القيم المتكررة في مستوى الاستدعاء التعاودي نفسه. بعد ذلك سنطبّق التراجع على مسألة N-Queens ونستكشف نشر القيود.

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

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

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

ماذا ستتعلم في «التبديلات والتوافيق»؟

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

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

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

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

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

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

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

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

  1. قالب التراجع: اختر واستكشف وتراجع
  2. المجموعات الجزئية ومجموعة القوى
  3. التبديلات والتوافيق
  4. مسألة الملكات N وانتشار القيود
← العودة إلى DSA Interview Prep