מבני נתונים ואלגוריתמים - מחברת קורס/מבחן סופי מועד א' סמסטר קיץ 2007 - תשובות

מתוך ויקיספר, אוסף הספרים והמדריכים החופשי

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



שאלה 1[עריכה]

ראשית נבנה את רשת הזרימה המתוארת בתרשים הבא. בגרף זה צמתים:

  1. צמתים , אחד לכל סטודנט
  2. צמתים , אחד לכל חברה
  3. צומת מקור וצומת בור

נגדיר כ את Length(W).

בגרף יש קשתות:

  1. קשת אחת לכל איבר בW בעלת קיבול 1
  2. קשת מ לכל בעלת קיבול 1
  3. קשת מכל ל בעלת קיבול
.
.

כעת נריץ את אלגוריתם Ford-Fulkerson. נחזיר כתשובה כל זוג שעבורו קיבלנו .



משפט:

האלגוריתם מחזיר תשובה נכונה.



הוכחה: אנו יודעים שאלגוריתם Ford-Fulkerson משפר תמיד בגדלים שלמים (שהרי הקיבולים שלמים). קל מאד להוכיח (פורמלית, באינדוקציה), שבמקרה זה, חלק מהקשתות מ ל יזרימו 1, והשאר 0 (אלו האפשרויות היחידות). נצייר את הגרף המתקבל ממחיקת הקשתות שאינן מזרימות כלום:

.
.

נניח שיש קשת בין ל. בהכרח הקשת מזרימה 1. מצד שני, הקשת המזרימה מ ל מזרימה גם היא 1. משימור הזרם נובע אם כן שאין קשת אחרת היוצאת מ.טענה כמעט זהה מראה שאין קשת אחרת הנכנסת ל. ברור גם שקשת זו היתה שייכת לגרף המקורי. מכאן נובע שהמערך המוחזר W' אכן משייך אחת לאחת סטודנט לחברה, ואינו ממציא שיוכים שלא הופיעו במקור. קל גם לראות שכל חברה אינה מעסיקה יותר סטודנטים מיכולתה. קל לראות שאם יש התאמה גדולה יותר, אז אפשר להגיע לזרימה גדולה יותר מהמושגת כאן, וזאת בסתירה לנכונות Ford-Fulkerson.



משפט:

סיבוכיות האלגוריתם .



הוכחה: אם משתמשים ברשימת שכנויות, אז בניית הגרף החדש היא .כבר ראינו בניתוח אלגוריתם Ford-Fulkerson, שסיבוכיותו לינארית במספר הקשתות כפול גודל הזרימה המירבית. מספר הקשתות כאן הוא , והזרימה המירבית היא

שאלה 2[עריכה]

המבנה הרקורסיבי[עריכה]

נגדיר כ את העלות הזולה ביותר לטיפול בקטע באורך .



משפט:

נתונה על ידי נוסחת הנסיגה הבאה:

  1. אם ,‏ אז .
  2. אם ,‏ אז .



הוכחה: נשים לב לנקודות הבאות:

  1. אם ,‏ אז אין מה לחתוך; ההובלה תעלה .
  2. אם ,‏ אז או שלא חותכים את הקטע, או שכן. אם לא חותכים, עלות השבירה 0, ועלות ההובלה . אם חותכים בנקודה , אז עלות הטיפול בחלק השמאלי תהיה , עפ"י ההגדרה, ,‏ עלות השבירה היא כמובן ,‏ ועלות הטיפול בחלק הימני תהיה, עפ"י ההגדרה, . נותר לקחת מינימום על פני , ולהחליט האם לחתוך או לא.



כדאי לדעת:

אפשר כמובן לחשוב על מבנה רקורסיבי אחר לבעיה, כמו לדוגמה המבנה בדג הסלמון.

מימוש נאיבי[עריכה]

להלן פסוודו-קוד המממש את נוסחת הנסיגה.

