מתכונת 6 במדעי המחשב - מבני נתונים ומסלול 899271
4–5 יח"ל · שאלון 899271 · 12 שאלות · 3 שעות · פתרון מלא לכל סעיף
מבחן מתכונת במדעי המחשב, מבני נתונים ומסלול, במבנה של בחינת הבגרות. כל שאלה בו בנויה כמו שאלה מבגרות אחרת. לכל סעיף יש תשובה ופתרון מלא, וכל קוד מופיע ב-Java וב-C#. פתרו בזמן של הבחינה, ורק אחר כך פתחו את הפתרונות.
הוראות הבחינה
א. משך הבחינה: שלוש שעות.
ב. מבנה השאלון ומפתח ההערכה: בשאלון זה שני פרקים.
פרק ראשון – 25×2 – 50 נקודות
פרק שני – 25×2 – 50 נקודות
סך הכול – 100 נקודות
ג. חומר עזר מותר בשימוש: כל חומר עזר, חוץ ממחשבון שיש בו אפשרות תכנות.
ד. הוראות מיוחדות:
(1) רשמו על הכריכה החיצונית של המחברת את שם המסלול שלמדתם.
המסלול הוא אחד משלושת המסלולים האלה: אלגוריתמים, מודלים חישוביים, תכנות מונחה עצמים.
(2) את כל התוכניות שיש לכתוב בשפת מחשב בפרקים הראשון והשני כתבו בשפה אחת בלבד – Java או C#.
הערה: לא יורדו נקודות אם תכתבו בתוכניות אות גדולה במקום אות קטנה או להפך.
יש לכתוב במחברת הבחינה בלבד. יש לרשום "טיוטה" בראש כל עמוד המשמש טיוטה.
כתיבת טיוטה בדפים שאינם במחברת הבחינה עלולה לגרום לפסילת הבחינה.
השאלות בשאלון זה מנוסחות בלשון רבים, אף על פי כן על כל תלמידה וכל תלמיד להשיב עליהן באופן אישי.
בהצלחה!
השאלות
בשאלון זה שני פרקים. יש לענות על שאלות משני הפרקים, לפי ההוראות בכל פרק.
הערה: בכל שאלה שנדרשת בה קליטה, אין צורך לבדוק את תקינות הקלט.
לפותרים בשפת Java: בכל שאלה שנדרשת בה קליטה, הניחו שבתוכנית כתובה ההוראה:
Scanner input = new Scanner (System.in);
שימו לב: בכל שאלה שנדרש בה מימוש אפשר להשתמש בפעולות של המחלקות: תור, מחסנית, עץ בינרי וחוליה, בלי לממש אותן. אם משתמשים בפעולות נוספות, יש לממש אותן.
פרק ראשון
ענו על שתיים מן השאלות 1–3 (לכל שאלה – 25 נקודות).
שאלה 1 · תור הדפסות לפי מספר עמודים (25 נקודות)
נתונה המחלקה PrintJob – עבודת הדפסה שנשלחה למדפסת משותפת במשרד, ולה שתי תכונות:
הניחו שלתכונות המחלקה יש פעולות get/Get ו־ set/Set.
כדי לקצר את זמן ההמתנה, המדפסת מדפיסה את העבודות לפי הסדר שלהלן:
עבודה שיש בה פחות עמודים מודפסת מוקדם יותר. כאשר יש יותר מעבודה אחת עם אותו מספר עמודים, העבודה שנשלחה קודם מודפסת מוקדם יותר.
כדי לשמור על סדר ההדפסה נבנתה המחלקה PrintQueue – תור הדפסות, ולה תכונה אחת:
דוגמה לתור q:

הניחו שהעבודות שיש בהן אותו מספר עמודים מוצגות בדוגמה לפי סדר שליחתן למדפסת.
ממשו את הפעולה שלפניכם השייכת לממשק המחלקה PrintQueue :
Java – public void addJob (PrintJob job)
C# – public void AddJob (PrintJob job)
הפעולה מקבלת עבודה חדשה – job ומכניסה אותה לתור q בהתאם לסדר ההדפסה הכתוב לעיל.
לדוגמה: עבור התור המוצג לעיל והעצם שלפניכם:

התור ייראה כך לאחר ההכנסה:

עובד ששלח למדפסת שתי עבודות יכול לבקש לאחד אותן לעבודה אחת.
ממשו את הפעולה שלפניכם השייכת לממשק המחלקה PrintQueue :
Java – public void combine (int code1, int code2)
C# – public void Combine (int code1, int code2)
הפעולה מקבלת קודים של שתי עבודות שונות הנמצאות בתור – code1 ו־ code2.
הפעולה תאחד את שתי העבודות לעבודה אחת: לעבודה code1 יתווספו כל העמודים של העבודה code2, והעבודה code2 תוצא מהתור. העבודה המאוחדת תוצב בתור במקום המתאים לה (בהתאם למספר העמודים החדש שלה). אם בתור כבר יש עבודות אחרות עם אותו מספר עמודים, העבודה המאוחדת תוצב אחריהן כאילו נשלחה אחריהן למדפסת.
◂ פתרון מלא לשאלה 1
סעיף א
מעבירים את q לתור עזר. לפני העבודה הראשונה שמספר העמודים שלה גדול ממש ממספר העמודים של job מכניסים את job. אם אין עבודה כזו, job נכנסת בסוף. אחר כך מחזירים את כל האיברים מתור העזר ל־q.
מה נדרש: להכניס עבודה חדשה למקום הנכון בתור, כך שהתור יישאר מסודר לפי מספר העמודים (מהקטן לגדול), ובאותו מספר עמודים – לפי סדר השליחה.
רעיון: התור תמיד מסודר לפי כללי ההדפסה. העבודה החדשה נשלחה אחרונה, ולכן היא צריכה לעמוד:
לכן מוציאים את העבודות מ־q אחת־אחת ומכניסים אותן לתור עזר temp. כשנתקלים לראשונה בעבודה שמספר העמודים שלה גדול מזה של job, מכניסים קודם את job ומסמנים inserted = true. אם התור הסתיים ו־job עדיין לא הוכנסה (בכל העבודות יש פחות עמודים או אותו מספר, או שהתור ריק), מכניסים את job בסוף. לבסוף מעבירים את temp בחזרה ל־q.
פתרון: הקוד המלא – בלשונית Java/C#. הפעולה נמצאת בתוך המחלקה PrintQueue וניגשת ישירות לתכונה q. הגישה לתכונות של PrintJob נעשית בפעולות get.
יעילות: O(n), כאשר n הוא מספר העבודות בתור. כל עבודה עוברת לתור העזר וחוזרת ממנו פעם אחת.
בדיקה: בתור 2, 5, 5, 12, 20, 40 (מספרי העמודים) מכניסים את 6075 עם 5 עמודים. העבודות 2041 (2), 3310 (5) ו־1187 (5) עוברות לתור העזר. העבודה 4402 (12) היא הראשונה עם יותר מ־5 עמודים, ולכן לפניה מוכנסת 6075. מתקבל:
2041/2, 3310/5, 1187/5, 6075/5, 4402/12, 2390/20, 5120/40, בדיוק כמו באיור. נבדקו גם הכנסה לתור ריק, הכנסת עבודה של 100 עמודים (נכנסת לסוף) והכנסת עבודה של עמוד אחד (נכנסת לראש התור).
טעות נפוצה: להשוות עם ≥ במקום עם >. במקרה כזה העבודה החדשה נכנסת לפני עבודות עם אותו מספר עמודים שנשלחו לפניה, וזה בניגוד לכללים.
public void addJob(PrintJob job)
{
Queue<PrintJob> temp = new Queue<PrintJob>();
boolean inserted = false;
while (!q.isEmpty())
{
PrintJob cur = q.remove();
// job נכנס לפני העבודה הראשונה שיש בה יותר עמודים ממנו
if (!inserted && cur.getPages() > job.getPages())
{
temp.insert(job);
inserted = true;
}
temp.insert(cur);
}
if (!inserted) // אין עבודה ארוכה ממנו – job בסוף התור
temp.insert(job);
while (!temp.isEmpty()) // העברה חזרה לתור q
q.insert(temp.remove());
}סעיף ב
מוציאים מהתור את שתי העבודות code1 ו־code2 (שאר העבודות נשארות באותו סדר), מעדכנים את מספר העמודים של code1: setPages(pages1 + pages2), ומכניסים אותה מחדש עם addJob מסעיף א.
מה נדרש: לאחד שתי עבודות שבתור לעבודה אחת ולהציב אותה במקום החדש המתאים לה.
רעיון: מחלקים את העבודה לשני שלבים:
לפי השאלה שתי העבודות נמצאות בתור, ולכן job1 ו־job2 אינם null.
פתרון: הקוד המלא – בלשונית Java/C#.
יעילות: O(n). מעבר אחד להוצאת שתי העבודות ועוד מעבר אחד בתוך addJob.
בדיקה (על התור מהדוגמה, לפני ההכנסה מסעיף א):
טעות נפוצה: לעדכן את מספר העמודים של code1 ולהשאיר אותה במקומה (התור כבר לא מסודר), או להפעיל את addJob בלי להוציא אותה קודם מהתור – אז היא מופיעה בתור פעמיים.
public void combine(int code1, int code2)
{
Queue<PrintJob> temp = new Queue<PrintJob>();
PrintJob job1 = null;
PrintJob job2 = null;
while (!q.isEmpty())
{
PrintJob cur = q.remove();
if (cur.getCode() == code1)
job1 = cur; // מוציאים את שתי העבודות מהתור
else if (cur.getCode() == code2)
job2 = cur;
else
temp.insert(cur); // שאר העבודות נשמרות לפי הסדר
}
while (!temp.isEmpty())
q.insert(temp.remove());
job1.setPages(job1.getPages() + job2.getPages());
addJob(job1); // הכנסה מחדש לפי מספר העמודים החדש
}שאלה 2 · משאית חלוקה – מחיקת חבילות משרשרת חוליות (25 נקודות)
נתונה המחלקה Parcel – חבילה, ולה שתי תכונות:
הניחו שיש פעולות get/Get ו־ set/Set לתכונות המחלקה.
נתונה המחלקה Truck – משאית חלוקה, ולה תכונה אחת:
הערה: החבילות אינן מסודרות בשרשרת בסדר מסוים, וכל חבילה מופיעה פעם אחת בלבד (ייתכן שלכמה חבילות יש אותו משקל).
ממשו את הפעולה של ממשק המחלקה Truck שלפניכם:
Java – public int removeFrom (int w, int k)
C# – public int RemoveFrom (int w, int k)
הפעולה תשאיר בשרשרת את k − 1 החבילות הראשונות שמשקלן w , ותמחק מן השרשרת את כל שאר החבילות שמשקלן w (כלומר את החבילה ה־ k במספר מבין החבילות שמשקלן w , ואת כל החבילות שמשקלן w שאחריה).
הפעולה תחזיר את כמות החבילות שנמחקו.
הניחו ש־ w ו־ k גדולים מ־ 0 , ושבתום הפעולה השרשרת לא תהיה ריקה.
הערה: שאר החבילות בשרשרת יישארו באותו הסדר. אם בשרשרת יש פחות מ־ k חבילות שמשקלן w , השרשרת תישאר ללא שום שינוי והפעולה תחזיר 0 .
דוגמה: בעבור w = 12 , k = 2 והשרשרת lst שלפניכם:

הפעולה תחזיר 3 והשרשרת תיראה כך בתום הפעולה:

הסבר: בשרשרת יש ארבע חבילות שמשקלן 12 . מכיוון ש־ k = 2 , החבילה הראשונה מביניהן ("P1") נשארה בשרשרת, ושלוש האחרות ("P3" , "P5" , "P7") נמחקו. הפעולה החזירה 3 .
דוגמה נוספת: בעבור אותה השרשרת מן הדוגמה המקורית שלעיל ו־ w = 12 , k = 1 , הפעולה תחזיר 4 והשרשרת תיראה כך בתום הפעולה:

הסבר: כאשר k = 1 לא נשארת בשרשרת אף חבילה שמשקלה 12 , ולכן נמחקו כל ארבע החבילות שמשקלן 12 (גם החבילה הראשונה בשרשרת), והפעולה החזירה 4 .
ממשו את הפעולה של ממשק המחלקה Truck שלפניכם:
Java – public int removeRepeats ()
C# – public int RemoveRepeats ()
הפעולה תמחק מן השרשרת חבילות, כך שמכל משקל תישאר בשרשרת רק החבילה הראשונה שזה משקלה. הפעולה תחזיר את סכום המשקלים הכולל של כל החבילות שנמחקו.
הערות:
דוגמה: בעבור אותה השרשרת מן הדוגמה המקורית בסעיף א, הפעולה תחזיר 43 , והשרשרת תיראה כך:

