בהינתן עולם כלשהו של איברים אנחנו רוצים להחזיק חלוקה של האיברים הללו לקבוצות זרות.
כל איבר מתחיל כקבוצה בפני עצמו וניתן לאחר שתי קבוצות לקבוצה גדולה ללא אפשרות פיצול.
Disjoint set או Union find הוא מבנה נתונים התומך בפעולות הבאות
make-set(x)
יוצר קבוצה עם האיבר החדש union(x,y)
מאחד בין שתי קבוצות המכילות את האיברים find-set(x)
מוצא את הקבוצה שלה איבר נחזיק רשימה מקושרת דו כיוונית לכל קבוצה ונגדיר לכל קבוצה איבר מזהה הנציג.
באופן הזה נוכל להפעיל את union בזמן קבוע כי נחזיק לכל רשימה מצביע לראש ולזנב, כלומר אם union יופעל על
החישוב של find-set פשוט ידרוש מאיתנו ללכת עד לראש הרשימה כלומר
אם נרשום את זמני הריצה בטבלה זה ייראה ככה
make-set | union | find-set |
---|---|---|
O(1) | O(1) | O(n) |
כל איבר יחזיק מצביע לאיבר הראשון ברשימה וכעת נוכל לגשת לנציג בזמן ריצה קבוע.
הבעיה שנוצרת ברעיון הזה היא שבמיזוג שתי קבוצות נצטרך להשקיע הרבה יותר זמן כדי לעדכן את המצביעים של רשימה אחת להצביע על ראש הרשימה השנייה (כמובן שנעדיף לקחת את הרשימה הקצרה יותר אבל עדיין זמן הריצה בודק את המקרה הגרוע שהוא
כלומר אם נרשום את זמני הריצה כעת זה ייראה ככה
make-set | union | find-set |
---|---|---|
O(1) | O(n) | O(1) |
נשים לב ש union בפועל לוקח
ב ניתוח לשיעורין העלות לשיעורין של פעולת union היא
נחזיק יער של כל הקבוצות, כאשר כל קבוצה תייצג רכיב קשירות ביער כלומר עץ כאשר השורש הוא ה נציג שהגדרנו.
לכל קודקוד
find-set(x)
פעולה החסומה בגובה העץ.
make-set(x)
יצירת שורש חדש שיצביע לעצמו
union(x,y)
ניקח את העץ בעל הגובה הקטן ביותר להיות בן של העץ בעל הגובה הגדול ביותר . נשייך לכל קודקוד דרגה rank שמייצגת את הגובה של הקודקוד (ביחס לתת העץ המושרש בו כלומר מהו הגובה של העץ כאשר הקודקוד הוא השורש)
find-set(x)
if parent(x) != x
find-set(x)
return x
make-set(x)
x.parent = x
x.rank = 0
union(x,y)
x = find-set(x)
y = find-set(y)
child = min_root(rank(x), rank(y))
parent = max_root(rank(x), rank(y))
parent.add-child(child)
נשים לב, ה rank של כל הקודקודים נשאר זהה, פרט למקרה שבו מאחדים שני עצים עם אותו rank, ואז השורש החדש מקדם את הrank ב1.
אם אנחנו משקיעים
את התהליך הזה נעשה לכל הקודקודים על המסלול מ
int find_set(int v) {
if (v == parent[v])
return v;
return parent[v] = find_set(parent[v]);
}
נשים לב שאין צורך לעדכן את ה rank של קודקודים שאינם מעודכנים, גם ככה משתמשים רק ב rank של השורש לביצוע union. גם אם הדרגה של השורש תפסיק לייצג את גובה העץ ממש, היא עדיין תהיה חסם עליון על גובה העץ.
(בפועל זה יכול לגרום למצב שנאחד עץ גבוה לתוך עץ נמוך, בכל מקרה זמן הריצה עדיין חסום בצורה טובה).
לכל
ההוכחה תחולק למקרים
א. אם יוצרים קודקוד אז בוודאי שמתקיים כי
ב. אם התקיים עבור
שינוי
לכל שורש עם rank שערכו k יש עץ עם לפחות
ההוכחה היא באינדוקצייה על מספר פעולות ה union כי אלה היחידות שמשנות את הדרגה.
בסיס: אם לא עשינו פעולת union אחת הדרגה בהכרח שווה ל 0 ובעץ יש לכל הפחות קודקוד אחד
צעד: נניח שמתקיים לפני הביצוע של union בפעם ה n. נסמן את הדרגה לפני ב
אם
אם
לכל קודקוד שדרגתו
אם יש
בסדרה של
נזכיר ש
1. Create disjoint sets for each vertex of the graph.
2. For every edge u, v in the graph
i) Find the root of the sets to which elements u and v belongs.
ii) If both u and v have the same root in disjoint sets, a cycle found.