0Pricing
Coding Interview Prep · درس

قالب التقسيم والغزو

استخلص القالب ذي الخطوات الثلاث (التقسيم، والغزو، والدمج) من merge sort، وطبّقه منهجيًا على أشكال جديدة من المسائل.

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

ما المقصود بالتقسيم والحل؟

يحل التقسيم والحل (D&C) المسألة عبر تقسيمها إلى مسائل فرعية مستقلة من النوع نفسه، ثم حل كل منها بصورة递 تكرارية ودمج حلولها. الكلمة الأساسية هنا هي مستقلة؛ إذ لا تشترك المسائل الفرعية في الحالة نفسها، بخلاف البرمجة الديناميكية DP التي تتداخل فيها المسائل. ومن الأمثلة الكلاسيكية: merge sort وbinary search وquick sort وclosest pair of points وfast matrix multiplication. يحقق التقسيم والحل عادةً زمنًا قدره O(n log n) من خلال القالب المؤلف من ثلاث خطوات.

# Divide and Conquer vs DP:
# D&C: sub-problems are INDEPENDENT (no overlap)
# DP:  sub-problems OVERLAP (same sub-problem solved multiple times)

# D&C examples:
# Merge sort: split array in half, sort each, merge
# Binary search: check midpoint, recurse on one half
# Max subarray (D&C): find max in left half, right half, crossing

# Recurrence pattern:
# T(n) = 2T(n/2) + O(n) → O(n log n)  [merge sort]
# T(n) = T(n/2) + O(1) → O(log n)     [binary search]
# T(n) = T(n/k) + O(n) → O(n log_k n) [k-way split]

القالب المؤلف من ثلاث خطوات

تتبع كل خوارزمية من خوارزميات التقسيم والحل ثلاث خطوات: (1) التقسيم — تقسيم المسألة إلى مسألتين فرعيتين أصغر أو أكثر، عادةً عند نقطة المنتصف. (2) الحل — حل كل مسألة فرعية بصورة递 تكرارية. حدّد حالة أساسية لإيقاف التكرار، وعادةً تكون n ≤ 1. (3) الدمج — دمج حلول المسائل الفرعية في الحل العام. يكمن الإبداع بالكامل في خطوة الدمج؛ أما التقسيم فعادةً لا يتجاوز تقسيم المسألة عند نقطة المنتصف.

def divide_and_conquer(arr, lo, hi):
    # BASE CASE: trivial sub-problem
    if lo >= hi:
        return base_case_result(arr, lo, hi)
    
    # DIVIDE: split at midpoint
    mid = (lo + hi) // 2
    
    # CONQUER: solve sub-problems recursively
    left_result  = divide_and_conquer(arr, lo, mid)
    right_result = divide_and_conquer(arr, mid + 1, hi)
    
    # COMBINE: merge results
    return combine(left_result, right_result, arr, lo, mid, hi)

def base_case_result(arr, lo, hi): return arr[lo]
def combine(l, r, arr, lo, mid, hi): return max(l, r)

فرز الدمج بوصفه المثال النموذجي

يوضح فرز الدمج التقسيم والحل على نحو مثالي: قسّم المصفوفة عند نقطة المنتصف. ثم حل كل نصف بفرزه بصورة递 تكرارية. وأخيرًا ادمج النصفين المرتبين في زمن O(n). تحدث جميع العمليات الأساسية في خطوة الدمج. علاقة التكرار هي: T(n) = 2T(n/2) + O(n). ووفقًا للحالة الثانية من Master Theorem: T(n) = O(n log n). وهذه أهم علاقة تكرار في التقسيم والحل التي ينبغي حفظها.

def merge_sort(arr):
    # BASE CASE
    if len(arr) <= 1:
        return arr
    # DIVIDE
    mid = len(arr) // 2
    # CONQUER
    left  = merge_sort(arr[:mid])
    right = merge_sort(arr[mid:])
    # COMBINE
    return merge(left, right)

