You are on page 1of 37

‫דאסט‪ :‬אלגוריתמים על גרפים‬

‫רועי שטוירמן‪31.5 ,‬‬


‫הגדרות‬
‫ו־‬ ‫קבוצה של זוגות קדקודים כך ש־‬ ‫קבוצה ו־‬ ‫תהי‬
‫‪ ,‬אך נדבוק בסימון‬ ‫הן אותה הצלע )פורמלית‪ ,‬הצלע היא הקבוצה‬
‫ייקרא גרף לא מכוון‪.‬‬ ‫(‪ .‬הזוג‬
‫ייקרא גרף מכוון‪ .‬זאת אומרת‪ ,‬כעת‬ ‫‪ .‬הזוג‬ ‫קבוצה ו־‬ ‫תהי‬
‫תת־קבוצה של זוגות‬ ‫‪ .‬נשים לב ש־‬ ‫מסירים את ההגבלה על הסימטריות של‬
‫קבוצה של קבוצות קדקודים‪ ,‬כלומר‬ ‫סדורים‪ ,‬בניגוד לגרף לא מכוון‪ ,‬שם‬
‫סימון מקוצר‬ ‫אז‬ ‫הן לא אותה הצלע‪ .‬אם ממספרים‬ ‫ו־‬
‫‪.‬‬ ‫של הצלע‬
‫‪ ,‬ובגרף פשוט לא־מכוון )גרף שבו אוסרים על צלעות‬ ‫‪,‬‬ ‫מסמנים‬
‫‪.‬‬ ‫‪,‬‬ ‫; בפרט‬ ‫כפולות ולולאות(‪ ,‬מתקיים‬
‫]‪[2‬‬
‫ייצוג של גרף באמצעות מטריצת שכנו ּיוֹ ת‬
‫‪ ,‬כאשר‬ ‫‪:‬‬ ‫היא מטריצה בגודל‬ ‫מטריצת שכנויות של גרף‬

‫אם בין הקדקודים קיימת‬ ‫‪ ,‬כלומר מטריצה שמציגה‬

‫צלע‪ ,‬ו־ אם לא‪ .‬בגרף לא מכוון המטריצה הזו היא כמובן סימטרית‪ ,‬כי‬

‫יש אפסים‪:‬‬ ‫לרוב נניח שאין לולאות‪ ,‬כלומר על האלכסון של‬

‫‪.‬‬ ‫גודל הייצוג הוא‬

‫]‪[3‬‬
‫ייצוג של גרף באמצעות רשימת שכנו ּיוֹ ת‬
‫רשימות מקושרות‪ ,‬כאשר כל‬ ‫היא רשימה של‬ ‫רשימת שכנויות של גרף‬
‫רשימה מקושרת מתארת את השכנים של קדקוד )בגרף מכוון‪ :‬כל הקודקודים שיש‬
‫אליהם‪ ,‬קרי רק הצלעות היוצאות מ־ ( ואורכה הוא‪ ,‬על־כן‪,‬‬ ‫צלע מהקודקוד‬
‫‪.‬‬ ‫בגרף מכוון(‪ .‬גודל הייצוג הוא‬ ‫)או‬

‫לדוגמה‪ ,‬עבור גרף מכוון‪:‬‬

‫]‪[4‬‬
[5]
[6]
‫תכונות והגדרות שימושיות מדיסקרטית‬
‫דרגה של קדקוד היא מספר השכנים שלו‪,‬‬

‫מתקיים‬ ‫למה )"משפט לחיצות הידיים"(‪ .‬בגרף לא־מכוון‬

‫∈‬

‫תיקרא מסלול )באורך ( אם לכל‬ ‫סדרת קדקודים‬


‫שונות זו מזו‪ .‬אם הקדקודים שונים‬ ‫‪+‬‬ ‫וגם הצלעות‬ ‫‪+‬‬ ‫מתקיים‬
‫זה מזה‪ ,‬המסלול ייקרא פשוט‪.‬‬
‫מוגדר כאורך המינימלי של מסלול ביניהן‪ .‬אם לא קיים כזה‪,‬‬ ‫המרחק בין‬
‫‪.‬‬ ‫נסמן‬
‫קוטר של גרף הוא המרחק המקסימלי בין זוג קדקודים בו‪.‬‬
‫]‪[7‬‬
‫גרפים מכוונים‬
‫שונות זו מזו‪ .‬אנו מאפשרים לולאות מהצורה‬ ‫ו־‬ ‫בגרפים מכוונים‪ ,‬הצלעות‬
‫)לעיתים(‪ ,‬ומגדירים בנפרד את דרגת הכניסה והיציאה של כל קדקוד כמספר‬
‫הצלעות הנכנסות והיוצאות לקדקוד בהתאם‪:‬‬

‫הדרגה היא הסכום שלהן‪:‬‬

‫בדומה למקרה הלא־מכוון‪ ,‬הפעם מתקיים‪:‬‬

‫∈‬ ‫∈‬

‫]‪[8‬‬
‫‪ ,‬קיים מסלול מ־ ל־‬ ‫גרף ייקרא קשיר חזק אם לכל שני קדקודים שונים‬
‫וגם ההיפך‪.‬‬
‫אם הוא גרף בעצמו )כלומר‬ ‫ייקרא תת־גרף של‬
‫‪.‬‬ ‫‪ .‬מסמנים‪:‬‬ ‫וכן‬ ‫( וגם מתקיים‬
‫קשיר חזק והוא מקסימלי‬ ‫ייקרא רכיב קשירות )חזקה( אם‬ ‫תת־גרף‬
‫‪ ,‬הגרף‬ ‫בתכונה זו‪ ,‬כלומר לכל קדקוד‬

‫איננו קשיר חזק‪.‬‬

‫]‪[9‬‬
‫גרפים ממושקלים‬
‫פונקציית‬ ‫גרף ו־‬ ‫כך ש־‬ ‫גרף ממושקל הוא שלשה‬
‫משקל על הצלעות‪ .‬לעיתים קרובות נניח ש־ נותנת משקלים חיוביים בלבד )אבל‬
‫לא תמיד נוכל להניח כך; חלק מהאלגוריתמים ידעו לטפל בצלעות עם משקל שלילי‪,‬‬
‫וחלק לא(‪.‬‬
‫‪,‬‬ ‫‪ ,‬או באופן כללי של קבוצת צלעות‬ ‫מגדירים גם משקל של מסלול‪,‬‬
‫כסכום המשקלים‪:‬‬
‫‪−‬‬
‫‪+‬‬
‫=‬

‫∈‬

‫]‪[10‬‬
‫אלגוריתם ה־‪BFS‬‬
‫נכתוב אלגוריתם חדש ואיטרטיבי שעובר על כל צלעות גרף‪ .‬הוא אלגוריתם סורק‬
‫שמחפש לרוחב הגרף )כלומר‪ ,‬סורק אותו לפי מרחק הולך וגדל מקדקוד הבסיס(‪.‬‬
‫בתרגיל ראיתם מימוש חלופי ל־‪ DFS‬בלי רקורסיה‪ ,‬עם שימוש במחסנית‪ ,‬ונשאלתם‬
‫מה היה קורה אילו היה משתמש בתור )‪ (Queue‬ולא במחסנית‪ .‬ובכן‪ ,‬האלגוריתם‬
‫‪ ,BFS‬כפי שניתן היה לחשוד‪ ,‬משתמש בתור‪ .‬להלן פסודו־קוד‪:‬‬

‫]‪[11‬‬
def BFS(G, start_vert, ): # start_vert V
Q = new queue
for v in V:
dist[v] = # mark as unvisited. No known path to it yet!

dist[start_vert] = 0
Q.enqueue(start_vert)
while Q is not empty:
u = Q.dequeue()
for every w so that (u, w) in E:
if dist[w] == : # if it is unvisited

[12]
‫שרטוט ‪ : .‬דוגמה לריצת ה־‪ ,BFS‬משמאל למעלה לימין למטה‪ .‬האלגוריתם מתחיל מהקדקוד 𝑠 ומחפש אחר‬
‫קדקוד המטרה ‪ ,Ѯ‬האות הקירילית העתיקה ‪ .Ksi‬באדום‪ :‬המרחק לכל קדקוד בכל איטרציה‪ .‬נשים לב שלתור‬
‫מכניסים איברים משמאל ומוציאם מימין )וכן לפי סדר נומרי(‪ .‬קדקודים שבוקרו מסומנים בכחול‪.‬‬

‫]‪[13‬‬
‫הוכחת הנכונות של ‪BFS‬‬
‫) מקצר‬ ‫טענה ‪ : .‬האלגוריתם ‪ BFS‬מבקר בכל הצמתים ברכיב הקשירות של‬
‫‪.(start_vert‬‬
‫‪ .‬נראה‬ ‫מסלול מ־ ל־ ‪:‬‬ ‫הוכחת טענה ‪ : .‬יהא‬
‫באינדוקציה על שהאלגוריתם מבקר ב־ ‪.‬‬
‫(‪.‬‬ ‫הטענה ברורה )נובע ישירות מהשורה‬ ‫בסיס האינדוקציה‪ :‬עבור‬
‫לא מבוקר‪ ,‬ומאחר שאנו‬ ‫צעד האינדוקציה‪ :‬אילו ביקרנו ב־ ‪ ,‬סיימנו‪ .‬אחרת‪,‬‬
‫מהנחת האינדוקציה‪ ,‬נקבל שהאלגוריתם יבצע את השורה‬ ‫‪−‬‬ ‫מבקרים ב־‬
‫‪ ,‬כלומר יבקר ב־ ‪.‬‬ ‫‪−‬‬

‫נוכיח עתה את נכונות האלגוריתם‪.‬‬


‫]‪[14‬‬
‫‪ ,‬האלגוריתם‪:‬‬ ‫טענה ‪ : .‬לכל‬
‫עבורם‪.‬‬ ‫‪ .1‬יכניס לתור את כל הקדקודים במרחק ויכתוב‬
‫ולפני‬ ‫‪ .2‬כל הקדקודים במרחק מ־ ייכנסו לתור אחרי כל הקדקודים שבמרחק‬
‫כל הצמתים במרחק גדול מ־ ‪.‬‬

‫הוכחת טענה ‪ : .‬באינדוקציה על כמובן‪.‬‬


‫מ־‬ ‫הטענה ברורה‪ ,‬יש רק קדקוד אחד שבמרחק‬ ‫בסיס האינדוקציה‪ :‬עבור‬
‫והוא עצמו‪ ,‬והוא הקדקוד הראשון שנכנס‪.‬‬
‫ונוכיח עבור ‪ .‬כל הקדקודים‬ ‫צעד האינדוקציה‪ :‬נניח שהטענה נכונה עבור‬
‫במרחק ייכנסו לתור בשלב זה או אחר לפי ) ‪ ,( .‬בהנחה שהגרף קשיר‪ .‬אם‬
‫‪ ,‬והוא יכניס אותו לתור‪ .‬מהנחת‬ ‫קדקוד במרחק מ־ ‪ ,‬יש לו שכן במרחק‬

‫]‪[15‬‬
‫‪ ,‬ומכאן שהמרחק של הקדקוד‬ ‫של שכן זה מורה על‬ ‫האינדוקציה‪ ,‬שדה ה־‬
‫יירשם כ־ ‪.‬‬
‫? חלק זה נובע ישירות מהנחת האינדוקציה עבור‬ ‫מדוע אחרי כל הצמתים במרחק‬
‫‪.‬‬ ‫‪ ,‬מאחר שאין לו שכנים ממרחק של פחות מ־‬
‫ייכנס על־ידי צומת‬ ‫מדוע לפני כל הצמתים במרחק גדול מ־ ? כי צומת במרחק‬
‫‪ .‬כלומר‪,‬‬ ‫בלבד‪ ,‬שנמצא תמיד בתור אחרי כל הצמתים במרחק‬ ‫במרחק‬
‫מהנחת האינדוקציה‪ ,‬לא נטפל בקדקוד ממרחק לפני כל הקדקודים ממרחק‬
‫‪ ,‬ואלה יכניסו לתור את כל הצמתים ממרחק ‪.‬‬

‫‪ ,‬ועל כל הצלעות בגרף‬ ‫נשים לב שהאלגוריתם ‪ BFS‬עובר בשלב הראשון על כל‬


‫עליהן‪ ,‬ומכאן שזמן הריצה שלו הוא‬ ‫פעמיים‪ ,‬ומבצע פעולות בסיבוכיות‬
‫‪.‬‬
‫]‪[16‬‬
[17]
‫ למציאת מספר רכיבי קשירות‬BFS‫שימוש ב־‬
def CC(G):
count = 0
for v V:
if not v.visited:
count++
BFS(G,v)
return count

[18]
[19]
‫אלגוריתם ה־‪DFS‬‬
‫ננסה כעת לכתוב אלגוריתם שייבחן אם גרף הוא קשיר‪ .‬נתחיל בגרפים לא מכוונים‪.‬‬
‫האלגוריתם הראשון שנעסוק בו הוא ‪ .(Depth First Search) DFS‬נגדיר את הפונקציה‬
‫‪:Explore‬‬
‫‪def Explore(v):‬‬
‫‪visited[v] = 1‬‬
‫)‪for all w so that (v, w‬‬ ‫‪E:‬‬
‫‪if visited[w] == 0:‬‬
‫)‪Explore(w‬‬
‫טענה ‪ Explore : .‬מגיעה לכל הקודקודים בגרף אליהם ניתן להגיע מ־ )תקף גם‬
‫למסלול מכוון(‪.‬‬

‫]‪[20‬‬
‫‪ .‬אם‬ ‫הוכחת טענה ‪ : .‬נניח שקיים מסלול בגרף‬
‫‪,‬‬ ‫‪+‬‬ ‫במסלול שלא המשיך ל־‬ ‫‪ Explore‬לא הגיעה ל־ ‪ ,‬אז קיים קדקוד‬
‫‪.‬‬ ‫בסתירה לכך ש־‪ Explore‬סוקרת את כל שכני‬
‫נכליל את האלגוריתם ‪ Explore‬לאלגוריתם האב ‪:DFS‬‬
‫‪def DFS(G):‬‬
‫‪for all v‬‬ ‫‪V:‬‬
‫‪visited[v] = 0‬‬
‫‪for all v‬‬ ‫‪V:‬‬
‫‪if visited[v] == 0:‬‬
‫)‪explore(v‬‬

‫]‪[21‬‬
‫ שימוש למציאת רכיבי קשירות‬:DFS‫אלגוריתם ה־‬
def Explore2(v):
visited[v] = 1
previsit(v)
for all w so that (v, w) E:
if visited[w] == 0:
Explore2(w)
postvisit(v)

def DFS2(G):
for all v in V:
visited[v] = 0
[22]
for all v in V:
if visited[v] == 0:
CC_num += 1
Explore2(v)

def previsit(v):
CC[v] = CC_num

[23]
‫‪def postvisit(v):‬‬ ‫‪# For now, it does nothing.‬‬
‫‪return‬‬

‫‪def CC_main_algorithm(G):‬‬
‫‪CC_num = 0‬‬
‫)‪DFS2(G‬‬

‫‪ ,‬ועוברים על כל‬ ‫מה הסיבוכיות? הוא עובר על כל הקדקודים ולכן היא לפחות‬
‫הצלעות לפחות פעם אחת )בגרף לא מכוון – בדיוק פעם אחת(‪ ,‬אז הסיבוכיות היא‬
‫‪.‬‬ ‫‪ ,‬ו־‬ ‫‪ ,‬כאשר‬

‫]‪[24‬‬
‫מיון טופולוגי‬
‫נניח שיש לנו רשימה של משימות לבצע‪ ,‬כך שיש בין חלקן תלות‪ :‬נניח‪ ,‬עלינו למלא‬
‫טופס לבקשת דרכון‪ ,‬אך לשם כך עלינו לגשת לדואר ולקחת חבילה‪ ,‬ולשם ביצוע‬
‫משימה זו עלינו לחדש את הדרכון‪ ,‬אך לשם כך עלינו להצטלם‪ ,‬ולשם כך עלינו‬
‫להסתפר‪ ,‬ולשם כך עלינו להוציא תו ירוק‪ ,‬אולם לשם כך עלינו להצטלם‪ ,‬וכן‬
‫הלאה‪ 1.‬נרצה לתת הגדרה עבור סידור שיאפשר לנו לבצע את המשימה )אם יש כזה(‪.‬‬

‫הוא סידור של הקדקודים כך‬ ‫הגדרה ‪ : .‬מיון טופולוגי של גרף מכוון‬


‫מתקיים ש־ נמצא לפני בסידור‪.‬‬ ‫שלכל צלע‬

‫‪ 1‬ראו חבילה הגיעה‪.‬‬

‫]‪[25‬‬
‫שרטוט ‪ : .‬גרף מכוון שניתן להגדיר‬
‫עליו מיון טופולוגי‪.‬‬
‫הערה‪" :‬כל‪-‬הנחלים הולכים אל‪-‬הים‪,‬‬
‫שרטוט ‪ : .‬גרף מכוון שלא ניתן להגדיר‬
‫והים איננו מלא; אל‪-‬מקום‪,‬‬
‫עליו מיון טופולוגי‬
‫שהנחלים הולכים‪--‬שם הם שבים‪,‬‬
‫ללכת‪".‬‬

‫נוכל להגדיר מיון טופולוגי כך‪:‬‬ ‫לדוגמה‪ ,‬בגרף ‪.‬‬


‫החרמון‬ ‫נחל השבעה‬ ‫שניר‬ ‫דן‬ ‫נחל החרמון‬ ‫נחל תבור‬
‫הירדן‬ ‫הכינרת‬

‫]‪[26‬‬
‫אולם בגרף ‪ .‬לא ניתן להגדיר מיון טופולוגי‪ ,‬שכן צריך ניסיון מעשי כדי להתקבל‬
‫לעבודה‪ ,‬אבל צריך לעבוד כדי לצבור ניסיון מעשי‪ ,‬ולכן תואר במדמ"ח אינו באמת‬
‫שימושי‪.‬‬

‫נבצע שינוי באלגוריתם‪ :‬נגדיר משתנה חדש‪ ,clock ,‬שבאמצעותו אנו ממספרים את‬
‫הכניסות והיציאות לכל קדקוד‪ .‬אנו שומרים ומעדכנים את ערך המשתנה ‪,clock‬‬
‫כך שהוא ממלא מעין פונקציה הקוראת ל"שעה" הנוכחית‪ ,‬כלומר ממספרת את‬
‫הדברים לפי סדר התרחשותם‪ .‬הפונקציות המעודכנות‪ previsit ,‬ו־‬
‫‪ postvisit‬נראות כך‪:‬‬
‫‪def previsit(v):‬‬
‫‪pre[v] = clock‬‬
‫‪clock += 1‬‬

‫]‪[27‬‬
‫‪def postvisit(v):‬‬
‫‪post[v] = clock‬‬
‫‪clock += 1‬‬

‫‪.‬‬ ‫נשים לב שזמן הריצה האסימפטוטי לא השתנה ועודנו‬

‫טענה ‪ : .‬לגרף מכוון שיש בו מעגל‪ ,‬לא קיים מיון טופולוגי‪.‬‬


‫‪ ,‬ונניח בשלילה שקיים‬ ‫הוכחת טענה ‪ : .‬נניח שקיים מעגל‬
‫‪ ,‬אבל‬ ‫מופיע אחרי‬ ‫מופיע במיון הטופולוגי‪ ,‬ולפי הגדרה‬ ‫מיון טופולוגי‪ .‬אז‬
‫‪.‬‬ ‫מופיע אחרי‬ ‫נקבל בסתירה ש־‬

‫]‪[28‬‬
‫תרגיל‬

‫לכל קדקוד אם אנו מניחים שברשימה המקושרת יש שדה ‪;size‬‬ ‫יציאה‪:‬‬


‫להחזרת מערך עם הדרגות של כל‬ ‫‪ .‬סך־הכול‪,‬‬ ‫אחרת‪,‬‬
‫אם לא )כי אז אנו עוברים על כל‬ ‫הקודקודים‪ ,‬ואם קיים השדה ‪ size‬אז‬
‫הצלעות היוצאות‪ ,‬דהיינו על כל הצלעות(‪.‬‬
‫‪ .‬נעבור‬ ‫‪ ,‬בגודל‬ ‫‪ .‬נאתחל מערך של אפסים‪,‬‬ ‫כניסה‪ :‬נסמן‬
‫על כל הצלעות ברשימה לפי הסדר‪ ,‬ובכל פעם שאנו סופרים צלע שנכנסת לקודקוד‬
‫‪ .++‬סה"כ‪ ,‬אנו עוברים על כל הצלעות ולכן זה יתבצע בזמן‬ ‫‪ ,‬נבצע‬
‫‪.‬‬ ‫של‬
‫]‪[29‬‬
‫⊤‬
‫פשוט מיוצג על־ידי המטריצה המשוחלפת‪,‬‬ ‫‪ ,‬אז‬ ‫מיוצג על־ידי מטריצה‬ ‫אם‬
‫⊤‬
‫)מדוע?(‪.‬‬ ‫‪ .‬חישוב המטריצה הזו לוקח‬
‫⊤‬
‫ב־‬ ‫‪ ,‬נעבור על כל צלע‬ ‫מיוצג על־ידי רשימת שכנויות‪ ,‬נאתחל גרף ריק ל־‬ ‫אם‬
‫⊤‬
‫‪.‬‬ ‫‪ .‬זה ייקח‬ ‫ב־‬ ‫ונשבץ אותה ברשימה של הקודקוד‬

‫]‪[30‬‬
[31]
[32]
[33]
[34]
‫כשנריץ ‪ BFS‬על הגרף מהקודקוד ‪ ,S‬קודם כל נכניס לתור את הקודקודים ‪. c,a‬נניח בלי‬
‫הגבלת הכלליות שהכנסנו את ‪ a‬קודם‪ .‬לכן‪ ,‬גם נוציא את ‪ a‬מהתור קודם‪ ,‬ואז נמשיך‬
‫]‪[35‬‬
‫בתהליך ‪. BFS‬כעת‪ a ,‬יסומן בתור הקודם של ‪ b‬ושל ‪) d‬הרי אפשר להגיע לשניהם‬
‫ממנו בצעד אחד( – אך זה לא המצב בתת־גרף המתואר‪ .‬לכן הוא לא יכול להיות תת־‬
‫גרף הקודמים מריצת ‪ BFS‬מהקודקוד ‪ .s‬עם זאת כל המסלולים בו הם אכן מסלולים‬
‫מאורך המסלול הקצר ביותר בגרף המקורי‪.‬‬

‫]‪[36‬‬
[37]

You might also like