Min-Cost(i)
1	if i == 1
2		return F(1)
	
3	guess-without = F(i)

4	guess-with = 
5	for j in [1, , i - 1]
6		if guess-with> Min-Cost(j) + k + Min-Cost(i - j)
7			guess-with = Min-Cost(j) + k + Min-Cost(i - j)		
		
8	if guess-with < guess-without
9		return guess-with
10	else
11		return guess-without

שימוש בmemoization[עריכה]

נוסיף memoization.

Min-Cost(i)
1	if M[i] != Nil
2		return M[i]

1	if i == 1
2		M[1] = F(1)
3		return F(1)
	
4	guess-without = F(i)

5	guess-with = 
6	for j in [1,  i - 1]]
7		if guess-with> Min-Cost(j) + k + Min-Cost(i - j)
8			guess-with = Min-Cost(j) + k + Min-Cost(i - j)		
		
9	if guess-with M[i] < guess-without
10		M[i] = guess-with
11		return guess-with
12	else
13		M[i] = guess-without
14		return guess-without

(נניח שהמערך הגלובלי Mבאורך nמאותחל תחילה כולו לNil.)

הדפסת נקודות השבירה[עריכה]

נוסיף גם מספיק מידע לצורך הדפסת פרטי הפתרון.

Min-Cost(i)
1	if M[i] != Nil
2		return M[i]

1	if i == 1
2		M[1] = F(1)
3		Break[1] = Nil
4		return F(1)
	
5	guess-without = F(i)

6	guess-with = 
7	for j in [1,  i - 1]]
8		if guess-with> Min-Cost(j) + k + Min-Cost(i - j)
9			guess-with = Min-Cost(j) + k + Min-Cost(i - j)		
10			if guess-with 
11				Break[i] = j
				
12	if guess-with 
13		M[i] = guess-with
14		return guess-with
15	else
16		M[i] = guess-without	
17		return guess-without

המערך הגלובלי Break מכיל את ההחלטה שעשינו לגבי כל קטע. Break[i]הוא Nilאם החלטנו לא לחלק את הקטע, והוא אם החלטנו לשבור בנקודה j.

כעת נדפיס את העצירות, ע"י קריאה לPrint-Breaks(n, 0). הפונקציה מוגדרת להלן.

Print-Breaks(i, start)
1	if Break[i] == Nil
2		return
	
3	j = Break[i]
	
4	Print(j + start)

5	Print-Nums(j, start)
6	Print-Nums(i - j, start + j)

הפונקציה מקבלת שני ארגומנטים: אורך הקטע, i,‏ ותחילת הקטע, start.‏ נניח שהפונקציה נקראה עבור שני ערכים ספיציפיים כאלה. אם Break[i] == Nil, אז לא חתכנו את הקטע, ואין מה להדפיס. אם לא, וBreak[i] == j, אז יש להדפיס את הנקודה j(תוך זכירה שאנו נמצאים startמתחילת כל הגשר), את החלק השמאלי, ואת החלק הימני.

ניתוח סיבוכיות[עריכה]

נגדיר את זמן הריצה של Min-Time(i) בהנחה שכל קריאה רקורסיבית היא כ. קל לראות ש. זמן הריצה חסום מלמעלה על ידי .

גם כאשר לוקחים בחשבון את האתחול והדפסת הפרטים, הפתרון עדיין .

שאלה 3[עריכה]

שימו לב:

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

נניח שזהו הגרף המקורי:

הבעיה המקורית.
הבעיה המקורית.

נבנה גרף חדש כך.

ראשית, נקח את צמתי הגרף , ונשכפל אותם 3 פעמים, בקבוצות ,‏ , ו. לכל צומת בקבוצה , יש צומת ‏ ב,‏ וצומת ‏ ‏ ב.‏ התרשים הבא מראה זאת.

שכפול הצמתים.
שכפול הצמתים.