הסבר: מבין ארבע החבילות שמשקלן 12 נשארה רק הראשונה ("P1"), ומבין שתי החבילות שמשקלן 7 נשארה רק הראשונה ("P2"). נמחקו "P3" , "P5" , "P7" ו־ "P6" , וסכום משקליהן 12 + 12 + 12 + 7 = 43 .
◂ פתרון מלא לשאלה 2
סעיף א
עוברים על השרשרת עם pos ו־prev (החוליה שלפניו) וסופרים ב־seen את החבילות במשקל w. חבילה במשקל w עם seen ≥ k נמחקת: אם prev == null – lst = pos.getNext(), אחרת prev.setNext(pos.getNext()), ו־count++; אחרת prev = pos. מחזירים count. יעילות O(L).
מה נדרש: לממש פעולה במחלקה Truck שמשאירה בשרשרת את k − 1 החבילות הראשונות שמשקלן w, מוחקת את כל שאר החבילות שמשקלן w, ומחזירה כמה חבילות נמחקו.
רעיון: עוברים על השרשרת פעם אחת מתחילתה ומחזיקים שתי הפניות: pos – החוליה הנבדקת, ו־prev – החוליה שלפניה בשרשרת (בהתחלה null). בנוסף סופרים ב־seen כמה חבילות במשקל w כבר פגשנו.
אחרי מחיקה prev לא משתנה, כי החוליה שלפני החוליה הבאה היא עדיין prev. אם יש פחות מ־k חבילות במשקל w, התנאי seen ≥ k לא מתקיים אף פעם, השרשרת לא משתנה ומוחזר 0.
פתרון:
public int removeFrom(int w, int k)
{
int seen = 0; // כמה חבילות במשקל w כבר נמצאו
int count = 0; // כמה חבילות נמחקו
Node<Parcel> prev = null; // החוליה שלפני pos (null – pos היא החוליה הראשונה)
Node<Parcel> pos = lst;
while (pos != null)
{
boolean match = pos.getValue().getWeight() == w;
if (match)
seen++;
if (match && seen >= k)
{
// מחיקת pos: מחברים את החוליה שלפניה לחוליה שאחריה
if (prev == null)
lst = pos.getNext();
else
prev.setNext(pos.getNext());
count++;
}
else
{
prev = pos; // pos נשארת בשרשרת
}
pos = pos.getNext();
}
return count;
}בדיקה: על השרשרת מהדוגמה (P1:12, P2:7, P3:12, P4:5, P5:12, P6:7, P7:12):
יעילות: כל חוליה נבדקת פעם אחת, ובכל חוליה מספר קבוע של פעולות ⇒ O(L), כאשר L מספר החוליות בשרשרת.
טעות נפוצה: לעדכן prev = pos גם אחרי מחיקה – אז prev מצביע על חוליה שכבר אינה בשרשרת, ואם שתי חבילות שצריך למחוק צמודות זו לזו, השנייה לא תימחק באמת (prev.setNext משנה חוליה שכבר נמחקה). טעות נוספת: לשכוח את המקרה שבו נמחקת החוליה הראשונה (k = 1) ויש לשנות את lst.
public int removeFrom(int w, int k)
{
int seen = 0; // כמה חבילות במשקל w כבר נמצאו
int count = 0; // כמה חבילות נמחקו
Node<Parcel> prev = null; // החוליה שלפני pos (null – pos היא החוליה הראשונה)
Node<Parcel> pos = lst;
while (pos != null)
{
boolean match = pos.getValue().getWeight() == w;
if (match)
seen++;
if (match && seen >= k)
{
// מחיקת pos: מחברים את החוליה שלפניה לחוליה שאחריה
if (prev == null)
lst = pos.getNext();
else
prev.setNext(pos.getNext());
count++;
}
else
{
prev = pos; // pos נשארת בשרשרת
}
pos = pos.getNext();
}
return count;
}סעיף ב
עוברים על השרשרת עם pos; לכל חוליה: w = המשקל שלה, k = removeFrom(w, 2) (מוחק את כל החבילות במשקל w פרט לראשונה, שהיא pos), sum += k·w. מחזירים sum (בדוגמה: 43). יעילות O(L²).
מה נדרש: להשאיר בשרשרת רק את החבילה הראשונה מכל משקל, ולהחזיר את סכום המשקלים של החבילות שנמחקו.
רעיון: עוברים על השרשרת עם pos. לכל חוליה pos שמשקלה w מזמנים את הפעולה מסעיף א: removeFrom(w, 2). עם k = 2 הפעולה משאירה את החבילה הראשונה במשקל w ומוחקת את כל האחרות. החבילה הראשונה במשקל w היא בדיוק pos: כל החבילות שלפני pos כבר טופלו, וכל החבילות שאחריהן ששקלו כמוהן כבר נמחקו – ולכן אף חבילה לפני pos אינה במשקל w. לכן pos עצמה לא נמחקת, ואפשר להמשיך ממנה ל־pos.getNext().
הפעולה removeFrom מחזירה כמה חבילות נמחקו, ולכן מוסיפים לסכום k·w. השרשרת לעולם לא מתרוקנת (pos נשארת), כך שההנחה של סעיף א מתקיימת.
פתרון (Java) – הפעולה removeFrom מסעיף א נמצאת באותה מחלקה:
// משתמשת בפעולה removeFrom מסעיף א
public int removeRepeats()
{
int sum = 0;
Node<Parcel> pos = lst;
while (pos != null)
{
int w = pos.getValue().getWeight();
// pos היא החבילה הראשונה במשקל w – מוחקים את כל שאר החבילות במשקל w
int k = removeFrom(w, 2);
sum = sum + k * w;
pos = pos.getNext();
}
return sum;
}בדיקה: על השרשרת מהדוגמה:
| pos | w | removeFrom(w, 2) מחקה | sum | השרשרת אחרי הצעד |
|---|---|---|---|---|
| P1 | 12 | 3 (P3, P5, P7) | 36 | P1:12, P2:7, P4:5, P6:7 |
| P2 | 7 | 1 (P6) | 43 | P1:12, P2:7, P4:5 |
| P4 | 5 | 0 | 43 | P1:12, P2:7, P4:5 |
הפעולה מחזירה 43 והשרשרת P1:12, P2:7, P4:5 – כמו בדוגמה. מקרים נוספים: שרשרת 4, 4, 4 (מוחזר 8, נשארת חבילה אחת) ושרשרת 1, 2, 3 (מוחזר 0, ללא שינוי).
יעילות: הלולאה עוברת על לכל היותר L חוליות, ובכל צעד removeFrom עוברת על כל השרשרת – O(L) ⇒ O(L·L) = O(L²).
// משתמשת בפעולה removeFrom מסעיף א
public int removeRepeats()
{
int sum = 0;
Node<Parcel> pos = lst;
while (pos != null)
{
int w = pos.getValue().getWeight();
// pos היא החבילה הראשונה במשקל w – מוחקים את כל שאר החבילות במשקל w
int k = removeFrom(w, 2);
sum = sum + k * w;
pos = pos.getNext();
}
return sum;
}שאלה 3 · עץ K־דליל (25 נקודות)
"עץ K־דליל" הוא עץ בינרי ובו כל מסלול משורש העץ לעלה כלשהו מכיל לכל היותר K צמתים שיש להם בן יחיד (כלומר צמתים שיש להם בן שמאלי בלבד או בן ימני בלבד).
שימו לב: ב"עץ K־דליל" ייתכן שאין שום מסלול המכיל K צמתים שיש להם בן יחיד (כלומר ייתכן שבכל אחד מן המסלולים יש פחות מ־ K צמתים כאלה).
דוגמה:

עץ זה אינו "עץ 1־דליל" כי בעץ יש מסלול שבו יותר מצומת אחד שיש לו בן יחיד (במסלול המודגש, שצמתיו צבועים באפור, יש שני צמתים שיש להם בן יחיד).
לעומת זאת, עץ זה הוא "עץ 2־דליל" כי אין בו מסלול שבו יותר משני צמתים שיש להם בן יחיד (ובאותה מידה הוא גם "עץ 3־דליל", וגם "עץ 4־דליל" וכן הלאה).
סרטטו "עץ 1־דליל" (כלומר שבכל מסלול של העץ יש לכל היותר צומת אחד שיש לו בן יחיד), ובו 10 צמתים, מקסימום 4 רמות, ולפחות שני צמתים שיש להם בן יחיד.
כתבו פעולה חיצונית ששמה isSparseK בשפת Java או IsSparseK בשפת C# , המקבלת עץ בינרי – root מטיפוס שלם שאינו ריק, וערך K שאינו שלילי, ומחזירה true אם העץ הוא "עץ K־דליל", ואם לא – היא מחזירה false .
◂ פתרון מלא לשאלה 3
סעיף א
לדוגמה: שורש 1 עם הבנים 2, 3; ל־2 הבנים 4, 5; ל־3 הבנים 6, 7; ל־4 רק בן שמאלי 8; ל־5 רק בן ימני 9; ל־6 רק בן שמאלי 10; 7 עלה. (10 צמתים, 4 רמות, 3 צמתים בעלי בן יחיד – כל אחד במסלול אחר.)
מה נדרש: עץ בינרי עם 10 צמתים, לכל היותר 4 רמות, לפחות שני צמתים שיש להם בן יחיד – ובכל מסלול מהשורש לעלה לכל היותר צומת אחד כזה.
רעיון: את הצמתים בעלי הבן היחיד צריך לפזר כך שאף שניים מהם לא יהיו על אותו מסלול – כלומר אף אחד מהם לא יהיה צאצא של אחר. נוח לבנות קודם עץ מלא בן 3 רמות (7 צמתים), ואז להוסיף בן יחיד לכמה מן העלים שלו: כל עלה כזה הופך לצומת בעל בן יחיד, והם נמצאים במסלולים שונים.
הבחנה: בכל עץ בינרי מספר הצמתים הוא 2·(מספר הצמתים בעלי שני בנים) + (מספר הצמתים בעלי בן יחיד) + 1. עם 10 צמתים, מספר הצמתים בעלי בן יחיד חייב להיות אי־זוגי, ולכן "לפחות שניים" פירושו לפחות שלושה.
פתרון – עץ לדוגמה (הצמתים בעלי הבן היחיד צבועים באפור; המספרים בצמתים רק למעקב):

בכתיב מילולי: לשורש 1 – הבנים 2 ו־3. ל־2 – הבנים 4 ו־5. ל־3 – הבנים 6 ו־7. ל־4 – רק בן שמאלי 8. ל־5 – רק בן ימני 9. ל־6 – רק בן שמאלי 10. הצמתים 7, 8, 9, 10 הם עלים.
בדיקה – כל המסלולים מהשורש לעלים:
| מסלול | צמתים בעלי בן יחיד |
|---|---|
| 1 → 2 → 4 → 8 | 1 (הצומת 4) |
| 1 → 2 → 5 → 9 | 1 (הצומת 5) |
| 1 → 3 → 6 → 10 | 1 (הצומת 6) |
| 1 → 3 → 7 | 0 |
10 צמתים, 4 רמות, 3 צמתים בעלי בן יחיד, ובכל מסלול לכל היותר אחד ⇒ "עץ 1־דליל". (ובדוגמה: isSparseK(עץ, 1) = true, isSparseK(עץ, 0) = false. בסריקה של כל צורות העצים בנות 4 רמות לכל היותר נמצאו 48 עצים שמקיימים את הדרישות.)
יש עוד תשובות נכונות רבות.
טעות נפוצה: לתת לצומת 8 (שאביו 4 הוא כבר בעל בן יחיד) בן אחד נוסף – אז 8 הופך לבעל בן יחיד ובמסלול יש 2 צמתים כאלה (וגם העץ יעבור את 4 הרמות).
סעיף ב
רקורסיה עם "תקציב" K: אם לצומת יש בן יחיד (hasLeft() != hasRight()) – אם K == 0 מחזירים false, אחרת K = K − 1; בודקים ברקורסיה כל בן קיים עם ה־K המעודכן; עלה – true. יעילות O(n).
מה נדרש: פעולה רקורסיבית שבודקת שבכל מסלול מהשורש לעלה יש לכל היותר K צמתים שיש להם בן יחיד.
רעיון: K הוא "התקציב" – כמה צמתים בעלי בן יחיד עוד מותר לפגוש מהצומת הנוכחי ועד סוף המסלול.
hasLeft() != hasRight()), הוא נמצא על כל המסלולים שעוברים דרכו: אם התקציב הוא 0 – העץ אינו K־דליל; אחרת התקציב יורד ב־1.פתרון:
public static boolean isSparseK(BinNode<Integer> root, int k)
{
// לצומת יש בן יחיד אם יש לו בן שמאלי בלבד או בן ימני בלבד
if (root.hasLeft() != root.hasRight())
{
if (k == 0)
return false;
k = k - 1; // הצומת "צורך" צומת אחד מתוך K
}
if (root.hasLeft() && !isSparseK(root.getLeft(), k))
return false;
if (root.hasRight() && !isSparseK(root.getRight(), k))
return false;
return true;
}בדיקה: על העץ מהדוגמה בשאלה: isSparseK(root, 1) = false (במסלול המודגש שני צמתים בעלי בן יחיד), isSparseK(root, 2) = true, isSparseK(root, 3) = true, isSparseK(root, 0) = false. על העץ מסעיף א: K = 1 ⇒ true, K = 0 ⇒ false. עץ שהוא עלה יחיד עם K = 0 ⇒ true.
יעילות: כל צומת נבדק לכל היותר פעם אחת, ובכל צומת מספר קבוע של פעולות ⇒ O(n), כאשר n מספר הצמתים בעץ.
טעות נפוצה: לספור את כל הצמתים בעלי בן יחיד בעץ (ולא בכל מסלול בנפרד) – בעץ מסעיף א יש שלושה כאלה, ובכל זאת הוא "עץ 1־דליל".
public static boolean isSparseK(BinNode<Integer> root, int k)
{
// לצומת יש בן יחיד אם יש לו בן שמאלי בלבד או בן ימני בלבד
if (root.hasLeft() != root.hasRight())
{
if (k == 0)
return false;
k = k - 1; // הצומת "צורך" צומת אחד מתוך K
}
if (root.hasLeft() && !isSparseK(root.getLeft(), k))
return false;
if (root.hasRight() && !isSparseK(root.getRight(), k))
return false;
return true;
}פרק שני (50 נקודות) – אלגוריתמים
יש לענות על שתי שאלות במסלול שלמדתם (לכל שאלה – 25 נקודות).
שאלה 4 · טענות על גרפים, מיון טופולוגי ועצי DFS (25 נקודות)
בשאלה זו שני סעיפים, א–ב, שאין קשר ביניהם. ענו על שני הסעיפים.
לפניכם שש טענות 1–6. בחרו בארבע מהן, כתבו את מספר הטענה, וציינו בנוגע לכל טענה שבחרתם אם היא נכונה או לא נכונה. אם הטענה נכונה – נמקו מדוע, ואם היא אינה נכונה – הביאו דוגמה נגדית מגרף שיש בו 4 קודקודים לפחות.
(1) נתון גרף ממושקל (אי־שלילי), קשיר ולא מכוון. בין כל שני קודקודים u ו־v, המסלול שביניהם בעץ הפורש המינימלי של הגרף הוא בהכרח מסלול קל ביותר (קצר ביותר) בין u ל־v בגרף.
נתון גרף מכוון ללא מעגלים ובו n קודקודים (אין בגרף קשתות כפולות). בהכרח יש בגרף לכל היותר n(n−1)/2 קשתות.
נתון גרף מכוון. אם סריקת DFS מקודקוד s מגלה את כל קודקודי הגרף, אז הגרף בהכרח קשיר היטב (יש מסלול מכוון מכל קודקוד לכל קודקוד).
נתון גרף לא מכוון ובו n קודקודים, שבו לכל קודקוד יש לפחות שכן אחד. בהכרח יש בגרף לכל היותר n/2 רכיבי קשירות.
נתון גרף מכוון ללא מעגלים, שבו יש קודקוד אחד בלבד שדרגת הכניסה שלו 0. לגרף זה יש בהכרח מיון טופולוגי אחד בלבד.
נתון גרף לא מכוון. הרצנו סריקת DFS מקודקוד s , ולא נמצאה בה אף קשת אחורה. לכן בגרף אין מעגלים.
נתון גרף לא מכוון G ובו 5 קודקודים: s , w , x , y , z .
לאחר הרצת DFS מקודקוד התחלה s התקבל העץ שבסרטוט הנתון:

בנוגע לכל אחד משלושת העצים שלפניכם, קבעו אם ייתכן שהוא התקבל מהרצת DFS על אותו הגרף G , מקודקוד התחלה z . נמקו את קביעותיכם.