def merge(left, right):
    result = []
    i = j = 0
    while i < len(left) and j < len(right):
        if left[i] <= right[j]:
            result.append(left[i]); i += 1
        else:
            result.append(right[j]); j += 1
    return result + left[i:] + right[j:]

print(merge_sort([5, 3, 8, 1, 9, 2]))  # [1,2,3,5,8,9]

مرجع سريع لـ Master Theorem

تحل Master Theorem علاقات التكرار من الشكل T(n) = aT(n/b) + f(n): Case 1: f(n) = O(n^(log_b(a) - ε)) ← T(n) = O(n^log_b(a)). Case 2: f(n) = O(n^log_b(a)) ← T(n) = O(n^log_b(a) × log n). Case 3: f(n) = Ω(n^(log_b(a) + ε)) ← T(n) = O(f(n)). في فرز الدمج: a=2 وb=2 وf(n)=O(n)، كما أن n^log_2(2)=n، ولذلك تنطبق Case 2، فنحصل على O(n log n).

# Master Theorem quick examples:
# T(n) = 2T(n/2) + O(n)    → a=2,b=2,f=n,n^log2(2)=n → Case2 → O(n log n)
# T(n) = 2T(n/2) + O(1)    → a=2,b=2,f=1,n^1=n >> 1  → Case1 → O(n)
# T(n) = 2T(n/2) + O(n^2)  → a=2,b=2,f=n^2,n^1 << n^2 → Case3 → O(n^2)
# T(n) = T(n/2) + O(1)     → a=1,b=2,f=1,n^log2(1)=1=f → Case2 → O(log n)
# T(n) = T(n/3)+T(2n/3)+O(n) → Master doesn't apply directly → O(n log n) by recursion tree

recurrences = [
    ('Merge sort: 2T(n/2)+n', 'O(n log n)'),
    ('Binary search: T(n/2)+1', 'O(log n)'),
    ('Naive matrix mult: 8T(n/2)+n^2', 'O(n^3)'),
    ('Strassen: 7T(n/2)+n^2', 'O(n^2.81)'),
]
for r, sol in recurrences: print(r, '->', sol)

المصفوفة الفرعية العظمى: أسلوب التقسيم والحل

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

def max_subarray_dc(nums, lo=None, hi=None):
    if lo is None: lo, hi = 0, len(nums) - 1
    if lo == hi: return nums[lo]
    mid = (lo + hi) // 2
    # Conquer
    left_max  = max_subarray_dc(nums, lo, mid)
    right_max = max_subarray_dc(nums, mid + 1, hi)
    # Cross-midpoint sum
    left_sum = curr = 0
    for i in range(mid, lo - 1, -1):
        curr += nums[i]
        left_sum = max(left_sum, curr)
    right_sum = curr = 0
    for i in range(mid + 1, hi + 1):
        curr += nums[i]
        right_sum = max(right_sum, curr)
    cross_max = left_sum + right_sum
    return max(left_max, right_max, cross_max)

nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4]
print(max_subarray_dc(nums))  # 6

دالة القوة: الأسّ السريع

تعمل خوارزمية Fast Power (LeetCode 50) على حساب x^n في زمن O(log n) باستخدام التقسيم والحل. إذا كان n زوجيًا: x^n = (x^(n/2))^2. وإذا كان n فرديًا: x^n = x × x^(n-1). عالج قيمة n السالبة باستخدام x^(-n) = 1/x^n. تعمل كل استدعاءات التكرار على تنصيف n، ولذلك يكون عمقها O(log n). هذا مثال واضح تكون فيه خطوة الدمج مجرد عملية ضرب؛ وهي خطوة بسيطة لكنها فعّالة.

