מתכונת 5 במדעי המחשב - מבני נתונים ומסלול 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 נקודות)
לשאלה זו שני נוסחים: בשפת Java ובשפת C# . ענו לפי הנוסח של השפה שבה אתם כותבים.
לפותרים בשפת Java
לפניכם הפעולה kkk , המקבלת תור – q ובו מספרים הגדולים מ־ 0 , ומספר שלם – k שאינו שלילי.
public static boolean kkk (Queue<Integer> q, int k)
{
q.insert (0);
int a = 0;
int b = 0;
int i = 0;
while (q.head() != 0)
{
i++;
if (i <= k)
{
a = a + q.head();
}
else
{
b = b + q.head();
}
q.insert (q.remove());
}
q.remove();
return a == b;
}נתון תור q מטיפוס שלם:

(1) עקבו בעזרת טבלת המעקב שלפניכם אחר הפעולה kkk(q , 3) , וכתבו מה הפעולה מחזירה.
| התור q | i | i <= k | a | b |
|---|---|---|---|---|
הסבירו מה הפעולה kkk עושה.
מהי סיבוכיות זמן הריצה של הפעולה kkk ? נמקו את תשובתכם.
לפניכם הפעולה mystery , המקבלת תור – q ובו מספרים הגדולים מ־ 0 , ומספר שלם – n שאינו שלילי. הפעולה mystery משתמשת בפעולה kkk מסעיף א.
public static int mystery (Queue<Integer> q, int n)
{
if (n == 0)
return 0;
if (kkk (q, n))
return n;
return mystery (q, n - 1);
}נתון התור q מסעיף א:

