المجموعات الجزئية ومجموعة القوى
ولّد جميع المجموعات الجزئية لمجموعة باستخدام التراجع وأقنعة البتات، وتعامل مع العناصر المكررة بترتيبها وتخطّي العناصر المتكررة.
المجموعات الجزئية ومجموعة القوى درس مجاني في DSA Interview Prep على CoddyKit. هذا هو الدرس 2 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في DSA Interview Prep، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة DSA Interview Prep 4 دروس في المجموع.
المجموعات الجزئية ومجموعة القوى
مجموعة القوى لمجموعة S هي مجموعة جميع المجموعات الجزئية الممكنة لـ S، بما في ذلك المجموعة الفارغة وS نفسها. وتحتوي مجموعة مكوّنة من n عناصر على 2ⁿ مجموعة جزئية بالضبط. بالنسبة إلى [1, 2, 3]، تكون المجموعات الجزئية الثماني هي: [], [1], [2], [3], [1,2], [1,3], [2,3], [1,2,3]. وهذه مسألة توافقيات أساسية تظهر في أسئلة المقابلات المتعلقة بالعثور على جميع التركيبات أو التقسيمات أو الاختيارات الممكنة.
# A set of n elements → 2^n subsets
for n in range(5):
print(f'n={n}: {2**n} subsets')
# n=0: 1 (just the empty set)
# n=1: 2 ([], [x])
# n=2: 4 ([], [a], [b], [a,b])
# n=3: 8 (as enumerated above)
# n=4: 16توليد المجموعات الجزئية بالتراجع
استخدموا قالب Choose-Explore-Unchoose. ويتمثل قرار التصميم الأساسي في أن تُضاف المسيرة الجزئية الحالية إلى النتائج فورًا (قبل اختيار مزيد من العناصر) عند كل استدعاء تكراري. وبهذه الطريقة، تُلتقط كل حالة — الفارغة والجزئية والكاملة — بوصفها مجموعة جزئية صالحة. قدّموا فهرس start للنظر فقط في العناصر الموجودة إلى يمين آخر عنصر مختار، مما يضمن عدم وجود تكرارات ويحافظ على الترتيب.
def subsets(nums):
result = []
def backtrack(start, path):
result.append(list(path)) # every state is a valid subset
for i in range(start, len(nums)):
path.append(nums[i]) # CHOOSE
backtrack(i + 1, path) # EXPLORE (advance start)
path.pop() # UNCHOOSE
backtrack(0, [])
return result
print(subsets([1, 2, 3]))
# [[], [1], [1,2], [1,2,3], [1,3], [2], [2,3], [3]]استخدام قناع البتات
البديل عن التراجع هو استخدام قناع البتات: تقابل كل مجموعة جزئية عددًا مكوّنًا من n بتات، حيث يعني كون البت i يساوي 1 أن العنصر i مضمن. كرّروا من 0 إلى 2ⁿ - 1، واستخرجوا البتات من كل عدد لبناء المجموعة الجزئية. هذا أسلوب تكراري، وغالبًا ما يكون أسرع في الممارسة، كما أنه سهل جدًا في البرمجة. لكنه لا يتعمم بالسهولة نفسها على المسائل ذات القيود، مثل حدّ المجموع.
def subsets_bitmask(nums):
n = len(nums)
result = []
for mask in range(1 << n): # 0 to 2^n - 1
subset = []
for i in range(n):
if mask & (1 << i): # bit i is set
subset.append(nums[i])
result.append(subset)
return result
print(subsets_bitmask([1, 2, 3]))
# Same 8 subsets, order may differالتوليد التكراري للمجموعات الجزئية
يبني الأسلوب التكراري مجموعة القوى عنصرًا تلو الآخر. ابدأوا بـ [[] ] (المجموعة الفارغة). مع كل عنصر جديد، انسخوا جميع المجموعات الجزئية الموجودة، ثم أضيفوا العنصر الجديد إلى كل نسخة. بعد معالجة n من العناصر، ستحتوي النتيجة على جميع المجموعات الجزئية وعددها 2ⁿ. وهذا مكافئ لاستخدام قناع البتات، لكنه أوضح لمن ليسوا معتادين على العمليات على مستوى البتات.
def subsets_iterative(nums):
result = [[]] # start with empty set
for num in nums:
# For each existing subset, create a new subset with num added
result += [subset + [num] for subset in result]
return result
print(subsets_iterative([1, 2, 3]))
# After num=1: [[], [1]]
# After num=2: [[], [1], [2], [1,2]]
# After num=3: [[], [1], [2], [1,2], [3], [1,3], [2,3], [1,2,3]]المجموعات الجزئية II: معالجة التكرارات
عندما تحتوي المدخلات على عناصر مكررة، يُولّد الأسلوب الساذج مجموعات جزئية مكررة. ففي [1, 2, 2]، سيُنتج كل ظهور من ظهوري 2 المجموعة [1, 2] بشكل مستقل. والحل هو فرز المصفوفة أولًا، ثم تخطي المرشح في المستوى الحالي إذا كان مساويًا للمرشح السابق في المستوى نفسه. وتحديدًا، داخل الحلقة: if i > start and nums[i] == nums[i-1]: continue.
def subsets_with_dups(nums):
nums.sort() # sort to group duplicates together
result = []
def backtrack(start, path):
result.append(list(path))
for i in range(start, len(nums)):
# Skip duplicates at the same tree level
if i > start and nums[i] == nums[i-1]:
continue
path.append(nums[i])
backtrack(i + 1, path)
path.pop()
backtrack(0, [])
return result
print(subsets_with_dups([1, 2, 2]))
# [[], [1], [1,2], [1,2,2], [2], [2,2]] — no duplicate subsetsكيف يعمل تخطي القيم المكررة
يتخطّى الشرط i > start and nums[i] == nums[i-1] القيمة المكررة فقط على مستوى الاستدعاء التعاودي نفسه (أي قيمة start نفسها). ولا يمنع اختيار القيمة نفسها في أعماق مختلفة. بالنسبة إلى [1, 2, 2]: في المستوى 0 نضمّن القيمة 2 الأولى (الفهرس 1)، ثم نضمّن في المستوى التالي (start=2) القيمة 2 الثانية لتكوين [2, 2]. لكن إذا حاولنا تضمين القيمة 2 الثانية مرة أخرى في المستوى 0، فسيتعرّف الشرط عليها ويتخطّاها.
# Visual: [1, 2, 2] sorted
# Level 0 (start=0): pick nothing, pick 1, pick first-2, pick second-2 (SKIP)
# Level 1 after picking 1 (start=1): pick first-2, pick second-2 (SKIP)
# Level 2 after picking 1,first-2 (start=2): pick second-2
# → [1,2,2] is generated but only once
nums = [1, 2, 2]
nums.sort()
result_set = set(tuple(sorted(s)) for s in subsets_with_dups(nums[:]))
result_naive = set(tuple(sorted(s)) for s in subsets(nums))
print('With dedup:', sorted(result_set))
print('Same results:', result_set == result_naive)
def subsets(nums):
result = []
def bt(start, path):
result.append(list(path))
for i in range(start, len(nums)):
path.append(nums[i]); bt(i+1, path); path.pop()
bt(0, [])
return result
def subsets_with_dups(nums):
result = []
def bt(start, path):
result.append(list(path))
for i in range(start, len(nums)):
if i > start and nums[i] == nums[i-1]: continue
path.append(nums[i]); bt(i+1, path); path.pop()
bt(0, [])
return result
print(len(subsets_with_dups([1,2,2])), 'unique subsets') # 6المجموعات الجزئية ذات الحجم الثابت (توافيق k)
إنشاء مجموعات جزئية حجم كل منها k بالضبط (LeetCode 77: التوافيق) يضيف شرط إنهاء مبكرًا: إذا لم تتمكن العناصر المتبقية من إكمال المسار إلى الحجم k، فاقطع البحث. شرط التقليم هو i > n - (k - len(path)): فإذا لم يتبقَّ عدد كافٍ من العناصر، فتوقّف مبكرًا. ويقلّل ذلك مساحة البحث بشكل ملحوظ مقارنةً بإنشاء جميع المجموعات الجزئية ثم تصفيتها.
def combine(n, k):
result = []
def backtrack(start, path):
if len(path) == k:
result.append(list(path))
return
# Prune: need (k - len(path)) more elements from [start..n]
# At most (n - start + 1) elements remain
if n - start + 1 < k - len(path):
return # not enough elements left
for i in range(start, n + 1):
path.append(i)
backtrack(i + 1, path)
path.pop()
backtrack(1, [])
return result
print(combine(4, 2)) # [[1,2],[1,3],[1,4],[2,3],[2,4],[3,4]]
print(len(combine(10, 3))) # C(10,3) = 120تطبيقات مجموعة القوى
يظهر نمط مجموعة القوى في العديد من صيغ مسائل المقابلات: (1) تقسيم العناصر إلى مجموعتين جزئيتين متساويتين — التحقّق مما إذا كانت أي مجموعة جزئية مجموعها يساوي total/2. (2) أقصى XOR لمجموعتين جزئيتين — تجربة جميع أزواج المجموعات الجزئية. (3) أقل تكلفة لاختيار k من العناصر — تعداد المجموعات الجزئية ذات الحجم k. ورغم أن التعداد المباشر أُسّي، فإن العديد من هذه المسائل تقبل حلولًا باستخدام البرمجة الديناميكية بعد التعرّف على بنيتها. ويساعدك تصور مجموعة القوى على تحديد فضاء الحالات حتى عندما ستعمل على تحسين الحل.
def max_subset_sum(nums, k):
'''Maximum sum of any k elements (for comparison: O(n log n) alternative)'''
# Backtracking approach: enumerate all k-subsets
max_s = [float('-inf')]
def bt(start, path, curr_sum):
if len(path) == k:
max_s[0] = max(max_s[0], curr_sum)
return
remaining_spots = k - len(path)
for i in range(start, len(nums)):
if len(nums) - i < remaining_spots: break # prune
bt(i+1, path+[nums[i]], curr_sum+nums[i])
bt(0, [], 0)
return max_s[0]
# Much faster: just sort and take top k
def max_subset_sum_fast(nums, k):
return sum(sorted(nums, reverse=True)[:k])
nums = [3, 1, 4, 1, 5, 9, 2, 6]
print(max_subset_sum(nums, 3)) # 20 (9+6+5)
print(max_subset_sum_fast(nums, 3)) # 20التحقق من مجموع مجموعة جزئية
تطرح مسألة مجموع المجموعة الجزئية السؤال التالي: هل توجد مجموعة جزئية من المصفوفة يساوي مجموعها قيمة مستهدفة؟ يمكن حلّها باستخدام التراجع (بتعقيد أُسّي) أو البرمجة الديناميكية (بتعقيد كثير الحدود). إصدار التراجع مباشر، لكنه يصبح غير عملي مع المدخلات الكبيرة. أمّا إصدار البرمجة الديناميكية (جدول منطقي dp[target+1]) فهو النهج المفضّل في المقابلات. ويساعدك فهم الطريقتين على توضيح المفاضلة: فالتراجع يعيد جميع الحلول، بينما تجيب البرمجة الديناميكية عن مسألة القرار بكفاءة.
# Backtracking version: finds a subset if it exists
def subset_sum_bt(nums, target):
def bt(start, remaining):
if remaining == 0: return True
if remaining < 0 or start == len(nums): return False
# Include nums[start]
if bt(start + 1, remaining - nums[start]): return True
# Exclude nums[start]
return bt(start + 1, remaining)
return bt(0, target)
# DP version: O(n * target) time
def subset_sum_dp(nums, target):
dp = {0}
for num in nums:
dp |= {s + num for s in dp}
return target in dp
print(subset_sum_bt([3, 1, 4, 1, 5], 6)) # True (1+5 or 1+1+4)
print(subset_sum_dp([3, 1, 4, 1, 5], 6)) # Trueتعقيد تعداد المجموعات الجزئية
يملك إنشاء جميع المجموعات الجزئية تعقيدًا زمنيًا لا يمكن تجنّبه قدره O(n × 2ⁿ)، إذ توجد 2ⁿ مجموعة جزئية، ويبلغ الحجم المتوسط لكل منها n/2. ولا يمكن لأي خوارزمية أن تحقق أداءً أفضل عندما تكون جميع المجموعات الجزئية مطلوبة. أمّا المسائل التي تطلب مجموعة جزئية واحدة تحقق خاصية معينة، مثل أكبر مجموع، فيُفضّل حلّها بالبرمجة الديناميكية أو الخوارزميات الجشعة. ومن أهم الأفكار في المقابلات: اسأل دائمًا ما إذا كنت تحتاج إلى تعداد جميع المجموعات الجزئية، أم إلى معرفة ما إذا كانت أي مجموعة جزئية تحقق شرطًا — فالإجابة تحدد ما إذا كان التعقيد الأُسّي أو كثير الحدود مقبولًا.
import time
def count_subsets(n):
nums = list(range(n))
result = []
def bt(start, path):
result.append(None) # count without storing
for i in range(start, len(nums)):
path.append(i); bt(i+1, path); path.pop()
bt(0, [])
return len(result)
for n in [10, 15, 20]:
start = time.time()
cnt = count_subsets(n)
elapsed = time.time() - start
print(f'n={n}: {cnt} subsets ({2**n} expected) in {elapsed:.3f}s')مقارنة الأساليب الثلاثة
عند إنشاء جميع المجموعات الجزئية: يُعدّ التراجع الأسلوب الأسهل للتعميم، إذ يتكيّف بسهولة مع القيم المكررة والقيود. أمّا قناع البتات فهو موجز وسريع، لكنه محدود عندما يكون n ≤ 30 (بسبب حجم العدد الصحيح). ويتميّز الأسلوب التكراري بكونه بديهيًا ويتجنب كلفة الاستدعاء التعاودي. تنتج الأساليب الثلاثة مخرجات بتعقيد O(n × 2ⁿ). في المقابلة، يبرهن التراجع على فهم عملية اتخاذ القرار التعاودية، وهو فهم يمكن تعميمه على مسائل أصعب. اذكر الأساليب الثلاثة عند مناقشة الحلول.
# All three approaches for [1,2,3]
nums = [1, 2, 3]
# 1. Backtracking
def bt(start, path, res):
res.append(list(path))
for i in range(start, len(nums)):
path.append(nums[i]); bt(i+1, path, res); path.pop()
res1 = []; bt(0, [], res1)
# 2. Bit masking
res2 = [[nums[i] for i in range(len(nums)) if mask & (1<<i)]
for mask in range(1<<len(nums))]
# 3. Iterative
res3 = [[]]
for num in nums:
res3 += [s+[num] for s in res3]
print('All produce', len(nums)**2, '-ish subsets:',
len(res1), len(res2), len(res3)) # all 8اختبار سريع
اختبروا فهمكم لمفاهيم Data Structures & Algorithms — Coding Interview Prep الواردة في هذا الدرس.
مراجعة الدرس
تعلّمتم في هذا الدرس أن: التراجع ينشئ جميع المجموعات الجزئية بإضافة كل مسار جزئي إلى النتائج قبل مواصلة الاستكشاف، وأن التعامل مع القيم المكررة يتم بترتيب القيم وتخطّي القيم المتكررة في مستوى الاستدعاء التعاودي نفسه باستخدام الشرط i > start and nums[i] == nums[i-1]، وأن قناع البتات يوفّر بديلًا تكراريًا موجزًا، حيث تقابل كل مجموعة جزئية قناع بتات فريدًا. بعد ذلك سنتناول التباديل والتوافيق، وهما مسألتان مرتبطتان بالتعداد لكن بقيود مختلفة.
الأسئلة الشائعة
هل درس «المجموعات الجزئية ومجموعة القوى» مجاني؟
نعم — نص درس «المجموعات الجزئية ومجموعة القوى» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة DSA Interview Prep، انتقل إلى CoddyKit PRO. تتضمن دورة DSA Interview Prep 4 دروس في المجموع.
ماذا ستتعلم في «المجموعات الجزئية ومجموعة القوى»؟
ولّد جميع المجموعات الجزئية لمجموعة باستخدام التراجع وأقنعة البتات، وتعامل مع العناصر المكررة بترتيبها وتخطّي العناصر المتكررة. تتمرن على DSA Interview Prep مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.
هل أحتاج إلى خبرة سابقة لأبدأ DSA Interview Prep؟
لا تُشترط خبرة سابقة. DSA Interview Prep على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 2 من أصل 4.
كم من الوقت يستغرق درس «المجموعات الجزئية ومجموعة القوى»؟
معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.
هل يمكنني كتابة وتشغيل أكواد في درس DSA Interview Prep هذا؟
نعم. كل درس في DSA Interview Prep يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.
جميع الدروس في هذه الدورة
- قالب التراجع: اختر واستكشف وتراجع
- المجموعات الجزئية ومجموعة القوى
- التبديلات والتوافيق
- مسألة الملكات N وانتشار القيود