Disjoint Set Data Structure

בהינתן עולם כלשהו של איברים אנחנו רוצים להחזיק חלוקה של האיברים הללו לקבוצות זרות.
כל איבר מתחיל כקבוצה בפני עצמו וניתן לאחר שתי קבוצות לקבוצה גדולה ללא אפשרות פיצול.

Disjoint set או Union find הוא מבנה נתונים התומך בפעולות הבאות

  1. make-set(x) יוצר קבוצה עם האיבר החדש x .
  2. union(x,y) מאחד בין שתי קבוצות המכילות את האיברים x,y (בהנחה שהן בשתי קבוצות שונות)
  3. find-set(x) מוצא את הקבוצה שלה איבר x שייך (נשים לב ש x הוא מצביע לאיזשהו איבר בקבוצה כלשהי)

מימוש ברשימה מקושרת

נחזיק רשימה מקושרת דו כיוונית לכל קבוצה ונגדיר לכל קבוצה איבר מזהה הנציג.
באופן הזה נוכל להפעיל את union בזמן קבוע כי נחזיק לכל רשימה מצביע לראש ולזנב, כלומר אם union יופעל על x,y בסדר הזה, ניקח את הזנב של הקבוצה שבה x נמצא, ונחבר ל נציג של הקבוצה שבה y נמצא
Pasted image 20221128171853.png|250

החישוב של find-set פשוט ידרוש מאיתנו ללכת עד לראש הרשימה כלומר O(n) זמן. בהנחה, (לא סבירה) שהאיחוד נעשה תמיד על הנציגים של הקבוצות. במקרה הכללי, דרוש לפני כן למצוא את הנציגים בשתי קריאות ל find-set כלומר O(n) .

אם נרשום את זמני הריצה בטבלה זה ייראה ככה

make-set union find-set
O(1) O(1) O(n)

ייעול לרשימה מקושרת

כל איבר יחזיק מצביע לאיבר הראשון ברשימה וכעת נוכל לגשת לנציג בזמן ריצה קבוע.
הבעיה שנוצרת ברעיון הזה היא שבמיזוג שתי קבוצות נצטרך להשקיע הרבה יותר זמן כדי לעדכן את המצביעים של רשימה אחת להצביע על ראש הרשימה השנייה (כמובן שנעדיף לקחת את הרשימה הקצרה יותר אבל עדיין זמן הריצה בודק את המקרה הגרוע שהוא O(n)) /
Pasted image 20221128172346.png|350

כלומר אם נרשום את זמני הריצה כעת זה ייראה ככה

make-set union find-set
O(1) O(n) O(1)

נשים לב ש union בפועל לוקח O(min(|Sx|,|Sy|)) שבמקרה הגרוע זה שקול ל n.

ב ניתוח לשיעורין העלות לשיעורין של פעולת union היא O(logn)

מימוש על ידי יערות

נחזיק יער של כל הקבוצות, כאשר כל קבוצה תייצג רכיב קשירות ביער כלומר עץ כאשר השורש הוא ה נציג שהגדרנו.

Pasted image 20221128173323.png|350

לכל קודקוד x נשמור מצביע לאבא שלו בעץ , האבא של הנציג יהיה עצמו. וכעת
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)

Pasted image 20221128181141.png|350
Pasted image 20221128181205.png|350
נשים לב, ה rank של כל הקודקודים נשאר זהה, פרט למקרה שבו מאחדים שני עצים עם אותו rank, ואז השורש החדש מקדם את הrank ב1.

דחיסת מסלולים

אם אנחנו משקיעים O(x) זמן כדי לשלוף ערך מסויים t אז בסוף הפעולה נהפוך את עומק x להיות 1 ע״י הגדרת t כבן ישיר של השורש.
את התהליך הזה נעשה לכל הקודקודים על המסלול מ x עד לשורש.

int find_set(int v) {
    if (v == parent[v])
        return v;
    return parent[v] = find_set(parent[v]);
}

Pasted image 20221128201253.png|450
נשים לב שאין צורך לעדכן את ה rank של קודקודים שאינם מעודכנים, גם ככה משתמשים רק ב rank של השורש לביצוע union. גם אם הדרגה של השורש תפסיק לייצג את גובה העץ ממש, היא עדיין תהיה חסם עליון על גובה העץ.
(בפועל זה יכול לגרום למצב שנאחד עץ גבוה לתוך עץ נמוך, בכל מקרה זמן הריצה עדיין חסום בצורה טובה).

תכונות

לכל x שאינו השורש מתקיים rank(x)<rank(π(x)) נשים לב ש π מייצג את האבא של קודקוד x.
ההוכחה תחולק למקרים
א. אם יוצרים קודקוד אז בוודאי שמתקיים כי x הוא שורש.
ב. אם התקיים עבור x עד לזמן מסויים, יכול להשתנות רק כתוצאה מהבאים
שינוי rank(x) , שינוי π(x) או שינוי של rank(π(x)) ללא שינוי של π(x) .

לכל שורש עם rank שערכו k יש עץ עם לפחות 2k קודקודים
ההוכחה היא באינדוקצייה על מספר פעולות ה union כי אלה היחידות שמשנות את הדרגה.
בסיס: אם לא עשינו פעולת union אחת הדרגה בהכרח שווה ל 0 ובעץ יש לכל הפחות קודקוד אחד 20=1 .
צעד: נניח שמתקיים לפני הביצוע של union בפעם ה n. נסמן את הדרגה לפני ב r1 ואת הדרגה אחרי הפעולה ב r2 ובאופן דומה נסמן ב s1,s2 את גדלי העצים לפני ואחרי הפעולה.

אם r1(x)r1(y) ובלי הגבלת הכלליות נניח r1(x)<r1(y) אז y יהיהי שורש העץ החדש

s2(y)=s1(x)+s1(y)2r1(x)+2r1(y)2r1(y)=2r2(y)

אם r1(x)=r1(y) ובלי הגבלת הכלליות נניח y יהיהי שורש העץ החדש אזי

s2(y)=s1(x)+s2(y)2r1(x)+2r1(y)=2r1(x)+1=2r2(x)

לכל קודקוד שדרגתו k, ניתן לשייך קבוצה של לפחות 2k קודקודים שונים כך שכל קודקוד במבנה יהיה משוייך לכל היותר לקודקוד אחד שדרגתו k

אם יש n איברים ביעד בסה״כ אז יש לכל היותר n2k קודקודים עם דרגה k

זמני ריצה

בסדרה של m פעולות על מבנה הנתונים שמכיל n איברים העלות הכוללת תהיה
O(nlogn+mlogn)O(mlogn)
נזכיר ש log זה בעצם כמה פעמים ניתן להוציא log עד שמגיעים למספר קטן או שווה ל1.

T(n)=T(logn)+1

שימושים

מציאת מעגלים בגרף לא מכוון

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.