محتويات الدورة

هياكل البيانات والخوارزميات / براهين التحسين والتجميعات المحفوظة / الدرس 15

المجموعات المنفصلة وغابات الامتداد الدنيا

احفظ ممثلي المكونات، واستخدم قاعدة جشعة قائمة على القطع دون خلط الاتصال بأقصر المسارات.

وقت القراءة التقريبي: 5 دقيقةتاريخ مراجعة المحتوى
في هذه الصفحة

أهداف الدرس

  • يسمي الممثل مكوّنا، وليس طريقا في الرسم
  • اربط المكوّن الأصغر تحت المكوّن الأكبر
  • يقبل Kruskal أخف اتصال آمن بين المكونات

يسمي الممثل مكوّنا، وليس طريقا في الرسم

يحافظ اتحاد المجموعات المنفصلة على تقسيم العناصر إلى مكونات. تعيد find ممثلا، وتدمج union المكونات عندما تختلف ممثلاتها. غابة روابط الأب تمثيل داخلي، وليست بالضرورة طريقا في الرسم الأصلي. وقد يغيّر ضغط الطريق تلك الغابة دون تغيير عضوية المكونات. ينبغي للتطبيقات مقارنة الممثلين للمساواة، بدلا من الاعتماد على معرّف جذر معين. تستخدم الأمثلة رؤوسا مرقمة من 0 إلى n-1، وترفض الفهارس المجهولة. تدعم البنية الدمج، وليس حذف حافة اعتباطية قد تقسم مكوّنا؛ فذلك يحتاج إلى عقد مختلف للاتصال الديناميكي.

اربط المكوّن الأصغر تحت المكوّن الأكبر

يجمع البرنامج الأول الاتحاد بالحجم مع تنصيف الطريق. كلما أضاف union حافة أب، دخل العنصر الموجود في المكوّن الأصغر في مكوّن حجمه ضعفا السابق على الأقل. يعطي ذلك عمقا لوغاريتميا حتى دون الاعتماد على الضغط، ولا يفعل الضغط إلا تقصير الطرق. توجد حدود مستهلكة أقوى لهذه التقنيات مجتمعة، لكن يكفي حد O(log n) الخشن لكل عملية لتحليل تنفيذ هذا الدرس. لا تكون أحجام الجذور معتمدة إلا عند الممثلين؛ فلا تقرأ حقل حجم متقادما في عقدة ليست جذرا على أنه حجم المكوّن الحالي. يعيد الاتحاد الذي لا يحدث دمجا False، لأن اتصالا جديدا لم يكن مطلوبا.

python مثال مستقل لخوارزمية بلغة Python 3، يستخدم بيانات محلية مصطنعة.
class DisjointSet:
    def __init__(self, n):
        self.parent = list(range(n))
        self.size = [1] * n
    def find(self, vertex):
        if not 0 <= vertex < len(self.parent):
            raise ValueError("Unknown vertex")
        while self.parent[vertex] != vertex:
            self.parent[vertex] = self.parent[self.parent[vertex]]
            vertex = self.parent[vertex]
        return vertex
    def union(self, a, b):
        a, b = self.find(a), self.find(b)
        if a == b:
            return False
        if self.size[a] < self.size[b]:
            a, b = b, a
        self.parent[b] = a
        self.size[a] += self.size[b]
        return True

sets = DisjointSet(4)
print(sets.union(0, 1))
print(sets.union(1, 0))
print(sets.find(0) == sets.find(1))
print(sets.find(0) == sets.find(2))
الناتج المتوقع
True
False
True
False

يحفظ الاتحاد المكرر التقسيم. ولا يعد رقم الممثل المحدد جزءا من العقد الخارجي.

يقبل Kruskal أخف اتصال آمن بين المكونات

تصل شجرة الامتداد الدنيا جميع رؤوس رسم متصل غير موجه وموزون بأقل مجموع أوزان للحواف، ودون دورة. يعالج Kruskal الحواف بترتيب غير متناقص للوزن، ولا يقبل حافة إلا إذا وصلت مكوّنين حاليين مختلفين. تبرر خاصية القطع اختيار أخف حافة عابرة لقطع يحترم الحواف المقبولة. تجيب المجموعات المنفصلة عن سؤال وجود الدورة بكفاءة، لكنها لا تثبت الحد الأدنى بنفسها. وتُسمح الأوزان السالبة في هذه المسألة. وإذا كان الرسم منفصلا، تعيد الطريقة نفسها غابة امتداد دنيا، ويجب إبقاء عدد المكونات ظاهرا، بدلا من إعلان شجرة امتداد واحدة على خلاف الواقع.

python مثال مستقل لخوارزمية بلغة Python 3، يستخدم بيانات محلية مصطنعة.
class DisjointSet:
    def __init__(self, n):
        self.parent = list(range(n))
        self.size = [1] * n
    def find(self, x):
        if not 0 <= x < len(self.parent):
            raise ValueError("Unknown vertex")
        while self.parent[x] != x:
            self.parent[x] = self.parent[self.parent[x]]
            x = self.parent[x]
        return x
    def union(self, a, b):
        a, b = self.find(a), self.find(b)
        if a == b:
            return False
        if self.size[a] < self.size[b]:
            a, b = b, a
        self.parent[b] = a
        self.size[a] += self.size[b]
        return True

def kruskal(n, edges):
    if n < 0:
        raise ValueError("Vertex count must be nonnegative")
    if any(not (0 <= a < n and 0 <= b < n) for _, a, b in edges):
        raise ValueError("Unknown vertex")
    sets = DisjointSet(n)
    total, chosen, components = 0, [], n
    for weight, a, b in sorted(edges):
        if sets.union(a, b):
            total += weight
            chosen.append((a, b))
            components -= 1
    return total, chosen, components