def my_pow(x, n):
    if n < 0:
        return 1 / my_pow(x, -n)
    # BASE CASE
    if n == 0: return 1
    # DIVIDE and CONQUER
    half = my_pow(x, n // 2)
    if n % 2 == 0:
        return half * half          # even: x^n = (x^(n/2))^2
    else:
        return x * half * half      # odd: x^n = x * (x^(n/2))^2

print(my_pow(2, 10))   # 1024
print(my_pow(2, -2))   # 0.25
print(my_pow(3, 5))    # 243
print(my_pow(0, 0))    # 1

تحويل مصفوفة مرتبة إلى BST

يستخدم Convert Sorted Array to BST (LeetCode 108) أسلوب التقسيم والحل: نختار نقطة المنتصف جذرًا، مما يضمن توازن الارتفاع، ثم نبني الشجرة الفرعية اليسرى بصورة递 تكرارية من النصف الأيسر، والشجرة الفرعية اليمنى من النصف الأيمن. ينتج عن ذلك BST متوازنة من حيث الارتفاع، بارتفاع أدنى قدره O(log n). ويحاكي هيكل التقسيم والحل البحثَ الثنائي؛ إذ تعيّن كل طبقة من التكرار نقطة المنتصف جذرًا للنطاق الفرعي الحالي.

class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val = val
        self.left = left
        self.right = right

def sorted_array_to_bst(nums):
    def helper(lo, hi):
        if lo > hi: return None
        mid = (lo + hi) // 2
        node = TreeNode(nums[mid])    # DIVIDE at midpoint
        node.left  = helper(lo, mid - 1)  # CONQUER left
        node.right = helper(mid + 1, hi)  # CONQUER right
        # COMBINE: already done by assignment
        return node
    return helper(0, len(nums) - 1)

def inorder(node):
    if not node: return []
    return inorder(node.left) + [node.val] + inorder(node.right)

root = sorted_array_to_bst([-10, -3, 0, 5, 9])
print(inorder(root))  # [-10,-3,0,5,9] (sorted, proving BST property)

متى لا يكون التقسيم والحل الخيار الأفضل

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

# When D&C hurts:
# Fibonacci with pure D&C (no memo): T(n) = T(n-1) + T(n-2) → O(2^n)
# Sub-problems OVERLAP → use DP or memoisation instead

def fib_dc(n):
    if n <= 1: return n
    return fib_dc(n-1) + fib_dc(n-2)  # O(2^n)!

def fib_dp(n):
    a, b = 0, 1
    for _ in range(n): a, b = b, a+b
    return a  # O(n)

print(fib_dp(30))  # fast
# fib_dc(40) would take seconds — do not run large values!

استخدام التقسيم والحل للبحث الثنائي في مصفوفة مرتبة

يمكن حل البحث في مصفوفة ثنائية الأبعاد (LeetCode 240) تكون صفوفها وأعمدتها مرتبة باستخدام التقسيم والحل: ابدأ من الزاوية العلوية اليمنى. إذا كانت القيمة الحالية أكبر من target، فتحرّك إلى اليسار، وبذلك تستبعد العمود. وإذا كانت القيمة الحالية أصغر من target، فتحرّك إلى الأسفل، وبذلك تستبعد الصف. وإذا تساوتا، فقد عثرت عليها. هذه الخوارزمية بزمن O(m+n) ليست تقسيمًا وحلًا تكراريًا من الناحية التقنية، لكنها تشترك معه في الفكرة الأساسية: استبعاد نصف مساحة البحث في كل خطوة.

def search_matrix(matrix, target):
    if not matrix: return False
    m, n = len(matrix), len(matrix[0])
    row, col = 0, n - 1  # start top-right
    while row < m and col >= 0:
        val = matrix[row][col]
        if val == target:
            return True
        elif val > target:
            col -= 1  # eliminate this column
        else:
            row += 1  # eliminate this row
    return False

matrix = [
    [1,   4,  7, 11, 15],
    [2,   5,  8, 12, 19],
    [3,   6,  9, 16, 22],
    [10, 13, 14, 17, 24],
    [18, 21, 23, 26, 30]
]
print(search_matrix(matrix, 5))   # True
print(search_matrix(matrix, 20))  # False

تحليل شجرة الاستدعاء التكراري

بالنسبة إلى علاقات تكرار التقسيم والحل التي لا تنطبق عليها Master Theorem، استخدم أسلوب شجرة الاستدعاء التكراري. ارسم كل مستوى من استدعاءات التكرار واجمع العمل المنجز في كل مستوى. في فرز الدمج، يوجد عند المستوى k عدد 2^k من المسائل الفرعية، حجم كل منها n/2^k. ويكون العمل في كل مستوى = 2^k × O(n/2^k) = O(n). أما إجمالي عدد المستويات = log n. لذلك يكون إجمالي العمل = O(n log n). تصلح هذه الطريقة البصرية لأي علاقة تكرار، وتبني حدسًا يوضح سبب وصول التقسيم والحل عادةً إلى O(n log n).

# Merge sort recursion tree analysis:
# Level 0: 1 problem of size n → O(n) work
# Level 1: 2 problems of size n/2 → 2*O(n/2) = O(n) work
# Level 2: 4 problems of size n/4 → 4*O(n/4) = O(n) work
# ...
# Level log(n): n problems of size 1 → n*O(1) = O(n) work
# Total levels = log(n)+1
# Total work = O(n) * O(log n) = O(n log n)

import math
n = 64
levels = int(math.log2(n)) + 1
print(f'n={n}: {levels} levels, {n}*{levels} = {n*levels} work units')
print(f'O(n log n) = O({n} * {int(math.log2(n))}) = O({n*int(math.log2(n))})')

شرح التقسيم والحل في المقابلة

عند عرض حل يعتمد على التقسيم والحل في مقابلة: (1) اذكر الخطوات الثلاث بوضوح: 'سأقسّم عند نقطة المنتصف، ثم أحل كل نصف بصورة递 تكرارية، وبعد ذلك أدمجهما.' (2) حدّد الحالة الأساسية بوضوح. (3) اشتق علاقة التكرار: T(n) = 2T(n/2) + O(n). (4) طبّق Master Theorem أو شجرة الاستدعاء التكراري لاشتقاق O(n log n). (5) اذكر متى يكون التقسيم والحل أفضل أو أسوأ من البدائل، مثل استخدام DP للمسائل الفرعية المتداخلة وKadane للمصفوفة الفرعية العظمى.

# D&C interview template to memorize:
def dc_template(problem, lo, hi):
    # 1. BASE CASE (state it first)
    if lo == hi: return solve_base(problem, lo)
    # 2. DIVIDE
    mid = (lo + hi) // 2
    # 3. CONQUER
    left  = dc_template(problem, lo, mid)
    right = dc_template(problem, mid + 1, hi)
    # 4. COMBINE (this is where the algorithm-specific logic goes)
    return combine_results(left, right, problem, lo, mid, hi)

def solve_base(p, i): return p[i]
def combine_results(l, r, p, lo, mid, hi): return max(l, r)

print('D&C template: base-divide-conquer-combine')
print('Complexity usually: T(n)=2T(n/2)+O(n) → O(n log n)')

اختبار سريع

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

مراجعة الدرس

في هذا الدرس تعلّمتَ ما يلي: يتبع التقسيم والحل القالب التالي: الحالة الأساسية ← التقسيم عند نقطة المنتصف ← الحل بصورة递 تكرارية ← الدمج، وتعطي العلاقة T(n) = 2T(n/2) + O(n) التعقيد O(n log n) وفقًا للحالة الثانية من Master Theorem، كما أن التقسيم والحل مثالي للمسائل الفرعية المستقلة، بينما تكون البرمجة الديناميكية ضرورية عندما تتداخل المسائل الفرعية. بعد ذلك سنطبّق التقسيم والحل على عدّ الانقلابات في مصفوفة باستخدام نسخة معدّلة من فرز الدمج.

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

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

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

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

استخلص القالب ذي الخطوات الثلاث (التقسيم، والغزو، والدمج) من merge sort، وطبّقه منهجيًا على أشكال جديدة من المسائل. تتمرن على 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. العنصر الغالب: تصويت Boyer-Moore
  4. وسيط مصفوفتين مرتبتين
← العودة إلى Coding Interview Prep