◂ פתרון מלא לשאלה 4
סעיף א(1)
לא נכונה. דוגמה נגדית: ריבוע a–b (2), b–c (2), c–d (2), a–d (3). העפ"מ הוא a–b , b–c , c–d (משקל 6). בעץ המסלול מ־a ל־d הוא a–b–c–d במשקל 6, אבל הקשת a–d לבדה שוקלת 3.
מה נדרש: לקבוע אם העפ"מ "מכיל" את המסלולים הקלים ביותר בין כל זוג קודקודים.
רעיון: העפ"מ ממזער את סכום כל הקשתות של העץ, ולא את המרחק בין זוג מסוים. קשת שמקצרת מאוד מרחק אחד עלולה להישאר מחוץ לעץ כי היא כבדה מעט יותר מהאחרות.
פתרון: הטענה לא נכונה. דוגמה נגדית עם 4 קודקודים:
| קשת | a–b | b–c | c–d | a–d |
|---|---|---|---|---|
| משקל | 2 | 2 | 2 | 3 |
קרוסקל מוסיף את שלוש הקשתות במשקל 2 (הן לא סוגרות מעגל), ודוחה את a–d (3) שסוגרת את המעגל. העפ"מ: a–b , b–c , c–d במשקל 6, והוא יחיד.
בעץ, המסלול מ־a ל־d הוא a–b–c–d במשקל 6, אבל בגרף יש מסלול קל יותר: הקשת a–d במשקל 3.
בדיקה: העפ"מ יחיד, במשקל 6, בלי a–d, ודייקסטרה מ־a נותן מרחק 3 ל־d.
טעות נפוצה: לבלבל בין עץ פורש מינימלי לבין עץ מסלולים קצרים (של דייקסטרה). אלה שני עצים שונים עם מטרות שונות.
סעיף א(2)
נכונה. לגרף מכוון ללא מעגלים יש מיון טופולוגי v₁, …, vₙ, וכל קשת יוצאת מקודקוד מוקדם לקודקוד מאוחר. לכל זוג מקומות i < j יש לכל היותר קשת אחת vᵢ→vⱼ, ומספר הזוגות הוא n(n−1)/2.
מה נדרש: להראות שבגרף מכוון ללא מעגלים יש לכל היותר n(n−1)/2 קשתות.
רעיון: נשתמש במיון טופולוגי – בגרף מכוון ללא מעגלים תמיד יש סדר שבו כל הקשתות "הולכות קדימה".
פתרון: הטענה נכונה. הגרף מכוון וללא מעגלים, ולכן יש לו מיון טופולוגי v₁, v₂, …, vₙ: לכל קשת vᵢ→vⱼ מתקיים i < j.
כל קשת נקבעת על ידי זוג מקומות (i, j) עם i < j, ולכל זוג כזה יש לכל היותר קשת אחת (vᵢ→vⱼ; הקשת ההפוכה אסורה, ואין קשתות כפולות). מספר הזוגות i < j הוא (n−1) + (n−2) + … + 1 = n(n−1)/2. לכן יש לכל היותר n(n−1)/2 קשתות.
(החסם מושג: הגרף שבו יש קשת vᵢ→vⱼ לכל i < j הוא ללא מעגלים ויש בו בדיוק n(n−1)/2 קשתות.)
בדיקה: למשל עם 2 עד 4 קודקודים: מספר הקשתות המרבי בגרף מכוון ללא מעגלים הוא 1, 3, 6 – כלומר n(n−1)/2.
סעיף א(3)
לא נכונה. דוגמה נגדית: s→a , s→b , s→c. DFS מ־s מגלה את כל 4 הקודקודים, אבל אין מסלול מ־a ל־s.
מה נדרש: לבדוק אם "כל הקודקודים ישיגים מ־s" מבטיח "כל קודקוד ישיג מכל קודקוד".
רעיון: DFS מ־s מגלה בדיוק את הקודקודים שיש אליהם מסלול מ־s. הוא לא אומר כלום על מסלולים חזרה אל s.
פתרון: הטענה לא נכונה. דוגמה נגדית עם 4 קודקודים:
s→a , s→b , s→c
DFS מ־s מגלה את a, b ו־c – כל הקודקודים. אבל מ־a לא יוצאת אף קשת, ולכן אין מסלול מ־a ל־s (או לכל קודקוד אחר). הגרף אינו קשיר היטב.
בדיקה: כל הקודקודים ישיגים מ־s, ומ־a לא ישיג אף קודקוד מלבדו.
טעות נפוצה: כדי לבדוק קשירות היטב צריך גם את הכיוון ההפוך: שכל הקודקודים ישיגים מ־s וגם ש־s ישיג מכל הקודקודים (למשל DFS מ־s בגרף ההפוך).
סעיף א(4)
נכונה. לאף קודקוד אין דרגה 0, ולכן כל רכיב קשירות מכיל קשת – כלומר לפחות 2 קודקודים. k רכיבים מכילים לפחות 2k קודקודים, ולכן 2k ≤ n, כלומר k ≤ n/2.
מה נדרש: חסם עליון למספר רכיבי הקשירות כשאין קודקוד מבודד.
רעיון: רכיב עם קודקוד אחד בלבד הוא קודקוד מבודד. אם אין כאלה – כל רכיב "גדול".
פתרון: הטענה נכונה. יהי x קודקוד ברכיב כלשהו. ל־x יש שכן y, והקשת x–y מחברת אותם – לכן y באותו רכיב. כלומר בכל רכיב יש לפחות 2 קודקודים.
אם יש k רכיבים, הם זרים זה לזה ולכן מכילים יחד לפחות 2k קודקודים: 2k ≤ n, ומכאן k ≤ n/2.
(החסם מושג למשל בגרף של n/2 קשתות נפרדות: a–b , c–d.)
סעיף א(5)
לא נכונה. דוגמה נגדית: s→a , s→b , a→c , b→c. רק ל־s דרגת כניסה 0, ויש שני מיונים טופולוגיים: s, a, b, c וגם s, b, a, c.
מה נדרש: לבדוק אם "מקור יחיד" מבטיח מיון טופולוגי יחיד.
רעיון: המיון יחיד רק אם בכל שלב של המיון יש בדיוק קודקוד אחד שאפשר לבחור. מקור יחיד קובע רק את השלב הראשון.
פתרון: הטענה לא נכונה. דוגמה נגדית עם 4 קודקודים:
s→a , s→b , a→c , b→c
אין בגרף מעגל, ודרגות הכניסה: s – 0, a – 1, b – 1, c – 2. רק ל־s דרגת כניסה 0.
אחרי s, גם a וגם b אפשר לשים בתור הבא (אין קשת ביניהם). מתקבלים שני מיונים טופולוגיים: s, a, b, c ו־s, b, a, c.
בדיקה: מבין 24 הסידורים של 4 הקודקודים, בדיוק שני סידורים מקיימים את כל הקשתות: sabc ו־sbac.
טעות נפוצה: לחשוב שמיון טופולוגי יחיד קשור רק לקודקוד ההתחלה. מיון יחיד מתקבל רק כשיש בגרף מסלול שעובר בכל הקודקודים.
סעיף א(6)
לא נכונה. דוגמה נגדית: s–a ובנוסף משולש נפרד b–c , c–d , d–b. DFS מ־s מגיע רק ל־a, ואין בו קשת אחורה – אבל בגרף יש את המעגל b–c–d.
מה נדרש: לבדוק אם היעדר קשתות אחורה בסריקה מ־s מוכיח שהגרף כולו חסר מעגלים.
רעיון: סריקת DFS מ־s עוברת רק על רכיב הקשירות של s. היעדר קשת אחורה מוכיח שאין מעגל ברכיב הזה בלבד.
פתרון: הטענה לא נכונה. דוגמה נגדית עם 5 קודקודים s, a, b, c, d וקשתות:
s–a , b–c , c–d , d–b
DFS מ־s: s → a, חוזרים. נבדקה רק הקשת s–a (קשת עץ). לא נמצאה קשת אחורה. אבל b–c–d–b הוא מעגל בגרף – ברכיב שה־DFS מ־s לא הגיע אליו.
(אילו היינו מריצים DFS על כל הגרף – מכל קודקוד שעוד לא התגלה – היינו מוצאים קשת אחורה ברכיב b, c, d.)
בדיקה: כל עצי ה־DFS מ־s מכילים את כל הקשתות של הרכיב של s (אין קשת אחורה), ובגרף יש מעגל.
סעיף ב
(i) ייתכן – אם ב־G יש קשת s–z: z→s→w, חזרה ל־s→x→y. (ii) לא ייתכן – הקשת s–x קיימת ב־G, ובעץ (ii) x ו־s הם אחים (בנים של y); ב־DFS בגרף לא מכוון אין קשת בין ענפים שונים. (iii) לא ייתכן – העץ משתמש בקשת x–w, ואין קשת כזו ב־G.
מה נדרש: לקבוע לכל אחד מהעצים (i), (ii), (iii) אם הוא יכול להיות עץ DFS מ־z של אותו גרף G.
רעיון: בסריקת DFS על גרף לא מכוון כל קשת שאינה בעץ מחברת קודקוד עם אב קדמון שלו (קשת אחורה); אין קשתות בין ענפים שונים של העץ. מזה נסיק אילו קשתות בוודאי ב־G, אילו אולי, ואילו בוודאי לא.
פתרון:
קשתות העץ הנתון (ולכן ב־G): s–x , x–y , y–z , s–w.
קשתות נוספות אפשריות (בין קודקוד לאב קדמון שלו): s–y , s–z , x–z.
קשתות שאינן יכולות להיות ב־G: w–x , w–y , w–z (w בענף אחר של s).
בפרט, השכן היחיד של w הוא s.
| עץ | קשתות העץ | קביעה | נימוק |
|---|---|---|---|
| (i) | z–s , s–w , s–x , x–y | ייתכן | s–w , s–x , x–y קיימות ב־G, והקשת s–z יכולה להיות ב־G (קשת אחורה בעץ הנתון). ראו את הסריקה למטה. |
| (ii) | z–y , y–x , y–s , s–w | לא ייתכן | x ו־s הם שני בנים של y – ענפים שונים, אבל הקשת s–x קיימת ב־G. כש־DFS היה בתוך x, הוא היה רואה את s שעוד לא התגלה ועובר אליו, ואז s היה צאצא של x. |
| (iii) | z–y , y–x , x–w , w–s | לא ייתכן | נדרשת קשת x–w, אבל השכן היחיד של w ב־G הוא s. |
סריקה שנותנת את (i), בגרף G שבו הקשתות הן העץ הנתון ועוד s–z: מתחילים ב־z ← בוחרים את השכן s ← מ־s עוברים ל־w (השכן היחיד שלו, s, כבר התגלה) ← חוזרים ל־s ועוברים ל־x ← מ־x עוברים ל־y (השכנים של y: x, z – כבר התגלו) ← חוזרים עד z; גם y כבר התגלה. התקבלו בדיוק הקשתות z–s , s–w , s–x , x–y.
בדיקה: בדיוק 8 גרפים על 5 הקודקודים נותנים את העץ הנתון מ־s (העץ ועוד תת־קבוצה של {s–y , s–z , x–z}). עץ (i) מתקבל מ־z בדיוק ב־4 הגרפים שיש בהם s–z, ועצים (ii) ו־(iii) לא מתקבלים באף גרף.
שאלה 5 · תכונות של גרף ומסלול עם מספר נסיעות מינימלי (25 נקודות)
בשאלה זו שני סעיפים, א–ב, שאין ביניהם קשר. יש לענות על שני הסעיפים.
א. להלן גרף G = (V, E) שאינו מכוון:

האם הגרף קשיר? אם לא – כמה רכיבי קשירות יש בו? נמקו.
האם הגרף דו־צדדי? אם כן – כתבו חלוקה מתאימה של הקודקודים לשתי קבוצות. נמקו.
האם יש בגרף מעגל? נמקו.
האם הגרף הוא עץ? נמקו.
חברת תחבורה מפעילה קווי אוטובוס ישירים בין שמונה תחנות: P , Q , R , S , T , U , V , W . בסרטוט שלהלן, חץ מתחנה X לתחנה Y מציין קו ישיר שנוסע מ־ X ל־ Y (בכיוון זה בלבד):