(1) עקבו אחר הפעולה mystery (q, 5) , וכתבו מה הפעולה מחזירה (אין צורך לעקוב אחר הפעולה kkk).
המעקב יכלול בכל קריאה את הערכים של q , n ואת הערך המוחזר.
לפניכם הצעה לטבלת מעקב (אין חובה להשתמש בטבלה זו).
| התור q שמתקבל בפעולה | הערך n שמתקבל בפעולה | kkk (q, n) == true | ערך מוחזר |
|---|---|---|---|
הסבירו מה הפעולה mystery עושה כאשר n הוא מספר האיברים בתור q .
מהי סיבוכיות זמן הריצה של הפעולה mystery כאשר n הוא מספר האיברים בתור q ? נמקו את תשובתכם.
◂ פתרון מלא לשאלה 1
סעיף א(1)
kkk(q, 3) מחזירה true (a = 3 + 5 + 2 = 10, b = 4 + 6 = 10); התור חוזר למצבו המקורי.
מה נדרש: לעקוב אחר kkk(q, 3) (ב־C#: Kkk) על התור שבאיור ולכתוב מה מוחזר. מראש התור (משמאל) לסופו: 3, 5, 2, 4, 6.
רעיון: הפעולה מכניסה 0 לסוף התור כ"סימן עצירה" ומסובבת את התור עד שה־0 מגיע לראש. i סופר את מקומו של האיבר הנוכחי בתור (1 לאיבר שבראש). k האיברים הראשונים נסכמים ב־a, וכל השאר – ב־b.
פתרון – טבלת מעקב (k = 3). כל שורה היא סיבוב אחד של הלולאה: התור – כפי שהוא בתחילת הסיבוב (הראש משמאל), i, a ו־b – הערכים בסוף הסיבוב.
| התור q (מראש התור לסופו) | i | i <= k | a | b |
|---|---|---|---|---|
| 3, 5, 2, 4, 6, 0 (לפני הלולאה) | 0 | 0 | 0 | |
| 3, 5, 2, 4, 6, 0 | 1 | true | 3 | 0 |
| 5, 2, 4, 6, 0, 3 | 2 | true | 8 | 0 |
| 2, 4, 6, 0, 3, 5 | 3 | true | 10 | 0 |
| 4, 6, 0, 3, 5, 2 | 4 | false | 10 | 4 |
| 6, 0, 3, 5, 2, 4 | 5 | false | 10 | 10 |
| 0, 3, 5, 2, 4, 6 (סוף הלולאה) | 5 | 10 | 10 |
בשורה האחרונה בראש התור 0, ולכן הלולאה מסתיימת. q.remove() מוציא את ה־0, והתור חוזר להיות 3, 5, 2, 4, 6.
a = 10 ו־b = 10 ⇒ a == b ⇒ הפעולה מחזירה true.
בדיקה: על הקוד של השאלה: kkk(q, 3) = true, והתור אחרי הקריאה: [3, 5, 2, 4, 6].
סעיף א(2)
מחזירה true אם סכום k האיברים הראשונים בתור (מן הראש) שווה לסכום שאר האיברים בתור, אחרת false; התור נשאר ללא שינוי.
מה נדרש: להסביר מה kkk עושה.
תשובה: הפעולה מחזירה true אם סכום k האיברים הראשונים בתור (מראש התור) שווה לסכום כל שאר האיברים בתור, ואחרת false. כלומר היא בודקת אם חלוקת התור אחרי k האיברים הראשונים יוצרת שני חלקים שסכומיהם שווים. בסיום הפעולה התור q חוזר למצבו המקורי.
הסבר: a סוכם את האיברים במקומות 1 עד k, ו־b את האיברים במקומות k + 1 עד n. אם k = 0 או k ≥ n, אחד החלקים ריק (סכומו 0) והשני חיובי, ולכן מוחזר false.
בדיקה: על התור 3, 5, 2, 4, 6: k = 0, 1, 2 ⇒ false ; k = 3 ⇒ true (10 = 10) ; k = 4, 5 ⇒ false.
סעיף א(3)
O(n) – n סיבובי לולאה (סיבוב אחד של התור), O(1) בכל סיבוב.
מה נדרש: סיבוכיות זמן הריצה של kkk, עם נימוק.
תשובה: O(n), כאשר n מספר האיברים בתור q.
נימוק: בכל סיבוב של הלולאה while איבר אחד עובר מראש התור לסופו, והלולאה נעצרת כשה־0 שהוכנס לסוף מגיע לראש – כלומר n סיבובים. בכל סיבוב מספר קבוע של פעולות O(1) (head, insert, remove, חיבור, השוואה). לכן n·O(1) + O(1) = O(n). הערך של k אינו משפיע על מספר הסיבובים.
סעיף ב(1)
mystery(q, 5) מחזירה 3 (n = 5, 4 – false; n = 3 – true).
מה נדרש: לעקוב אחר mystery(q, 5) (ב־C#: Mystery) ולכתוב מה מוחזר. התור: 3, 5, 2, 4, 6 (סכום כולל 20).
רעיון: mystery יורדת מ־n כלפי מטה: אם n == 0 היא מחזירה 0; אם kkk(q, n) מחזירה true היא מחזירה n; אחרת היא קוראת לעצמה עם n − 1. התור לא משתנה בין הקריאות.
פתרון – טבלת מעקב:
| התור q שמתקבל בפעולה | הערך n שמתקבל בפעולה | kkk (q, n) == true | ערך מוחזר |
|---|---|---|---|
| 3, 5, 2, 4, 6 | 5 | false (20 ≠ 0) | 3 (מהקריאה mystery(q, 4)) |
| 3, 5, 2, 4, 6 | 4 | false (14 ≠ 6) | 3 (מהקריאה mystery(q, 3)) |
| 3, 5, 2, 4, 6 | 3 | true (10 = 10) | 3 |
הפעולה מחזירה 3.
בדיקה: על הקוד של השאלה: mystery(q, 5) = 3, והתור בסוף: [3, 5, 2, 4, 6].
סעיף ב(2)
מחזירה את המספר k של האיברים שבראש התור שסכומם שווה לסכום שאר האיברים (המקום שבו אפשר לחלק את התור לשני חלקים שווים בסכומם), או 0 אם אין חלוקה כזו; התור לא משתנה.
מה נדרש: להסביר מה mystery(q, n) מחזירה כאשר n הוא גודל התור.
תשובה: הפעולה מחזירה את המספר k כך שסכום k האיברים הראשונים בתור שווה לסכום שאר האיברים – כלומר את המקום שבו אפשר "לחתוך" את התור לשני חלקים (ראש וסוף) שסכומיהם שווים. אם אין חלוקה כזו – הפעולה מחזירה 0. התור q לא משתנה.
הסבר: mystery בודקת את k = n, n − 1, …, 1 לפי הסדר בעזרת kkk (סעיף א), ומחזירה את ה־k הראשון שעבורו kkk מחזירה true. אם לא נמצא כזה, היא מגיעה ל־n = 0 ומחזירה 0. כיוון שכל המספרים חיוביים, סכום k האיברים הראשונים גדל ממש כש־k גדל, ולכן יש לכל היותר מקום אחד כזה.
בדיקה: 1, 2, 3, 4 ⇒ 0 ; 7, 7 ⇒ 1 ; 6, 1, 2, 3 ⇒ 1 ; 2, 2, 2, 2, 8 ⇒ 4.
סעיף ב(3)
O(n²) במקרה הגרוע: עד n + 1 קריאות רקורסיביות, ובכל אחת (פרט לאחרונה) kkk ב־O(n).
מה נדרש: סיבוכיות זמן הריצה של mystery(q, n), עם נימוק.
תשובה: O(n²) במקרה הגרוע.
נימוק: כל קריאה ל־mystery עם n > 0 מבצעת קריאה אחת ל־kkk, שעולה O(n) (סעיף א(3) – תלוי בגודל התור, לא בפרמטר), ועוד מספר קבוע של פעולות. הפרמטר יורד ב־1 בכל קריאה. במקרה הגרוע – כשאין בתור חלוקה לשני חלקים שווים – יש n קריאות עם kkk ועוד קריאה אחת עם 0 ⇒ n·O(n) + O(1) = O(n²).
(במקרה הטוב החלוקה נמצאת מהר – למשל 1, 1, 1, 3 ⇒ k = 3 בקריאה השנייה – ואז O(n).)
שאלה 2 · רשימת השמעה – מחיקה מסוף שרשרת חוליות (25 נקודות)
נתונה המחלקה Song – שיר, ולה שתי תכונות:
הניחו שיש פעולות get/Get ו־ set/Set לתכונות המחלקה.
נתונה המחלקה Playlist – רשימת השמעה, ולה תכונה אחת:
הערה: השירים אינם מסודרים בשרשרת לפי אורכם, וכל שיר מופיע פעם אחת בלבד.
ממשו את הפעולה של ממשק המחלקה Playlist שלפניכם:
Java – public int removeLastLong (int n, int t)
C# – public int RemoveLastLong (int n, int t)
הפעולה תמחק מן השרשרת את n השירים האחרונים בשרשרת שאורך כל אחד מהם גדול מ־ t . אם יש בשרשרת פחות מ־ n שירים שאורכם גדול מ־ t , יימחקו כל השירים האלה.
הפעולה תחזיר את כמות השירים שנמחקו.
הניחו ש־ n גדול מ־ 0 וקטן מכמות השירים בשרשרת, וש־ t אינו שלילי.
הערה: שאר השירים בשרשרת יישארו באותו הסדר. אם אין בשרשרת שום שיר שאורכו גדול מ־ t , השרשרת תישאר ללא שום שינוי והפעולה תחזיר 0 .
דוגמה: בעבור n = 2 , t = 200 והשרשרת lst שלפניכם:

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

הסבר: בשרשרת יש שלושה שירים שאורכם גדול מ־ 200 : "Rain" , "Blue" ו־ "Fire" . מכיוון ש־ n = 2 , נמחקו שני האחרונים מביניהם – "Blue" ו־ "Fire" , והשיר "Rain" נשאר.
דוגמה נוספת: בעבור אותה השרשרת מן הדוגמה המקורית שלעיל ו־ n = 5 , t = 190 , הפעולה תחזיר 4 והשרשרת תיראה כך בתום הפעולה:

הסבר: יש בשרשרת רק ארבעה שירים שאורכם גדול מ־ 190 ("Rain" , "Blue" , "Fire" ו־ "Wind"). לכן נמחקו כל ארבעתם, והפעולה החזירה 4 .
ממשו את הפעולה של ממשק המחלקה Playlist שלפניכם:
Java – public int shorten (int maxTotal)
C# – public int Shorten (int maxTotal)
הפעולה תקצר את רשימת ההשמעה כך שסכום האורכים של השירים שיישארו בשרשרת יהיה לכל היותר maxTotal : כל עוד סכום האורכים של השירים בשרשרת גדול מ־ maxTotal , הפעולה תמחק מן השרשרת את השיר הארוך ביותר. אם יש כמה שירים ארוכים ביותר (באותו אורך), יימחק האחרון מביניהם.
הפעולה תחזיר את כמות השירים שנמחקו.
הניחו ש־ maxTotal גדול או שווה לאורך של כל אחד מן השירים בשרשרת.
הערות:
דוגמה: בעבור אותה השרשרת מן הדוגמה המקורית בסעיף א ו־ maxTotal = 1000 , הפעולה תחזיר 2 , והשרשרת תיראה כך:

הסבר: סכום האורכים בהתחלה הוא 1360 . נמחק השיר הארוך ביותר "Blue" (240), והסכום ירד ל־ 1120 . הסכום עדיין גדול מ־ 1000 , ולכן נמחק שוב השיר הארוך ביותר: יש שני שירים באורך 210 ("Rain" ו־ "Fire"), ונמחק האחרון מביניהם – "Fire" . הסכום ירד ל־ 910 , שאינו גדול מ־ 1000 , והפעולה החזירה 2 .
◂ פתרון מלא לשאלה 2
סעיף א
מעבר ראשון: total = מספר השירים שאורכם > t; keep = max(total − n, 0). מעבר שני עם pos ו־prev: שיר ארוך כש־keep == 0 נמחק (prev == null ⇒ lst = pos.getNext(), אחרת prev.setNext(pos.getNext())), count++; שיר ארוך כש־keep > 0 נשאר ו־keep−−. מחזירים count. יעילות O(L).
מה נדרש: למחוק את n השירים האחרונים שאורכם גדול מ־t (או את כולם, אם יש פחות מ־n כאלה) ולהחזיר כמה שירים נמחקו.
רעיון: בשרשרת חוליות אי אפשר ללכת אחורה, ולכן קודם סופרים.
כיוון ש־n קטן מכמות השירים, תמיד נשאר לפחות שיר אחד והשרשרת לא מתרוקנת.
פתרון:
public int removeLastLong(int n, int t)
{
// מעבר ראשון: כמה שירים ארוכים מ-t יש בשרשרת
int total = 0;
Node<Song> pos = lst;
while (pos != null)
{
if (pos.getValue().getLength() > t)
total++;
pos = pos.getNext();
}
// כמה מן השירים הארוכים (הראשונים שבהם) יישארו בשרשרת
int keep = total - n;
if (keep < 0)
keep = 0;
// מעבר שני: מדלגים על keep השירים הארוכים הראשונים ומוחקים את השאר
int count = 0;
Node<Song> prev = null; // החוליה שלפני pos
pos = lst;
while (pos != null)
{
boolean isLong = pos.getValue().getLength() > t;
if (isLong && keep == 0)
{
if (prev == null)
lst = pos.getNext();
else
prev.setNext(pos.getNext());
count++;
}
else
{
if (isLong)
keep--;
prev = pos;
}
pos = pos.getNext();
}
return count;
}בדיקה: על השרשרת מהדוגמה (Rain:210, Echo:185, Blue:240, Moon:150, Fire:210, Wind:195, Sky:170):
יעילות: שני מעברים על השרשרת, בכל חוליה מספר קבוע של פעולות ⇒ 2·L פעולות = O(L), כאשר L מספר החוליות.
טעות נפוצה: למחוק את n השירים הארוכים הראשונים (כמו בפעולת מחיקה רגילה) – הדרישה היא האחרונים, ולכן צריך לדעת מראש כמה שירים ארוכים יש.
public int removeLastLong(int n, int t)
{
// מעבר ראשון: כמה שירים ארוכים מ-t יש בשרשרת
int total = 0;
Node<Song> pos = lst;
while (pos != null)
{
if (pos.getValue().getLength() > t)
total++;
pos = pos.getNext();
}
// כמה מן השירים הארוכים (הראשונים שבהם) יישארו בשרשרת
int keep = total - n;
if (keep < 0)
keep = 0;
// מעבר שני: מדלגים על keep השירים הארוכים הראשונים ומוחקים את השאר
int count = 0;
Node<Song> prev = null; // החוליה שלפני pos
pos = lst;
while (pos != null)
{
boolean isLong = pos.getValue().getLength() > t;
if (isLong && keep == 0)
{
if (prev == null)
lst = pos.getNext();
else
prev.setNext(pos.getNext());
count++;
}
else
{
if (isLong)
keep--;
prev = pos;
}
pos = pos.getNext();
}
return count;
}סעיף ב
מחשבים sum של כל האורכים; כל עוד sum > maxTotal: מוצאים את האורך המרבי max, מוחקים את השיר האחרון באורך max בעזרת removeLastLong(1, max − 1), sum −= max, count++. מחזירים count (בדוגמה: 2). יעילות O(L²).
מה נדרש: למחוק שוב ושוב את השיר הארוך ביותר (האחרון מביניהם אם יש כמה) עד שסכום האורכים אינו גדול מ־maxTotal, ולהחזיר כמה שירים נמחקו.
רעיון:
removeLastLong(1, max - 1) מוחקת את האחרון מביניהם.הלולאה מסתיימת, כי בכל סיבוב נמחק שיר. השרשרת לא מתרוקנת: כל עוד sum > maxTotal ≥ אורך כל שיר, יש בשרשרת לפחות שני שירים, ולכן n = 1 קטן מכמות השירים, כפי שדורשת ההנחה של סעיף א (t = max − 1 אינו שלילי, כי max ≥ 1).
פתרון (Java) – הפעולה removeLastLong מסעיף א נמצאת באותה מחלקה:
// משתמשת בפעולה removeLastLong מסעיף א
public int shorten(int maxTotal)
{
// סכום אורכי כל השירים
int sum = 0;
Node<Song> pos = lst;
while (pos != null)
{
sum = sum + pos.getValue().getLength();
pos = pos.getNext();
}
int count = 0;
while (sum > maxTotal)
{
// מציאת אורך השיר הארוך ביותר
int max = lst.getValue().getLength();
pos = lst.getNext();
while (pos != null)
{
if (pos.getValue().getLength() > max)
max = pos.getValue().getLength();
pos = pos.getNext();
}
// השירים שאורכם גדול מ-(max-1) הם בדיוק השירים שאורכם max;
// מוחקים את האחרון מביניהם
removeLastLong(1, max - 1);
sum = sum - max;
count++;
}
return count;
}בדיקה: על השרשרת מהדוגמה עם maxTotal = 1000:
| סיבוב | sum לפני | max | השיר שנמחק | sum אחרי |
|---|---|---|---|---|
| 1 | 1360 | 240 | Blue | 1120 |
| 2 | 1120 | 210 | Fire (האחרון מבין Rain ו־Fire) | 910 |
910 ≤ 1000 ⇒ הלולאה נעצרת, מוחזר 2, והשרשרת Rain, Echo, Moon, Wind, Sky – כמו בדוגמה. מקרים נוספים: maxTotal = 1360 (מוחזר 0, ללא שינוי), maxTotal = 700 (מוחזר 3: Blue, Fire, Rain), maxTotal = 240 (מוחזר 6, נשאר Moon).
יעילות: המעבר הראשון O(L). בכל סיבוב של הלולאה – מעבר למציאת max ועוד removeLastLong, שהיא O(L) (סעיף א) ⇒ O(L) לסיבוב. יש לכל היותר L − 1 סיבובים ⇒ O(L²).
טעות נפוצה: למחוק את השיר הארוך ביותר הראשון שנמצא – כשיש כמה שירים באותו אורך מרבי, הדרישה היא למחוק את האחרון.
// משתמשת בפעולה removeLastLong מסעיף א
public int shorten(int maxTotal)
{
// סכום אורכי כל השירים
int sum = 0;
Node<Song> pos = lst;
while (pos != null)
{
sum = sum + pos.getValue().getLength();
pos = pos.getNext();
}
int count = 0;
while (sum > maxTotal)
{
// מציאת אורך השיר הארוך ביותר
int max = lst.getValue().getLength();
pos = lst.getNext();
while (pos != null)
{
if (pos.getValue().getLength() > max)
max = pos.getValue().getLength();
pos = pos.getNext();
}
// השירים שאורכם גדול מ-(max-1) הם בדיוק השירים שאורכם max;
// מוחקים את האחרון מביניהם
removeLastLong(1, max - 1);
sum = sum - max;
count++;
}
return count;
}שאלה 3 · זוגות נגדיים בעץ בינרי (25 נקודות)
בעץ בינרי, הרמה של צומת היא מספר הקשתות במסלול מן השורש אל הצומת: רמת השורש היא 0 , רמת הבנים של השורש היא 1 , וכן הלאה.
"זוג נגדי" בעץ הוא שני מספרים בעץ שאחד מהם הוא המספר הנגדי של השני (למשל 5 ו־ −5). פער הרמות של זוג נגדי הוא ההפרש, בערך מוחלט, בין הרמות של שני המספרים.
נתון עץ בינרי t מטיפוס שלם, המכיל מספרים חיוביים ושליליים שאינם 0 , וכולם שונים זה מזה (כלומר אין שני מספרים זהים בעץ).
כתבו פעולה ששמה levelGap בשפת Java או LevelGap בשפת C# המקבלת את העץ t ומספר שלם num (חיובי או שלילי) המופיע בעץ.
הפעולה תחזיר את פער הרמות של הזוג הנגדי ש־ num שייך אליו.
אם המספר הנגדי ל־ num אינו מופיע בעץ, הפעולה תחזיר −1 .
אפשר לכתוב פעולות עזר.
הערות:
– חובה לשמור על העץ t .
– אין להשתמש בסעיף זה במערך. פתרון הכולל שימוש במערך לא יזוכה בנקודות.
דוגמאות: עבור העץ t שלפניכם:

עבור num = 5 הפעולה תחזיר 2 (5 ברמה 0 , ו־ −5 ברמה 2).
עבור num = −1 הפעולה תחזיר 1 (−1 ברמה 3 , ו־ 1 ברמה 2).
עבור num = −6 הפעולה תחזיר −1 (משום שהמספר הנגדי לו, 6 , אינו מופיע בעץ).
כתבו פעולה ששמה maxGap בשפת Java או MaxGap בשפת C# המקבלת את העץ t . הפעולה תחזיר את פער הרמות הגדול ביותר של זוג נגדי בעץ. אם אין בעץ שום זוג נגדי, הפעולה תחזיר −1 .
אפשר להשתמש בפעולה שכתבתם בסעיף א.
הערות:
– בפעולה זו אין חובה לשמור על העץ t .
– אין להשתמש בסעיף זה במערך. פתרון הכולל שימוש במערך לא יזוכה בנקודות.
לדוגמה: עבור העץ t שבדוגמה לעיל הפעולה תחזיר 3 .
הסבר: בעץ חמישה זוגות נגדיים: 5 ו־ −5 (פער 2), 8 ו־ −8 (פער 1), 1 ו־ −1 (פער 1), 12 ו־ −12 (פער 1), 3 ו־ −3 (פער 3). לזוג 3 ו־ −3 פער הרמות הגדול ביותר.
◂ פתרון מלא לשאלה 3
סעיף א
פעולת עזר רקורסיבית level(t, x) מחזירה את הרמה של x בעץ t, או −1 אם x לא בעץ: אם t ריק ⇒ −1; אם בשורש ⇒ 0; אחרת מחפשים בתת־העץ השמאלי ואם לא נמצא – בימני, ואם נמצא מחזירים רמה + 1. levelGap: אם level(t, −num) == −1 מחזירים −1, אחרת מחזירים |level(t, num) − level(t, −num)|.
מה נדרש: למצוא את הרמה של num ואת הרמה של −num, ולהחזיר את ההפרש ביניהן בערך מוחלט (או −1 אם −num לא בעץ).
רעיון: כותבים פעולת עזר רקורסיבית level(t, x) שמחזירה את הרמה של x בעץ t:
num ≠ 0, ולכן num ו־−num הם שני מספרים שונים. levelGap מחשבת את level(t, −num); אם התוצאה −1 מחזירים −1, אחרת מחזירים |level(t, num) − level(t, −num)|. (num מופיע בעץ, ולכן level(t, num) אינו −1.)
פתרון: הקוד המלא – בלשונית Java/C#. הפעולות רק קוראות את העץ (getValue, getLeft, getRight) ולא משנות אותו, ולכן העץ נשמר.
יעילות: level עוברת על כל צומת לכל היותר פעם אחת – O(n). levelGap קוראת לה פעמיים – O(n).
בדיקה (העץ שבשאלה. רמה 0: 5; רמה 1: −8, 3; רמה 2: 1, −5, 12, 8; רמה 3: −12, −1, −6; רמה 4: −3):
| num | רמת num | רמת −num | תוצאה |
|---|---|---|---|
| 5 | 0 | 2 | 2 |
| −1 | 3 | 2 | 1 |
| 3 | 1 | 4 | 3 |
| −8 | 1 | 2 | 1 |
| −6 | 3 | לא נמצא | −1 |
מתקבלים 2, 1, 3, 1, −1, והעץ לא משתנה.
טעות נפוצה: להחזיר מ־level את lev + 1 גם כאשר lev == −1 – ואז מספר שלא נמצא מקבל רמה 0 (−1 + 1), כאילו הוא בשורש.
// פעולת עזר: הרמה של x בעץ t (רמת השורש של t היא 0), או -1 אם x לא בעץ
public static int level(BinNode<Integer> t, int x)
{
if (t == null)
return -1;
if (t.getValue() == x)
return 0;
int lev = level(t.getLeft(), x);
if (lev == -1)
lev = level(t.getRight(), x);
if (lev == -1)
return -1;
return lev + 1; // x נמצא בתת-עץ, רמה אחת מתחת לשורש
}
public static int levelGap(BinNode<Integer> t, int num)
{
int other = level(t, -num);
if (other == -1) // המספר הנגדי ל-num לא נמצא בעץ
return -1;
return Math.abs(level(t, num) - other);
}סעיף ב
סריקה רקורסיבית של כל צמתי העץ, תוך שמירת הפניה לשורש העץ כולו: maxGapFrom(root, node) – אם node ריק מחזירים −1; אחרת מחזירים את המקסימום מבין levelGap(root, node.getValue()), maxGapFrom(root, node.getLeft()) ו־maxGapFrom(root, node.getRight()). maxGap(t) מחזירה maxGapFrom(t, t).
מה נדרש: פער הרמות הגדול ביותר מבין כל הזוגות הנגדיים בעץ, או −1 אם אין זוג כזה.
רעיון: כל זוג נגדי כולל מספר כלשהו שבעץ. לכן עוברים על כל הצמתים, ולכל ערך v מחשבים levelGap(t, v) מסעיף א, ושומרים את המקסימום.
את המעבר על כל הצמתים עושים ברקורסיה (סריקה תחילית). חשוב: את levelGap צריך להפעיל על העץ כולו ולא על תת־העץ הנוכחי – המספר הנגדי יכול להיות בכל מקום בעץ, והרמות נמדדות מהשורש של t. לכן פעולת העזר maxGapFrom מקבלת שתי הפניות: root – שורש העץ כולו (לא משתנה), ו־node – הצומת הנוכחי בסריקה.
ערך ההתחלה −1 חשוב: פער של 0 (זוג באותה רמה, למשל שני אחים) הוא פער חוקי, ולכן −1 מסמן "אין זוג". אם ל־v אין נגדי, levelGap מחזירה −1, וזה לא משנה את המקסימום. (כל זוג נבדק פעמיים, פעם מכל צד, וזה לא משנה את התוצאה.)
פתרון: הקוד המלא – בלשונית Java/C#.
יעילות: לכל אחד מ־n הצמתים יש קריאה ל־levelGap שעולה O(n), ולכן הפעולה היא O(n²).
בדיקה: בעץ שבשאלה הפערים הם 2, 1, 1, 1 ו־3, ולשאר המספרים (−6) −1. התקבל 3. מקרים נוספים: עץ עם צומת אחד ⇒ −1; עץ ששורשו 1 ובניו 2 ו־−2 ⇒ 0; עץ ששורשו 7 ובנו השמאלי −7 ⇒ 1; העץ שבשאלה בלי הצומת −3 ⇒ 2.
טעות נפוצה: לקרוא ל־levelGap(node, …) על תת־העץ הנוכחי במקום על העץ כולו – ואז לא נמצאים זוגות שהמספרים שלהם בענפים שונים, והרמות נמדדות מהצומת הלא נכון.
public static int maxGap(BinNode<Integer> t)
{
return maxGapFrom(t, t);
}
// root – העץ כולו (בו מחפשים את הזוגות), node – תת-העץ שעוברים עליו כעת
public static int maxGapFrom(BinNode<Integer> root, BinNode<Integer> node)
{
if (node == null)
return -1;
int max = levelGap(root, node.getValue());
int left = maxGapFrom(root, node.getLeft());
int right = maxGapFrom(root, node.getRight());
if (left > max)
max = left;
if (right > max)
max = right;
return max;
}פרק שני (50 נקודות) – אלגוריתמים
יש לענות על שתי שאלות במסלול שלמדתם (לכל שאלה – 25 נקודות).
שאלה 4 · אתר סקי – הדרך המהירה ביותר במורד ההר (25 נקודות)
באתר סקי יש תחנות על צלע ההר, ובין חלק מן התחנות יש מסלולי גלישה. כל מסלול גלישה יורד מתחנה גבוהה יותר לתחנה נמוכה יותר, ולכן אפשר לגלוש בו רק בכיוון אחד (בכיוון החץ).
לכל מסלול גלישה יש זמן גלישה בדקות. בכל תחנה יש נקודת ביקורת, וגולש שעובר דרך התחנה חייב לעצור בה; לכל תחנה יש זמן עצירה בדקות.
הזמן של דרך בין שתי תחנות הוא סכום זמני הגלישה של מסלולי הגלישה בדרך, ועוד סכום זמני העצירה של התחנות שהגולש עובר דרכן. הזמן אינו כולל את זמן העצירה בתחנה שבה הגולש מתחיל ואת זמן העצירה בתחנה שבה הוא מסיים. הדרך המהירה ביותר בין שתי תחנות היא הדרך שהזמן שלה הקטן ביותר.
לפניכם אתר הסקי SKI . המספר ליד כל חץ הוא זמן הגלישה במסלול, והמספר שבמלבן ליד כל תחנה הוא זמן העצירה בה (בדקות):

דוגמה: הזמן של הדרך A → B → D → G → H הוא 25 דקות: זמני הגלישה 3 + 2 + 4 + 3 = 12 , ועוד זמני העצירה בתחנות B , D , G (בלי A ובלי H): 4 + 6 + 3 = 13 .
באתר SKI , מהי הדרך המהירה ביותר מתחנה A לתחנה H ? כתבו את שמות התחנות בדרך זו לפי הסדר, ואת הזמן שלה (אין צורך לבצע מעקב).
כתבו אלגוריתם יעיל ככל האפשר המוצא בכל אתר סקי כזה את הדרך המהירה ביותר מתחנה K₁ לתחנה K₂ .
הערה: יש לכתוב אלגוריתם יעיל שאינו עובר על כל הדרכים האפשריות.
סרטטו את הגרף המייצג את אתר הסקי SKI הנתון לעיל, באופן שיתאים לאלגוריתם שכתבתם.
◂ פתרון מלא לשאלה 4
סעיף א
A → C → E → F → H , 23 דקות (גלישה 6 + 3 + 2 + 4 = 15 , עצירות C , E , F : 1 + 5 + 2 = 8).
מה נדרש: למצוא דרך מ־ A ל־ H שסכום זמני הגלישה ועוד זמני העצירה בתחנות הביניים בה מינימלי.
רעיון: יש לשקול את שני סוגי הזמנים יחד: דרך עם מעט מסלולים עלולה לעבור בתחנה עם עצירה ארוכה, ולהפך.
פתרון: הדרכים המהירות ביותר מ־ A ל־ H (זמן = גלישה + עצירות בתחנות הביניים):
| דרך | גלישה | עצירות | זמן |
|---|---|---|---|
| A → C → E → F → H | 6 + 3 + 2 + 4 = 15 | 1 + 5 + 2 = 8 | 23 |
| A → B → E → F → H | 3 + 4 + 2 + 4 = 13 | 4 + 5 + 2 = 11 | 24 |
| A → C → E → G → H | 6 + 3 + 3 + 3 = 15 | 1 + 5 + 3 = 9 | 24 |
| A → C → F → H | 6 + 11 + 4 = 21 | 1 + 2 = 3 | 24 |
| A → B → D → G → H | 12 | 13 | 25 |
הדרך המהירה ביותר: A → C → E → F → H , 23 דקות.
בדיקה: יש 11 דרכים מ־ A ל־ H ; המינימום 23 מתקבל רק בדרך זו.
טעות נפוצה: לכלול בזמן את העצירה ב־ A (3) או ב־ H (2), או להתעלם מזמני העצירה ולחפש רק את סכום זמני הגלישה הקטן (אז נבחרת דרך כמו A → B → D → G → H , עם 12 דקות גלישה בלבד, שזמנה האמיתי 25).
סעיף ב(1)
בונים גרף מכוון: קודקוד לכל תחנה, ולכל מסלול גלישה u → v קשת שמשקלה slope(u, v) + stop(v). כל המסלולים יורדים, ולכן הגרף ללא מעגלים (גמ"ל). ממיינים מיון טופולוגי, מרפים את הקשתות לפי הסדר (מסלולים קלים בגמ"ל) מ־ K₁ , ומחזירים את הדרך לפי π ואת הזמן d[K₂] − stop(K₂). זמן ריצה O(|V| + |E|).
מה נדרש: אלגוריתם כללי ויעיל לדרך המהירה ביותר, כשהזמן מורכב מזמנים על הקשתות (גלישה) ועל הקודקודים (עצירה).
רעיון:
פתרון:
FastestWay(SKI, K₁, K₂):
// שלב 1: בניית גרף מכוון ממושקל
לכל תחנה – קודקוד
לכל מסלול גלישה מ־u אל v – קשת u → v שמשקלה slope(u, v) + stop(v)
// שלב 2: מיון טופולוגי (הגרף ללא מעגלים)
order ← מיון טופולוגי של הקודקודים
// שלב 3: מסלולים קלים בגמ"ל
לכל קודקוד v: d[v] ← ∞ ; π[v] ← null
d[K₁] ← 0
לכל קודקוד u לפי הסדר ב־order:
אם d[u] ≠ ∞:
לכל קשת u → v שמשקלה w:
אם d[u] + w < d[v]:
d[v] ← d[u] + w ; π[v] ← u
אם d[K₂] = ∞: החזר "אין דרך"
// שלב 4: שחזור הדרך והזמן
way ← רשימה ריקה ; x ← K₂
כל עוד x ≠ null:
הוסף את x לתחילת way ; x ← π[x]
החזר את way ואת הזמן d[K₂] − stop(K₂)המיון הטופולוגי – לפי דרגות כניסה (כל פעם מוציאים קודקוד שדרגת הכניסה שלו 0) או לפי DFS (סדר יורד של זמני סיום). כשמעבדים את u לפי הסדר, כל הקשתות שנכנסות ל־ u כבר טופלו, ולכן d[u] כבר סופי.
זמן ריצה: בניית הגרף O(|V| + |E|), מיון טופולוגי O(|V| + |E|), כל קשת מורפית פעם אחת O(|E|) ⇒ סך הכול O(|V| + |E|).
בדיקה: על האתר SKI עם K₁ = A , K₂ = H . סדר טופולוגי: A , B , C , D , E , F , G , H . משקלי הקשתות (גלישה + עצירה ביעד) בסוגריים; בעמודות – d בסוף כל צעד ובסוגריים π:
| קודקוד שמעובד | קשתות שנבדקו (משקל) | עדכונים | A | B | C | D | E | F | G | H |
|---|---|---|---|---|---|---|---|---|---|---|
| אתחול | – | – | 0 | ∞ | ∞ | ∞ | ∞ | ∞ | ∞ | ∞ |
| A | A→B (7), A→C (7) | d[B]=7, d[C]=7 | 0 | 7 (A) | 7 (A) | ∞ | ∞ | ∞ | ∞ | ∞ |
| B | B→D (8), B→E (9) | d[D]=15, d[E]=16 | 0 | 7 (A) | 7 (A) | 15 (B) | 16 (B) | ∞ | ∞ | ∞ |
| C | C→E (8), C→F (13) | d[E]=15, d[F]=20 | 0 | 7 (A) | 7 (A) | 15 (B) | 15 (C) | 20 (C) | ∞ | ∞ |
| D | D→E (6), D→G (7) | d[G]=22 | 0 | 7 (A) | 7 (A) | 15 (B) | 15 (C) | 20 (C) | 22 (D) | ∞ |
| E | E→F (4), E→G (6), E→H (12) | d[F]=19, d[G]=21, d[H]=27 | 0 | 7 (A) | 7 (A) | 15 (B) | 15 (C) | 19 (E) | 21 (E) | 27 (E) |
| F | F→H (6) | d[H]=25 | 0 | 7 (A) | 7 (A) | 15 (B) | 15 (C) | 19 (E) | 21 (E) | 25 (F) |
| G | G→H (5) | – | 0 | 7 (A) | 7 (A) | 15 (B) | 15 (C) | 19 (E) | 21 (E) | 25 (F) |
| H | – | – | 0 | 7 (A) | 7 (A) | 15 (B) | 15 (C) | 19 (E) | 21 (E) | 25 (F) |
d[H] = 25 , ולכן הזמן הוא 25 − stop(H) = 25 − 2 = 23 . שחזור: H ← F ← E ← C ← A , כלומר A → C → E → F → H – כמו בסעיף א.
טעות נפוצה: לשכוח להחסיר את stop(K₂) בסוף, או לשים את זמן העצירה על הקשת היוצאת מהתחנה בלי לטפל בתחנת המוצא (אז העצירה ב־ K₁ נספרת בטעות).
סעיף ב(2)
גרף מכוון ללא מעגלים עם הקודקודים A–H ו־13 קשתות; משקל הקשת u → v הוא זמן הגלישה ועוד זמן העצירה ב־ v : A→B 7 , A→C 7 , B→D 8 , B→E 9 , C→E 8 , C→F 13 , D→E 6 , D→G 7 , E→F 4 , E→G 6 , E→H 12 , F→H 6 , G→H 5 .
מה נדרש: לסרטט את הגרף שהאלגוריתם מסעיף (1) מקבל כקלט.
רעיון: אותם קודקודים ואותם חצים; על כל קשת u → v רושמים slope(u, v) + stop(v). זמני העצירה כבר אינם רשומים ליד התחנות – הם "נכנסו" למשקלי הקשתות.
פתרון:

בדיקה: משקל הדרך A → B → D → G → H בגרף הוא 7 + 8 + 7 + 5 = 27 = 25 + stop(H) , כלומר הזמן מהדוגמה ועוד 2 . לכל 11 הדרכים מ־ A ל־ H : משקל בגרף − 2 = הזמן האמיתי.
שאלה 5 · צביעה בשני צבעים – האלגוריתם TwoColors (25 נקודות)
לפניכם גרף לא מכוון G = (V, E) .

| צמתי הגרף | V = {1, 2, 3, 4, 5, 6, 7} |
| קשתות הגרף | E = {(1 – 2), (1 – 3), (2 – 4), (2 – 5), (3 – 5), (3 – 6), (4 – 7), (5 – 6), (5 – 7), (6 – 7)} |
הצומת M מוגדר "שכן" של הצומת U , אם יש קשת ביניהם.
לפניכם טבלת שכנויות המציגה את השכנים של כל צומת בגרף G , מסודרים מן הקטן אל הגדול. הצמתים השכנים של 1 ושל 4 נתונים בטבלה.
השלימו את הטבלה.
| U | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|---|---|---|---|---|---|---|---|
| M | 2 , 3 | 2 , 7 |
נתון האלגוריתם TwoColors (G, s) , שמנסה לצבוע את צמתי הגרף בשני צבעים, 0 ו־ 1 , כך שלכל שני צמתים שכנים יהיו צבעים שונים. נריץ את האלגוריתם מן הצומת s = 1 .
שלב אתחול נתונים באלגוריתם:
| color: | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|---|---|---|---|---|---|---|---|
| 0 |
| parent: | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|---|---|---|---|---|---|---|---|
שלב הרצת האלגוריתם:
א. נכתוב בתא של M במערך color את הצבע ההפוך לצבע של U (כלומר את הערך 1 − color[U]).
ב. נעדכן שהצומת U הוא ההורה של M במערך parent .
ג. נכניס את M לתור.
עקבו בטבלת מעקב אחרי ריצת האלגוריתם TwoColors על הגרף הנתון G , מן הצומת 1 .
המעקב צריך לכלול בכל איטרציה את הפריטים שלהלן:
| התור | U | color 1 | 2 | 3 | 4 | 5 | 6 | 7 | parent 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| ←[ ] | |||||||||||||||
| ←[ ] |
כתבו את רשימת הצמתים שהאלגוריתם מחזיר.
◂ פתרון מלא לשאלה 5
סעיף א
2: 1, 4, 5 ; 3: 1, 5, 6 ; 5: 2, 3, 6, 7 ; 6: 3, 5, 7 ; 7: 4, 5, 6
מה נדרש: להשלים לכל צומת את רשימת שכניו, מן הקטן אל הגדול.
רעיון: עוברים על רשימת הקשתות E . כל קשת (X – Y) מוסיפה את Y לשכני X ואת X לשכני Y .
פתרון:
| U | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|---|---|---|---|---|---|---|---|
| M | 2 , 3 | 1 , 4 , 5 | 1 , 5 , 6 | 2 , 7 | 2 , 3 , 6 , 7 | 3 , 5 , 7 | 4 , 5 , 6 |
בדיקה: סכום הדרגות 2 + 3 + 3 + 2 + 4 + 3 + 3 = 20 = 2·10 , פעמיים מספר הקשתות, כנדרש.
סעיף ב
הוצאות מן התור לפי הסדר: 1, 2, 3, 4, 5. באיטרציה החמישית (U = 5) השכן 6 באותו צבע (0) ⇐ עצירה. בסוף: color = (0, 1, 1, 0, 0, 0, 1) לצמתים 1–7 ; parent: 2←1, 3←1, 4←2, 5←2, 6←3, 7←4.
מה נדרש: להריץ את TwoColors צעד אחר צעד ולרשום אחרי כל איטרציה את התור, את U ואת שני המערכים.
רעיון: זו סריקת BFS מן הצומת 1 , שבה כל צומת חדש מקבל צבע הפוך לצבע של הצומת שממנו התגלה. אם נמצאים שני שכנים באותו צבע – אי אפשר לצבוע את הגרף בשני צבעים, והאלגוריתם עוצר. השכנים נבדקים לפי הסדר שבטבלה מסעיף א. התור כתוב משמאל (ראש התור) לימין (סוף התור).
פתרון:
| התור (אחרי האיטרציה) | U | color[1] | color[2] | color[3] | color[4] | color[5] | color[6] | color[7] | parent[1] | parent[2] | parent[3] | parent[4] | parent[5] | parent[6] | parent[7] |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| [1] (אתחול) | 0 | ||||||||||||||
| [2, 3] | 1 | 0 | 1 | 1 | 1 | 1 | |||||||||
| [3, 4, 5] | 2 | 0 | 1 | 1 | 0 | 0 | 1 | 1 | 2 | 2 | |||||
| [4, 5, 6] | 3 | 0 | 1 | 1 | 0 | 0 | 0 | 1 | 1 | 2 | 2 | 3 | |||
| [5, 6, 7] | 4 | 0 | 1 | 1 | 0 | 0 | 0 | 1 | 1 | 1 | 2 | 2 | 3 | 4 | |
| [6, 7] | 5 | 0 | 1 | 1 | 0 | 0 | 0 | 1 | 1 | 1 | 2 | 2 | 3 | 4 |
בשורה האחרונה U = 5 , ובזמן הבדיקה של השכן 6 נמצא ש־ color[6] = color[5] = 0 , ולכן האלגוריתם עוצר (בתור נשארו 6 ו־ 7).
בדיקה: מעקב אחרי TwoColors בדיוק כמתואר נותן את אותה טבלה.
טעות נפוצה: לעצור כבר באיטרציה 3 , כשהצומת 5 נבדק כשכן של 3 . הצבעים שלהם שונים (5 בצבע 0 , 3 בצבע 1), ולכן אין שם התנגשות.
סעיף ג
6 , 5 , 2 , 1
מה נדרש: הרשימה שמוחזרת כשנמצאים שני שכנים באותו צבע.
רעיון: הרשימה מתחילה ב־ M = 6 , ואחריו U = 5 , ומשם עולים במערך parent עד צומת שאין לו הורה.
פתרון: 6 , 5 ⇐ parent[5] = 2 ⇐ parent[2] = 1 ⇐ parent[1] ריק (עצירה).
הרשימה: 6 , 5 , 2 , 1 .
הערה: מכאן אפשר לראות שהגרף אינו דו־צדדי. נמשיך מן הצומת 6 במערך parent : parent[6] = 3 , parent[3] = 1 . יחד עם הקשת 5–6 מתקבל המעגל 1–2–5–6–3–1 , שיש בו 5 קשתות – מעגל באורך אי־זוגי.
בדיקה: התוצאה: [6, 5, 2, 1].
שאלה 6 · האלגוריתם של דייקסטרה בגרף מכוון (25 נקודות)
לפניכם הגרף המכוון הממושקל G :

כתבו אלגוריתם המוצא בגרף מכוון ממושקל (אי־שלילי) כלשהו, שבו n קודקודים מ־ v₀ עד vₙ₋₁ , וקודקוד בגרף – vⱼ , את המסלולים הקלים ביותר מקודקוד vⱼ אל שאר הקודקודים שבגרף. בנוסף, לכל קודקוד v האלגוריתם יחזיר את מספר הקשתות שבמסלול הקל ביותר שנמצא מ־ vⱼ אל v .
מהי סיבוכיות זמן הריצה של האלגוריתם? נמקו את תשובתכם.
בעבור גרף G הנתון, מצאו בעזרת האלגוריתם שכתבתם את המסלול הקל ביותר מקודקוד A לכל אחד מן הקודקודים, וסרטטו טבלת מעקב כמפורט:
המעקב יכלול בכל איטרציה את קבוצת הקודקודים הקבועים (שכבר ביקרנו בהם) – P ואת קבוצת הקודקודים הזמניים (שבהם עדיין לא ביקרנו) – T . נוסף על כך, בעבור כל קודקוד יצוין אורך המסלול עד אליו וזהות הקודקוד הקודם לו (ה"הורה" שלו).
בעבור גרף G הנתון, סרטטו את עץ המסלולים הקלים ביותר (מקודקוד A).
◂ פתרון מלא לשאלה 6
סעיף א(1)
דייקסטרה מ־vⱼ עם מערך נוסף e[]: אתחול d[vⱼ]=0, e[vⱼ]=0, שאר d=∞; בכל איטרציה מעבירים מ־T ל־P את u בעל d מינימלי, ולכל קשת יוצאת (u, v) עם v ∈ T: אם d[u]+w(u,v) < d[v] אז d[v] ← d[u]+w(u,v), π[v] ← u, e[v] ← e[u]+1.
מה נדרש: מסלולים קלים ביותר ממקור אחד vⱼ לכל הקודקודים בגרף מכוון עם משקלים אי־שליליים, ולכל קודקוד – כמה קשתות יש במסלול שנמצא.
רעיון – האלגוריתם של דייקסטרה: d[v] – אורך המסלול הקל ביותר שנמצא עד כה מ־ vⱼ אל v ; π[v] – הקודקוד שלפני v במסלול; P – הקודקודים הקבועים, T – הזמניים. בכל איטרציה מעבירים ל־P את הקודקוד מ־T בעל d הקטן ביותר (בלי משקלים שליליים אי אפשר להגיע אליו בדרך קלה יותר), ומרפים את הקשתות היוצאות ממנו. את מספר הקשתות e[v] מעדכנים יחד עם π: אם המסלול הטוב ביותר אל v עובר עכשיו דרך u , אז יש בו קשת אחת יותר מבמסלול אל u .
פתרון:
DijkstraEdges(G, w, vⱼ):
לכל קודקוד v ב־V: d[v] ← ∞ ; π[v] ← NIL ; e[v] ← 0
d[vⱼ] ← 0
P ← ∅ ; T ← V
כל עוד T ≠ ∅:
u ← הקודקוד ב־T שעבורו d[u] מינימלי
T ← T − {u} ; P ← P ∪ {u}
לכל קשת יוצאת (u, v) כך ש־ v ∈ T:
אם d[u] + w(u, v) < d[v]:
d[v] ← d[u] + w(u, v)
π[v] ← u
e[v] ← e[u] + 1
החזר את d , π ו־ eבסיום d[v] הוא משקל המסלול הקל ביותר, המסלול עצמו מתקבל בהליכה לאחור v, π[v], π[π[v]], … עד vⱼ , ו־ e[v] הוא מספר הקשתות בו. אם d[v] = ∞ – אין מסלול מ־ vⱼ אל v .
(בגרף מכוון עוברים רק על הקשתות היוצאות מ־ u ; קשת הנכנסת ל־ u אינה מאפשרת לצאת ממנו.)
בדיקה: המעקב על G מ־A מופיע בסעיף ב(1). בסוף האלגוריתם: e[B] = 1, e[D] = 1, e[C] = 2, e[G] = 2, e[F] = 3, e[E] = 4, e[H] = 5 – בדיוק מספר הקשתות במסלולים שבטבלה שם. שימו לב: אל H המסלול הקל ביותר (18) עובר ב־5 קשתות, אף שיש מסלול A → B → E → H עם 3 קשתות בלבד (משקלו 20).
סעיף א(2)
O(n²) (מימוש במערכים). עם ערימת מינימום: O((n + |E|)·log n).
מה נדרש: זמן הריצה של האלגוריתם מסעיף א(1) כפונקציה של n (ומספר הקשתות |E|).
פתרון (מימוש פשוט – d ו־π במערכים, הגרף ברשימות שכנויות):
לכן זמן הריצה הוא O(n²).
(הערה: אם שומרים את T בערימת מינימום, זמן הריצה O((n + |E|)·log n) – טוב יותר בגרף דליל.)
סעיף ב(1)
d/π: A 0/– , B 4/A , D 9/A , C 11/D , F 14/C , E 15/F , G 16/D , H 18/E. סדר הקביעה: A, B, D, C, F, E, G, H.
מה נדרש: להריץ את דייקסטרה מ־A על G ולהראות בכל איטרציה את P, T ואת d/π של כל קודקוד.
רעיון: בכל איטרציה בוחרים מ־T את הקודקוד בעל d מינימלי (בגרף הזה לא היה שוויון בשום שלב), מעבירים אותו ל־P ומרפים את הקשתות היוצאות ממנו אל קודקודים שב־T.
פתרון – טבלת המעקב (בכל תא: d/π; מודגש – הקודקוד שנקבע באיטרציה):
| איטרציה | הקודקוד שנבחר | P (קבועים) | T (זמניים) | A | B | C | D | E | F | G | H |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 (אתחול) | – | { } | {A, B, C, D, E, F, G, H} | 0/– | ∞/– | ∞/– | ∞/– | ∞/– | ∞/– | ∞/– | ∞/– |
| 1 | A | {A} | {B, C, D, E, F, G, H} | 0/– | 4/A | 14/A | 9/A | ∞/– | ∞/– | ∞/– | ∞/– |
| 2 | B | {A, B} | {C, D, E, F, G, H} | 0/– | 4/A | 12/B | 9/A | 17/B | ∞/– | ∞/– | ∞/– |
| 3 | D | {A, B, D} | {C, E, F, G, H} | 0/– | 4/A | 11/D | 9/A | 17/B | ∞/– | 16/D | ∞/– |
| 4 | C | {A, B, D, C} | {E, F, G, H} | 0/– | 4/A | 11/D | 9/A | 17/B | 14/C | 16/D | ∞/– |
| 5 | F | {A, B, D, C, F} | {E, G, H} | 0/– | 4/A | 11/D | 9/A | 15/F | 14/C | 16/D | 22/F |
| 6 | E | {A, B, D, C, F, E} | {G, H} | 0/– | 4/A | 11/D | 9/A | 15/F | 14/C | 16/D | 18/E |
| 7 | G | {A, B, D, C, F, E, G} | {H} | 0/– | 4/A | 11/D | 9/A | 15/F | 14/C | 16/D | 18/E |
| 8 | H | {A, B, D, C, F, E, G, H} | { } | 0/– | 4/A | 11/D | 9/A | 15/F | 14/C | 16/D | 18/E |
פירוט ההרפיות:
המסלולים הקלים ביותר מ־A:
| קודקוד | אורך | מסלול |
|---|---|---|
| B | 4 | A → B |
| D | 9 | A → D |
| C | 11 | A → D → C |
| F | 14 | A → D → C → F |
| E | 15 | A → D → C → F → E |
| G | 16 | A → D → G |
| H | 18 | A → D → C → F → E → H |
בדיקה: הרפיה חוזרת של כל הקשתות (בלמן־פורד) נותנת אותם מרחקים, ולכל קודקוד מסלול קל ביותר יחיד.
טעות נפוצה: להתייחס לגרף כלא מכוון – למשל להשתמש בקשת E → B כאילו אפשר לעבור בה מ־B אל E במשקל 2. מ־B אל E יש רק את הקשת B → E במשקל 13.
סעיף ב(2)
קשתות העץ (π[v] → v): A→B, A→D, D→C, D→G, C→F, F→E, E→H.
מה נדרש: לשרטט את העץ שנוצר מקשתות ההורה π.
רעיון: לכל קודקוד v ≠ A מוסיפים את הקשת π[v] → v . מתקבלות n − 1 = 7 קשתות היוצרות עץ ששורשו A , ומכל קודקוד יש בו מסלול מכוון יחיד מ־A.
פתרון: לפי השורה האחרונה בטבלה מסעיף ב(1): π[B] = A, π[D] = A, π[C] = D, π[G] = D, π[F] = C, π[E] = F, π[H] = E.

ובצורת עץ:
A
├── B (4)
└── D (9)
├── C (11)
│ └── F (14)
│ └── E (15)
│ └── H (18)
└── G (16)בדיקה: סכום המשקלים על המסלול מהשורש לכל קודקוד בעץ שווה ל־ d שלו (למשל A → D → C → F → E → H: 9 + 2 + 3 + 1 + 3 = 18).
פרק שני (50 נקודות) – מודלים חישוביים
יש לענות על שתי שאלות במסלול שלמדתם (לכל שאלה – 25 נקודות).
שאלה 7 · רגולריות של שפות ואוטומט סופי דטרמיניסטי (25 נקודות)
לפניכם שני סעיפים א–ב, שאין קשר ביניהם. ענו על שני הסעיפים.
לפניכם שלוש שפות L₁ – L₃ מעל הא"ב {a, b} (בכל השפות n ו־ m הם מספרים שלמים, n, m ≥ 0):
L₁ = {aⁿ b aᵐ | m < 30 , n ≥ m}
L₂ = {aⁿ b aᵐ | n ≥ m > 30}
L₃ = {aⁿ b aᵐ | 0 < n · m < 100}
בנוגע לכל אחת מן השפות L₁ – L₃ , כתבו אם היא רגולרית או לא. נמקו את תשובתכם (די בהסבר מילולי; אין צורך באוטומט).
לפניכם שלוש שפות L₄ – L₆ מעל הא"ב {a, b} :
L₄ = L₁ ∪ L₂
L₅ = L₂ ∩ R(L₂)
L₆ = L₂ ∩ L₃
בנוגע לכל אחת מן השפות L₄ – L₆ , כתבו אם היא רגולרית או לא. נמקו את תשובתכם (די בהסבר מילולי; אין צורך באוטומט).
לפניכם השפה L מעל הא"ב {a, b, c} :
> L = {aⁿ bᵐ cᵏ w | n, m, k > 0}
>
> w היא מילה מעל הא"ב {a, b} המאופיינת באופן שלהלן:
> - אם n + 2m מתחלק ב־ 3 בלי שארית, w היא מילה שמתחילה באות a .
> - אם n + 2m מתחלק ב־ 3 עם שארית 1 , w היא מילה שאורכה 2 לכל היותר (גם המילה הריקה מתאימה).
> - אם n + 2m מתחלק ב־ 3 עם שארית 2 , w היא מילה ריקה.
דוגמה למילה השייכת לשפה L : aa b cc ba (aⁿ = aa , bᵐ = b , cᵏ = cc , w = ba)
הסבר: n = 2 , m = 1 . n + 2m = 4 מתחלק ב־ 3 עם שארית 1 , ו־ w היא מילה שאורכה 2 .
בנו אוטומט סופי דטרמיניסטי שאינו מלא המקבל את השפה L .
◂ פתרון מלא לשאלה 7
סעיף א(1)
L₁ – רגולרית; L₂ – לא רגולרית; L₃ – רגולרית (שפה סופית).
מה נדרש: לקבוע לכל אחת מהשפות L₁–L₃ אם היא רגולרית, ולנמק במילים.
רעיון: בכל שפה צריך להשוות את n (מספר ה־a לפני ה־b) ל־m (מספר ה־a אחריו). השאלה היא אם ההשוואה דורשת לזכור מספר לא חסום.
פתרון:
בדיקה: ב־L₃ כל המילים עם n, m < 100; ב־L₁ אין מילה עם m ≥ 30; ב־L₂ הרישות aⁿ b (n > 30) נבדלות זו מזו.
טעות נפוצה: לחשוב ש־L₁ אינה רגולרית "כי n אינו חסום". n לא חסום, אבל כדי להשוות אותו ל־m < 30 מספיק לדעת אם n קטן מ־30 ואם כן – מה ערכו.
סעיף א(2)
L₄ – לא רגולרית; L₅ = {aⁿ b aⁿ | n > 30} – לא רגולרית; L₆ = ∅ – רגולרית.
מה נדרש: לקבוע רגולריות של L₄, L₅, L₆ (R(L) = השפה ההפוכה: כל מילה נכתבת מהסוף להתחלה).
פתרון:
בדיקה: L₄ = {n ≥ m , m ≠ 30} , L₄ ∩ {m > 30} = L₂ , L₅ = {n = m > 30} , ו־L₂ ∩ L₃ ריקה.
טעות נפוצה: לומר ש־L₄ רגולרית "כי L₁ רגולרית" – איחוד של שפה רגולרית עם שפה לא רגולרית יכול לצאת לא רגולרי; צריך לבדוק את השפה שמתקבלת.
סעיף ב
13 מצבים: S (התחלה), A0–A2, B0–B2, C0–C2, Y, L1, L2; מקבלים: C1, C2, Y, L1, L2 (טבלת המעברים בפתרון).
מה נדרש: אוטומט סופי דטרמיניסטי (לא חייב להיות מלא) ל־L.
רעיון: המילה מתפרקת באופן יחיד: רצף a (n), רצף b (m), רצף c (k), ואחריו w – שאין בה c. צריך לזכור רק את r = (n + 2m) mod 3 (שלוש אפשרויות) ואת השלב: כל a מוסיף 1 לסכום, וכל b מוסיף 2. k לא משפיע – רק צריך להיות חיובי.
מצבים מקבלים: C1 ו־C2 (w ריקה מותרת בשארית 1 ו־2), Y, L1, L2. C0 אינו מקבל – בשארית 0 המילה w חייבת להתחיל ב־a ולכן אינה ריקה.
פתרון: מצב התחלה: S. מצבים מקבלים: C1, C2, Y, L1, L2.
| מצב | a | b | c |
|---|---|---|---|
| → S | A1 | – | – |
| A0 | A1 | B2 | – |
| A1 | A2 | B0 | – |
| A2 | A0 | B1 | – |
| B0 | – | B2 | C0 |
| B1 | – | B0 | C1 |
| B2 | – | B1 | C2 |
| C0 | Y | – | C0 |
| (C1) מקבל | L1 | L1 | C1 |
| (C2) מקבל | – | – | C2 |
| (Y) מקבל | Y | Y | – |
| (L1) מקבל | L2 | L2 | – |
| (L2) מקבל | – | – | – |
("–" = אין מעבר, המילה נדחית.)
בדיקה:
(abc: n + 2m = 3 , שארית 0, w ריקה – נדחית; abbc: 1 + 4 = 5 , שארית 2 – מתקבלת; aabbcab: 2 + 4 = 6 , שארית 0, w = ab מתחילה ב־a – מתקבלת.)
שאלה 8 · אוטומט מחסנית דטרמיניסטי – השלמת מעברים (25 נקודות)
נתונה השפה L מעל הא"ב {a, b, c} :
L = {aⁿ bᵐ cᵏ |
}
דוגמה למילה בשפה L בעבור n זוגי: aa bbbbb ccc (n=2 , m=5 , k=3) .
הסבר: n%2 = 0 , ומתקיים 5 = 2 + 3 וגם n, k > 0 .
דוגמה למילה בשפה L בעבור n אי־זוגי: aaa b cccc (n=3 , m=1 , k=4) .
הסבר: n%2 = 1 , ומתקיים 4 = 3 + 1 .
לפניכם חמש מילים. בעבור כל אחת מהן, ציינו אם המילה שייכת לשפה L . נמקו את תשובתכם.
abcc, aabb, aaaccc, aabbbbcc, aaabcc .
כתבו את המילה הקצרה ביותר בשפה L בעבור n זוגי, ואת המילה הקצרה ביותר בשפה L בעבור n אי־זוגי.
לפניכם אוטומט מחסנית דטרמיניסטי המקבל את השפה L . באוטומט קיימים כל המצבים, המעברים וסימני הקלט הנדרשים. אולם מלבד המעברים בין q₀ ל־ q₁ , חסרות במעברים האות שבראש המחסנית והפעולה על המחסנית (מעברים אלה ממוספרים מ־ 1 עד 17). נוסף על כך, המצבים המקבלים באוטומט לא סומנו. סימני המחסנית שבהם משתמש האוטומט הם S , A , B .
בעבור כל מעבר ממוספר, ציינו את מספרו והשלימו את הסימון החסר: האות שבראש המחסנית והפעולה שעל המחסנית (דחוף/push או שלוף/pop או ללא שינוי). נוסף על כך, כתבו מה הם המצבים המקבלים באוטומט (אין צורך להעתיק את האוטומט למחברתכם).

◂ פתרון מלא לשאלה 8
סעיף א
abcc – שייכת; aabb – לא שייכת; aaaccc – שייכת; aabbbbcc – שייכת; aaabcc – לא שייכת.
מה נדרש: לבדוק חמש מילים מול הגדרת L.
רעיון: כל המילים הן מהצורה aⁿ bᵐ cᵏ. מחשבים n, m, k, בודקים את זוגיות n ומפעילים את התנאי המתאים.
פתרון:
| מילה | n | m | k | n זוגי? | בדיקה | שייכת? |
|---|---|---|---|---|---|---|
| abcc | 1 | 1 | 2 | לא | k = n + m : 2 = 1 + 1 ✓ | כן |
| aabb | 2 | 2 | 0 | כן | אמנם 2 = 2 + 0 , אבל k = 0 – התנאי k > 0 לא מתקיים | לא |
| aaaccc | 3 | 0 | 3 | לא | 3 = 3 + 0 ✓ (m = 0 מותר) | כן |
| aabbbbcc | 2 | 4 | 2 | כן | m = n + k : 4 = 2 + 2 ✓ , n, k > 0 ✓ | כן |
| aaabcc | 3 | 1 | 2 | לא | 2 ≠ 3 + 1 = 4 ✗ | לא |
בדיקה: לפי הגדרת השפה: abcc ✓, aabb ✗, aaaccc ✓, aabbbbcc ✓, aaabcc ✗ – וכך גם האוטומט מסעיף ג.
סעיף ב
n זוגי: aabbbc . n אי־זוגי: ac .
מה נדרש: המילה הקצרה ביותר בכל אחד מהמקרים.
פתרון:
בדיקה: בין המילים עד אורך 8: המילה הקצרה ביותר עם n זוגי – aabbbc בלבד; עם n אי־זוגי – ac בלבד.
סעיף ג
(1) b,A/שלוף (2) b,A/שלוף (3) b,S/ללא שינוי (4) b,S/ללא שינוי (5) b,S/דחוף B (6) b,B/דחוף B (7) c,S/שלוף (8) c,B/שלוף (9) c,B/שלוף (10) c,S/שלוף (11) b,A/דחוף B ; S/דחוף B (12) b,B/דחוף B (13) c,B/שלוף (14) c,A/שלוף (15) c,S/שלוף (16) c,B/שלוף ; A/שלוף (17) c,S/שלוף. מצבים מקבלים: q₆ , q₁₀ .
מה נדרש: להשלים לכל מעבר ממוספר את ראש המחסנית והפעולה, ולציין את המצבים המקבלים.
רעיון: המעברים בין q₀ ל־q₁ נתונים: ה־a הראשון דוחף S , וכל a נוסף דוחף A. אחרי n אותיות a המחסנית היא S A…A (ראש המחסנית מימין) – בדיוק n סימנים – והאוטומט ב־q₀ אם n זוגי וב־q₁ אם n אי־זוגי. (כש־n = 1 ראש המחסנית הוא S ; כש־n ≥ 2 הוא A.)
ענף n זוגי (q₀ → q₂ … q₇ , q₆): צריך m = n + k , כלומר n אותיות b ראשונות "מתאימות" ל־a , ושאר ה־b (יש k כאלה, k ≥ 1) מתאימות ל־c.
ענף n אי־זוגי (q₁ → q₈ , q₉ , q₁₀): צריך k = n + m , כלומר כל c "מוחק" a או b אחד.
פתרון: (ראש המחסנית / פעולה)
| מעבר | מ־ → אל | אות | ראש המחסנית / פעולה |
|---|---|---|---|
| (1) | q₀ → q₂ | b | A / שלוף |
| (2) | q₂ → q₂ | b | A / שלוף |
| (3) | q₂ → q₃ | b | S / ללא שינוי |
| (4) | q₃ → q₄ | b | S / ללא שינוי |
| (5) | q₄ → q₅ | b | S / דחוף B |
| (6) | q₅ → q₅ | b | B / דחוף B |
| (7) | q₄ → q₆ | c | S / שלוף |
| (8) | q₅ → q₇ | c | B / שלוף |
| (9) | q₇ → q₇ | c | B / שלוף |
| (10) | q₇ → q₆ | c | S / שלוף |
| (11) | q₁ → q₈ | b | A / דחוף B ; S / דחוף B |
| (12) | q₈ → q₈ | b | B / דחוף B |
| (13) | q₈ → q₉ | c | B / שלוף |
| (14) | q₁ → q₉ | c | A / שלוף |
| (15) | q₁ → q₁₀ | c | S / שלוף |
| (16) | q₉ → q₉ | c | B / שלוף ; A / שלוף |
| (17) | q₉ → q₁₀ | c | S / שלוף |
מצבים מקבלים: q₆ , q₁₀ .
האוטומט דטרמיניסטי: מכל מצב, לכל זוג (אות קלט, ראש מחסנית) יש לכל היותר מעבר אחד.
בדיקה:
הערה: ייתכנו השלמות נוספות (למשל בסימני מחסנית אחרים); השלמה נכונה אם האוטומט נשאר דטרמיניסטי ומקבל בדיוק את L.
טעות נפוצה: לסמן את q₃ כמקבל (אז aabb , שבה k = 0 , הייתה מתקבלת), או לרשום במעבר 11 רק A – ואז abbccc ודומותיה (n = 1) נדחות.
שאלה 9 · מכונת טיורינג – השוואה בין שני מספרים אונריים (25 נקודות)
לפניכם מכונת טיורינג:

על כל מעבר רשום: הסימן הנקרא / הסימן הנכתב, כיוון התזוזה. q₀ הוא המצב ההתחלתי, ו־ q₉ הוא מצב העצירה.
המכונה מקבלת בתחילת הסרט שני מספרים אונריים שביניהם הסימן # . בתחילת הפעולה הראש נמצא על התא הראשון שמימין ל־ ⊢ .
עקבו אחר המכונה עבור הסרט שלהלן:
| ⊢ | 1 | 1 | # | 1 | Δ | Δ | . . . |
|---|
כתבו מה יתקבל על הסרט בסיום הפעולה.
כתבו מה יתקבל על הסרט שלהלן בסיום הפעולה:
| ⊢ | 1 | 1 | # | 1 | 1 | Δ | Δ | . . . |
|---|
הערה: בסעיף זה אין צורך להראות מעקב.
מה המכונה מבצעת עבור שני מספרים אונריים הגדולים מאפס?
לפניכם שלושה מקרים, 1–3. בעבור כל מקרה ציינו אם המכונה פועלת נכון או לא נכון.
אם המכונה פועלת נכון, הציגו באמצעות דוגמה את סרט הקלט ואת סרט הפלט. אם המכונה פועלת לא נכון,
הסבירו מדוע.
◂ פתרון מלא לשאלה 9
סעיף א
המכונה עוצרת במצב q9 והסרט: ⊢ Δ Δ > X Δ Δ … (בתא של # נכתב הסימן >).
מה נדרש: להריץ את המכונה על ⊢11#1 ולכתוב את הסרט בסוף.
קריאת הסרטוט: התווית "x / y, כיוון" על חץ פירושה: קוראים x, כותבים y ומזיזים את הראש לכיוון הנתון. טבלת המעברים של המכונה:
| מצב | קורא | כותב | תזוזה | מצב חדש |
|---|---|---|---|---|
| q0 | 1 | Δ | ימין | q1 |
| q0 | # | # | ימין | q5 |
| q1 | 1 | 1 | ימין | q1 |
| q1 | # | # | ימין | q2 |
| q2 | X | X | ימין | q2 |
| q2 | 1 | X | שמאל | q3 |
| q2 | Δ | Δ | שמאל | q6 |
| q3 | X | X | שמאל | q3 |
| q3 | # | # | שמאל | q4 |
| q4 | 1 | 1 | שמאל | q4 |
| q4 | Δ | Δ | ימין | q0 |
| q5 | X | X | ימין | q5 |
| q5 | 1 | 1 | שמאל | q7 |
| q5 | Δ | Δ | שמאל | q8 |
| q6 | X | X | שמאל | q6 |
| q6 | # | > | שמאל | q9 |
| q7 | X | X | שמאל | q7 |
| q7 | # | < | שמאל | q9 |
| q8 | X | X | שמאל | q8 |
| q8 | # | = | שמאל | q9 |
רעיון: בכל "סיבוב" המכונה מוחקת 1 אחד מתחילת המספר השמאלי (q0) ומסמנת ב־ X את ה־ 1 הראשון שעוד לא סומן במספר הימני (q2), ואז חוזרת שמאלה (q3, q4). כשאחד המספרים נגמר היא כותבת בתא של # סימן השוואה.
פתרון – מעקב (התאים ממוספרים מ־ 0 = ⊢):
| צעד | מצב | תא (מיקום הראש) | נקרא | נכתב | תזוזה | מצב חדש | הסרט אחרי הצעד |
|---|---|---|---|---|---|---|---|
| 1 | q0 | 1 | 1 | Δ | ימין | q1 | ⊢ Δ 1 # 1 Δ |
| 2 | q1 | 2 | 1 | 1 | ימין | q1 | ⊢ Δ 1 # 1 Δ |
| 3 | q1 | 3 | # | # | ימין | q2 | ⊢ Δ 1 # 1 Δ |
| 4 | q2 | 4 | 1 | X | שמאל | q3 | ⊢ Δ 1 # X Δ |
| 5 | q3 | 3 | # | # | שמאל | q4 | ⊢ Δ 1 # X Δ |
| 6 | q4 | 2 | 1 | 1 | שמאל | q4 | ⊢ Δ 1 # X Δ |
| 7 | q4 | 1 | Δ | Δ | ימין | q0 | ⊢ Δ 1 # X Δ |
| 8 | q0 | 2 | 1 | Δ | ימין | q1 | ⊢ Δ Δ # X Δ |
| 9 | q1 | 3 | # | # | ימין | q2 | ⊢ Δ Δ # X Δ |
| 10 | q2 | 4 | X | X | ימין | q2 | ⊢ Δ Δ # X Δ |
| 11 | q2 | 5 | Δ | Δ | שמאל | q6 | ⊢ Δ Δ # X Δ |
| 12 | q6 | 4 | X | X | שמאל | q6 | ⊢ Δ Δ # X Δ |
| 13 | q6 | 3 | # | > | שמאל | q9 | ⊢ Δ Δ > X Δ |
התוצאה: המכונה עצרה במצב q9, ועל הסרט: ⊢ Δ Δ > X Δ Δ …
בדיקה: המעקב נבנה צעד אחר צעד לפי טבלת המעברים.
סעיף ב
⊢ Δ Δ = X X Δ … (המכונה עוצרת ב־ q9).
מה נדרש: הסרט הסופי עבור 11#11 (בלי מעקב).
פתרון (לפי הרעיון מסעיף א):
הסרט בסיום: ⊢ Δ Δ = X X Δ … – שני המספרים שווים.
בדיקה: עבור 11#11 המכונה עוצרת במצב q9 והסרט הוא ⊢ΔΔ=XX.
סעיף ג
המכונה משווה בין שני המספרים x ו־ y וכותבת בתא של # את הסימן > אם x > y, את הסימן < אם x < y ואת הסימן = אם x = y.
מה נדרש: לתאר מה המכונה עושה עבור קלט 1ˣ#1ʸ, כאשר x, y > 0.
רעיון: בכל סיבוב (q0 → q1 → q2 → q3 → q4 → q0) נמחק 1 אחד מהמספר השמאלי ו־ 1 אחד מהמספר הימני מסומן ב־ X – כלומר "מתאימים" זוג אחדים. הסיבובים נמשכים עד שאחד המספרים נגמר:
פתרון: המכונה משווה בין x ל־ y וכותבת במקום הסימן # את תוצאת ההשוואה: > אם x > y, < אם x < y, = אם x = y. בנוסף, אחדים של המספר השמאלי נמחקו (min(x, y) אחדים, ועוד אחד כש־ x > y), ו־ min(x, y) אחדים של המספר הימני הפכו ל־ X.
בדיקה: לכל הזוגות הקטנים של x, y נכתב בתא של # הסימן הנכון והמכונה עוצרת ב־ q9 (למשל 11#1 → >, 11#11 → =, 1#11 → <).
סעיף ד
מה נדרש: לבדוק את שלושת המקרים שבהם לפחות אחד המספרים הוא 0 (מספר אונרי 0 = אין אחדים).
פתרון:
מקרה 1 – x = 0, y > 0: המכונה פועלת נכון.
q0 קורא מיד # ⇒ q5 עובר ימינה וקורא 1 (אין X, כי לא היו סיבובים) ⇒ q7 זז שמאלה אל # וכותב בו <. 0 < y ✓.
דוגמה: קלט ⊢ # 1 1 Δ … ⇒ פלט ⊢ < 1 1 Δ …
מקרה 2 – y = 0, x > 0: המכונה פועלת נכון.
q0 מוחק את ה־ 1 בתא 1, q1 עובר אל #, q2 קורא מיד Δ (אין אחדים במספר הימני) ⇒ q6 זז שמאלה אל # וכותב בו >. x > 0 ✓.
דוגמה: קלט ⊢ 1 1 # Δ … ⇒ פלט ⊢ Δ 1 > Δ …
מקרה 3 – x = 0, y = 0: המכונה פועלת נכון.
q0 קורא # ⇒ q5 קורא Δ ⇒ q8 זז שמאלה אל # וכותב בו =.
דוגמה: קלט ⊢ # Δ … ⇒ פלט ⊢ = Δ …
בכל שלושת המקרים המכונה מגיעה למצב העצירה q9 עם תוצאת ההשוואה הנכונה: הסימן נקבע רק לפי הבדיקה שמגלה שמספר נגמר (q0 קורא #, או q2 / q5 קוראים Δ), ובכל המקרים הראש חוזר משם אל התא של # בלי לצאת מגבולות הסרט.
בדיקה: x=0,y=2 → ⊢<11 (q9); x=2,y=0 → ⊢Δ1> (q9); x=y=0 → ⊢= (q9).
תכנות מונחה עצמים
יש לענות על שתי שאלות במסלול שלמדתם (לכל שאלה – 25 נקודות).
שאלה 10 · חברת שליחויות – ירושה, הכלה ופעולה פולימורפית (25 נקודות)
כדי לנהל את המשלוחים שלה בנתה חברת השליחויות "מדלת לדלת" את המחלקה Address — כתובת, שלה שלוש תכונות: עיר — city (מטיפוס מחרוזת), רחוב — street (מטיפוס מחרוזת), מספר בית — number (מטיפוס שלם).
נוסף על כך, לצורך ניהול המשלוחים נבנתה המחלקה Shipment — משלוח.
לחברה יש שלושה סוגי משלוחים: חבילות, מכתבים ומשטחים (משטח שעליו ארגזים). כדי לשמור את המידע שבכל סוג משלוח, בנתה החברה את המחלקות האלה: חבילה — Parcel , מכתב — Letter , משטח — Pallet .
התכונות של המחלקות הן בהתאם לדרישות המידע שלפניכם:
המידע ששומרים על חבילה — Parcel :
המידע ששומרים על מכתב — Letter :
המידע ששומרים על משטח — Pallet :
המידע ששומרים על משלוח — Shipment :
אותן דרישות המידע המפורטות בסוגי המשלוחים שלעיל ושצריכות להישמר במחלקה Shipment בהתאם לעקרונות תכנות מונחה עצמים.
נוסף על המחלקות האמורות, נבנתה המחלקה Depot — מחסן, השומרת עד 3,000 משלוחים — Shipment , במערך — arr בגודל 3,000 מטיפוס Shipment .
לפניכם תרשים חלקי של המחלקות:

שימו לב: ירושה מסומנת באמצעות החץ ◁——— (המשולש הריק בצד מחלקת־העל), והכלה מסומנת באמצעות הסימן ◆——— (המעוין המלא בצד המחלקה המכילה).
על הסעיפים שלפניכם יש לענות לפי עקרונות של תכנות מונחה עצמים.
העתיקו את התרשים למחברת והוסיפו את המחלקה Address לתרשים. כתבו את כותרת המחלקה משלוח — Shipment ואת התכונות שלה.
הניחו שבשאר המחלקות כתובות התכונות המתאימות, ושבכל המחלקות יש פעולות בונות ופעולות get ו־ set .
כתבו במחלקה Depot פעולה ששמה registeredLetters המקבלת עצם — adr מטיפוס Address , ומחזירה מערך מטיפוס Letter בגודל 60 , המכיל את כל המכתבים הרשומים הנשלחים לעיר שבכתובת adr .
שימו לב:
public boolean sameCity (Address other)הפעולה מחזירה true אם העיר בכתובת הנוכחית זהה לעיר בכתובת other , אחרת היא מחזירה false . אפשר להשתמש בפעולה בלי לממש אותה.
לפניכם כותרת הפעולה הפנימית — needsCare במחלקה Shipment :
public boolean needsCare()זימון הפעולה יחזיר true אם המשלוח (Shipment) הוא חבילה (Parcel) שבירה, או אם המשלוח הוא משטח (Pallet) שיש עליו לפחות ארגז אחד שמשקלו גדול מ־ 30 ק"ג, אחרת יוחזר false .
ממשו את הפעולה, והוסיפו פעולות למחלקות המתאימות כדי לבצע את הנדרש. ציינו בעבור כל פעולה באיזו מחלקה היא מוספת.
הערה: בסעיף זה אין להשתמש בפעולה instanceof ובפעולות של המחלקה Object .
◂ פתרון מלא לשאלה 10
סעיף א
Shipment מכילה את Address (◆ בצד Shipment). public class Shipment { private String code; private double weight; private Address address; } – שלוש המחלקות Parcel, Letter, Pallet יורשות ממנה.
מה נדרש – להוסיף את Address לתרשים ולכתוב את כותרת המחלקה Shipment עם התכונות שלה.
רעיון
פתרון
תרשים המחלקות המלא:

כותרת המחלקה ותכונותיה – :
public class Shipment {
private String code; // קוד המשלוח
private double weight; // משקל המשלוח בק"ג
private Address address; // כתובת היעד
}בדיקה – המחלקה (יחד עם Address) תקינה ב־Java וב־C#; בגרסה המלאה (סעיפים ב–ג) נוספות לה פעולה בונה ופעולות get.
טעות נפוצה: לכתוב ב־Shipment גם את fragile או registered – תכונות אלה שייכות רק לסוג משלוח אחד; או לשכוח שהכתובת היא עצם מטיפוס Address (ולא מחרוזת).
public class Shipment {
private String code; // קוד המשלוח
private double weight; // משקל המשלוח בק"ג
private Address address; // כתובת היעד
}סעיף ב
לולאה על arr; אם arr[i] instanceof Letter (C#: is Letter) – ממירים ל־Letter, ואם getRegistered() וגם getAddress().sameCity(adr) – מכניסים ל־result[k++]; מחזירים את המערך בגודל 60.
מה נדרש – לעבור על כל המשלוחים במחסן ולהחזיר מערך Letter בגודל 60 עם המכתבים הרשומים לעיר שבכתובת adr.
רעיון
arr[i] instanceof Letter (ב־C#: arr[i] is Letter), ורק אחר כך ממירים (Letter) arr[i] כדי לזמן את getRegistered.פתרון – :
public class Depot {
private Shipment[] arr;
public Depot(Shipment[] arr) {
this.arr = arr;
}
public Letter[] registeredLetters(Address adr) {
Letter[] result = new Letter[60];
int k = 0;
for (int i = 0; i < this.arr.length; i++) {
if (this.arr[i] instanceof Letter) {
Letter let = (Letter) this.arr[i];
if (let.getRegistered() && let.getAddress().sameCity(adr)) {
result[k] = let;
k++;
}
}
}
return result;
}
}(הפעולה הבונה של Depot נכתבה לצורך הבדיקה בלבד.)
בדיקה – מחסן עם 3,000 משלוחים (מכתבים רשומים ולא רשומים, חבילות ומשטחים, בשלוש ערים), ובו בדיוק 60 מכתבים רשומים לחיפה: הפעולה החזירה 60 מכתבים, כולם רשומים, כולם לחיפה ואף אחד מהם אינו null (Java ו־C#).
טעות נפוצה: לזמן arr[i].getRegistered() בלי בדיקת טיפוס והמרה – זו שגיאת הידור, כי ל־Shipment אין פעולה כזאת; או להמיר (Letter) arr[i] לפני הבדיקה – אם העצם הוא חבילה, תתקבל שגיאת ריצה.
public class Depot {
private Shipment[] arr;
public Depot(Shipment[] arr) {
this.arr = arr;
}
public Letter[] registeredLetters(Address adr) {
Letter[] result = new Letter[60];
int k = 0;
for (int i = 0; i < this.arr.length; i++) {
if (this.arr[i] instanceof Letter) {
Letter let = (Letter) this.arr[i];
if (let.getRegistered() && let.getAddress().sameCity(adr)) {
result[k] = let;
k++;
}
}
}
return result;
}
}סעיף ג
Shipment: needsCare() מחזירה false. Parcel דורסת: return fragile. Pallet דורסת: לולאה על boxWeights – true אם יש משקל > 30, אחרת false. ב־Letter אין צורך בפעולה.
מה נדרש – פעולה אחת needsCare שמחזירה תשובה שונה לפי סוג המשלוח, בלי instanceof (ב־C#: בלי is/as).
רעיון – פולימורפיזם (דריסה):
return false (מתאים למכתב, ולכן אין צורך לכתוב את הפעולה ב־Letter).s.needsCare() על הפניה מטיפוס Shipment, מופעלת הגרסה של הטיפוס בפועל של העצם.virtual ובמחלקות היורשות override.פתרון – Java (הפעולה ב־Shipment, ב־Parcel וב־Pallet):
public class Shipment {
private String code; // קוד המשלוח
private double weight; // משקל המשלוח בק"ג
private Address address; // כתובת היעד
public Shipment(String code, double weight, Address address) {
this.code = code;
this.weight = weight;
this.address = address;
}
public String getCode() { return code; }
public double getWeight() { return weight; }
public Address getAddress() { return address; }
public boolean needsCare() {
return false;
}
}
public class Parcel extends Shipment {
private boolean fragile;
public Parcel(String code, double weight, Address address, boolean fragile) {
super(code, weight, address);
this.fragile = fragile;
}
public boolean getFragile() { return fragile; }
public boolean needsCare() {
return this.fragile;
}
}
public class Pallet extends Shipment {
private double[] boxWeights;
public Pallet(String code, double weight, Address address, double[] boxWeights) {
super(code, weight, address);
this.boxWeights = boxWeights;
}
public double[] getBoxWeights() { return boxWeights; }
public boolean needsCare() {
for (int i = 0; i < this.boxWeights.length; i++) {
if (this.boxWeights[i] > 30) {
return true;
}
}
return false;
}
}הפעולות שנוספו: needsCare במחלקה Shipment (מחזירה false), דריסה של needsCare במחלקה Parcel ודריסה של needsCare במחלקה Pallet. במחלקה Letter לא נוספה פעולה.
בדיקה – חבילה שבירה → true; חבילה לא שבירה → false; מכתב → false; משטח עם ארגזים {12.5, 30} → false (30 אינו גדול מ־30); משטח עם {12.5, 30.5, 4} → true.
טעות נפוצה: להחזיר false מתוך הלולאה כבר בארגז הראשון שאינו כבד – צריך להחזיר false רק אחרי שבדקנו את כל הארגזים.
public class Shipment {
private String code; // קוד המשלוח
private double weight; // משקל המשלוח בק"ג
private Address address; // כתובת היעד
public Shipment(String code, double weight, Address address) {
this.code = code;
this.weight = weight;
this.address = address;
}
public String getCode() { return code; }
public double getWeight() { return weight; }
public Address getAddress() { return address; }
public boolean needsCare() {
return false;
}
}
public class Parcel extends Shipment {
private boolean fragile;
public Parcel(String code, double weight, Address address, boolean fragile) {
super(code, weight, address);
this.fragile = fragile;
}
public boolean getFragile() { return fragile; }
public boolean needsCare() {
return this.fragile;
}
}
public class Pallet extends Shipment {
private double[] boxWeights;
public Pallet(String code, double weight, Address address, double[] boxWeights) {
super(code, weight, address);
this.boxWeights = boxWeights;
}
public double[] getBoxWeights() { return boxWeights; }
public boolean needsCare() {
for (int i = 0; i < this.boxWeights.length; i++) {
if (this.boxWeights[i] > 30) {
return true;
}
}
return false;
}
}שאלה 11 · Card, Gift, Wish – תכונה סטטית ושרשור פעולות בונות (25 נקודות)
לשאלה זו שני נוסחים: בשפת Java ובשפת C# .
לפותרים בשפת Java
נתונות המחלקות Card , Gift , Wish :
public class Card {
public static int made = 0;
private int code;
public Card() {
made++;
code = made * 10;
}
public Card (int c) {
code = c;
}
public String toString() {
return code + " of " + made;
}
}
---------------------------------------------
public class Gift extends Card {
private String text;
public Gift() {
text = "Hi";
}
public Gift (String t) {
super (7);
text = t;
}
public String toString() {
return text + ": " + super.toString();
}
}
---------------------------------------------
public class Wish extends Gift {
private int stars;
public Wish (int s) {
super ("Wow");
stars = s;
}
public String toString() {
return super.toString() + " " + stars + "*";
}
}נתונה המחלקה Test :
public class Test
{
public static void main (String[] args)
{
Card[] arr = new Card[6];
arr[0] = … // (1)
arr[1] = … // (2)
arr[2] = … // (3)
arr[3] = … // (4)
arr[4] = … // (5)
arr[5] = … // (6)
for (int i = 0; i < arr.length; i++) {
System.out.println (arr[i]);
}
}
}השלימו את השורות (1)–(6) כך שהפלט יהיה:
Hi: 10 of 3
Wow: 7 of 3 3*
20 of 3
Bye: 7 of 3
Hi: 30 of 3
Wow: 7 of 3 5*הכותרת של המחלקה Wish השתנתה לכותרת:
public class Wish extends Cardהאם שינוי זה יגרום לשגיאה? אם כן – הסבירו את השגיאה, אם לא – רשמו מה יהיה הפלט של הפעולה main אחרי השינוי (השורות החסרות הן כפי שכתבתם בסעיף א).
הערה: אין שינויים נוספים במחלקה.
◂ פתרון מלא לשאלה 11
סעיף א
(1) new Gift() (2) new Wish(3) (3) new Card() (4) new Gift("Bye") (5) new Gift() (6) new Wish(5)
מה נדרש – לבחור לכל תא במערך עצם (ופעולה בונה) כך שתודפס בדיוק השורה הנתונה.
רעיון
made היא תכונה סטטית – אחת לכל העצמים. היא גדלה רק בפעולה הבונה Card(). ההדפסה מתבצעת אחרי שכל ששת העצמים נוצרו, ולכן בכל השורות מודפס הערך הסופי של made – 3. כלומר Card() רצה בדיוק 3 פעמים.code נקבעת ביצירת העצם: ב־Card() היא made·10 באותו רגע (10, 20, 30 לפי סדר היצירה), וב־Card(int c) היא c.Gift() קוראת במובלע ל־Card() (made++), ו־text = "Hi".Gift(String t) קוראת במפורש ל־Card(7) – בלי made++, ו־code = 7, text = t.Wish(int s) קוראת ל־Gift("Wow") ← Card(7) – גם היא בלי made++; text = "Wow", code = 7, stars = s.code of made; Gift מוסיפה לפניה text: ; Wish מוסיפה אחריה רווח, stars ו־*.| שורה | פלט נדרש | עצם | made אחרי היצירה | code |
|---|---|---|---|---|
| (1) | Hi: 10 of 3 | new Gift() | 1 | 10 |
| (2) | Wow: 7 of 3 3* | new Wish(3) | 1 | 7 |
| (3) | 20 of 3 | new Card() | 2 | 20 |
| (4) | Bye: 7 of 3 | new Gift("Bye") | 2 | 7 |
| (5) | Hi: 30 of 3 | new Gift() | 3 | 30 |
| (6) | Wow: 7 of 3 5* | new Wish(5) | 3 | 7 |
השורה 20 of 3 (בלי טקסט) היא עצם Card. היא לא יכולה להיות new Card(20): אז made היה גדל רק פעמיים (בשורות 1 ו־5), השורה (5) הייתה Hi: 20 וכל השורות היו מסתיימות ב־of 2. גם Wow: ... חייבת להיות Wish – new Gift("Wow") לא מדפיסה כוכביות.
פתרון – השורות החסרות (זהות ב־Java וב־C#):
arr[0] = new Gift(); // (1)
arr[1] = new Wish(3); // (2)
arr[2] = new Card(); // (3)
arr[3] = new Gift("Bye"); // (4)
arr[4] = new Gift(); // (5)
arr[5] = new Wish(5); // (6)התוכנית המלאה בלשונית Java/C#.
בדיקה – הפלט ב־Java וב־C# הוא בדיוק הפלט הנדרש:
Hi: 10 of 3
Wow: 7 of 3 3*
20 of 3
Bye: 7 of 3
Hi: 30 of 3
Wow: 7 of 3 5*מעבר על כל הבחירות האפשריות של פעולות בונות (עם הארגומנטים שמופיעים בפלט) מראה שזה הפתרון היחיד.
טעות נפוצה: לחשוב שבשורה (1) יודפס Hi: 10 of 1 – made סטטית, וההדפסה מתבצעת רק אחרי שכל העצמים נוצרו. טעות נוספת: לחשוב ש־Wish מגדילה את made – היא עוברת דרך Gift(String) ← Card(int).
public class Test
{
public static void main (String[] args)
{
Card[] arr = new Card[6];
arr[0] = new Gift(); // (1)
arr[1] = new Wish(3); // (2)
arr[2] = new Card(); // (3)
arr[3] = new Gift("Bye"); // (4)
arr[4] = new Gift(); // (5)
arr[5] = new Wish(5); // (6)
for (int i = 0; i < arr.length; i++) {
System.out.println (arr[i]);
}
}
}סעיף ב
כן, שגיאת הידור: הפעולה הבונה של Wish קוראת ל־super("Wow") (ב־C#: base("Wow")), ועכשיו מחלקת הבסיס היא Card, שאין לה פעולה בונה שמקבלת מחרוזת (יש רק Card() ו־Card(int)).
מה נדרש – לקבוע אם Wish שיורשת ישירות מ־Card גורמת לשגיאה, ולהסביר.
רעיון – אחרי השינוי מחלקת הבסיס של Wish היא Card. נבדוק כל מה שב־Wish פונה למחלקת הבסיס:
super ("Wow") (ב־C#: : base ("Wow")) – מחפשים ב־Card פעולה בונה שמקבלת String. ב־Card יש רק Card() ו־Card(int c), ומחרוזת לא מומרת ל־int. שגיאת הידור.super.toString() – קיימת ב־Card, ולכן היא לא הבעיה.Card[] עדיין תקינה, כי Wish עדיין יורשת מ־Card.פתרון – כן, השינוי גורם לשגיאת הידור בפעולה הבונה של Wish: הקריאה super("Wow") מחפשת פעולה בונה של Card עם פרמטר מסוג מחרוזת, ואין כזו (הפעולה Gift(String t) כבר אינה מחלקת הבסיס של Wish). התוכנית לא תעבור הידור ולכן לא תרוץ ולא יודפס דבר.
בדיקה – הידור של הגרסה ששונתה נכשל בשתי השפות:
Wish.java:4: error: incompatible types: String cannot be converted to interror CS1503: Argument 1: cannot convert from 'string' to 'int'טעות נפוצה: לחשוב שהבעיה היא ב־super.toString() או ב־Card[] arr, או לענות שאין שגיאה ולחשב פלט – הפעולה הבונה של Wish היא הבעיה.
שאלה 12 · חנות כלי נגינה – הכללה למחלקת־על חדשה ודריסה (25 נקודות)
חנות כלי הנגינה "צליל" מעוניינת למכור את מוצריה גם באופן מקוון (אונליין). בשלב הראשון החליט בעל החנות למכור באופן מקוון רק גיטרות ופסנתרים.
לשם כך נבנו המחלקות האלה:
– המחלקה Instrument , המייצגת כלי נגינה
– המחלקה Guitar , המייצגת גיטרה
– המחלקה Piano , המייצגת פסנתר
public class Instrument {
private int code; // קוד המוצר
private String brand; // שם היצרן
private int year; // שנת הייצור
private double price; // מחיר
}
public class Guitar extends Instrument {
private int strings; // מספר המיתרים
}
public class Piano extends Instrument {
private boolean electric; // האם הפסנתר חשמלי
}הניחו שיש פעולות get ו־ set בעבור התכונות בכל המחלקות.
המחלקה Order מייצגת הזמנה. למחלקה שלוש תכונות:
(1) כתבו את כותרת המחלקה Order ואת התכונות שלה.
סרטטו תרשים הייררכייה המתאר את הקשר בין המחלקות: Instrument , Guitar , Piano , Order .
יש לסמן ירושה באמצעות החץ ◁——— והכלה באמצעות הסימן ◆——— .
בשלב השני החליט בעל החנות למכור באופן מקוון גם תווים (חוברות תווים).
לפניכם המחלקה SheetMusic המייצגת חוברת תווים:
public class SheetMusic {
private int code; // קוד המוצר
private String title; // שם היצירה
private String composer; // שם המלחין
private double price; // מחיר
}הניחו שיש פעולות get ו־ set בעבור תכונות המחלקה.
כדי שהמערך items במחלקה Order יוכל להכיל גם חוברות תווים, נעשו הפעולות האלה:
– הוגדרה המחלקה Item – פריט.
– טיפוס המשתנה items במחלקה Order שוּנה ל־ Item .
– נכתבו במחלקות המתאימות (כולל במחלקה SheetMusic) יחסי הורשה חדשים בינן ובין המחלקה Item .
– חלק מן התכונות שהופיעו עד עכשיו בכמה מן המחלקות האחרות (כולל המחלקה SheetMusic) הועברו למחלקה Item , בהתאם לעקרונות תכנות מונחה עצמים (מעבר לתכונות אלו אין תכונות נוספות במחלקה Item).
(1) סרטטו תרשים הייררכייה מעודכן המתאר את הקשר בין המחלקות: Instrument , Guitar , Piano , SheetMusic , Item , Order .
יש לסמן ירושה באמצעות החץ ◁——— והכלה באמצעות הסימן ◆——— .
כִּתבו את כותרת המחלקה Item ותכונותיה.
כִּתבו מחדש את המחלקות שחל בהן שינוי בעקבות ההוספה של המחלקה Item .
שימו לב: יש להתייחס גם ליחסי הורשה חדשים וגם לשינוי שחל בתכונות המחלקה.
בשלב האחרון החליט בעל החנות להכריז על מבצע: ברכישה מקוונת כל הפריטים יימכרו בהנחה של 20% , ובעבור רכישת פסנתר יתווספו למחיר שאחרי ההנחה דמי הובלה של 150 שקלים.
נתונה הפעולה הפנימית במחלקה Item :
public double getFinalPrice()זימון הפעולה מחזיר את המחיר שהלקוח ישלם בעבור הפריט (גיטרה/פסנתר/חוברת תווים) – בהתאם לכל פריט.
ממשו את הפעולה במחלקה Item , והוסיפו פעולה/ות במחלקה/ות המתאימה/ות כדי לבצע את הנדרש. ציינו בעבור כל פעולה שהוספתם באיזו מחלקה היא נוספה.
אין להשתמש בפעולה instanceof ובפעולות המחלקה Object ואין לשנות את תכונות המחלקות.
◂ פתרון מלא לשאלה 12
סעיף א(1)
public class Order { private String customer; private Instrument[] items; private int num; }
מה נדרש – כותרת המחלקה Order ושלוש התכונות שלה.
רעיון – המערך צריך להכיל גם עצמי Guitar וגם עצמי Piano, ולכן טיפוס האיברים שלו הוא מחלקת־העל המשותפת – Instrument (הפניה מטיפוס מחלקת־על יכולה להפנות לעצם של מחלקה יורשת).
פתרון – :
public class Order {
private String customer; // שם הלקוח
private Instrument[] items; // הכלים בהזמנה – גיטרות ופסנתרים
private int num; // מספר הכלים הנוכחי בהזמנה
}בדיקה – המחלקה תקינה יחד עם המחלקות הנתונות, ב־Java וב־C#.
טעות נפוצה: להגדיר שני מערכים נפרדים (Guitar[] ו־Piano[]) – השאלה מבקשת מערך אחד המכיל את שני הסוגים.
public class Order {
private String customer; // שם הלקוח
private Instrument[] items; // הכלים בהזמנה – גיטרות ופסנתרים
private int num; // מספר הכלים הנוכחי בהזמנה
}סעיף א(2)
Guitar ◁— Instrument, Piano ◁— Instrument (ירושה); Order ◆— Instrument (הכלה).
מה נדרש – תרשים הייררכייה של Instrument , Guitar , Piano , Order.
רעיון
extends).פתרון

בדיקה – התרשים מתאים לקוד: Guitar extends Instrument, Piano extends Instrument, ובמחלקה Order התכונה Instrument[] items.
סעיף ב(1)
Instrument ו־SheetMusic יורשות מ־Item; Guitar ו־Piano יורשות מ־Instrument; Order ◆— Item.
מה נדרש – תרשים מעודכן אחרי הוספת Item ו־SheetMusic.
רעיון
פתרון

בדיקה – הקוד המעודכן (סעיף ב(3)) תקין, ומערך מטיפוס Item יכול לקבל עצמי Guitar , Piano ו־SheetMusic.
טעות נפוצה: לחבר את Guitar ו־Piano ישירות ל־Item ולמחוק את Instrument – אז התכונות brand ו־year היו צריכות להופיע פעמיים.
סעיף ב(2)
public class Item { private int code; private double price; }
מה נדרש – כותרת Item ותכונותיה.
רעיון – מעבירים ל־Item רק את התכונות שמשותפות לכל הפריטים: code ו־price מופיעות גם ב־Instrument וגם ב־SheetMusic. brand ו־year שייכות רק לכלי נגינה, ו־title ו־composer רק לחוברת תווים.
פתרון – :
public class Item {
private int code; // קוד המוצר
private double price; // מחיר
}בדיקה – המחלקה תקינה ב־Java וב־C# יחד עם שאר המחלקות המעודכנות.
public class Item {
private int code; // קוד המוצר
private double price; // מחיר
}סעיף ב(3)
Instrument extends Item {brand, year}; SheetMusic extends Item {title, composer}; Order: Item[] items. Guitar ו־Piano ללא שינוי.
מה נדרש – לכתוב מחדש את המחלקות שהשתנו.
רעיון
פתרון – :
public class Instrument extends Item {
private String brand; // שם היצרן
private int year; // שנת הייצור
}
public class SheetMusic extends Item {
private String title; // שם היצירה
private String composer; // שם המלחין
}
public class Order {
private String customer; // שם הלקוח
private Item[] items; // הפריטים בהזמנה
private int num; // מספר הפריטים הנוכחי בהזמנה
}בדיקה – כל המחלקות (כולל Guitar ו־Piano ללא שינוי) תקינות, והשמה של new Guitar(), new Piano() ו־new SheetMusic() למערך מטיפוס Item תקינה (Java ו־C#).
טעות נפוצה: לשכוח את Order – גם בה חל שינוי (טיפוס המערך).
public class Instrument extends Item {
private String brand; // שם היצרן
private int year; // שנת הייצור
}
public class SheetMusic extends Item {
private String title; // שם היצירה
private String composer; // שם המלחין
}
public class Order {
private String customer; // שם הלקוח
private Item[] items; // הפריטים בהזמנה
private int num; // מספר הפריטים הנוכחי בהזמנה
}סעיף ג
Item: getFinalPrice() { return price * 0.8; } Piano (דריסה): return super.getFinalPrice() + 150; (ב־C#: virtual/override ו־base.GetFinalPrice()).
מה נדרש – getFinalPrice ב־Item ודריסה במקום המתאים, בלי instanceof (ב־C#: בלי is/as).
רעיון
price * 0.8 (התכונה price פרטית ב־Item, ולכן רק ב־Item אפשר לגשת אליה ישירות).super.getFinalPrice() + 150 – משתמשים בחישוב של מחלקת־העל ומוסיפים עליו. אין צורך לכתוב את הפעולה ב־Instrument , ב־Guitar או ב־SheetMusic.virtual ב־Item ו־override ב־Piano, וזימון base.GetFinalPrice().פתרון – :
public class Item {
private int code; // קוד המוצר
private double price; // מחיר
public Item(int code, double price) {
this.code = code;
this.price = price;
}
public int getCode() { return code; }
public double getPrice() { return price; }
public double getFinalPrice() {
return this.price * 0.8;
}
}
public class Piano extends Instrument {
private boolean electric; // האם הפסנתר חשמלי
public Piano(int code, double price, String brand, int year, boolean electric) {
super(code, price, brand, year);
this.electric = electric;
}
public double getFinalPrice() {
return super.getFinalPrice() + 150;
}
}הפעולות שנוספו: getFinalPrice במחלקה Item, ודריסה של getFinalPrice במחלקה Piano.
בדיקה – גיטרה במחיר 1000 → 800; פסנתר במחיר 5000 → 4150; פסנתר חשמלי במחיר 2500 → 2150; תווים במחיר 60 → 48 (Java ו־C#).
טעות נפוצה: לכתוב ב־Piano price * 0.8 + 150 – price פרטית ב־Item ולכן זו שגיאת הידור; וגם אם הייתה נגישה, שכפול החישוב סותר את עקרון השימוש החוזר. הדרך הנכונה: super.getFinalPrice() + 150.
public class Item {
private int code; // קוד המוצר
private double price; // מחיר
public Item(int code, double price) {
this.code = code;
this.price = price;
}
public int getCode() { return code; }
public double getPrice() { return price; }
public double getFinalPrice() {
return this.price * 0.8;
}
}
public class Piano extends Instrument {
private boolean electric; // האם הפסנתר חשמלי
public Piano(int code, double price, String brand, int year, boolean electric) {
super(code, price, brand, year);
this.electric = electric;
}
public double getFinalPrice() {
return super.getFinalPrice() + 150;
}
}טבלת התשובות
| שאלה | סעיף | תשובה |
|---|---|---|
| 1 | א(1) | kkk(q, 3) מחזירה true (a = 3 + 5 + 2 = 10, b = 4 + 6 = 10); התור חוזר למצבו המקורי. |
| א(2) | מחזירה true אם סכום k האיברים הראשונים בתור (מן הראש) שווה לסכום שאר האיברים בתור, אחרת false; התור נשאר ללא שינוי. | |
| א(3) | O(n) – n סיבובי לולאה (סיבוב אחד של התור), O(1) בכל סיבוב. | |
| ב(1) | mystery(q, 5) מחזירה 3 (n = 5, 4 – false; n = 3 – true). | |
| ב(2) | מחזירה את המספר k של האיברים שבראש התור שסכומם שווה לסכום שאר האיברים (המקום שבו אפשר לחלק את התור לשני חלקים שווים בסכומם), או 0 אם אין חלוקה כזו; התור לא משתנה. | |
| ב(3) | O(n²) במקרה הגרוע: עד n + 1 קריאות רקורסיביות, ובכל אחת (פרט לאחרונה) kkk ב־O(n). | |
| 2 | א | מעבר ראשון: total = מספר השירים שאורכם > t; keep = max(total − n, 0). מעבר שני עם pos ו־prev: שיר ארוך כש־keep == 0 נמחק (prev == null ⇒ lst = pos.getNext(), אחרת prev.setNext(pos.getNext())), count++; שיר ארוך כש־keep > 0 נשאר ו־keep−−. מחזירים count. יעילות O(L). |
| ב | מחשבים sum של כל האורכים; כל עוד sum > maxTotal: מוצאים את האורך המרבי max, מוחקים את השיר האחרון באורך max בעזרת removeLastLong(1, max − 1), sum −= max, count++. מחזירים count (בדוגמה: 2). יעילות O(L²). | |
| 3 | א | פעולת עזר רקורסיבית level(t, x) מחזירה את הרמה של x בעץ t, או −1 אם x לא בעץ: אם t ריק ⇒ −1; אם בשורש ⇒ 0; אחרת מחפשים בתת־העץ השמאלי ואם לא נמצא – בימני, ואם נמצא מחזירים רמה + 1. levelGap: אם level(t, −num) == −1 מחזירים −1, אחרת מחזירים |level(t, num) − level(t, −num)|. |
| ב | סריקה רקורסיבית של כל צמתי העץ, תוך שמירת הפניה לשורש העץ כולו: maxGapFrom(root, node) – אם node ריק מחזירים −1; אחרת מחזירים את המקסימום מבין levelGap(root, node.getValue()), maxGapFrom(root, node.getLeft()) ו־maxGapFrom(root, node.getRight()). maxGap(t) מחזירה maxGapFrom(t, t). | |
| 4 | א | A → C → E → F → H , 23 דקות (גלישה 6 + 3 + 2 + 4 = 15 , עצירות C , E , F : 1 + 5 + 2 = 8). |
| ב(1) | בונים גרף מכוון: קודקוד לכל תחנה, ולכל מסלול גלישה u → v קשת שמשקלה slope(u, v) + stop(v). כל המסלולים יורדים, ולכן הגרף ללא מעגלים (גמ"ל). ממיינים מיון טופולוגי, מרפים את הקשתות לפי הסדר (מסלולים קלים בגמ"ל) מ־ K₁ , ומחזירים את הדרך לפי π ואת הזמן d[K₂] − stop(K₂). זמן ריצה O(|V| + |E|). | |
| ב(2) | גרף מכוון ללא מעגלים עם הקודקודים A–H ו־13 קשתות; משקל הקשת u → v הוא זמן הגלישה ועוד זמן העצירה ב־ v : A→B 7 , A→C 7 , B→D 8 , B→E 9 , C→E 8 , C→F 13 , D→E 6 , D→G 7 , E→F 4 , E→G 6 , E→H 12 , F→H 6 , G→H 5 . | |
| 5 | א | 2: 1, 4, 5 ; 3: 1, 5, 6 ; 5: 2, 3, 6, 7 ; 6: 3, 5, 7 ; 7: 4, 5, 6 |
| ב | הוצאות מן התור לפי הסדר: 1, 2, 3, 4, 5. באיטרציה החמישית (U = 5) השכן 6 באותו צבע (0) ⇐ עצירה. בסוף: color = (0, 1, 1, 0, 0, 0, 1) לצמתים 1–7 ; parent: 2←1, 3←1, 4←2, 5←2, 6←3, 7←4. | |
| ג | 6 , 5 , 2 , 1 | |
| 6 | א(1) | דייקסטרה מ־vⱼ עם מערך נוסף e[]: אתחול d[vⱼ]=0, e[vⱼ]=0, שאר d=∞; בכל איטרציה מעבירים מ־T ל־P את u בעל d מינימלי, ולכל קשת יוצאת (u, v) עם v ∈ T: אם d[u]+w(u,v) < d[v] אז d[v] ← d[u]+w(u,v), π[v] ← u, e[v] ← e[u]+1. |
| א(2) | O(n²) (מימוש במערכים). עם ערימת מינימום: O((n + |E|)·log n). | |
| ב(1) | d/π: A 0/– , B 4/A , D 9/A , C 11/D , F 14/C , E 15/F , G 16/D , H 18/E. סדר הקביעה: A, B, D, C, F, E, G, H. | |
| ב(2) | קשתות העץ (π[v] → v): A→B, A→D, D→C, D→G, C→F, F→E, E→H. | |
| 7 | א(1) | L₁ – רגולרית; L₂ – לא רגולרית; L₃ – רגולרית (שפה סופית). |
| א(2) | L₄ – לא רגולרית; L₅ = {aⁿ b aⁿ | n > 30} – לא רגולרית; L₆ = ∅ – רגולרית. | |
| ב | 13 מצבים: S (התחלה), A0–A2, B0–B2, C0–C2, Y, L1, L2; מקבלים: C1, C2, Y, L1, L2 (טבלת המעברים בפתרון). | |
| 8 | א | abcc – שייכת; aabb – לא שייכת; aaaccc – שייכת; aabbbbcc – שייכת; aaabcc – לא שייכת. |
| ב | n זוגי: aabbbc . n אי־זוגי: ac . | |
| ג | (1) b,A/שלוף (2) b,A/שלוף (3) b,S/ללא שינוי (4) b,S/ללא שינוי (5) b,S/דחוף B (6) b,B/דחוף B (7) c,S/שלוף (8) c,B/שלוף (9) c,B/שלוף (10) c,S/שלוף (11) b,A/דחוף B ; S/דחוף B (12) b,B/דחוף B (13) c,B/שלוף (14) c,A/שלוף (15) c,S/שלוף (16) c,B/שלוף ; A/שלוף (17) c,S/שלוף. מצבים מקבלים: q₆ , q₁₀ . | |
| 9 | א | המכונה עוצרת במצב q9 והסרט: ⊢ Δ Δ > X Δ Δ … (בתא של # נכתב הסימן >). |
| ב | ⊢ Δ Δ = X X Δ … (המכונה עוצרת ב־ q9). | |
| ג | המכונה משווה בין שני המספרים x ו־ y וכותבת בתא של # את הסימן > אם x > y, את הסימן < אם x < y ואת הסימן = אם x = y. | |
| ד | 1.נכון: ⊢ # 1 1 Δ → ⊢ < 1 1 Δ. 2. נכון: ⊢ 1 1 # Δ → ⊢ Δ 1 > Δ. 3. נכון: ⊢ # Δ → ⊢ = Δ. | |
| 10 | א | Shipment מכילה את Address (◆ בצד Shipment). public class Shipment { private String code; private double weight; private Address address; } – שלוש המחלקות Parcel, Letter, Pallet יורשות ממנה. |
| ב | לולאה על arr; אם arr[i] instanceof Letter (C#: is Letter) – ממירים ל־Letter, ואם getRegistered() וגם getAddress().sameCity(adr) – מכניסים ל־result[k++]; מחזירים את המערך בגודל 60. | |
| ג | Shipment: needsCare() מחזירה false. Parcel דורסת: return fragile. Pallet דורסת: לולאה על boxWeights – true אם יש משקל > 30, אחרת false. ב־Letter אין צורך בפעולה. | |
| 11 | א | (1) new Gift() (2) new Wish(3) (3) new Card() (4) new Gift("Bye") (5) new Gift() (6) new Wish(5) |
| ב | כן, שגיאת הידור: הפעולה הבונה של Wish קוראת ל־super("Wow") (ב־C#: base("Wow")), ועכשיו מחלקת הבסיס היא Card, שאין לה פעולה בונה שמקבלת מחרוזת (יש רק Card() ו־Card(int)). | |
| 12 | א(1) | public class Order { private String customer; private Instrument[] items; private int num; } |
| א(2) | Guitar ◁— Instrument, Piano ◁— Instrument (ירושה); Order ◆— Instrument (הכלה). | |
| ב(1) | Instrument ו־SheetMusic יורשות מ־Item; Guitar ו־Piano יורשות מ־Instrument; Order ◆— Item. | |
| ב(2) | public class Item { private int code; private double price; } | |
| ב(3) | Instrument extends Item {brand, year}; SheetMusic extends Item {title, composer}; Order: Item[] items. Guitar ו־Piano ללא שינוי. | |
| ג | Item: getFinalPrice() { return price * 0.8; } Piano (דריסה): return super.getFinalPrice() + 150; (ב־C#: virtual/override ו־base.GetFinalPrice()). |