print(kruskal(4, [(1,0,1), (4,0,2), (2,1,2), (3,2,3), (7,1,3)]))
print(kruskal(3, [(2,0,1)]))
الناتج المتوقع
(6, [(0, 1), (1, 2), (2, 3)], 1)
(2, [(0, 1)], 2)

يعطي الرسم الأول شجرة مجموع وزنها 6. ويحوي الرسم الثاني مكوّنين، ولذلك يعطي غابة.

ميّز أهداف الامتداد من أهداف المسافة

تصغّر شجرة الامتداد الدنيا مجموع كلف الوصل المختارة للشبكة كلها. وتصغّر شجرة أقصر المسارات المسافات من مصدر إلى كل رأس، وفقا لهدف مختلف. ولا يستلزم أي من الضمانين الآخر. فقد يكون طريق الشجرة المختارة بين رأسين أطول من طريق آخر في الرسم الأصلي. وكذلك ليست سلسلة آباء المجموعات المنفصلة طريقا مستعادا عبر الحواف المقبولة. احفظ تمثيل مجاورة مستقلا للحواف المختارة عندما يحتاج التطبيق إلى طرق فعلية في الغابة. وقد تعطي بدائل الأوزان المتساوية عدة غابات مثلى بالتساوي؛ لذلك اختبر قيمة الهدف والصحة البنيوية، لا تسلسلا فريدا للحواف، ما لم يكن ترتيب التعادل محددا.

احسب كلفة الفرز والرؤوس المعزولة الممثلة

يكلف فرز E حافة O(E log E) مقارنة في النموذج المعتاد. وتضيف عمليات المكونات O(E log V) باستخدام الحد الخشن للاتحاد بالحجم، بينما تكلف التهيئة O(V). يغطي هذا الوصف الحواف المتوازية أيضا، دون افتراض صامت بأن E محدود بمربع V. تستخدم المصفوفات O(V) مساحة، وتحوي الغابة المقبولة V-1 حافة على الأكثر عندما يكون V موجبا، وللفرز كلفة تخزين خاصة به. تُعد الرؤوس المعزولة مكونات حتى إن لم تقع على أي حافة. ولا تدمج الحلقة الذاتية ممثلين مختلفين أبدا؛ لذلك يرفضها اختبار الاتحاد بأمان.

افحص التقسيم وأمثلية الغابة كلّا على حدة

قارن عضوية المكونات بعد تسلسلات اتحاد صغيرة بتقسيم بسيط قائم على تسميات. ولرسوم صغيرة، عد المجموعات الجزئية المرشحة غير الدورية التي تمتد مكونات الرسم، وقارن مجموع الوزن داخل كل مكوّن أصلي. تحقّق من انتماء الحواف المختارة إلى المدخل، ومن عدم تكوين دورة، ومن أن عدد الحواف المقبولة يساوي V-components بالضبط. أدرج الحواف المكررة والأوزان السالبة والمتساوية والحلقات الذاتية والرؤوس المنفصلة. لا تفرض رقم جذر واحدا أو حلا أمثل متعادلا بعينه. يكمل العد المحدود حجة القطع، ويبقي التحقق كله حسابات محلية صغيرة دون تشغيل شبكة حقيقية أو خدمة رسوم.

الأخطاء الشائعة وإصلاحها

عرض روابط الأب الداخلية كأنها طرق في الرسم الأصلي

لماذا يحدث؟
اختلطت بنية الممثل بالحواف المختارة.
كيف تصلحه؟
احفظ الحواف المقبولة مستقلة لاستعادة الطرق.
كيف تتجنب تكراره؟
افحص كل حافة في الطريق المعروض بمقارنتها بالرسم الأصلي.

تسمية ناتج منفصل شجرة امتداد

لماذا يحدث؟
أُسقط عدد المكونات بعد المعالجة.
كيف تصلحه؟
أعد حالة الغابة، أو ارفض المدخل المنفصل إذا اقتصر العقد على شجرة.
كيف تتجنب تكراره؟
أدرج عينة تحوي رأسا معزولا.

اعتبار هوية الجذر بيانات عضوية دائمة

لماذا يحدث؟
قد يغيّر الاتحاد وضغط الطريق الممثلين.
كيف تصلحه؟
قارن نتائج find الحالية، أو أتح معرّف مجال مستقرا منفصلا.
كيف تتجنب تكراره؟
اختبر الاتحادات بترتيبات مختلفة.

اختبر فهمك

1. لماذا يستطيع الاتحاد بالحجم تحديد العمق حتى قبل أخذ الضغط في الحسبان؟

اعرض الحل المشروح

كلما رُبط مكوّن العقدة تحت مكوّن آخر، تضاعف حجم المكوّن الحاوي لها على الأقل. وبدءا من حجم 1، لا يمكن أن يحدث ذلك إلا عددا لوغاريتميا من المرات قبل بلوغ حجم لا يتجاوز n. ولا يفعل الضغط إلا تقصير تلك الطرق.

2. لماذا تصح حافة سالبة في Kruskal، لكنها تسبب مشكلة لـ Dijkstra؟

اعرض الحل المشروح

تقارن حجتا القطع والدورة في Kruskal أوزان الوصل دون اشتراط أن تكون امتدادات الطريق غير سالبة. أما برهان استقرار المسافات في Dijkstra فيشترط الامتدادات غير السالبة. تتبع الافتراضات أهدافا وبراهين مختلفة، وليست حظرا عاما للأعداد السالبة في الرسوم.

أهم ما تعلمته

  • يسمي الممثل مكوّنا، وليس طريقا في الرسم
  • افحص التقسيم وأمثلية الغابة كلّا على حدة

المراجع الرسمية