(1) סרטטו את רשימת הסמיכויות המייצגת את קווי האוטובוס בין התחנות.
בעקבות עבודות בכביש הוחלט:
(2) סרטטו את רשימת הסמיכויות המתקבלת לאחר השינויים המצוינים לעיל.
דני רוצה לנסוע מתחנה מסוימת לתחנה אחרת. הוא רוצה לבדוק אם הוא יכול להגיע לאותה תחנה, ואם הוא אכן יכול – הוא רוצה למצוא מסלול נסיעה שבו מספר הקווים שהוא נוסע בהם הוא הקטן ביותר.
מהו האלגוריתם שעליו להשתמש בו למטרות אלה? הסבירו.
הפעילו את האלגוריתם שכתבתם עליו בסעיף (3)I על הגרף שהתקבל לאחר השינויים (סעיף (2)), כדי למצוא מסלול נסיעה מתחנה P לתחנה T . כתבו את מסלול הנסיעה שמצאתם.
יש לבצע מעקב מפורט בכל שלב בהתאם לאלגוריתם שהפעלתם. (הניחו שהשכנים של כל תחנה נסרקים לפי סדר אלפביתי.)
◂ פתרון מלא לשאלה 5
סעיף א(1)
לא. אין מסלול, למשל, בין 1 ל־ 5 . יש בגרף 2 רכיבי קשירות: {1, 2, 3, 4} ו־ {5, 6, 7}.
מה נדרש: לבדוק אם בין כל שני קודקודים יש מסלול, ואם לא – לספור את רכיבי הקשירות.
רעיון: מוצאים את כל הקודקודים שאפשר להגיע אליהם מ־ 1 ; אם לא הגענו לכולם – הגרף אינו קשיר, וממשיכים מקודקוד שלא הגענו אליו.
פתרון: מ־ 1 מגיעים ל־ 2 , 4 ודרכם ל־ 3 – וזהו: אף קשת אינה יוצאת מ־ {1, 2, 3, 4} אל שאר הקודקודים. לכן אין מסלול מ־ 1 ל־ 5 , והגרף אינו קשיר. מ־ 5 מגיעים ל־ 6 ודרכו ל־ 7 . יש 2 רכיבי קשירות: {1, 2, 3, 4} ו־ {5, 6, 7}.
בדיקה: BFS מכל קודקוד שלא סומן מוצא את שני הרכיבים [1, 2, 3, 4] ו־ [5, 6, 7].
סעיף א(2)
כן. חלוקה מתאימה: {1, 3, 5, 7} ו־ {2, 4, 6} – כל קשת מחברת קודקוד מקבוצה אחת לקודקוד מהקבוצה השנייה.
מה נדרש: לחלק את הקודקודים לשתי קבוצות כך שאין קשת בתוך אותה קבוצה.
רעיון: צובעים כל רכיב לסירוגין בשני צבעים (כמו בסריקת BFS לפי שכבות). המעגל היחיד 1 – 2 – 3 – 4 – 1 הוא באורך 4 (זוגי), ולכן הצביעה מצליחה.
פתרון: ברכיב הראשון: 1 → קבוצה X , השכנים 2 , 4 → קבוצה Y , ו־ 3 (שכן של 2 ושל 4) → קבוצה X . ברכיב השני: 5 → X , 6 → Y , 7 → X .
X = {1, 3, 5, 7} , Y = {2, 4, 6}. הקשתות: 1–2 , 2–3 , 3–4 , 4–1 , 5–6 , 6–7 – כולן בין X ל־ Y . לכן הגרף דו־צדדי.
בדיקה: החלוקה תקינה לכל 6 הקשתות. (יש חלוקה נכונה נוספת: אפשר להחליף את הצדדים ברכיב השני, למשל {1, 3, 6} ו־ {2, 4, 5, 7}.)
סעיף א(3)
כן: 1 – 2 – 3 – 4 – 1 .
מה נדרש: למצוא מסלול סגור בגרף שאינו חוזר על קשת (או להראות שאין).
רעיון: בגרף לא מכוון עם n קודקודים ו־ c רכיבי קשירות, אם יש יותר מ־ n − c קשתות – בהכרח יש מעגל. אבל כאן אפשר פשוט להצביע על המעגל.
פתרון: המסלול 1 – 2 – 3 – 4 – 1 חוזר לקודקוד שממנו התחיל ועובר ב־4 קשתות שונות – זה מעגל. (בדיקה בספירה: n = 7 , c = 2 , ויש 6 קשתות > 7 − 2 = 5.)
בדיקה: 6 > 5 ⇒ יש מעגל.
סעיף א(4)
לא. עץ הוא גרף קשיר ללא מעגלים, והגרף הנתון אינו קשיר (סעיף (1)) וגם יש בו מעגל (סעיף (3)).
מה נדרש: לבדוק את שני התנאים של עץ – קשיר וללא מעגלים.
רעיון: מספיק שאחד התנאים לא מתקיים.
פתרון: הגרף אינו קשיר (יש 2 רכיבי קשירות), וגם יש בו מעגל 1 – 2 – 3 – 4 – 1 . לכן הגרף אינו עץ. (הרכיב {5, 6, 7} לבדו הוא עץ, אבל הגרף כולו לא.)
בדיקה: עץ עם 7 קודקודים צריך להיות קשיר ובעל 6 קשתות; כאן אמנם יש 6 קשתות, אבל הגרף אינו קשיר – דוגמה לכך שמספר הקשתות לבדו אינו מספיק.
טעות נפוצה: לענות "כן" רק משום שמספר הקשתות הוא n − 1 = 6 , בלי לבדוק קשירות.
סעיף ב(1)
P: Q, S | Q: R, U | R: T, V | S: R, V | T: Q | U: T, W | V: P, W | W: T
מה נדרש: לייצג את הגרף המכוון ברשימת סמיכויות.
רעיון: לכל תחנה רושמים את התחנות שהחצים היוצאים ממנה מגיעים אליהן.
פתרון (שכנים לפי סדר אלפביתי):
| תחנה | התחנות שאפשר להגיע אליהן בנסיעה אחת (שכנים) |
|---|---|
| P | Q → S |
| Q | R → U |
| R | T → V |
| S | R → V |
| T | Q |
| U | T → W |
| V | P → W |
| W | T |
בדיקה: בסרטוט 14 חצים, ובטבלה 2 + 2 + 2 + 2 + 1 + 2 + 2 + 1 = 14 איברים. שימו לב ש־ Q → R ו־ T → Q הם שני חצים שונים, וש־ V → P הוא החץ הנכנס היחיד ל־ P .
סעיף ב(2)
P: Q, S | Q: R | R: V | S: V | T: Q | U: (ריקה) | V: P, W | W: T
מה נדרש: למחוק מרשימת הסמיכויות את הקשתות שבוטלו.
רעיון: ביטול "כל הקווים הנכנסים והיוצאים" של U פירושו למחוק את הרשימה של U (U → T , U → W) וגם את U מכל רשימה שהוא מופיע בה (Q → U). בנוסף נמחקות S → R ו־ R → T .
פתרון:
| תחנה | התחנות שאפשר להגיע אליהן בנסיעה אחת (שכנים) |
|---|---|
| P | Q → S |
| Q | R |
| R | V |
| S | V |
| T | Q |
| U | – |
| V | P → W |
| W | T |
בדיקה: נמחקו 5 קשתות ונשארו 14 − 5 = 9 , ובטבלה 2 + 1 + 1 + 1 + 1 + 0 + 2 + 1 = 9 .
טעות נפוצה: למחוק את הרשימה של U ולשכוח את הקשת הנכנסת Q → U ברשימה של Q.
סעיף ב(3) I
סריקה לרוחב (BFS) מתחנת המוצא, עם שמירת מרחק d והורה π לכל תחנה. BFS מגלה את התחנות לפי מספר הקשתות מהמקור, ולכן d[y] הוא מספר הנסיעות המינימלי; אם y לא התגלתה – אי אפשר להגיע אליה. את המסלול משחזרים לפי π מהיעד אחורה.
מה נדרש: אלגוריתם שמוצא מסלול עם מספר קשתות מינימלי בגרף מכוון לא ממושקל.
רעיון: כל נסיעה בקו היא קשת אחת, ולכן "מספר הקווים הקטן ביותר" = המסלול הקצר ביותר בגרף לא ממושקל. זה בדיוק מה ש־BFS מוצא: הוא מגלה קודם את כל הקודקודים במרחק 1, אחר כך במרחק 2, וכן הלאה.
פתרון:
BFS(G, s):
לכל קודקוד v: d[v] ← ∞ ; π[v] ← null
d[s] ← 0 ; Q ← תור ריק ; Q.insert(s)
כל עוד Q אינו ריק:
u ← Q.remove()
לכל v ברשימת הסמיכויות של u:
אם d[v] = ∞:
d[v] ← d[u] + 1 ; π[v] ← u ; Q.insert(v)אם בסוף d[y] = ∞ – אי אפשר להגיע ל־ y . אחרת המסלול מתקבל מ־ y , π[y] , π[π[y]] , … עד s (בסדר הפוך). זמן הריצה O(|V| + |E|).
בדיקה: DFS או מיון טופולוגי היו מוצאים מסלול כלשהו, אבל לא בהכרח עם מספר נסיעות מינימלי; דייקסטרה מתאים לגרף ממושקל ומיותר כאן (כל המשקלים 1). לכן BFS הוא הבחירה הנכונה והיעילה.
סעיף ב(3) II
P → S → V → W → T (4 נסיעות). U אינה ישיגה מ־ P .
מה נדרש: להריץ BFS מ־ P על הגרף שלאחר השינויים ולשחזר את המסלול ל־ T .
רעיון: בכל צעד מוציאים תחנה מהתור, ולכל שכן שעוד לא התגלה קובעים d = d[u] + 1 , π = u ומכניסים לתור.
פתרון – מעקב:
| צעד | הוצא מהתור | שכנים שנבדקו | תחנות חדשות שסומנו (d , π) | התור אחרי הצעד |
|---|---|---|---|---|
| 0 | – | – | P (0 , –) | [P] |
| 1 | P | Q, S | Q (1 , P); S (1 , P) | [Q, S] |
| 2 | Q | R | R (2 , Q) | [S, R] |
| 3 | S | V | V (2 , S) | [R, V] |
| 4 | R | V | – (כולם כבר סומנו) | [V] |
| 5 | V | P, W | W (3 , V) | [W] |
| 6 | W | T | T (4 , W) | [T] |
| 7 | T | Q | – (כולם כבר סומנו) | [] |
הערכים הסופיים:
| תחנה | P | Q | R | S | T | U | V | W |
|---|---|---|---|---|---|---|---|---|
| d | 0 | 1 | 2 | 1 | 4 | ∞ | 2 | 3 |
| π | – | P | Q | P | W | – | S | V |
שחזור המסלול מ־ T : π[T] = W , π[W] = V , π[V] = S , π[S] = P . לכן המסלול הוא P → S → V → W → T , ובו d[T] = 4 נסיעות.
בדיקה: לאחר השינויים יש רק שני מסלולים פשוטים מ־ P ל־ T : P → S → V → W → T (4 נסיעות) ו־ P → Q → R → V → W → T (5 נסיעות), והמסלול שנמצא הוא הקצר. לפני השינויים היה אפשר להגיע ב־3 נסיעות (למשל P → Q → R → T).
שאלה 6 · גרף "גשר" (25 נקודות)
גרף "גשר" – Bₙ הוא גרף לא מכוון שמורכב משני "קודקודי קצה" – L ו־ R , ומ־ n "קודקודי עמוד" שמסומנים 1 עד n (כך שסך הכול יש n + 2 קודקודים בגרף). כל "קודקוד עמוד" מחובר בקשת ל־ L וגם ל־ R , ובנוסף יש קשת אחת שמחברת את L ל־ R . מספר "קודקודי העמוד" בגרף גדול מ־ 1 (כלומר n > 1).
דוגמה: הגרף שלפניכם הוא גרף "גשר" – B₄ (הקשת L–R מודגשת).