כעת נוסיף גם קשתות:

  1. במקום כל קשת בגרף המקורי, למעט הקשת השלילית, נמתח קשת מהצומת המתאים ל ב לצומת המתאים ל ב.
  2. במקום כל קשת בגרף המקורי, למעט הקשת השלילית, נמתח קשת מהצומת המתאים ל ב לצומת המתאים ל ב.
  3. במקום הקשת השלילית (בגרף המקורי), נמתח קשת מהצומת המתאים ל ב לצומת המתאים ל ב במחיר 0. התרשים הבא מראה זאת.
הוספת הקשתות.
הוספת הקשתות.

נניח שלקשת השלילית היה מחיר . בנוסף נמתח עוד שני סוגי קשתות:

  1. לכל צומת ב, נמתח קשת במחיר בין ב לבין ‏ ב.
  2. לכל צומת ב, נמתח קשת במחיר 0 בין ב לבין ‏ ב.(לא נצייר קשתות אלו מטעמי נוחות.)

נשים לב שאם כל הגרפים הנ"ל מיוצגים ברשימת שכנויות, אז סיבוכיות הבניה היא .

המשפט הבא מסביר את חשיבות הגרף החדש.



משפט:

מחיר המסלול הזול ביותר מ ל בגרף המקורי, קטן בדיוק ב ממחיר המסלול הזול ביותר מ ל ב בגרף החדש.



הוכחה: בגרף המקורי, ייתכן שהמסלול הזול ביותר מ ל לא השתמש בקשת השלילילת, וייתכן שהשתשמש בו. במקרה הראשון, תהיה קפיצה מ ישירות ל, ובמקרה השני, תהיה קפיצה מ ל ומ ל. הקפיצה מ ישירות ל עולה יותר מאשר המסלול המקורי. הקפיצות מ ל ומ ל גם כן עולות יותר מאשר המסלול המקורי, שכן עלות כל אחת משתי הקשתות היא 0, והנוסע לא קיבל את התשלום מהחברה.

כעת כל הקשתות אינן שליליות, ומותר להריץ את Dijkstra.

קל לראות שהסיבוכיות הכוללת היא זו של Dijkstra.

שאלה 4[עריכה]

  1. הטענה נכונה. נניח שבערימה יש איברים. האיבר האחרון בערימה הוא באינדקס , ואביו (אם יש לו) בהכרח יושב ב. לכן, למרות שהלולאה אינה עוברת על כל האיברים, היא מדלגת רק על ערימות בעלי איבר יחיד, שהן, עפ"י תכונת הערימה, תקינות. הלולאה תתקן כל ערימה שדורשת תיקון, בדיוק מהסיבה שראינו בהרצאה. הסיבוכיות היא . אפשר לראות שהלולאה עוברת על חצי מהאיברים, ולכן היא . מצד שני, היא עושה פחות מBuild-Heap, וזו היתה , ולכן גם לולאה זו .
  2. הטענה נכונה. אם ילדו השמאלי של איבר במקום נמצא באינדקס , וילדו הימני נמצא באינדקס , אז כל מסלול מהשורש לעלה עובר על סדרת אינדקסים מונוטונית עולה. לכן, אם המערך ממויין, כל מסלול מהשורש לעלה יעבור על סדרת ערכים מונוטונית לא-יורדת. הסיבוכיות הנה מיון מיזוג, כלומר .
  3. הטענה אינה נכונה, ומפורכת (לדוגמה) ע"י . הסיבוכיות הנה , בדיוק מהניתוח שכבר ראינו לגבי הפתרון מההרצאה.
  4. הטענה נכונה. היות ש Bubble-Up(bh, i) אינו יכול להשפיע על איברים באינדקס מעל , הקוד שקול לסדרת פעולות Insert. מאותה סיבה, הסיבכויות היא זו של סדרת פעולות Insert, כלומר .
  5. הטענה אינה נכונה, ומפורכת (לדוגמה) ע"י . הסיבכויות היא זו של סדרת פעולות Insert, כלומר .