"קשתות שמאליות" הן הקשתות שמחברות את L ל"קודקודי העמוד" (למשל בדוגמה לעיל, הקשת שמחברת את L לקודקוד 1 , הקשת שמחברת את L לקודקוד 2 וכן הלאה).
"קשתות ימניות" הן הקשתות שמחברות את R ל"קודקודי העמוד" (למשל בדוגמה לעיל, הקשת שמחברת את R לקודקוד 3).
"קשת הגשר" היא הקשת שמחברת את L ל־ R .
נתון גרף "גשר" – Bₙ .
(1) מה הן הדרגות של "קודקודי הקצה" בגרף, ומהי הדרגה של כל "קודקוד עמוד"? נמקו את תשובתכם.
כמה "קשתות שמאליות", כמה "קשתות ימניות" וכמה קשתות יש בגרף בסך הכול? נמקו את תשובתכם.
האם הגרף הוא דו־צדדי? נמקו את תשובתכם.
נתון גרף ממושקל שהוא "גשר" – Bₙ , שבו לכל "הקשתות השמאליות" משקל 1 , לכל "הקשתות הימניות" משקל 2 , ול"קשת הגשר" משקל 3 .
כמה עפ"מים (עצים פורשים מינימליים) שונים יש בגרף? נמקו את תשובתכם.
נתון גרף ממושקל שהוא "גשר" – Bₙ , שבו לכל "הקשתות השמאליות" ולכל "הקשתות הימניות" משקל w (גדול מ־ 0), ול"קשת הגשר" משקל g (גדול מ־ 0).
(1) הציבו ערכים ל־ w ו־ g , שבעבורם כל מסלול קצר (קל) בין L ל־ R יעבור דרך "קודקוד עמוד". נמקו את תשובתכם.
נתון כי g = 12 . מהו הערך הגדול ביותר של w שבעבורו כל מסלול קצר (קל) בין L ל־ R עדיין יעבור דרך "קודקוד עמוד"? נמקו את תשובתכם.
◂ פתרון מלא לשאלה 6
סעיף א(1)
הדרגה של L ושל R היא n + 1 ; הדרגה של כל קודקוד עמוד היא 2 .
מה נדרש: הדרגות של הקודקודים ב־ Bₙ .
רעיון: סופרים את הקשתות שנוגעות בכל קודקוד.
פתרון: L מחובר לכל אחד מ־ n קודקודי העמוד ובנוסף ל־ R (קשת הגשר) ⇐ דרגה n + 1 ; וכך גם R . כל קודקוד עמוד מחובר רק ל־ L ול־ R ⇐ דרגה 2 .
בדיקה: ב־ B₄ שבאיור: ל־ L שכנים 1, 2, 3, 4, R (דרגה 5), ולקודקוד 3 שכנים L, R . כך גם לכל 2 ≤ n ≤ 8 .
סעיף א(2)
n קשתות שמאליות, n קשתות ימניות, וסך הכול 2n + 1 קשתות (כולל קשת הגשר).
מה נדרש: מספר הקשתות מכל סוג ובסך הכול.
פתרון: לכל אחד מ־ n קודקודי העמוד יש קשת אחת ל־ L ⇐ n קשתות שמאליות, וקשת אחת ל־ R ⇐ n קשתות ימניות. עם קשת הגשר: 2n + 1 קשתות.
בדיקה: לפי משפט סכום הדרגות: 2·(n + 1) + n·2 = 4n + 2 = 2·(2n + 1) ⇐ 2n + 1 קשתות. ב־ B₄ : 4 + 4 + 1 = 9 קשתות.
סעיף א(3)
לא. למשל L , R , 1 יוצרים משולש – מעגל באורך אי־זוגי.
מה נדרש: לקבוע אם אפשר לחלק את הקודקודים לשני צדדים כך שכל קשת עוברת בין הצדדים.
רעיון: גרף הוא דו־צדדי אם ורק אם אין בו מעגל באורך אי־זוגי.
פתרון: הגרף אינו דו־צדדי לכל n > 1 . הקודקודים L , R , 1 יוצרים מעגל באורך 3 : L–R (קשת הגשר), R–1 (קשת ימנית), 1–L (קשת שמאלית). אם L בצד אחד, אז R ו־ 1 (שניהם שכנים של L) חייבים להיות שניהם בצד השני – אבל יש ביניהם קשת. סתירה.
(הערה: בלי קשת הגשר הגרף כן היה דו־צדדי – L ו־ R בצד אחד וכל קודקודי העמוד בצד השני.)
בדיקה: צביעה בשני צבעים נכשלת לכל 2 ≤ n ≤ 8 .
סעיף ב
n עפ"מים (כל אחד במשקל n + 2): כל n הקשתות השמאליות ועוד קשת ימנית אחת לבחירה.
מה נדרש: מספר העצים הפורשים המינימליים.
רעיון: לכל עץ פורש על n + 2 קודקודים יש בדיוק n + 1 קשתות. נמצא חסם תחתון למשקל, ונספור את העצים שמשיגים אותו.
פתרון:
(גם בקרוסקל: קודם נבחרות כל n הקשתות במשקל 1 ; אחר כך הקשת הראשונה במשקל 2 מחברת את R , וכל קשת נוספת תיצור מעגל. סדר שונה בין הקשתות במשקל 2 נותן עפ"מ אחר.)
בדיקה: לכל 2 ≤ n ≤ 7 יש בדיוק n עפ"מים במשקל n + 2 .
טעות נפוצה: לענות "עפ"מ אחד" – כל הקשתות הימניות באותו משקל, וכל אחת מהן יכולה להיות הקשת שמחברת את R .
סעיף ג(1)
למשל w = 1 , g = 3 (כל בחירה עם 2w < g מתאימה).
מה נדרש: משקלים שבהם קשת הגשר לבדה אינה מסלול קל ביותר בין L ל־ R .
רעיון: מסלול פשוט מ־ L ל־ R הוא או קשת הגשר לבדה (משקל g), או מסלול שאינו משתמש בה ועובר בקודקודי עמוד. מסלול מן הסוג השני חייב לצאת מ־ L בקשת שמאלית ולהיכנס ל־ R בקשת ימנית, ולכן שוקל לפחות 2w ; המסלולים L–i–R שוקלים בדיוק 2w .
פתרון: אם 2w < g , המסלולים L–i–R (משקל 2w) קלים ממש מקשת הגשר, ולכן כל מסלול קל ביותר עובר בקודקוד עמוד. דוגמה: w = 1 , g = 3 : דרך עמוד 1 + 1 = 2 < 3 .
בדיקה: עם w = 1 , g = 3 התנאי מתקיים, ובאופן כללי התנאי מתקיים בדיוק כאשר 2w < g .
סעיף ג(2)
צריך 2w < 12 , כלומר w < 6 . במשקלים שלמים – w = 5 (ב־ w = 6 גם קשת הגשר היא מסלול קל).
מה נדרש: הערך הגדול ביותר של w כאשר g = 12 .
רעיון: לפי סעיף (1), התנאי הוא שהמסלול דרך קודקוד עמוד (2w) יהיה קל ממש מקשת הגשר (g = 12).
פתרון: צריך 2w < 12 ⇐ w < 6 . אם המשקלים שלמים, הערך הגדול ביותר הוא w = 5 : אז דרך עמוד 5 + 5 = 10 < 12 .
ב־ w = 6 : יש מסלולים קלים במשקל 12 גם דרך עמוד (6 + 6) וגם בקשת הגשר (12). קשת הגשר אינה עוברת בקודקוד עמוד, ולכן לא כל מסלול קל עובר בקודקוד עמוד.
(אם w יכול להיות לא שלם, כל w < 6 מתאים ואין "גדול ביותר"; התשובה המקובלת היא w = 5 למשקלים שלמים, או "w קטן מ־ 6".)
בדיקה: עם g = 12 ו־ w = 1, 2, 3, 4, 5, 5.9, 6, 7 : התנאי מתקיים רק ל־ w = 1, 2, 3, 4, 5, 5.9 .
טעות נפוצה: לענות w = 6 – במצב של שוויון קשת הגשר היא מסלול קל נוסף שאינו עובר בקודקוד עמוד.
פרק שני (50 נקודות) – מודלים חישוביים
יש לענות על שתי שאלות במסלול שלמדתם (לכל שאלה – 25 נקודות).
שאלה 7 · רגולריות של שפות ואוטומט סופי דטרמיניסטי (25 נקודות)
לפניכם שני סעיפים א–ב, שאין קשר ביניהם. ענו על שני הסעיפים.
לפניכם שלוש שפות L₁ – L₃ מעל הא"ב {a, b, c} :
L₁ = {bᵐ aⁿ cᵐ | 0 < n < 50 < m}
L₂ = {bᵐ aⁿ cᵐ | 0 < m < 50 < n}
L₃ = {bᵐ aⁿ cᵐ | 0 < n < 50 < m < 2n}
בנוגע לכל אחת מן השפות L₁ – L₃ , כתבו אם היא רגולרית או לא. נמקו את תשובתכם (די בהסבר מילולי; אין צורך באוטומט).
לפניכם שלוש שפות L₄ – L₆ מעל הא"ב {a, b, c} :
L₄ = L₁ ∩ L₂
L₅ = L₁ ∪ L₃
L₆ = L̅₁ ∩ L₂
בנוגע לכל אחת מן השפות L₄ – L₆ , כתבו אם היא רגולרית או לא. נמקו את תשובתכם (די בהסבר מילולי; אין צורך באוטומט).
לפניכם השפה L מעל הא"ב {a, b, c} :
> L = {bⁿ cᵐ w | n, m > 0}
>
> w היא מילה מעל הא"ב {a, b} המאופיינת באופן שלהלן:
> - אם המכפלה n · m היא מספר זוגי, w היא מילה שמספר האותיות b בה זוגי (גם מילה ללא b כלל, וגם המילה הריקה).
> - אם המכפלה n · m היא מספר אי־זוגי, w היא מילה שיש בה בדיוק אות a אחת.
דוגמה למילה השייכת לשפה L : bbb c bab (bⁿ = bbb , cᵐ = c , w = bab)
הסבר: n = 3 , m = 1 . המכפלה n · m = 3 היא מספר אי־זוגי, ו־ w היא מילה שיש בה בדיוק אות a אחת.
בנו אוטומט סופי דטרמיניסטי שאינו מלא המקבל את השפה L .
◂ פתרון מלא לשאלה 7
סעיף א(1)
L₁ – לא רגולרית; L₂ – רגולרית; L₃ – רגולרית (שפה סופית).
מה נדרש: לקבוע לכל אחת מהשפות L₁–L₃ אם היא רגולרית, ולנמק במילים.
רעיון: אוטומט סופי זוכר רק כמות חסומה של מידע. בכל השפות מספר ה־c חייב להיות שווה למספר ה־b (m). השאלה היא אם m חסום או לא.
פתרון:
בדיקה: ב־L₃ יש מספר סופי של מילים, הגדול ביותר בהן m = 97 (קטן מ־100); ב־L₂ תמיד m ≤ 49 ו־n לא חסום; ב־L₁ תמיד n ≤ 49 ו־m לא חסום, והרישות bᵐ a (m > 50) נבדלות זו מזו.
טעות נפוצה: לחשוב ש־L₃ אינה רגולרית "כי m אינו חסום במפורש". החסם m < 2n יחד עם n < 50 נותן m < 100.
סעיף א(2)
L₄ = ∅ – רגולרית; L₅ = L₁ – לא רגולרית; L₆ = L₂ – רגולרית.
מה נדרש: לקבוע רגולריות של L₄, L₅, L₆ (L̅ = המשלים של L).
רעיון: לפני שמפעילים כללי סגירות, כדאי לבדוק מה הקשר בין השפות עצמן (זרות? מוכלות?).
פתרון:
בדיקה: L₁ ∩ L₂ ריקה, L₃ ⊆ L₁ , וכל מילה של L₂ אינה ב־L₁.
טעות נפוצה: לומר ש־L₆ אינה רגולרית "כי L₁ אינה רגולרית ולכן גם המשלים שלה". זה נכון שהמשלים L̅₁ אינו רגולרי, אבל החיתוך שלו עם L₂ יצא בדיוק L₂ – צריך לבדוק את השפה שמתקבלת, ולא להסיק מהרכיבים.
סעיף ב
10 מצבים: S (התחלה), P1, P0, X, Y1, Y0, E0, E1, W0, W1; מקבלים: X, Y0, E0, W1 (טבלת המעברים בפתרון).
מה נדרש: אוטומט סופי דטרמיניסטי (לא חייב להיות מלא – מעבר שחסר פירושו דחייה) ל־L.
רעיון: המילה מתפרקת באופן יחיד: רצף b (n), רצף c (m), ואחריו w – שאין בה c. כדי לדעת אם n · m זוגי מספיק לזכור את הזוגיות של n ושל m: המכפלה אי־זוגית רק כששניהם אי־זוגיים.
במצבים X ו־Y0 (מכפלה זוגית, w ריקה עד כה – 0 אותיות b) המילה כבר יכולה להסתיים, ולכן הם מקבלים.
פתרון: מצב התחלה: S. מצבים מקבלים: X, Y0, E0, W1.
| מצב | a | b | c |
|---|---|---|---|
| → S | – | P1 | – |
| P1 | – | P0 | Y1 |
| P0 | – | P1 | X |
| (X) מקבל | E0 | E1 | X |
| Y1 | W1 | W0 | Y0 |
| (Y0) מקבל | E0 | E1 | Y1 |
| (E0) מקבל | E0 | E1 | – |
| E1 | E1 | E0 | – |
| W0 | W1 | W0 | – |
| (W1) מקבל | – | W1 | – |
("–" = אין מעבר, המילה נדחית.)
שימו לב: מ־Y0 ו־Y1 ה־c הבא מחליף את זוגיות m, ולכן Y1 ⇄ Y0 ; מ־X ה־c נשאר ב־X (n זוגי – המכפלה תמיד זוגית).
בדיקה:
(bcc: n·m = 2 זוגי, w ריקה – מתקבלת; bc: n·m = 1 אי־זוגי, w ריקה ואין בה a – נדחית; bcaa: שתי אותיות a – נדחית.)
שאלה 8 · מכונת טיורינג – המרחק בין שתי מילים בינריות (25 נקודות)
מרחק בין שתי מילים שאורכן זהה, המורכבות אך ורק מן הספרות 0 ו־1 , הוא מספר המקומות שבהם הספרות בשתי המילים שונות זו מזו.
דוגמה: המרחק בין x = 10110 ובין y = 11100 הוא 2 , כי הספרות שונות רק במקומות 1 ו־3 :
| 0 | 1 | 2 | 3 | 4 | |
|---|---|---|---|---|---|
| x | 1 | 0 | 1 | 1 | 0 |
| y | 1 | 1 | 1 | 0 | 0 |
| שונות? | ✔ | ✔ |
כתבו מכונת טיורינג המקבלת בתחילת הסרט קלט של שתי מילים שאורכן זהה, המורכבות אך ורק מן הספרות 0 ו־1 . המכונה מחזירה את המרחק בין שתי המילים כמספר אונרי, כלומר כרצף של אחדות שמספרן שווה למרחק.
הנחיות:
– שתי המילים בקלט מופרדות ב־ # , ואורך כל אחת מהן 1 לפחות.
– אין צורך לשמור על הקלט (אפשר לשנות את הקלט לסימנים שונים).
– המספר האונרי המוחזר יופיע במקום כלשהו בסרט בין שני סימני $ . אם המרחק הוא 0 , יופיעו על הסרט שני סימני $ צמודים.
– אין צורך לבדוק את תקינות הקלט.
דוגמה:
הסרט לפני ההרצה:
| ⊢ | 1 | 0 | 1 | 1 | 0 | # | 1 | 1 | 1 | 0 | 0 | Δ | Δ | ... |
|---|
הסרט לאחר ההרצה:
| ... | $ | 1 | 1 | $ | ... |
|---|
כתבו מכונת טיורינג המקבלת בתחילת הסרט שתי מילים כמתואר לעיל, ומחזירה בין שני סימני $ את המרחק ביניהן כמספר אונרי.
◂ פתרון מלא לשאלה 8
פתרון
כותבים $ אחרי המילה השנייה. בכל סבב: הספרה השמאלית שלא טופלה במילה הראשונה ← X (זוכרים אותה במצב), הספרה השמאלית שלא טופלה במילה השנייה ← Y; אם הן שונות – כותבים 1 בתא הריק הראשון בסוף הסרט. כשאין יותר ספרות במילה הראשונה – כותבים $ בתא הריק הראשון. התוצאה: ⊢X…X#Y…Y$1ᵈ$.
מה נדרש: מכונת טיורינג שמקבלת ⊢x#y (x, y מעל {0, 1}, באותו אורך) ומשאירה על הסרט $1ᵈ$, כאשר d הוא מספר המקומות שבהם x ו־y שונות.
רעיון:
פתרון:
| מצב | קורא | כותב | תזוזה | מצב חדש | הסבר |
|---|---|---|---|---|---|
| q0 | ⊢ | ⊢ | R | q1 | התחלה |
| q1 | 0, 1, # | אותו סימן | R | q1 | ימינה עד סוף הקלט |
| q1 | Δ | $ | L | q2 | $ הפותח – מיד אחרי המילה השנייה |
| q2 | 0, 1, X, Y, #, $ | אותו סימן | L | q2 | שמאלה עד ⊢ |
| q2 | ⊢ | ⊢ | R | q3 | סבב חדש |
| q3 | X | X | R | q3 | דילוג על ספרות שטופלו |
| q3 | 0 | X | R | q4 | זוכרים 0 |
| q3 | 1 | X | R | q5 | זוכרים 1 |
| q3 | # | # | R | q9 | המילה הראשונה נגמרה – סיום |
| q4 | 0, 1 | אותו סימן | R | q4 | ימינה עד # |
| q4 | # | # | R | q6 | למילה השנייה |
| q5 | 0, 1 | אותו סימן | R | q5 | ימינה עד # |
| q5 | # | # | R | q7 | למילה השנייה |
| q6 | Y | Y | R | q6 | דילוג |
| q6 | 0 | Y | L | q2 | 0 מול 0 – אין הבדל |
| q6 | 1 | Y | R | q8 | 0 מול 1 – הבדל |
| q7 | Y | Y | R | q7 | דילוג |
| q7 | 1 | Y | L | q2 | 1 מול 1 – אין הבדל |
| q7 | 0 | Y | R | q8 | 1 מול 0 – הבדל |
| q8 | 0, 1, Y, $ | אותו סימן | R | q8 | ימינה עד התא הריק |
| q8 | Δ | 1 | L | q2 | מוסיפים 1 למונה |
| q9 | 1, Y, $ | אותו סימן | R | q9 | ימינה עד התא הריק |
| q9 | Δ | $ | R | h | $ הסוגר – עצירה |
מעקב – הקלט 10110#11100 (הסרט בכל פעם שהמכונה מתחילה סבב חדש, כלומר נכנסת ל־q3, ובסוף):
| סבב | הסרט |
|---|---|
| 0 | ⊢10110#11100$ |
| 1 | ⊢X0110#Y1100$ |
| 2 | ⊢XX110#YY100$1 |
| 3 | ⊢XXX10#YYY00$1 |
| 4 | ⊢XXXX0#YYYY0$11 |
| 5 | ⊢XXXXX#YYYYY$11 |
| 6 | ⊢XXXXX#YYYYY$11$ |
בין שני ה־$: 11 ⇒ המרחק 2 ✔ (כמו בדוגמה). ב־0110#0110 התוצאה ⊢XXXX#YYYY$$ – מרחק 0.
בדיקה: לכל הזוגות x, y המכונה עוצרת, על הסרט נשארים בדיוק שני סימני $, וביניהם בדיוק d אחדות. למשל 10110#11100 ⇒ $11$ (148 צעדים), 1#0 ⇒ $1$, 000#111 ⇒ $111$.
טעות נפוצה: לכתוב את ה־1 של המונה בלי $ מפריד בין המילה השנייה למונה – אז אי אפשר להבדיל בין 1 של המילה השנייה ל־1 של המונה. ה־$ הראשון נכתב לפני הסבב הראשון.
שאלה 9 · מכונת טיורינג – המקסימום של שני מספרים אונריים (25 נקודות)
לפניכם מכונת טיורינג:

על כל מעבר רשום: הסימן הנקרא / הסימן הנכתב, כיוון התזוזה. q₀ הוא המצב ההתחלתי, ו־ q₁₀ הוא מצב העצירה.
המכונה מקבלת בתחילת הסרט שני מספרים אונריים שביניהם הסימן # . בתחילת הפעולה הראש נמצא על התא הראשון שמימין ל־ ⊢ .
עקבו אחר המכונה עבור הסרט שלהלן:
| ⊢ | 1 | # | 1 | 1 | Δ | Δ | . . . |
|---|
כתבו מה יתקבל על הסרט בסיום הפעולה.
כתבו מה יתקבל על הסרט שלהלן בסיום הפעולה:
| ⊢ | 1 | 1 | 1 | # | 1 | Δ | Δ | . . . |
|---|
הערה: בסעיף זה אין צורך להראות מעקב.
מה המכונה מבצעת עבור שני מספרים אונריים הגדולים מאפס?
לפניכם שלושה מקרים, 1–3. בעבור כל מקרה ציינו אם המכונה פועלת נכון או לא נכון.
אם המכונה פועלת נכון, הציגו באמצעות דוגמה את סרט הקלט ואת סרט הפלט. אם המכונה פועלת לא נכון,
הסבירו מדוע.
◂ פתרון מלא לשאלה 9
סעיף א
המכונה עוצרת במצב q10 והסרט: ⊢ Δ Δ 1 1 Δ Δ … (נשארו שני אחדים).
מה נדרש: להריץ את המכונה על ⊢1#11 ולכתוב את הסרט בסוף.
קריאת הסרטוט: התווית "x / y, כיוון" על חץ פירושה: קוראים x, כותבים y ומזיזים את הראש לכיוון הנתון. כשרשומות על חץ שתי תוויות – יש שני מעברים. טבלת המעברים של המכונה:
| מצב | קורא | כותב | תזוזה | מצב חדש |
|---|---|---|---|---|
| q0 | 1 | a | ימין | q1 |
| q0 | # | Δ | שמאל | q5 |
| q1 | 1 | 1 | ימין | q1 |
| q1 | # | # | ימין | q2 |
| q2 | b | b | ימין | q2 |
| q2 | 1 | b | שמאל | q3 |
| q2 | Δ | Δ | שמאל | q8 |
| q3 | b | b | שמאל | q3 |
| q3 | # | # | שמאל | q4 |
| q4 | 1 | 1 | שמאל | q4 |
| q4 | a | a | ימין | q0 |
| q5 | a | Δ | שמאל | q5 |
| q5 | ⊢ | ⊢ | ימין | q6 |
| q6 | Δ | Δ | ימין | q6 |
| q6 | b | 1 | ימין | q7 |
| q6 | 1 | 1 | ימין | q7 |
| q7 | b | 1 | ימין | q7 |
| q7 | 1 | 1 | ימין | q7 |
| q7 | Δ | Δ | שמאל | q10 |
| q8 | b | Δ | שמאל | q8 |
| q8 | # | Δ | שמאל | q9 |
| q9 | a | 1 | שמאל | q9 |
| q9 | 1 | 1 | שמאל | q9 |
| q9 | ⊢ | ⊢ | ימין | q10 |
רעיון: בכל "סיבוב" המכונה מסמנת ב־ a את ה־ 1 הבא של המספר השמאלי ומסמנת ב־ b את ה־ 1 הבא של המספר הימני (q0–q4). כשאחד המספרים נגמר היא מוחקת את המספר ה"קצר" ואת #, ומחזירה ל־ 1 את כל הסימנים של המספר ה"ארוך".
פתרון – מעקב (התאים ממוספרים מ־ 0 = ⊢):
| צעד | מצב | תא (מיקום הראש) | נקרא | נכתב | תזוזה | מצב חדש | הסרט אחרי הצעד |
|---|---|---|---|---|---|---|---|
| 1 | q0 | 1 | 1 | a | ימין | q1 | ⊢ a # 1 1 Δ |
| 2 | q1 | 2 | # | # | ימין | q2 | ⊢ a # 1 1 Δ |
| 3 | q2 | 3 | 1 | b | שמאל | q3 | ⊢ a # b 1 Δ |
| 4 | q3 | 2 | # | # | שמאל | q4 | ⊢ a # b 1 Δ |
| 5 | q4 | 1 | a | a | ימין | q0 | ⊢ a # b 1 Δ |
| 6 | q0 | 2 | # | Δ | שמאל | q5 | ⊢ a Δ b 1 Δ |
| 7 | q5 | 1 | a | Δ | שמאל | q5 | ⊢ Δ Δ b 1 Δ |
| 8 | q5 | 0 | ⊢ | ⊢ | ימין | q6 | ⊢ Δ Δ b 1 Δ |
| 9 | q6 | 1 | Δ | Δ | ימין | q6 | ⊢ Δ Δ b 1 Δ |
| 10 | q6 | 2 | Δ | Δ | ימין | q6 | ⊢ Δ Δ b 1 Δ |
| 11 | q6 | 3 | b | 1 | ימין | q7 | ⊢ Δ Δ 1 1 Δ |
| 12 | q7 | 4 | 1 | 1 | ימין | q7 | ⊢ Δ Δ 1 1 Δ |
| 13 | q7 | 5 | Δ | Δ | שמאל | q10 | ⊢ Δ Δ 1 1 Δ |
התוצאה: המכונה עצרה במצב q10, ועל הסרט: ⊢ Δ Δ 1 1 Δ Δ …
בדיקה: המעקב נבנה צעד אחר צעד לפי טבלת המעברים.
סעיף ב
⊢ 1 1 1 Δ Δ … (המכונה עוצרת ב־ q10).
מה נדרש: הסרט הסופי עבור 111#1 (בלי מעקב).
פתרון (לפי הרעיון מסעיף א):
הסרט בסיום: ⊢ 1 1 1 Δ Δ … – שלושה אחדים, max(3, 1) = 3.
בדיקה: עבור 111#1 המכונה עוצרת במצב q10 והסרט הוא ⊢111.
סעיף ג
המכונה מחשבת את המקסימום max(x, y) של שני המספרים: בסיום על הסרט נשארים max(x, y) אחדים ברצף (וכל שאר התאים ריקים).
מה נדרש: לתאר את הפונקציה שהמכונה מחשבת עבור קלט 1ˣ#1ʸ, כאשר x, y > 0.
רעיון: בכל סיבוב (q0 → q1 → q2 → q3 → q4 → q0) מסומן 1 אחד של x ב־ a ו־ 1 אחד של y ב־ b – מתאימים זוג. הסיבובים נמשכים עד שאחד המספרים נגמר:
פתרון: המכונה מחשבת את המקסימום max(x, y): בסיום על הסרט יש רצף של max(x, y) אחדים, וכל שאר התאים ריקים (Δ). כש־ x > y הרצף מתחיל בתא 1; כש־ x ≤ y הוא נמצא במקום שבו היה המספר הימני.
בדיקה: לכל הזוגות הקטנים x, y שאינם שניהם 0 מתקבלים max(x, y) אחדים והמכונה עוצרת ב־ q10 (למשל 1#11 → 11, 111#1 → 111).
סעיף ד
מה נדרש: לבדוק את שלושת המקרים שבהם לפחות אחד המספרים הוא 0 (מספר אונרי 0 = אין אחדים). התוצאה הנכונה היא max(x, y).
פתרון:
מקרה 1 – x = 0, y > 0: המכונה פועלת נכון.
q0 קורא מיד # ⇒ מוחק אותו וזז שמאלה אל ⊢ (q5 לא מוחק כלום, כי אין a) ⇒ q6 זז ימינה, מדלג על התא הריק ומגיע ל־ 1 ⇒ q7 עובר על האחדים ועוצר ב־ q10. נשארים y אחדים = max(0, y).
דוגמה: קלט ⊢ # 1 1 Δ … ⇒ פלט ⊢ Δ 1 1 Δ …
מקרה 2 – y = 0, x > 0: המכונה פועלת נכון.
q0 מסמן את תא 1 ב־ a, q1 עובר אל #, q2 קורא מיד Δ ⇒ q8 מוחק את #, ו־ q9 הולך שמאלה: משאיר את האחדים, מחזיר את ה־ a ל־ 1 ועוצר ב־ ⊢. נשארים x אחדים = max(x, 0).
דוגמה: קלט ⊢ 1 1 # Δ … ⇒ פלט ⊢ 1 1 Δ …
מקרה 3 – x = 0, y = 0: המכונה פועלת לא נכון.
קלט ⊢ # Δ … : q0 קורא # ⇒ כותב Δ וזז שמאלה; q5 קורא ⊢ ⇒ זז ימינה ל־ q6. עכשיו כל הסרט ריק. q6 זז ימינה על Δ ומחפש b או 1 כדי לעבור ל־ q7 – אבל אין כאלה, ולכן הוא ממשיך ימינה בלי סוף. המכונה לעולם אינה מגיעה למצב העצירה q10 (לולאה אינסופית).
הסיבה: q6 מניח שאחרי התאים הריקים יש לפחות 1 אחד של המספר הימני (או b). כש־ x ≤ y זה נכון רק אם y > 0; כש־ y = 0 אין סימן שיעצור את החיפוש.
בדיקה: x=0,y=2 → ⊢Δ11 (q10); x=2,y=0 → ⊢11 (q10); x=y=0 → המכונה לא עוצרת: היא נשארת ב־ q6 וזזה ימינה בלי סוף.
תכנות מונחה עצמים
יש לענות על שתי שאלות במסלול שלמדתם (לכל שאלה – 25 נקודות).
שאלה 10 · השכרת מחסנים – ירושה, הכלה ופעולת מחיר (25 נקודות)
בחברה להשכרת מחסנים "שמור ובטוח" פותחה מערכת ממוחשבת שבה המחלקות האלה:
Lease – חוזה אחסון, Unit – יחידת אחסון, Locker – תא אחסון, ColdRoom – חדר קירור, Container – מכולה.
להלן פירוט תכונות המחלקות:
boxes – מספר הארגזים שהשוכר מאחסן (מטיפוס שלם).
מספר הקומה שבה נמצא התא – floor (מטיפוס שלם).
הטמפרטורה בחדר – temp (מטיפוס שלם).
מכולה מקורה – roofed (בוליאני. אם המכולה עומדת תחת גג התכונה היא אמת ואם לא, היא שקר).
סרטטו תרשים הייררכייה המתאר את הקשר בין המחלקות של המערכת הממוחשבת.
יש לסמן ירושה באמצעות החץ
והכלה באמצעות הסימן
.
כתבו את כותרות המחלקות ואת התכונות שלהן. הניחו שהפעולות get ו־ set קיימות בכל התכונות של המחלקות,
ואין צורך לממש אותן.
נתונה הפעולה הבונה של המחלקה Lease :
public Lease (String tenant, int months, int boxes)אין צורך לממש את הפעולה.
(1) לפניכם כותרת הפעולה הבונה של המחלקה Unit . הפעולה מקבלת שם שוכר, מספר חודשי שכירות,
מספר ארגזים וקוד יחידה.
public Unit (String tenant, int months, int boxes, String code)ממשו את הפעולה הבונה.
לפניכם כותרת הפעולה הבונה של המחלקה ColdRoom . הפעולה מקבלת שם שוכר, מספר חודשי שכירות, מספר ארגזים,
קוד יחידה וטמפרטורה.
public ColdRoom (String tenant, int months, int boxes, String code, int temp)ממשו את הפעולה הבונה.
התעריף הבסיסי שהחברה גובה בעבור השכרת יחידת אחסון – Unit הוא 100 שקלים לכל חודש שכירות ו־ 4 שקלים
לכל ארגז.
מחיר השכירות של תא אחסון (Locker) הוא לפי התעריף הבסיסי.
מחיר השכירות של חדר קירור (ColdRoom) הוא לפי התעריף הבסיסי ונוסף על כך 35 שקלים לכל חודש שכירות.
מחיר השכירות של מכולה (Container) הוא 80% מן התעריף הבסיסי.
ג. הפעולה price מחזירה מספר ממשי השווה לסכום שהשוכר נדרש לשלם בעבור כל אחד מסוגי היחידות שהחברה
משכירה (בהתאם לעצם שזימן את הפעולה).
(1) כתבו את הפעולה price במחלקה Unit (כאמור לעיל, הסכום לתשלום הוא לפי התעריף הבסיסי).
הוסיפו את הפעולה price במחלקה/ות האחרות כדי לבצע את הנדרש (רק במחלקות שיש בהן צורך), לפי
העקרונות של תכנות מונחה עצמים.
הערה: אין להשתמש בפעולה instanceOf בסעיף זה ובפעולות של המחלקה Object ואין לשנות את תכונות המחלקות.
פתרון הכולל שימושים כאלה לא יזוכה בנקודות.
◂ פתרון מלא לשאלה 10
סעיף א(1)
Locker, ColdRoom ו־Container יורשות מ־Unit (חץ ירושה אל Unit); Unit מכילה עצם מטיפוס Lease (הכלה, המעוין בצד של Unit).
מה נדרש – לסרטט את הקשרים בין חמש המחלקות: מי יורש ממי ומי מכיל את מי.
רעיון – תא אחסון, חדר קירור ומכולה הם סוגים של יחידת אחסון ("is-a") ⇒ הם יורשים מ־Unit. לכל יחידת אחסון יש חוזה ("has-a") ⇒ הכלה: ב־Unit יש תכונה lease מטיפוס Lease. התכונות code ו־lease שמופיעות גם בפירוט של Locker, ColdRoom ו־Container הן התכונות שעוברות אליהן בירושה מ־Unit – לכן לא מגדירים אותן שוב, ולא מחברים את Lease ישירות לכל אחת מהן.
פתרון
Lease ───────────◆ Unit
△
┌────────────┼─────────────┐
│ │ │
Locker ColdRoom Containerבדיקה – Locker "היא" Unit ✓, ColdRoom "היא" Unit ✓, Container "היא" Unit ✓; ל־Unit "יש" Lease ✓; חוזה אינו יחידת אחסון, לכן אין ירושה בין Lease ל־Unit.
טעות נפוצה: לסמן הכלה של Lease בכל אחת משלוש המחלקות היורשות – החוזה כבר נמצא ב־Unit ועובר בירושה.
סעיף א(2)
Lease: tenant, months, boxes; Unit: code, lease; Locker extends Unit: floor; ColdRoom extends Unit: temp; Container extends Unit: roofed.
מה נדרש – לכתוב את כותרות חמש המחלקות ואת התכונות שלהן (בלי פעולות).
רעיון – התכונות פרטיות (private). במחלקות היורשות כותבים רק את התכונה החדשה – code ו־lease כבר נמצאות ב־Unit ועוברות בירושה. ב־Java הירושה נכתבת extends Unit, וב־C# : Unit. התכונה lease היא מטיפוס המחלקה Lease (הכלה).
פתרון
public class Lease {
private String tenant;
private int months;
private int boxes;
}
public class Unit {
private String code;
private Lease lease;
}
public class Locker extends Unit {
private int floor;
}
public class ColdRoom extends Unit {
private int temp;
}
public class Container extends Unit {
private boolean roofed;
}בדיקה – שני הקבצים עוברים הידור (Java ו־C#).
טעות נפוצה: להגדיר שוב code ו־lease במחלקות היורשות – כך נוצרות תכונות כפולות שמסתירות את אלה של Unit.
public class Lease {
private String tenant;
private int months;
private int boxes;
}
public class Unit {
private String code;
private Lease lease;
}
public class Locker extends Unit {
private int floor;
}
public class ColdRoom extends Unit {
private int temp;
}
public class Container extends Unit {
private boolean roofed;
}סעיף ב(1)
this.lease = new Lease(tenant, months, boxes); this.code = code;
מה נדרש – לממש את הפעולה הבונה של Unit שמקבלת את נתוני החוזה ואת קוד היחידה.
רעיון – Unit מכילה חוזה, ולכן הפעולה הבונה יוצרת עצם Lease חדש מהנתונים שקיבלה (tenant, months, boxes) בעזרת הפעולה הבונה הנתונה של Lease, ושומרת אותו בתכונה lease. את code שומרים ישירות.
פתרון
(בתוך המחלקה Unit):
public Unit(String tenant, int months, int boxes, String code) {
this.lease = new Lease(tenant, months, boxes);
this.code = code;
}בדיקה – new Unit("Noa", 3, 10, "U-01") יוצר חוזה של 3 חודשים ו־10 ארגזים, והמחיר שלו 340 (ראו סעיף ג).
טעות נפוצה: לכתוב this.tenant = tenant בתוך Unit – ל־Unit אין תכונות כאלה; הנתונים שייכים לעצם ה־Lease.
public Unit(String tenant, int months, int boxes, String code) {
this.lease = new Lease(tenant, months, boxes);
this.code = code;
}סעיף ב(2)
Java: super(tenant, months, boxes, code); this.temp = temp; C#: : base(tenant, months, boxes, code) { this.temp = temp; }
מה נדרש – לממש את הפעולה הבונה של ColdRoom.
רעיון – התכונות code ו־lease פרטיות ב־Unit, ולכן ColdRoom לא יכולה לאתחל אותן בעצמה – היא מעבירה את ארבעת הנתונים הראשונים לפעולה הבונה של מחלקת־העל (super(...) ב־Java, : base(...) ב־C#), ואחר כך מאתחלת את התכונה שלה – temp. ב־Java הקריאה ל־super חייבת להיות השורה הראשונה. (בקוד המלא שלפניכם מופיעה גם הפעולה price מסעיף ג2.)
פתרון
public class ColdRoom extends Unit {
private int temp;
public ColdRoom(String tenant, int months, int boxes, String code, int temp) {
super(tenant, months, boxes, code);
this.temp = temp;
}
public double price() {
return super.price() + 35 * getLease().getMonths();
}
}בדיקה – new ColdRoom("Noa", 3, 10, "C-02", 4) תקין בשתי השפות; החוזה שנוצר בתוכו הוא של 3 חודשים ו־10 ארגזים.
public class ColdRoom extends Unit {
private int temp;
public ColdRoom(String tenant, int months, int boxes, String code, int temp) {
super(tenant, months, boxes, code);
this.temp = temp;
}
public double price() {
return super.price() + 35 * getLease().getMonths();
}
}סעיף ג(1)
Java: return 100 * this.lease.getMonths() + 4 * this.lease.getBoxes(); (C#: public virtual double Price() עם GetMonths/GetBoxes)
מה נדרש – לכתוב ב־Unit פעולה שמחזירה את המחיר לפי התעריף הבסיסי: 100 ש"ח לחודש ועוד 4 ש"ח לארגז.
רעיון – מספר החודשים ומספר הארגזים שמורים בעצם החוזה, והתכונות שלו פרטיות – לכן ניגשים אליהן דרך פעולות ה־get של Lease (לפי השאלה הן קיימות). ב־C# מגדירים את הפעולה virtual כדי שאפשר יהיה לדרוס אותה במחלקות היורשות (סעיף ג2).
פתרון
(בתוך Unit):
public double price() {
return 100 * this.lease.getMonths() + 4 * this.lease.getBoxes();
}בדיקה – 3 חודשים ו־10 ארגזים: 100·3 + 4·10 = 300 + 40 = 340. התוצאה: 340 בשתי השפות.
טעות נפוצה: לגשת ל־lease.months ישירות – התכונה פרטית במחלקה Lease, ולכן זו שגיאת הידור.
public double price() {
return 100 * this.lease.getMonths() + 4 * this.lease.getBoxes();
}סעיף ג(2)
Locker – אין צורך (יורשת את התעריף הבסיסי). ColdRoom: return super.price() + 35 * getLease().getMonths(); Container: return super.price() * 0.8; (C#: override ו־base.Price())
מה נדרש – להוסיף price רק במחלקות שבהן המחיר שונה מהתעריף הבסיסי, בלי לבדוק את סוג העצם.
רעיון – זהו פולימורפיזם: כל מחלקה דורסת את price לפי הכלל שלה, והפעולה המתאימה נבחרת בזמן ריצה לפי סוג העצם שזימן אותה. תא אחסון משלם לפי התעריף הבסיסי – לכן לא כותבים price ב־Locker, והיא יורשת אותה מ־Unit. בחדר קירור ובמכולה משתמשים בחישוב הבסיסי דרך super.price() (ב־C# base.Price()), כדי לא לשכפל את הנוסחה. בחדר הקירור צריך גם את מספר החודשים: התכונה lease פרטית ב־Unit, ולכן מגיעים אליה דרך getLease() (פעולת get שקיימת לפי השאלה), ומשם getMonths(). ב־C# הפעולות היורשות מסומנות override (והפעולה ב־Unit virtual).
פתרון
public class ColdRoom extends Unit {
private int temp;
public ColdRoom(String tenant, int months, int boxes, String code, int temp) {
super(tenant, months, boxes, code);
this.temp = temp;
}
public double price() {
return super.price() + 35 * getLease().getMonths();
}
}
public class Container extends Unit {
private boolean roofed;
public Container(String tenant, int months, int boxes, String code, boolean roofed) {
super(tenant, months, boxes, code);
this.roofed = roofed;
}
public double price() {
return super.price() * 0.8;
}
}בדיקה – מערך Unit[] עם Unit, Locker, ColdRoom ו־Container (3 חודשים, 10 ארגזים): 340, 340, 445 (=340+35·3), 272 (=340·0.8). חדר קירור לחודש אחד בלי ארגזים: 100+35 = 135; מכולה ל־6 חודשים ו־25 ארגזים: (600+100)·0.8 = 560. כל התוצאות זהות ב־Java וב־C#.
טעות נפוצה: לכתוב בחדר הקירור this.lease.getMonths() – lease פרטית ב־Unit ואינה נגישה במחלקה היורשת. וגם: ב־C# לשכוח virtual/override, לכתוב price מיותרת ב־Locker, או לבדוק את סוג העצם במקום לדרוס.
public class ColdRoom extends Unit {
private int temp;
public ColdRoom(String tenant, int months, int boxes, String code, int temp) {
super(tenant, months, boxes, code);
this.temp = temp;
}
public double price() {
return super.price() + 35 * getLease().getMonths();
}
}
public class Container extends Unit {
private boolean roofed;
public Container(String tenant, int months, int boxes, String code, boolean roofed) {
super(tenant, months, boxes, code);
this.roofed = roofed;
}
public double price() {
return super.price() * 0.8;
}
}שאלה 11 · פעולות בונות לפי תרשים עצמים – שתי הפניות (25 נקודות)
לפניכם כותרות המחלקות UU , TT והתכונות שלהן.
הפעולות הבונות של המחלקות מסומנות ב־ * (תיתכן יותר מפעולה בונה אחת למחלקה).
public class TT
{
private int p;
***
}
public class UU extends TT
{
private TT left;
private TT right;
***
}הערה: שימו לב – לשתי המחלקות אין פעולות get ו־ set .
לפניכם המחלקה Test , הכוללת פעולה ראשית:
public class Test {
public static void main(String[] args) {
TT t1 = new TT ();
TT t2 = new TT (4);
TT t3 = new TT (t2);
UU u1 = new UU (t1, t2);
UU u2 = new UU (t3);
UU u3 = new UU (7);
UU u4 = new UU (3, t2);
}
}לפניכם תרשים של עצמים שנוצרו בעקבות הרצת קטע הקוד.
כתבו במחלקות TT ו־ UU את הפעולות הבונות הנדרשות כדי לקבל את העצמים שבתרשים.
ציינו בעבור כל אחד מן העצמים שנוצרו את הפעולה הבונה המתאימה לו.
הערה: אין להוסיף פעולות שאינן בונות במחלקות TT ו־ UU . פתרון המוסיף פעולות שאינן בונות לא יזוכה בנקודות.

◂ פתרון מלא לשאלה 11
פתרון
TT: TT() { p = 10 }, TT(int p), TT(TT other) { p = other.p }. UU: UU(TT left, TT right) { super(); this.left = left; this.right = right }, UU(TT t) { super(t); left = t; right = null }, UU(int p) { super(p); left = null; right = new TT() }, UU(int p, TT t) { super(p); left = t; right = new TT(t) }.
מה נדרש – להשלים ב־TT וב־UU פעולות בונות בלבד, כך שהרצת Test תיצור בדיוק את העצמים שבתרשים, ולציין לכל עצם איזו פעולה בונה יצרה אותו.
רעיון – לפי סוגי הפרמטרים: ל־TT יש קריאות בלי פרמטר, עם int ועם TT – שלוש פעולות בונות. ל־UU יש קריאות עם (TT, TT), עם TT, עם int ועם (int, TT) – ארבע פעולות בונות. את הערכים קובעים לפי התרשים:
new TT () נותן p = 10.t3 = new TT (t2) הוא עצם נפרד (קופסה משלו) עם p = 4 – פעולה בונה מעתיקה.super(...)/base(...).TT(), ולכן super(); left ו־right מפנים לאותם עצמים t1 ו־t2. (גם super(left) נותן כאן p = 10, כי p של t1 הוא 10 – גם פתרון כזה תואם לתרשים.)super(t). left מפנה אל t3 עצמו ו־right = null.new TT().new TT(t).פתרון
public class TT
{
private int p;
public TT()
{
this.p = 10;
}
public TT(int p)
{
this.p = p;
}
public TT(TT other)
{
this.p = other.p;
}
}
public class UU extends TT
{
private TT left;
private TT right;
public UU(TT left, TT right)
{
super();
this.left = left;
this.right = right;
}
public UU(TT t)
{
super(t);
this.left = t;
this.right = null;
}
public UU(int p)
{
super(p);
this.left = null;
this.right = new TT();
}
public UU(int p, TT t)
{
super(p);
this.left = t;
this.right = new TT(t);
}
}הפעולה הבונה של כל עצם:
| עצם | הקריאה | הפעולה הבונה | מה מתקבל |
|---|---|---|---|
| t1 | new TT () | TT() | p = 10 |
| t2 | new TT (4) | TT(int p) | p = 4 |
| t3 | new TT (t2) | TT(TT other) – פעולה בונה מעתיקה | עצם חדש עם p = 4 |
| u1 | new UU (t1, t2) | UU(TT left, TT right) | p = 10, left → t1, right → t2 |
| u2 | new UU (t3) | UU(TT t) | p = 4 (העתקה דרך super/base), left → t3, right = null |
| u3 | new UU (7) | UU(int p) | p = 7, left = null, right → עצם TT חדש עם p = 10 |
| u4 | new UU (3, t2) | UU(int p, TT t) | p = 3, left → t2, right → עצם TT חדש עם p = 4 (העתק של t2) |
בדיקה – ערכי השדות וההפניות אחרי Test: t1…t3 = 10, 4, 4 ו־t3 אינו t2; u1: p=10, left הוא t1, right הוא t2; u2: p=4, left הוא t3, right=null; u3: p=7, left=null, right עצם חדש עם p=10; u4: p=3, left הוא t2, right עצם חדש עם p=4 – הכול תואם לתרשים.
טעות נפוצה: ב־u4 לכתוב this.right = t – אז right היה מפנה אל t2 עצמו ולא לקופסה חדשה. וגם: ב־u2 לנסות לכתוב super(t.p) – p פרטית ב־TT ואין פעולת get, ולכן משתמשים בפעולה הבונה המעתיקה super(t).
public class TT
{
private int p;
public TT()
{
this.p = 10;
}
public TT(int p)
{
this.p = p;
}
public TT(TT other)
{
this.p = other.p;
}
}
public class UU extends TT
{
private TT left;
private TT right;
public UU(TT left, TT right)
{
super();
this.left = left;
this.right = right;
}
public UU(TT t)
{
super(t);
this.left = t;
this.right = null;
}
public UU(int p)
{
super(p);
this.left = null;
this.right = new TT();
}
public UU(int p, TT t)
{
super(p);
this.left = t;
this.right = new TT(t);
}
}שאלה 12 · פולימורפיזם, העמסה ודריסה – מעקב והדפסה (X, Y, Z) (25 נקודות)
לפניכם המחלקות X, Y, Z :
public class X {
public int val;
public X() {
this.val = 3;
}
public X(int val) {
this.val = val;
}
public int calc(int k) {
return k + this.val;
}
public int calc(Y other) {
return this.val * other.val;
}
}public class Y extends X {
public Y() {
super(4);
}
public Y(int k) {
this.val = this.val + calc(k);
}
public int calc(X other) {
return other.val - this.val;
}
public int calc(Y other) {
return this.val + 2 * other.val;
}
}public class Z extends Y {
public Z(int k) {
super(k);
this.val = this.val * 2;
}
public int calc(Y other) {
return other.val - 1;
}
public int calc(Z other) {
return this.val + other.val + 10;
}
}לפניכם קטע קוד שרץ ללא שגיאות:
X x = new X();
X xy = new Y();
X xz = new Z(0);
Y y = new Y(2);
Y yz = new Z(1);
Z z = new Z(-3);הציגו את העצמים שנוצרו (סוג העצם, סוג ההפניה, וערך התכונה val).
לפניכם המשך קטע הקוד (ממוספר) שרץ ללא שגיאות.
ציינו את מספר שורת ההדפסה, וכתבו מה הקוד מדפיס.
1. System.out.println(x.calc(y));
2. System.out.println(xy.calc(y));
3. System.out.println(y.calc(x));
4. System.out.println(xz.calc(z));
5. System.out.println(((Y)xz).calc(xy));
6. System.out.println(z.calc(5));
7. System.out.println(xy.calc(xy.calc(2)));
8. System.out.println(yz.calc(x));
9. System.out.println(xy.calc(z));
10. System.out.println(z.calc(z));◂ פתרון מלא לשאלה 12
סעיף א
x: X/X val=3 · xy: הפניה X, עצם Y, val=4 · xz: הפניה X, עצם Z, val=12 · y: Y/Y val=8 · yz: הפניה Y, עצם Z, val=14 · z: Z/Z val=6
מה נדרש – לכל אחד מששת העצמים: סוג העצם (המחלקה שאחרי new), סוג ההפניה (הטיפוס שבהצהרה) וערך התכונה val.
רעיון – עוקבים אחרי שרשרת הפעולות הבונות. פעולה בונה של מחלקה יורשת שאינה מזמנת במפורש פעולה בונה של מחלקת הבסיס (super(...) / base(...)) מזמנת אוטומטית את הפעולה הבונה בלי פרמטרים של מחלקת הבסיס. רק אחרי שהיא מסתיימת מתבצע שאר הגוף.
new X() ⇐ val = 3.new Y() ⇐ super(4) / base(4) ⇐ X(4) ⇐ val = 4.new Y(k) ⇐ אין קריאה מפורשת, ולכן קודם X() ⇐ val = 3. אחר כך val = val + calc(k): הארגומנט מטיפוס int, ולכן נבחרת calc(int k) של X, שמחזירה k + 3 ⇐ val = 6 + k.new Z(k) ⇐ super(k) ⇐ Y(k) ⇐ val = 6 + k, ואחר כך val = val · 2.| הפניה | חישוב |
|---|---|
new Z(0) | Y(0): 3 + (0 + 3) = 6 ⇐ 6 · 2 = 12 |
new Y(2) | 3 + (2 + 3) = 8 |
new Z(1) | Y(1): 3 + (1 + 3) = 7 ⇐ 7 · 2 = 14 |
new Z(-3) | Y(-3): 3 + (-3 + 3) = 3 ⇐ 3 · 2 = 6 |
פתרון
| הפניה | סוג ההפניה | סוג העצם | val |
|---|---|---|---|
| x | X | X | 3 |
| xy | X | Y | 4 |
| xz | X | Z | 12 |
| y | Y | Y | 8 |
| yz | Y | Z | 14 |
| z | Z | Z | 6 |
בדיקה – מדפיסים לכל עצם את המחלקה שלו ואת val. התקבלו בדיוק הערכים שבטבלה.
טעות נפוצה: לחשוב ש־new Y(2) מתחילה מ־val = 4 (כמו new Y()). הפעולה הבונה Y(int k) לא מזמנת את Y(), אלא את X() של מחלקת הבסיס, ולכן val מתחיל מ־3.
סעיף ב
1: 24 · 2: 20 · 3: -5 · 4: 5 · 5: -8 · 6: 11 · 7: 10 · 8: -11 · 9: 16 · 10: 22
מה נדרש – מה מודפס בכל אחת מעשר השורות.
רעיון – שני שלבים בכל קריאה:
מצב העצמים (מסעיף א): x=X(3), xy=Y(4), xz=Z(12), y=Y(8), yz=Z(14), z=Z(6).
פתרון
| שורה | הפניה . פעולה (טיפוס הארגומנט) | חתימה שנבחרה | מתבצעת בפועל | חישוב | פלט |
|---|---|---|---|---|---|
| 1 | x:X . calc(Y) | calc(Y) | X.calc(Y) | 3 · 8 | 24 |
| 2 | xy:X . calc(Y) | calc(Y) | Y.calc(Y) (דריסה) | 4 + 2·8 | 20 |
| 3 | y:Y . calc(X) | calc(X) | Y.calc(X) | 3 − 8 | -5 |
| 4 | xz:X . calc(Z) | calc(Y) (ל־X אין calc(Z); Z הוא Y) | Z.calc(Y) (דריסה) | 6 − 1 | 5 |
| 5 | (Y)xz:Y . calc(X) | calc(X) | Y.calc(X) (Z לא דורסת אותה) | 4 − 12 | -8 |
| 6 | z:Z . calc(int) | calc(int) | X.calc(int) | 5 + 6 | 11 |
| 7 | xy.calc(xy.calc(2)) | calc(int) פעמיים | X.calc(int) | 2+4=6, 6+4 | 10 |
| 8 | yz:Y . calc(X) | calc(X) | Y.calc(X) | 3 − 14 | -11 |
| 9 | xy:X . calc(Z) | calc(Y) | Y.calc(Y) (דריסה) | 4 + 2·6 | 16 |
| 10 | z:Z . calc(Z) | calc(Z) (המתאימה ביותר) | Z.calc(Z) | 6 + 6 + 10 | 22 |
הפלט:
24
20
-5
5
-8
11
10
-11
16
22בדיקה – הפלט ב־Java וב־C# זהה.
הבדל בין הנוסחים: ב־C#, Calc(Y) מוגדרת virtual ב־X ו־override ב־Y וב־Z, ולכן היא נדרסת כמו ב־Java. לעומתה, Calc(X) של Y ו־Calc(Z) של Z הן העמסות חדשות. ב־C# המהדר בוחר קודם מבין הפעולות שהוגדרו (לא כ־override) במחלקה של טיפוס ההפניה, אם אחת מהן מתאימה לארגומנט. בעשר השורות שבשאלה הבחירה הזו זהה לבחירה ב־Java (למשל בשורה 10 נבחרת Calc(Z), ובשורה 6 Calc(Z) ו־Calc(X) לא מתאימות ל־int ולכן נבחרת Calc(int)), ולכן התוצאות זהות.
טעות נפוצה: בשורה 4 לחשוב שהפעולה שתתבצע היא calc(Z) של Z, כי הארגומנט והעצם הם מטיפוס Z. טיפוס ההפניה xz הוא X, ול־X יש רק calc(int) ו־calc(Y). לכן נבחרת calc(Y), ובזמן ריצה מתבצעת הגרסה הדורסת של Z. באופן דומה, בשורה 5 הארגומנט xy מוצהר כ־X, ולכן נבחרת calc(X) ולא calc(Y).
טבלת התשובות
| שאלה | סעיף | תשובה |
|---|---|---|
| 1 | א | מעבירים את q לתור עזר. לפני העבודה הראשונה שמספר העמודים שלה גדול ממש ממספר העמודים של job מכניסים את job. אם אין עבודה כזו, job נכנסת בסוף. אחר כך מחזירים את כל האיברים מתור העזר ל־q. |
| ב | מוציאים מהתור את שתי העבודות code1 ו־code2 (שאר העבודות נשארות באותו סדר), מעדכנים את מספר העמודים של code1: setPages(pages1 + pages2), ומכניסים אותה מחדש עם addJob מסעיף א. | |
| 2 | א | עוברים על השרשרת עם pos ו־prev (החוליה שלפניו) וסופרים ב־seen את החבילות במשקל w. חבילה במשקל w עם seen ≥ k נמחקת: אם prev == null – lst = pos.getNext(), אחרת prev.setNext(pos.getNext()), ו־count++; אחרת prev = pos. מחזירים count. יעילות O(L). |
| ב | עוברים על השרשרת עם pos; לכל חוליה: w = המשקל שלה, k = removeFrom(w, 2) (מוחק את כל החבילות במשקל w פרט לראשונה, שהיא pos), sum += k·w. מחזירים sum (בדוגמה: 43). יעילות O(L²). | |
| 3 | א | לדוגמה: שורש 1 עם הבנים 2, 3; ל־2 הבנים 4, 5; ל־3 הבנים 6, 7; ל־4 רק בן שמאלי 8; ל־5 רק בן ימני 9; ל־6 רק בן שמאלי 10; 7 עלה. (10 צמתים, 4 רמות, 3 צמתים בעלי בן יחיד – כל אחד במסלול אחר.) |
| ב | רקורסיה עם "תקציב" K: אם לצומת יש בן יחיד (hasLeft() != hasRight()) – אם K == 0 מחזירים false, אחרת K = K − 1; בודקים ברקורסיה כל בן קיים עם ה־K המעודכן; עלה – true. יעילות O(n). | |
| 4 | א(1) | לא נכונה. דוגמה נגדית: ריבוע a–b (2), b–c (2), c–d (2), a–d (3). העפ"מ הוא a–b , b–c , c–d (משקל 6). בעץ המסלול מ־a ל־d הוא a–b–c–d במשקל 6, אבל הקשת a–d לבדה שוקלת 3. |
| א(2) | נכונה. לגרף מכוון ללא מעגלים יש מיון טופולוגי v₁, …, vₙ, וכל קשת יוצאת מקודקוד מוקדם לקודקוד מאוחר. לכל זוג מקומות i < j יש לכל היותר קשת אחת vᵢ→vⱼ, ומספר הזוגות הוא n(n−1)/2. | |
| א(3) | לא נכונה. דוגמה נגדית: s→a , s→b , s→c. DFS מ־s מגלה את כל 4 הקודקודים, אבל אין מסלול מ־a ל־s. | |
| א(4) | נכונה. לאף קודקוד אין דרגה 0, ולכן כל רכיב קשירות מכיל קשת – כלומר לפחות 2 קודקודים. k רכיבים מכילים לפחות 2k קודקודים, ולכן 2k ≤ n, כלומר k ≤ n/2. | |
| א(5) | לא נכונה. דוגמה נגדית: s→a , s→b , a→c , b→c. רק ל־s דרגת כניסה 0, ויש שני מיונים טופולוגיים: s, a, b, c וגם s, b, a, c. | |
| א(6) | לא נכונה. דוגמה נגדית: s–a ובנוסף משולש נפרד b–c , c–d , d–b. DFS מ־s מגיע רק ל־a, ואין בו קשת אחורה – אבל בגרף יש את המעגל b–c–d. | |
| ב | (i) ייתכן – אם ב־G יש קשת s–z: z→s→w, חזרה ל־s→x→y. (ii) לא ייתכן – הקשת s–x קיימת ב־G, ובעץ (ii) x ו־s הם אחים (בנים של y); ב־DFS בגרף לא מכוון אין קשת בין ענפים שונים. (iii) לא ייתכן – העץ משתמש בקשת x–w, ואין קשת כזו ב־G. | |
| 5 | א(1) | לא. אין מסלול, למשל, בין 1 ל־ 5 . יש בגרף 2 רכיבי קשירות: {1, 2, 3, 4} ו־ {5, 6, 7}. |
| א(2) | כן. חלוקה מתאימה: {1, 3, 5, 7} ו־ {2, 4, 6} – כל קשת מחברת קודקוד מקבוצה אחת לקודקוד מהקבוצה השנייה. | |
| א(3) | כן: 1 – 2 – 3 – 4 – 1 . | |
| א(4) | לא. עץ הוא גרף קשיר ללא מעגלים, והגרף הנתון אינו קשיר (סעיף (1)) וגם יש בו מעגל (סעיף (3)). | |
| ב(1) | P: Q, S | Q: R, U | R: T, V | S: R, V | T: Q | U: T, W | V: P, W | W: T | |
| ב(2) | P: Q, S | Q: R | R: V | S: V | T: Q | U: (ריקה) | V: P, W | W: T | |
| ב(3) I | סריקה לרוחב (BFS) מתחנת המוצא, עם שמירת מרחק d והורה π לכל תחנה. BFS מגלה את התחנות לפי מספר הקשתות מהמקור, ולכן d[y] הוא מספר הנסיעות המינימלי; אם y לא התגלתה – אי אפשר להגיע אליה. את המסלול משחזרים לפי π מהיעד אחורה. | |
| ב(3) II | P → S → V → W → T (4 נסיעות). U אינה ישיגה מ־ P . | |
| 6 | א(1) | הדרגה של L ושל R היא n + 1 ; הדרגה של כל קודקוד עמוד היא 2 . |
| א(2) | n קשתות שמאליות, n קשתות ימניות, וסך הכול 2n + 1 קשתות (כולל קשת הגשר). | |
| א(3) | לא. למשל L , R , 1 יוצרים משולש – מעגל באורך אי־זוגי. | |
| ב | n עפ"מים (כל אחד במשקל n + 2): כל n הקשתות השמאליות ועוד קשת ימנית אחת לבחירה. | |
| ג(1) | למשל w = 1 , g = 3 (כל בחירה עם 2w < g מתאימה). | |
| ג(2) | צריך 2w < 12 , כלומר w < 6 . במשקלים שלמים – w = 5 (ב־ w = 6 גם קשת הגשר היא מסלול קל). | |
| 7 | א(1) | L₁ – לא רגולרית; L₂ – רגולרית; L₃ – רגולרית (שפה סופית). |
| א(2) | L₄ = ∅ – רגולרית; L₅ = L₁ – לא רגולרית; L₆ = L₂ – רגולרית. | |
| ב | 10 מצבים: S (התחלה), P1, P0, X, Y1, Y0, E0, E1, W0, W1; מקבלים: X, Y0, E0, W1 (טבלת המעברים בפתרון). | |
| 8 | - | כותבים $ אחרי המילה השנייה. בכל סבב: הספרה השמאלית שלא טופלה במילה הראשונה ← X (זוכרים אותה במצב), הספרה השמאלית שלא טופלה במילה השנייה ← Y; אם הן שונות – כותבים 1 בתא הריק הראשון בסוף הסרט. כשאין יותר ספרות במילה הראשונה – כותבים $ בתא הריק הראשון. התוצאה: ⊢X…X#Y…Y$1ᵈ$. |
| 9 | א | המכונה עוצרת במצב q10 והסרט: ⊢ Δ Δ 1 1 Δ Δ … (נשארו שני אחדים). |
| ב | ⊢ 1 1 1 Δ Δ … (המכונה עוצרת ב־ q10). | |
| ג | המכונה מחשבת את המקסימום max(x, y) של שני המספרים: בסיום על הסרט נשארים max(x, y) אחדים ברצף (וכל שאר התאים ריקים). | |
| ד | 1.נכון: ⊢ # 1 1 Δ → ⊢ Δ 1 1 Δ. 2. נכון: ⊢ 1 1 # Δ → ⊢ 1 1 Δ. 3. לא נכון: q6 מחפש ימינה b או 1 ואין כאלה – הוא זז ימינה על תאים ריקים בלי סוף, והמכונה אינה עוצרת. | |
| 10 | א(1) | Locker, ColdRoom ו־Container יורשות מ־Unit (חץ ירושה אל Unit); Unit מכילה עצם מטיפוס Lease (הכלה, המעוין בצד של Unit). |
| א(2) | Lease: tenant, months, boxes; Unit: code, lease; Locker extends Unit: floor; ColdRoom extends Unit: temp; Container extends Unit: roofed. | |
| ב(1) | this.lease = new Lease(tenant, months, boxes); this.code = code; | |
| ב(2) | Java: super(tenant, months, boxes, code); this.temp = temp; C#: : base(tenant, months, boxes, code) { this.temp = temp; } | |
| ג(1) | Java: return 100 * this.lease.getMonths() + 4 * this.lease.getBoxes(); (C#: public virtual double Price() עם GetMonths/GetBoxes) | |
| ג(2) | Locker – אין צורך (יורשת את התעריף הבסיסי). ColdRoom: return super.price() + 35 * getLease().getMonths(); Container: return super.price() * 0.8; (C#: override ו־base.Price()) | |
| 11 | - | TT: TT() { p = 10 }, TT(int p), TT(TT other) { p = other.p }. UU: UU(TT left, TT right) { super(); this.left = left; this.right = right }, UU(TT t) { super(t); left = t; right = null }, UU(int p) { super(p); left = null; right = new TT() }, UU(int p, TT t) { super(p); left = t; right = new TT(t) }. |
| 12 | א | x: X/X val=3 · xy: הפניה X, עצם Y, val=4 · xz: הפניה X, עצם Z, val=12 · y: Y/Y val=8 · yz: הפניה Y, עצם Z, val=14 · z: Z/Z val=6 |
| ב | 1: 24 · 2: 20 · 3: -5 · 4: 5 · 5: -8 · 6: 11 · 7: 10 · 8: -11 · 9: 16 · 10: 22 |