מתכונת 8 במדעי המחשב - מבני נתונים ומסלול 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
לפניכם הפעולה ppp , המקבלת תור – q ובו מספרים הגדולים מ־ 0 , ומספר שלם – k הגדול מ־ 0 .
public static boolean ppp (Queue<Integer> q, int k)
{
q.insert (-1);
int max = 0;
int c = 0;
while (q.head() != -1)
{
int x = q.remove();
if (x > max)
{
max = x;
c++;
}
q.insert (x);
}
q.remove();
return c >= k;
}נתון תור q מטיפוס שלם:

(1) עקבו בעזרת טבלת המעקב שלפניכם אחר הפעולה ppp(q , 4) , וכתבו מה הפעולה מחזירה.
| התור q | x | x > max | max | c |
|---|---|---|---|---|
הסבירו מה הפעולה ppp עושה.
מהי סיבוכיות זמן הריצה של הפעולה ppp ? נמקו את תשובתכם.
לפניכם הפעולה find , המקבלת תור – q ובו מספרים הגדולים מ־ 0 , ומספר שלם – k הגדול מ־ 0 . הפעולה find משתמשת בפעולה ppp מסעיף א.
public static int find (Queue<Integer> q, int k)
{
if (!ppp (q, k))
return k - 1;
return find (q, k + 1);
}נתון התור q מסעיף א:

(1) עקבו אחר הפעולה find (q, 1) , וכתבו מה הפעולה מחזירה (אין צורך לעקוב אחר הפעולה ppp).
המעקב יכלול בכל קריאה את הערכים של q , k ואת הערך המוחזר.
לפניכם הצעה לטבלת מעקב (אין חובה להשתמש בטבלה זו).
| התור q שמתקבל בפעולה | הערך k שמתקבל בפעולה | ppp (q, k) == true | ערך מוחזר |
|---|---|---|---|
הסבירו מה הפעולה find עושה כאשר מזמנים אותה עם k = 1 .
מהי סיבוכיות זמן הריצה של הפעולה find כאשר מזמנים אותה עם k = 1 ? נמקו את תשובתכם.
◂ פתרון מלא לשאלה 1
סעיף א(1)
ppp(q, 4) מחזירה false (c = 3: האיברים 4, 7, 9 גדולים מכל האיברים שלפניהם); התור חוזר למצבו המקורי.
מה נדרש: לעקוב אחר ppp(q, 4) (ב־C#: Ppp) על התור שבאיור ולכתוב מה מוחזר. מראש התור (משמאל) לסופו: 4, 2, 7, 7, 5, 9, 3.
רעיון: הפעולה מכניסה −1 לסוף התור כ"סימן עצירה" (כל המספרים בתור גדולים מ־0). אחר כך היא מסובבת את התור: מוציאה כל איבר x מהראש ומחזירה אותו לסוף, עד שה־−1 מגיע לראש. במהלך הסיבוב max הוא הגדול ביותר מבין האיברים שכבר עברו, ו־c גדל ב־1 בכל פעם ש־x גדול ממש מ־max.
פתרון – טבלת מעקב (k = 4). כל שורה היא סיבוב אחד של הלולאה: התור – כפי שהוא בתחילת הסיבוב (הראש משמאל), max ו־c – הערכים בסוף הסיבוב.
| התור q (מראש התור לסופו) | x | x > max | max | c |
|---|---|---|---|---|
| 4, 2, 7, 7, 5, 9, 3, −1 (לפני הלולאה) | 0 | 0 | ||
| 4, 2, 7, 7, 5, 9, 3, −1 | 4 | true | 4 | 1 |
| 2, 7, 7, 5, 9, 3, −1, 4 | 2 | false | 4 | 1 |
| 7, 7, 5, 9, 3, −1, 4, 2 | 7 | true | 7 | 2 |
| 7, 5, 9, 3, −1, 4, 2, 7 | 7 | false | 7 | 2 |
| 5, 9, 3, −1, 4, 2, 7, 7 | 5 | false | 7 | 2 |
| 9, 3, −1, 4, 2, 7, 7, 5 | 9 | true | 9 | 3 |
| 3, −1, 4, 2, 7, 7, 5, 9 | 3 | false | 9 | 3 |
| −1, 4, 2, 7, 7, 5, 9, 3 (סוף הלולאה) | 9 | 3 |
בשורה האחרונה בראש התור −1, ולכן הלולאה מסתיימת. q.remove() מוציא את ה־−1, והתור חוזר להיות 4, 2, 7, 7, 5, 9, 3 – בדיוק כמו בהתחלה.
c = 3, והתנאי c >= k הוא 3 >= 4 ⇒ הפעולה מחזירה false.
בדיקה: על הקוד של השאלה: ppp(q, 4) = false, והתור אחרי הקריאה: [4, 2, 7, 7, 5, 9, 3].
טעות נפוצה: לספור גם את ה־7 השני – הוא אינו גדול ממש מ־max = 7, ולכן c לא גדל.
סעיף א(2)
מחזירה true אם בתור יש לפחות k איברים שכל אחד מהם גדול מכל האיברים שלפניו בתור ("שיאים"; האיבר שבראש התור תמיד נספר), אחרת false; התור נשאר ללא שינוי.
מה נדרש: להסביר מה ppp עושה.
הסבר: הפעולה עוברת על איברי התור לפי הסדר מהראש לסוף, ו־max שומר את הגדול ביותר מבין האיברים שכבר נבדקו. c סופר את האיברים שגדולים מכל האיברים שלפניהם ("שיאים"): האיבר הראשון תמיד נספר (כי max מתחיל ב־0 וכל המספרים חיוביים), ואיבר נוסף נספר רק אם הוא שובר את השיא הקודם.
תשובה: הפעולה מחזירה true אם בתור q יש לפחות k איברים שכל אחד מהם גדול מכל האיברים שלפניו בתור, ואחרת false. בסיום הפעולה התור q חוזר למצבו המקורי (אותם איברים באותו סדר).
בדיקה: על התור 4, 2, 7, 7, 5, 9, 3: השיאים הם 4, 7, 9 ⇒ k = 1, 2, 3 ⇒ true; k = 4, 5, 6, 7 ⇒ false.
טעות נפוצה: לחשוב שהפעולה סופרת ערכים שונים בתור (בתור שלנו יש 6 ערכים שונים), או איברים שגדולים רק מן האיבר הסמוך שלפניהם – הספירה היא מול כל האיברים הקודמים. למשל בתור 5, 1, 3 יש שיא אחד בלבד (5), אף ש־3 גדול מ־1.
סעיף א(3)
O(n) – סיבוב אחד של התור: n סיבובי לולאה, O(1) בכל סיבוב.
מה נדרש: סיבוכיות זמן הריצה של ppp, עם נימוק.
תשובה: O(n), כאשר n מספר האיברים בתור q.
נימוק: הלולאה while מעבירה בכל סיבוב איבר אחד מראש התור לסופו, ונעצרת כשה־−1 (שהוכנס לסוף) מגיע לראש – כלומר בדיוק n סיבובים. בכל סיבוב מתבצע מספר קבוע של פעולות שכל אחת O(1) (head, remove, insert, השוואה). מחוץ ללולאה – מספר קבוע של פעולות. סך הכול n·O(1) + O(1) = O(n).
סעיף ב(1)
find(q, 1) מחזירה 3 (k = 1, 2, 3 – true; k = 4 – false ⇒ מוחזר 4 − 1 = 3).
מה נדרש: לעקוב אחר find(q, 1) (ב־C#: Find) ולכתוב מה מוחזר. התור: 4, 2, 7, 7, 5, 9, 3.
רעיון: find עולה מ־k = 1 כלפי מעלה: כל עוד ppp(q, k) מחזירה true היא קוראת לעצמה עם k + 1, ובפעם הראשונה ש־ppp(q, k) מחזירה false היא מחזירה k − 1. התור לא משתנה בין הקריאות (ppp מחזירה אותו למצבו המקורי). לפי סעיף א, בתור יש 3 שיאים (4, 7, 9).
פתרון – טבלת מעקב:
| התור q שמתקבל בפעולה | הערך k שמתקבל בפעולה | ppp (q, k) == true | ערך מוחזר |
|---|---|---|---|
| 4, 2, 7, 7, 5, 9, 3 | 1 | true | 3 (מהקריאה find(q, 2)) |
| 4, 2, 7, 7, 5, 9, 3 | 2 | true | 3 (מהקריאה find(q, 3)) |
| 4, 2, 7, 7, 5, 9, 3 | 3 | true | 3 (מהקריאה find(q, 4)) |
| 4, 2, 7, 7, 5, 9, 3 | 4 | false | 4 − 1 = 3 |
הפעולה מחזירה 3.
בדיקה: על הקוד של השאלה: find(q, 1) = 3, והתור בסוף: [4, 2, 7, 7, 5, 9, 3].
סעיף ב(2)
find(q, 1) מחזירה את מספר האיברים בתור שגדולים מכל האיברים שלפניהם (מספר ה"שיאים"); התור לא משתנה.
מה נדרש: להסביר מה מחזירה find(q, 1).
תשובה: הפעולה מחזירה את מספר האיברים בתור שכל אחד מהם גדול מכל האיברים שלפניו (מספר ה"שיאים" בתור). התור q לא משתנה.
הסבר: נסמן ב־R את מספר השיאים בתור. לפי סעיף א, ppp(q, k) מחזירה true בדיוק כאשר R ≥ k. find מנסה את k = 1, 2, 3, … לפי הסדר, ועוצרת ב־k הראשון שעבורו R < k, כלומר ב־k = R + 1, ואז מחזירה k − 1 = R. הרקורסיה תמיד נעצרת, כי R ≤ n (מספר האיברים), ולכן לכל המאוחר בקריאה עם k = n + 1 מתקבל false.
בדיקה: 1, 2, 3, 4, 5 ⇒ 5 ; 9, 1, 2, 3 ⇒ 1 ; 3, 3, 3 ⇒ 1 ; 2, 1, 5, 4, 8, 8, 6 ⇒ 3.
סעיף ב(3)
O(n²) במקרה הגרוע: עד n + 1 קריאות רקורסיביות, בכל אחת ppp ב־O(n).
מה נדרש: סיבוכיות זמן הריצה של find(q, 1), עם נימוק.
תשובה: O(n²) במקרה הגרוע (n – מספר האיברים בתור).
נימוק: כל קריאה ל־find מבצעת קריאה אחת ל־ppp, שעולה O(n) (סעיף א(3)), ועוד מספר קבוע של פעולות. k מתחיל ב־1 וגדל ב־1 בכל קריאה עד R + 1, כאשר R מספר השיאים – כלומר יש R + 1 קריאות. במקרה הגרוע – כשהתור עולה ממש (כל איבר הוא שיא, R = n) – יש n + 1 קריאות, וכל אחת O(n) ⇒ (n + 1)·O(n) = O(n²).
(במקרה הטוב – כשהאיבר שבראש התור הוא הגדול ביותר, R = 1 – יש שתי קריאות בלבד ⇒ O(n).)
שאלה 2 · משחק קפיצות על שתי שרשרות חוליות (25 נקודות)
במשחק קפיצות משתמשים בשתי שרשרות של חוליות:
vals – הפניה לשרשרת חוליות של מספרים שלמים – הנקודות שבכל משבצת.
jumps – הפניה לשרשרת חוליות של מספרים שלמים חיוביים – אורך הקפיצה מכל משבצת.
הערה: כמות החוליות בשתי השרשרות שווה. החוליה במיקום k בשרשרת vals והחוליה במיקום k בשרשרת jumps מתארות את אותה משבצת.
מהלך המשחק: השחקן עומד על משבצת, אוסף את הנקודות שבה, ואז קופץ קדימה מספר משבצות השווה לאורך הקפיצה של המשבצת שעליה הוא עומד (קפיצה באורך 1 – למשבצת הבאה). כך הוא ממשיך עד שאסף את מספר האיסופים הנדרש, או עד שקפץ אל מעבר למשבצת האחרונה.
דוגמה: שתי השרשרות שלפניכם. שחקן שמתחיל במשבצת 1 אוסף 4 נקודות וקופץ 2 משבצות – למשבצת 3; שם הוא אוסף 1 וקופץ 3 – למשבצת 6; שם הוא אוסף 5 וקופץ 1 – למשבצת 7; שם הוא אוסף 2 וקופץ 4 – אל מעבר לסוף השרשרת.

ממשו את הפעולה שלפניכם:
Java – public static int collect (Node<Integer> vals, Node<Integer> jumps, int steps)
C# – public static int Collect (Node<int> vals, Node<int> jumps, int steps)
steps הוא מספר האיסופים שהשחקן מבצע.
הפעולה תחזיר את סכום הנקודות שאוסף שחקן שמתחיל במשבצת הראשונה (החוליה הראשונה בשתי השרשרות) ומבצע steps איסופים.
הניחו ש־ steps גדול מ־ 0 . אין לשנות את השרשרות שהתקבלו.
הערה: אם השחקן קופץ אל מעבר למשבצת האחרונה לפני שביצע steps איסופים, הפעולה תחזיר את סכום הנקודות שאסף עד אז.
דוגמה: בעבור שתי השרשרות בדוגמה שלעיל ו־ steps = 1 , הפעולה תחזיר 4 .
דוגמה נוספת: בעבור אותן שתי שרשרות ו־ steps = 3 , הפעולה תחזיר 10 (4 + 1 + 5 = 10).
דוגמה נוספת: בעבור אותן שתי שרשרות ו־ steps = 10 , הפעולה תחזיר 12 (4 + 1 + 5 + 2 = 12), כי אחרי 4 איסופים השחקן קופץ אל מעבר לסוף השרשרת.
ממשו את הפעולה שלפניכם:
Java – public static boolean reach (Node<Integer> vals, Node<Integer> jumps, int steps, int target)
C# – public static bool Reach (Node<int> vals, Node<int> jumps, int steps, int target)
הפעולה תחזיר true אם קיימת משבצת כלשהי (חוליה במיקום כלשהו בשרשרת vals והחוליה במיקום זהה בשרשרת jumps), כך ששחקן שמתחיל ממנה ומבצע steps איסופים (כמתואר בסעיף א) אוסף בדיוק target נקודות. אחרת הפעולה תחזיר false .
אפשר להשתמש בפעולה שכתבתם בסעיף א. הניחו ש־ steps גדול מ־ 0 . אין לשנות את השרשרות שהתקבלו.
דוגמה: בעבור שתי השרשרות בדוגמה שלעיל, steps = 2 ו־ target = 11 , הפעולה תחזיר true , כי שחקן שמתחיל במשבצת 4 אוסף 6, קופץ 2 משבצות – למשבצת 6, ואוסף 5: 6 + 5 = 11.
דוגמה נוספת: בעבור אותן שתי שרשרות, steps = 2 ו־ target = 9 , הפעולה תחזיר false .
◂ פתרון מלא לשאלה 2
סעיף א
pv, pj על החוליה הראשונה; כל עוד count < steps וגם pv != null: sum += pv.getValue(), count++, ואז מקדמים את pv ו־pj במקביל jump = pj.getValue() פעמים (עוצרים אם pv נהיה null).
מה נדרש: לדמות את המשחק מהמשבצת הראשונה: steps איסופים, או פחות אם השחקן יוצא מהשרשרת.
רעיון: שני מצביעים מתקדמים יחד – pv על הנקודות ו־pj על אורך הקפיצה של אותה משבצת. בכל צעד: מוסיפים לסכום את pv.getValue(), סופרים איסוף, שומרים את אורך הקפיצה jump ומקדמים את שני המצביעים jump חוליות. אם תוך כדי הקפיצה pv נהיה null – השחקן יצא מהשרשרת, והלולאה החיצונית נעצרת בגלל התנאי pv != null. (מכיוון שהשרשרות באורך שווה, pj נהיה null בדיוק כש־pv נהיה null.) הפעולה רק קוראת מהשרשרות.
פתרון:
public static int collect(Node<Integer> vals, Node<Integer> jumps, int steps) {
int sum = 0;
int count = 0; // כמה ערכים כבר נאספו
Node<Integer> pv = vals;
Node<Integer> pj = jumps; // הקפיצה באותו מיקום כמו pv
while (count < steps && pv != null) {
sum = sum + pv.getValue();
count++;
int jump = pj.getValue();
for (int i = 0; i < jump && pv != null; i++) { // מתקדמים jump חוליות בשתי השרשרות
pv = pv.getNext();
pj = pj.getNext();
}
}
return sum;
}בדיקה: על השרשרות מהדוגמה עם steps = 10:
| count | משבצת | נקודות | sum | קפיצה ← משבצת הבאה |
|---|---|---|---|---|
| 1 | 1 | 4 | 4 | 2 ← 3 |
| 2 | 3 | 1 | 5 | 3 ← 6 |
| 3 | 6 | 5 | 10 | 1 ← 7 |
| 4 | 7 | 2 | 12 | 4 ← מעבר לסוף (null) |
התוצאות: steps = 1 ← 4, steps = 3 ← 10, steps = 10 ← 12 – כמו בדוגמאות. השרשרות לא השתנו.
טעות נפוצה: לקרוא את אורך הקפיצה אחרי שמתחילים להזיז את pj, או לקדם רק את pv – אז pj כבר לא מצביע על אותה משבצת, וכל הקפיצות הבאות שגויות. טעות נוספת: לשכוח לבדוק pv != null בתוך הקפיצה ולקבל חריגה כשקופצים אל מעבר לסוף.
public static int collect(Node<Integer> vals, Node<Integer> jumps, int steps) {
int sum = 0;
int count = 0; // כמה ערכים כבר נאספו
Node<Integer> pv = vals;
Node<Integer> pj = jumps; // הקפיצה באותו מיקום כמו pv
while (count < steps && pv != null) {
sum = sum + pv.getValue();
count++;
int jump = pj.getValue();
for (int i = 0; i < jump && pv != null; i++) { // מתקדמים jump חוליות בשתי השרשרות
pv = pv.getNext();
pj = pj.getNext();
}
}
return sum;
}סעיף ב
pv, pj עוברים במקביל על כל המיקומים; לכל מיקום: אם collect(pv, pj, steps) == target מחזירים true; בסוף false.
מה נדרש: לבדוק אם יש משבצת התחלה שממנה steps איסופים נותנים בדיוק target.
רעיון: הפעולה collect מסעיף א מתחילה מהחוליות שמועברות אליה – ואפשר להעביר לה חוליות מאמצע השרשרות; היא מתייחסת אליהן כאל "המשבצת הראשונה". לכן עוברים עם שני מצביעים במקביל, pv על vals ו־pj על jumps (תמיד באותו מיקום), ולכל מיקום קוראים collect(pv, pj, steps). אם התוצאה שווה ל־target – מחזירים true מיד. אם עברנו על כל המיקומים – false. הקפיצות תמיד קדימה, ולכן החוליות שלפני משבצת ההתחלה לא משפיעות על המשחק ממנה.
פתרון: (משתמשים בפעולה collect / Collect מסעיף א)
public static boolean reach(Node<Integer> vals, Node<Integer> jumps, int steps, int target) {
Node<Integer> pv = vals;
Node<Integer> pj = jumps; // אותו מיקום בשתי השרשרות
while (pv != null) {
if (collect(pv, pj, steps) == target) // משחק שמתחיל מהמיקום הנוכחי
return true;
pv = pv.getNext();
pj = pj.getNext();
}
return false;
}בדיקה: על השרשרות מהדוגמה עם steps = 2, ערכי collect לכל משבצת התחלה:
| התחלה | המשחק | סכום |
|---|---|---|
| 1 | 4 ← משבצת 3: 1 | 5 |
| 2 | 7 ← משבצת 3: 1 | 8 |
| 3 | 1 ← משבצת 6: 5 | 6 |
| 4 | 6 ← משבצת 6: 5 | 11 |
| 5 | 3 ← משבצת 6: 5 | 8 |
| 6 | 5 ← משבצת 7: 2 | 7 |
| 7 | 2 ← מעבר לסוף | 2 |
לכן target = 11 ← true (התחלה 4), ו־target = 9 ← false (אין 9 בטבלה), כמו בדוגמאות. נבדקו גם steps = 3, target = 13 ← true, steps = 1, target = 2 ← true (המשבצת האחרונה), ושרשרות של חוליה אחת.
טעות נפוצה: לקדם רק את pv ולהעביר ל־collect את jumps מההתחלה – אז אורכי הקפיצה לא שייכים למשבצות הנכונות.
public static boolean reach(Node<Integer> vals, Node<Integer> jumps, int steps, int target) {
Node<Integer> pv = vals;
Node<Integer> pj = jumps; // אותו מיקום בשתי השרשרות
while (pv != null) {
if (collect(pv, pj, steps) == target) // משחק שמתחיל מהמיקום הנוכחי
return true;
pv = pv.getNext();
pj = pj.getNext();
}
return false;
}שאלה 3 · עץ סכומים (25 נקודות)
בשאלה זו אפשר להשתמש בפעולה החיצונית sumTree / SumTree שלהלן בלי לממש אותה.
| כותרת הפעולה | תיאור הפעולה | דוגמאות |
|---|---|---|
בשפת Java:<br>public static int sumTree (BinNode<Integer> tree)<br>בשפת C#:<br>public static int SumTree (BinNode<int> tree) | הפעולה מקבלת הפניה לעץ בינארי של מספרים שלמים – tree, ומחזירה את סכום הערכים בכל הצמתים של העץ. אם tree הוא null, הפעולה מחזירה 0. | – בעבור עץ שבו צומת יחיד עם הערך 6, הפעולה תחזיר 6.<br>– בעבור tree = null, הפעולה תחזיר 0. |
צאצאים של צומת בעץ בינארי הם כל הצמתים שבתת־העץ השמאלי שלו ובתת־העץ הימני שלו (הבנים שלו, הבנים של הבנים שלו, וכן הלאה).
עץ בינארי של מספרים שלמים הוא "עץ סכומים" אם הערך של כל צומת שאינו עלה שווה לסכום הערכים של כל הצאצאים שלו. (על העלים אין שום דרישה.)
ממשו את הפעולה החיצונית שלהלן:
Java – public static boolean isSumTree (BinNode<Integer> tree)
C# – public static bool IsSumTree (BinNode<int> tree)
הפעולה מקבלת הפניה לעץ בינארי של מספרים שלמים – tree שאינו null. הפעולה תחזיר true אם tree הוא "עץ סכומים", אחרת הפעולה תחזיר false.
דוגמה:
בעבור העץ הנתון הפעולה תחזיר true.

הסבר: 28 = 10 + 2 + 4 + 1 + 3 + 4 + 4 (כל הצאצאים של השורש), 10 = 2 + 4 + 1 + 3, 4 = 1 + 3, וגם בצומת 4 שמימין לשורש: 4 = 4.
דוגמה:
בעבור העץ הנתון הפעולה תחזיר false.

הסבר: אף על פי שבשורש מתקיים 25 = 5 + 2 + 4 + 7 + 7, בצומת 5 לא מתקיים התנאי: סכום הצאצאים שלו הוא 2 + 4 = 6.
הערה: עץ שיש בו צומת יחיד הוא "עץ סכומים".
ממשו את הפעולה isSumTree / IsSumTree המתוארת לעיל.
◂ פתרון מלא לשאלה 3
פתרון
עלה ← true. בצומת אחר: אם ערכו שונה מ־ sumTree(left) + sumTree(right) ← false; אחרת בודקים ברקורסיה כל בן קיים. בדוגמה הראשונה מתקבל true, ובשנייה false (בצומת 5: 2 + 4 = 6).
מה נדרש: לבדוק שבכל צומת שאינו עלה, הערך שווה לסכום הערכים של כל הצאצאים שלו.
רעיון (רקורסיה):
פתרון:
public static boolean isSumTree(BinNode<Integer> tree) {
if (!tree.hasLeft() && !tree.hasRight()) // עלה – אין לו צאצאים, ואין מה לבדוק
return true;
int desc = sumTree(tree.getLeft()) + sumTree(tree.getRight()); // סכום כל הצאצאים
if (tree.getValue() != desc)
return false;
if (tree.hasLeft() && !isSumTree(tree.getLeft()))
return false;
if (tree.hasRight() && !isSumTree(tree.getRight()))
return false;
return true;
}יעילות (לא נדרש): בכל צומת שאינו עלה מופעלת sumTree על שני תתי־העצים שלו, וזמנה ליניארי בגודלם. בעץ "שרשרת" של n צמתים זה n + (n − 1) + ... + 1 ⇒ O(n²) במקרה הגרוע.
בדיקה:
העץ בדוגמה הראשונה:
| צומת | סכום הצאצאים (sumTree שמאל + sumTree ימין) | שווה? |
|---|---|---|
| 28 (שורש) | 20 + 8 = 28 | כן |
| 10 | 2 + 8 = 10 | כן |
| 2 | עלה | – |
| 4 (בן של 10) | 1 + 3 = 4 | כן |
| 1, 3 | עלים | – |
| 4 (בן ימני של השורש) | 0 + 4 = 4 | כן |
| 4 (עלה) | עלה | – |
כל הבדיקות עברו ⇒ true.
העץ בדוגמה השנייה: בשורש 11 + 14 = 25 – תקין; ממשיכים לבן השמאלי 5: 2 + 4 = 6 ≠ 5 ⇒ false (בלי לבדוק את תת־העץ הימני).
התוצאות שבדוגמאות מתקבלות, וגם: צומת יחיד 9 ← true; העץ 0 עם הבנים 3 ו־ -3 ← true; שרשרת שמאלית של שלושה צמתים עם הערכים 12, 6, 6 (מהשורש למטה) ← true, והשרשרת 6, 6, 6 ← false (סכום הצאצאים של השורש הוא 12).
טעות נפוצה: להשוות את ערך הצומת רק לסכום הבנים שלו (getValue של השמאלי + getValue של הימני) במקום לסכום כל הצאצאים. בדוגמה הראשונה זה ייתן בשורש 10 + 4 = 14 ≠ 28, ולכן false – תשובה שגויה.
public static boolean isSumTree(BinNode<Integer> tree) {
if (!tree.hasLeft() && !tree.hasRight()) // עלה – אין לו צאצאים, ואין מה לבדוק
return true;
int desc = sumTree(tree.getLeft()) + sumTree(tree.getRight()); // סכום כל הצאצאים
if (tree.getValue() != desc)
return false;
if (tree.hasLeft() && !isSumTree(tree.getLeft()))
return false;
if (tree.hasRight() && !isSumTree(tree.getRight()))
return false;
return true;
}פרק שני (50 נקודות) – אלגוריתמים
יש לענות על שתי שאלות במסלול שלמדתם (לכל שאלה – 25 נקודות).
שאלה 4 · מסלול עם מספר מזערי של קשתות ורכיב הקשירות העשיר בקשתות (25 נקודות)
נתון גרף לא מכוון ולא קשיר G(V, E) שבו n קודקודים, מ־ v₀ עד vₙ₋₁ . הגרף מתאר מפה של כפרים: כל קודקוד הוא כפר, וכל קשת היא כביש המחבר שני כפרים.
כתבו אלגוריתם שמקבל שני קודקודים שונים בגרף – vₛ ו־ vₜ , ומחזיר מסלול מ־ vₛ אל vₜ שמספר הקשתות בו הוא הקטן ביותר (את רשימת הקודקודים שבמסלול, לפי הסדר מ־ vₛ אל vₜ ). אם אין מסלול בין שני הקודקודים, האלגוריתם יחזיר רשימה ריקה.
הערה: יש לכתוב אלגוריתם יעיל שאינו עובר על כל המסלולים האפשריים.
דוגמה: עבור הגרף שלפניכם והקודקודים 2 ו־ 9 , האלגוריתם יחזיר את המסלול 2 , 4 , 9 .

הסבר: גם 2 , 6 , 10 , 9 הוא מסלול מקודקוד 2 אל קודקוד 9 , אבל יש בו 3 קשתות, ובמסלול 2 , 4 , 9 יש רק 2 קשתות. עבור הקודקודים 0 ו־ 9 האלגוריתם יחזיר רשימה ריקה.
"רכיב קשירות" בגרף לא מכוון G(V, E) הוא קבוצת קודקודים שבה בין כל שני קודקודים יש מסלול, ואין שום קשת היוצאת מקודקוד בקבוצה לקודקוד שאינו בקבוצה. קודקוד שאין קשת בינו ובין שום קודקוד אחר יהיה בקבוצה משלו.
דוגמה: עבור הגרף שבדוגמה לעיל, שלושת רכיבי הקשירות מסומנים בקו מקווקו.

כתבו אלגוריתם המוצא ומחזיר את רכיב הקשירות שבו מספר הקשתות הוא הגדול ביותר.
למשל עבור הדוגמה שלעיל, האלגוריתם יחזיר את הקודקודים 2 , 4 , 6 , 9 , 10 (ברכיב זה יש 6 קשתות, ברכיב {0, 3, 5, 7, 11} יש 4 קשתות, וברכיב {1, 8} יש קשת אחת).
הניחו שיש רק רכיב קשירות אחד שבו מספר הקשתות הוא הגדול ביותר.
הערה: יש לכתוב אלגוריתם יעיל שאינו עובר על כל המסלולים האפשריים.
◂ פתרון מלא לשאלה 4
סעיף א
BFS מ־ vₛ שבו לכל קודקוד שמתגלה שומרים את ה"הורה" π[v] (הקודקוד שממנו התגלה). אם vₜ לא התגלה – מחזירים רשימה ריקה; אחרת הולכים מ־ vₜ אחורה לפי π עד vₛ ומחזירים את המסלול בסדר הפוך. זמן ריצה O(|V| + |E|). בדוגמה: 2, 4, 9.
מה נדרש: מסלול מ־ vₛ אל vₜ עם מספר קשתות מינימלי, או רשימה ריקה אם אין מסלול.
רעיון: ב־BFS מ־ vₛ כל קודקוד v מתגלה לראשונה דרך מסלול קצר ביותר (במספר קשתות). אם נשמור לכל קודקוד את ההורה π[v] – הקודקוד שממנו התגלה – המסלול vₛ → … → π[v] → v הוא מסלול קצר ביותר. לכן מריצים BFS (אפשר לעצור כש־ vₜ יוצא מהתור), ואז משחזרים את המסלול מ־ vₜ אחורה.
פתרון:
ShortestRoute(G, vₛ, vₜ):
לכל קודקוד u ב־V: visited[u] ← false ; π[u] ← NIL
visited[vₛ] ← true
Q ← תור ריק ; Q.insert(vₛ)
כל עוד Q אינו ריק וגם visited[vₜ] = false:
u ← Q.remove()
לכל שכן v של u:
אם visited[v] = false:
visited[v] ← true ; π[v] ← u
Q.insert(v)
אם visited[vₜ] = false: החזר רשימה ריקה // אין מסלול
path ← רשימה ריקה ; x ← vₜ
כל עוד x ≠ NIL:
הוסף את x לתחילת path
x ← π[x]
החזר את path(העצירה ברגע ש־ vₜ מתגלה היא רק חיסכון; גם בלי העצירה התוצאה זהה.)
זמן ריצה: האתחול O(|V|); כל קודקוד נכנס לתור פעם אחת וכל רשימת שכנים נסרקת פעם אחת ⇒ O(|V| + |E|); שחזור המסלול – לכל היותר |V| צעדים. סך הכול O(|V| + |E|).
בדיקה – על הגרף שבדוגמה מ־2 אל 9 (שכנים בסדר עולה; בסוגריים – ההורה). אחרי צעד 2 הקודקוד 9 כבר התגלה, והלולאה נעצרת:
| צעד | הוצא מהתור | שכנים חדשים (הורה) | התור אחרי הצעד |
|---|---|---|---|
| 1 | 2 | 4 (2), 6 (2) | [4, 6] |
| 2 | 4 | 9 (4), 10 (4) | [6, 9, 10] |
שחזור: 9 ← π[9] = 4 ← π[4] = 2 ⇒ המסלול 2, 4, 9 – כמו בדוגמה. עבור 0 ו־9: ה־BFS מ־0 מגלה רק את {0, 5, 11, 3, 7}, ולכן מוחזרת רשימה ריקה.
טעות נפוצה: להשתמש ב־DFS – הוא מוצא מסלול, אבל לא בהכרח מסלול עם מספר קשתות מינימלי (למשל DFS מ־2 שעובר על השכנים בסדר יורד מוצא 2, 6, 10, 9 – 3 קשתות).
סעיף ב
עוברים על הקודקודים לפי הסדר; מכל קודקוד שעוד לא סומן מריצים BFS שאוסף את הרכיב ומסכם את דרגות הקודקודים שלו. מספר הקשתות ברכיב = סכום הדרגות / 2. שומרים את הרכיב עם המספר הגדול ביותר. זמן ריצה O(|V| + |E|). בדוגמה: {2, 4, 6, 9, 10} עם 6 קשתות.
מה נדרש: לחלק את הגרף לרכיבי קשירות, לספור בכל רכיב את הקשתות, ולהחזיר את הרכיב עם הכי הרבה קשתות.
רעיון: BFS מקודקוד s מגלה בדיוק את רכיב הקשירות של s. כל קשת של הגרף נמצאת כולה בתוך רכיב אחד (שני קצותיה באותו רכיב), ובסכום הדרגות של קודקודי הרכיב כל קשת נספרת פעמיים – פעם מכל קצה. לכן מספר הקשתות ברכיב = (סכום הדרגות של קודקודיו) / 2. את הדרגה מקבלים בזמן שעוברים על רשימת השכנים של הקודקוד ב־BFS.
פתרון:
MostEdgesComponent(G):
לכל קודקוד u ב־V: visited[u] ← false
best ← ריק ; maxEdges ← −1
לכל קודקוד s מ־ v₀ עד vₙ₋₁:
אם visited[s] = false: // s פותח רכיב קשירות חדש
comp ← רשימה ובה s ; degSum ← 0
visited[s] ← true ; Q ← תור ריק ; Q.insert(s)
כל עוד Q אינו ריק:
u ← Q.remove()
לכל שכן v של u:
degSum ← degSum + 1 // סופרים את הקשת (u, v) מצד u
אם visited[v] = false:
visited[v] ← true ; הוסף את v ל־comp ; Q.insert(v)
edges ← degSum / 2
אם edges > maxEdges:
maxEdges ← edges ; best ← comp
החזר את bestזמן ריצה: הלולאה החיצונית n פעמים; בכל הסריקות יחד כל קודקוד נכנס לתור פעם אחת וכל רשימת שכנים נסרקת פעם אחת (visited אינו מאופס) ⇒ O(|V| + |E|).
בדיקה – על הגרף שבדוגמה:
| קודקוד שפותח רכיב | הרכיב | מספר קודקודים | סכום דרגות | מספר קשתות | maxEdges אחרי |
|---|---|---|---|---|---|
| 0 | {0, 3, 5, 7, 11} | 5 | 8 | 4 | 4 |
| 1 | {1, 8} | 2 | 2 | 1 | 4 |
| 2 | {2, 4, 6, 9, 10} | 5 | 12 | 6 | 6 |
מוחזר הרכיב {2, 4, 6, 9, 10} – כמו בדוגמה. שימו לב שלרכיב {0, 3, 5, 7, 11} יש אותו מספר קודקודים (5), אבל רק 4 קשתות.
טעות נפוצה: לבחור את הרכיב עם הכי הרבה קודקודים, או לסכם דרגות בלי לחלק ב־2 (אז כל קשת נספרת פעמיים – ההשוואה בין הרכיבים עדיין נכונה, אבל המספר המדווח שגוי).
שאלה 5 · משימות קודמות ושרשרת המשימות הארוכה ביותר (25 נקודות)
נתון גרף מכוון ללא מעגלים G(V, E) שבו n קודקודים, מ־ v₀ עד vₙ₋₁ . הגרף מייצג משימות בפרויקט: קשת מ־ vᵢ אל vₖ פירושה שאת המשימה vᵢ יש לסיים לפני שמתחילים את המשימה vₖ .
הגרף מיוצג ברשימות סמיכויות: לכל קודקוד יש רשימה של הקודקודים שהקשתות היוצאות ממנו מגיעות אליהם.
כתבו אלגוריתם המוצא ומחזיר את כל הקודקודים שיש מהם מסלול אל הקודקוד vⱼ (כלומר, את כל המשימות שיש לסיים לפני שמתחילים את המשימה vⱼ).
הערה: יש לכתוב אלגוריתם יעיל שאינו עובר על כל המסלולים האפשריים.
דוגמה: עבור הגרף שלפניכם, וקודקוד 6 , האלגוריתם יחזיר את הקודקודים 0 , 2 , 3 , 8 .

הסבר: יש קשת ישירה אל קודקוד 6 מכל אחד מן הקודקודים 2 , 3 , 8 , ויש מסלול 0 → 2 → 6 . מאף קודקוד אחר אין מסלול אל 6 .
"שרשרת משימות" היא מסלול מכוון בגרף. אורך השרשרת הוא מספר הקשתות שבה.
כתבו אלגוריתם המוצא ומחזיר את השרשרת הארוכה ביותר בגרף G(V, E) – את הקודקודים שבה, לפי הסדר.
דוגמה: עבור הגרף שבדוגמה לעיל, האלגוריתם יחזיר את הקודקודים 0 , 2 , 5 , 1 , 7 , 4 (שרשרת באורך 5 ; הקודקודים שלה צבועים באפור):

הניחו שיש רק שרשרת אחת שהיא הארוכה ביותר.
הערה: יש לכתוב אלגוריתם יעיל שאינו עובר על כל המסלולים האפשריים.
◂ פתרון מלא לשאלה 5
סעיף א
בונים את הגרף ההפוך (כל קשת u → v הופכת ל־ v → u) ב־O(|V| + |E|), ומריצים בו BFS (או DFS) מ־ vⱼ . כל קודקוד שמתגלה (חוץ מ־ vⱼ) מוחזר. זמן ריצה O(|V| + |E|). בדוגמה מתגלים 2 , 3 , 8 ואחר כך 0 .
מה נדרש: למצוא את כל הקודקודים x שיש מסלול x ⇝ vⱼ , בלי לעבור על כל המסלולים.
רעיון: יש מסלול מ־ x אל vⱼ בגרף G אם ורק אם יש מסלול מ־ vⱼ אל x בגרף ההפוך Gᵀ (אותו מסלול, בכיוון ההפוך). ברשימות הסמיכויות של G רשומות רק הקשתות היוצאות, ולכן קודם בונים את Gᵀ (רשימות של הקשתות הנכנסות), ואז סריקה לרוחב מ־ vⱼ ב־ Gᵀ מגלה בדיוק את הקודקודים המבוקשים.
פתרון:
Prerequisites(G, vⱼ):
// שלב 1: בניית הגרף ההפוך – כל קשת u → v הופכת ל־ v → u
לכל קודקוד u: radj[u] ← רשימה ריקה
לכל קודקוד u ב־V:
לכל v ברשימת הסמיכויות של u:
הוסף את u ל־radj[v]
// שלב 2: סריקה לרוחב מ־vⱼ בגרף ההפוך
לכל קודקוד u: visited[u] ← false
visited[vⱼ] ← true ; Q ← תור ריק ; Q.insert(vⱼ)
R ← רשימה ריקה
כל עוד Q אינו ריק:
u ← Q.remove()
לכל w ב־radj[u]:
אם visited[w] = false:
visited[w] ← true ; הוסף את w ל־R ; Q.insert(w)
החזר את Rזמן ריצה: בניית הגרף ההפוך – מעבר אחד על כל הרשימות, O(|V| + |E|). ב־BFS כל קודקוד נכנס לתור פעם אחת לכל היותר וכל רשימה נסרקת פעם אחת – O(|V| + |E|). סך הכול O(|V| + |E|).
בדיקה – על הגרף שבדוגמה עם vⱼ = 6 (ב־ radj[6] נמצאים 2 , 3 , 8 – הקודקודים שיש מהם קשת אל 6):
| צעד | הוצא מהתור | קשתות נכנסות (מי מצביע עליו) | חדשים שסומנו | התור אחרי הצעד | R |
|---|---|---|---|---|---|
| 0 | – | – | 6 (המקור) | [6] | {} |
| 1 | 6 | 2, 3, 8 | 2, 3, 8 | [2, 3, 8] | {2, 3, 8} |
| 2 | 2 | 0 | 0 | [3, 8, 0] | {2, 3, 8, 0} |
| 3 | 3 | – | – | [8, 0] | {2, 3, 8, 0} |
| 4 | 8 | – | – | [0] | {2, 3, 8, 0} |
| 5 | 0 | – | – | [] | {2, 3, 8, 0} |
התוצאה {0, 2, 3, 8} – כמו בדוגמה.
טעות נפוצה: להריץ BFS מ־ vⱼ בגרף המקורי – כך מוצאים את המשימות שתלויות ב־ vⱼ (שבאות אחריה) ולא את אלה שקודמות לה. גם הרצת סריקה מכל קודקוד בנפרד ("האם מגיעים ממנו ל־ vⱼ ?") נכונה אבל לא יעילה: O(|V|·(|V| + |E|)).
סעיף ב
מיון טופולוגי, ואז מעבר על הקודקודים לפי הסדר: L[v] = האורך המקסימלי של שרשרת שמסתיימת ב־ v (L[v] = max(L[u] + 1) על כל קשת u → v), עם שמירת הקודם P[v]. מחזירים את השרשרת מהקודקוד שבו L מקסימלי אחורה לפי P. זמן ריצה O(|V| + |E|). בדוגמה: 0 , 2 , 5 , 1 , 7 , 4 .
מה נדרש: מסלול ארוך ביותר (במספר קשתות) בגרף מכוון ללא מעגלים.
רעיון: בגרף כללי זו בעיה קשה, אבל בגרף ללא מעגלים אפשר לעבור על הקודקודים בסדר טופולוגי: כשמגיעים לקודקוד u , כל הקודקודים שיש מהם קשת אל u כבר טופלו, ולכן L[u] – אורך השרשרת הארוכה ביותר שמסתיימת ב־ u – כבר סופי. אז מעדכנים את השכנים: L[v] = max(L[v], L[u] + 1). זה בדיוק אלגוריתם המסלולים בגמ"ל, עם "מקסימום" במקום "מינימום" (או: מסלול קל ביותר כשמשקל כל קשת הוא −1).
פתרון:
LongestChain(G):
// שלב 1: מיון טופולוגי לפי דרגות כניסה
לכל קודקוד u: in[u] ← 0
לכל קודקוד u, לכל v ברשימת הסמיכויות של u: in[v] ← in[v] + 1
Q ← תור ובו כל הקודקודים שדרגת הכניסה שלהם 0 ; order ← רשימה ריקה
כל עוד Q אינו ריק:
u ← Q.remove() ; הוסף את u לסוף order
לכל v ברשימת הסמיכויות של u:
in[v] ← in[v] − 1
אם in[v] = 0: Q.insert(v)
// שלב 2: L[v] = אורך השרשרת הארוכה ביותר שמסתיימת ב־v
לכל קודקוד v: L[v] ← 0 ; P[v] ← null
לכל קודקוד u לפי הסדר ב־order:
לכל v ברשימת הסמיכויות של u:
אם L[u] + 1 > L[v]:
L[v] ← L[u] + 1 ; P[v] ← u
// שלב 3: שחזור השרשרת
end ← הקודקוד שבו L מקסימלי
chain ← רשימה ריקה ; x ← end
כל עוד x ≠ null:
הוסף את x לתחילת chain ; x ← P[x]
החזר את chainזמן ריצה: חישוב דרגות הכניסה ומיון טופולוגי O(|V| + |E|); מעבר שני – כל קשת נבדקת פעם אחת O(|V| + |E|); מציאת המקסימום והשחזור O(|V|). סך הכול O(|V| + |E|).
בדיקה – על הגרף שבדוגמה. סדר טופולוגי (דרגות כניסה, תור): 0, 3, 8, 2, 9, 5, 6, 1, 7, 4 . הערכים של L אחרי עיבוד כל קודקוד:
| קודקוד (לפי הסדר הטופולוגי) | L של הקודקוד | עדכונים לשכנים | L[0] | L[1] | L[2] | L[3] | L[4] | L[5] | L[6] | L[7] | L[8] | L[9] |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 0 | L[2]=1 (P=0) | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 3 | 0 | L[5]=1 (P=3), L[6]=1 (P=3) | 0 | 0 | 1 | 0 | 0 | 1 | 1 | 0 | 0 | 0 |
| 8 | 0 | L[9]=1 (P=8) | 0 | 0 | 1 | 0 | 0 | 1 | 1 | 0 | 0 | 1 |
| 2 | 1 | L[5]=2 (P=2), L[6]=2 (P=2) | 0 | 0 | 1 | 0 | 0 | 2 | 2 | 0 | 0 | 1 |
| 9 | 1 | L[4]=2 (P=9) | 0 | 0 | 1 | 0 | 2 | 2 | 2 | 0 | 0 | 1 |
| 5 | 2 | L[1]=3 (P=5) | 0 | 3 | 1 | 0 | 2 | 2 | 2 | 0 | 0 | 1 |
| 6 | 2 | L[4]=3 (P=6) | 0 | 3 | 1 | 0 | 3 | 2 | 2 | 0 | 0 | 1 |
| 1 | 3 | L[7]=4 (P=1) | 0 | 3 | 1 | 0 | 3 | 2 | 2 | 4 | 0 | 1 |
| 7 | 4 | L[4]=5 (P=7) | 0 | 3 | 1 | 0 | 5 | 2 | 2 | 4 | 0 | 1 |
| 4 | 5 | – | 0 | 3 | 1 | 0 | 5 | 2 | 2 | 4 | 0 | 1 |
L מקסימלי הוא L[4] = 5 . שחזור: 4 ← 7 ← 1 ← 5 ← 2 ← 0 , כלומר 0 , 2 , 5 , 1 , 7 , 4 – כמו בדוגמה. זו השרשרת הארוכה היחידה בגרף.
טעות נפוצה: לחשב L בסדר מספרי של הקודקודים (0 , 1 , 2 , …) במקום בסדר טופולוגי – אז, למשל, קודקוד 1 מעובד לפני 5 , עוד לפני ש־ L[1] קיבל את ערכו הסופי, והתשובה שגויה.
שאלה 6 · גרף־עץ־שבור (25 נקודות)
גרף לא מכוון נקרא "גרף־עץ־שבור" אם יש בו שני קודקודים שאין ביניהם קשת, וכאשר מוסיפים קשת ביניהם מקבלים עץ.
סרטטו גרף שיש בו 6 קודקודים והוא "גרף־עץ־שבור".
נתון "גרף־עץ־שבור" ובו n קודקודים.
(1) האם אפשר לדעת את מספר הקשתות בגרף זה? נמקו את תשובתכם. אם עניתם שאפשר, ציינו את מספר הקשתות.
האם אפשר לדעת את מספר רכיבי הקשירות בגרף זה? נמקו את תשובתכם. אם עניתם שאפשר, ציינו את מספר רכיבי הקשירות.
נתון גרף לא מכוון G ובו n קודקודים (n ≥ 2), המיוצג על ידי רשימת סמיכויות.
(1) כתבו אלגוריתם יעיל המחזיר "אמת" אם הגרף הוא "גרף־עץ־שבור" ואחרת הוא מחזיר "שקר".
מהי סיבוכיות זמן הריצה של האלגוריתם שכתבתם? נמקו את תשובתכם.
◂ פתרון מלא לשאלה 6
סעיף א
למשל: קודקודים 1–6, קשתות 1–2 , 2–3 , 4–5 , 4–6. אין קשת בין 3 ל־4, והוספת הקשת 3–4 נותנת עץ.
מה נדרש: דוגמה לגרף לא מכוון עם 6 קודקודים, שיש בו שני קודקודים לא שכנים שהוספת קשת ביניהם נותנת עץ.
רעיון: מתחילים מעץ על 6 קודקודים (5 קשתות) ומוחקים ממנו קשת אחת. החזרת הקשת שנמחקה מחזירה את העץ.
פתרון: (הקשת המקווקוות באדום אינה בגרף – היא הקשת שמוסיפים.)

קשתות הגרף: 1–2 , 2–3 , 4–5 , 4–6 (4 קשתות).
בין 3 ל־4 אין קשת. אם מוסיפים את הקשת 3–4 מתקבלות 5 קשתות: 1–2 , 2–3 , 3–4 , 4–5 , 4–6. הגרף קשיר ואין בו מעגל – זה עץ. לכן זהו "גרף־עץ־שבור".
בדיקה: הגרף מקיים את ההגדרה, ואחרי הוספת 3–4 מתקבל עץ.
סעיף ב(1)
כן. מספר הקשתות הוא n−2.
מה נדרש: לקבוע אם מספר הקשתות ב"גרף־עץ־שבור" עם n קודקודים ידוע, ואם כן – מהו.
רעיון: בכל עץ עם n קודקודים יש בדיוק n−1 קשתות.
פתרון: כן. לפי ההגדרה יש שני קודקודים u, v שאין ביניהם קשת, וכשמוסיפים את הקשת u–v מתקבל עץ על אותם n קודקודים. לעץ הזה יש n−1 קשתות, והוא מכיל את כל קשתות הגרף ועוד קשת אחת. לכן בגרף יש n−1 − 1 = n−2 קשתות.
בדיקה: בדוגמה מסעיף א: 6 קודקודים ו־4 קשתות. גם בבדיקה הממוחשבת – לכל אחד מ־1,209 הגרפים־עצים־שבורים עם 2 עד 6 קודקודים היו בדיוק n−2 קשתות.
סעיף ב(2)
כן. יש בדיוק 2 רכיבי קשירות.
מה נדרש: לקבוע את מספר רכיבי הקשירות של "גרף־עץ־שבור".
רעיון: הגרף הוא עץ שהורדנו ממנו קשת אחת. בעץ כל קשת היא "גשר": אין בין קצותיה מסלול אחר.
פתרון: כן. נסמן ב־T את העץ שמתקבל אחרי הוספת הקשת u–v. הגרף שלנו הוא T בלי הקשת u–v.
לכן בגרף בדיוק 2 רכיבי קשירות (הרכיב של u והרכיב של v). כל אחד מהם הוא עץ.
בדיקה: בדוגמה מסעיף א הרכיבים הם {1, 2, 3} ו־{4, 5, 6}. גם בבדיקה הממוחשבת – לכל הגרפים־עצים־שבורים היו בדיוק 2 רכיבים.
סעיף ג(1)
G הוא גרף־עץ־שבור ⇔ יש בו בדיוק n−2 קשתות ובדיוק 2 רכיבי קשירות. האלגוריתם: סופרים קשתות (עוצרים אם יש יותר מ־n−2); אם ≠ n−2 – שקר; אחרת סופרים רכיבי קשירות בעזרת BFS מכל קודקוד שעוד לא התגלה: אם התקבלו בדיוק 2 – אמת, אחרת שקר.
מה נדרש: אלגוריתם יעיל שבודק אם גרף (ברשימת סמיכויות) הוא "גרף־עץ־שבור".
רעיון: נוכיח ש־G הוא גרף־עץ־שבור אם ורק אם יש בו n−2 קשתות ו־2 רכיבי קשירות.
אז מספיק לספור קשתות ורכיבי קשירות.
פתרון – האלגוריתם (הגרף פשוט – בלי קשתות מקבילות ולולאות; כל קשת מופיעה פעמיים ברשימת הסמיכויות – פעם אצל כל אחד מקצותיה):
בדיקה: האלגוריתם מחזיר תמיד אותה תשובה כמו ההגדרה עצמה ("יש זוג קודקודים לא שכנים שהוספת קשת ביניהם נותנת עץ"). על הדוגמה מסעיף א מתקבל "אמת".
טעות נפוצה: לבדוק רק "יש n−2 קשתות". גרף עם n−2 קשתות יכול להיות בעל יותר משני רכיבים – למשל משולש ועוד שני קודקודים מבודדים (5 קודקודים, 3 קשתות, 3 רכיבים) – ואז אין קשת אחת שהוספתה נותנת עץ. חייבים לבדוק גם את מספר הרכיבים.
סעיף ג(2)
O(n) (ובאופן כללי O(V+E), אבל בזכות העצירה המוקדמת E ≤ n−2).
מה נדרש: סיבוכיות האלגוריתם מסעיף ג(1), עם נימוק.
רעיון: נחשב כל שלב בנפרד, ונשים לב שהעצירה המוקדמת מגבילה את מספר הקשתות שנבדקות.
פתרון:
סך הכול: O(n). (בלי העצירה המוקדמת הסיבוכיות היא O(n + E), שהיא עדיין לינארית בגודל הקלט – רשימת הסמיכויות.)
פרק שני (50 נקודות) – מודלים חישוביים
יש לענות על שתי שאלות במסלול שלמדתם (לכל שאלה – 25 נקודות).
שאלה 7 · שפות רגולריות, תכונות סגירות והשלמת אוטומט עם מפריד (25 נקודות)
בשאלה זו שני סעיפים, א–ב, שאין קשר ביניהם. ענו על שני הסעיפים.
לפניכם שלוש שפות מעל הא"ב {a , b} . הסימון n_a(w) הוא מספר המופעים של a במילה w , הסימון n_b(w) הוא מספר המופעים של b במילה w , והסימון |w| הוא אורך המילה w .
L₁ = { w | n_a(w) = n_b(w) }
L₂ = { w | n_a(w) − n_b(w) מתחלק ב־3 ללא שארית }
L₃ = { w | |w| ≥ 12 }
לפניכם ארבעה סעיפים (1)–(4). ענו על כולם (בנימוק מספיק הסבר מילולי, אין צורך באוטומט).
(1) בנוגע לכל אחת מן השפות L₁–L₃ , כתבו אם היא רגולרית או לא רגולרית. נמקו את תשובתכם.
האם השפה L₁ ∩ L₃ רגולרית? נמקו את תשובתכם.
האם השפה L₁ ∩ L̅₃ רגולרית? נמקו את תשובתכם.
האם השפה L₂ ∩ L̅₁ רגולרית? נמקו את תשובתכם.
נתונה השפה L מעל הא"ב {a,b,#} :
L = { u#v | u, v ∈ {a,b}⁺ וגם (u מסתיימת באות a או |v| % 3 = 0) }
הסבר: כל מילה בשפה L מורכבת משני חלקים לא ריקים u ו־v, שבהם רק האותיות a ו־b, ובין שני החלקים סימן # אחד. כמו כן, החלק u מסתיים באות a , או שאורך החלק v מתחלק ב־3 .
דוגמה למילה בשפה L היא bba#ab , כי החלק u = bba מסתיים באות a .
דוגמה נוספת למילה בשפה L היא ab#bab , כי אורך החלק v = bab הוא 3 , ומתחלק ב־3 .
דוגמה למילה שאינה בשפה L היא abb#ab . החלק u = abb אינו מסתיים באות a , ואורך החלק v = ab הוא 2 – לא מתחלק ב־3 .
(1) לפניכם חמש מילים: ba#b , ab#aab , ab#ab , a# , aa#b#a.
העתיקו כל אחת מן המילים למחברתכם, וקבעו אם היא שייכת לשפה L או לא שייכת לשפה L . נמקו את קביעותיכם.
נתון לפניכם אוטומט סופי דטרמיניסטי שאינו מלא המקבל את השפה L . באוטומט קיימים כל המצבים וכל המעברים הנדרשים, אך בכמה מן המעברים חסרים סימני הקלט (התווים במעברים), והמצבים המקבלים באוטומט אינם מסומנים כלל. מצב ההתחלה הוא p₀ . שימו לב: במעבר אחד יכולים להופיע כמה סימני קלט (למשל a,b).
העתיקו את האוטומט למחברתכם, הוסיפו את סימני הקלט החסרים, וסמנו את המצבים המקבלים.
הערה: האוטומט צריך להישאר סופי דטרמיניסטי שאינו מלא. אין להוסיף בו מצבים או מעברים, ואין לשנות את סימני הקלט המופיעים בו. תשובה שיהיה בה שינוי באוטומט הנתון לא תזכה בנקודות.

◂ פתרון מלא לשאלה 7
סעיף א(1)
L₁ – לא רגולרית; L₂ – רגולרית (3 מצבים); L₃ – רגולרית.
מה נדרש: לקבוע לכל אחת מהשפות אם היא רגולרית, ולנמק.
רעיון: לאוטומט סופי יש זיכרון חסום. נבדוק כמה מידע צריך לזכור בזמן הקריאה.
פתרון:
L₁ – לא רגולרית. צריך לזכור את ההפרש n_a − n_b, שיכול להיות גדול כרצוננו (למשל אחרי aⁱ ההפרש הוא i). נימוק מדויק: אילו היה אוטומט סופי עם p מצבים, בין המילים a⁰, a¹, …, aᵖ היו שתיים, aⁱ ו־aʲ עם i ≠ j, שמסתיימות באותו מצב. אז האוטומט היה מקבל או דוחה יחד את aⁱbⁱ (בשפה) ואת aʲbⁱ (לא בשפה) – סתירה.
L₂ – רגולרית. מספיק לזכור את ההפרש n_a − n_b מודולו 3 – שלוש אפשרויות. כל a מוסיפה 1 וכל b מחסירה 1 (מודולו 3). המצב Dᵣ אומר שההפרש עד כה נותן שארית r:
| מצב | a | b | מקבל? |
|---|---|---|---|
| D₀ (התחלה) | D₁ | D₂ | כן |
| D₁ | D₂ | D₀ | לא |
| D₂ | D₀ | D₁ | לא |
L₃ – רגולרית. סופרים את האורך עד 12 בלבד: מצבים C₀…C₁₂, כל אות מקדמת Cᵢ → Cᵢ₊₁, ומ־C₁₂ לולאה על a,b. המצב המקבל: C₁₂.
בדיקה: שני האוטומטים מקבלים בדיוק את המילים של L₂ ושל L₃.
סעיף א(2)
לא. L₁ ∩ L̅₃ סופית, ולכן אילו L₁ ∩ L₃ הייתה רגולרית, L₁ = (L₁ ∩ L₃) ∪ (L₁ ∩ L̅₃) הייתה רגולרית – סתירה.
מה נדרש: לקבוע אם L₁ ∩ L₃ – המילים עם מספר שווה של a ושל b שאורכן לפחות 12 – רגולרית.
רעיון: L₃ מוציאה מ־L₁ רק מספר סופי של מילים (הקצרות), וזה לא "מציל" את L₁.
פתרון: לא רגולרית. מתקיים L₁ = (L₁ ∩ L₃) ∪ (L₁ ∩ L̅₃). החלק L₁ ∩ L̅₃ מכיל רק מילים שאורכן קטן מ־12, ולכן הוא סופי ורגולרי. נניח בשלילה ש־L₁ ∩ L₃ רגולרית. אז L₁ היא איחוד של שתי שפות רגולריות ולכן רגולרית – סתירה לסעיף (1).
במילים: גם במילים הארוכות (למשל aⁱbⁱ עם i ≥ 6) האוטומט צריך לזכור את ההפרש n_a − n_b, שאינו חסום.
בדיקה: aⁿbⁿ שייכת ל־L₁ ∩ L₃ לכל n ≥ 6 (השפה אינסופית), וכל זוג תחיליות aⁱ, aʲ (i ≠ j) מובחן על ידי הסיומת bⁱ.
טעות נפוצה: לענות "רגולרית, כי L₃ רגולרית וחיתוך של רגולריות הוא רגולרי". כלל הסגירות דורש ששתי השפות יהיו רגולריות, ו־L₁ אינה רגולרית.
סעיף א(3)
כן. L̅₃ = כל המילים באורך קטן מ־12, ולכן L₁ ∩ L̅₃ סופית – וכל שפה סופית רגולרית.
מה נדרש: לקבוע אם L₁ ∩ L̅₃ רגולרית (L̅₃ – המשלים של L₃ ביחס ל־{a,b}*).
רעיון: המשלים של "אורך לפחות 12" הוא "אורך לכל היותר 11" – יש מספר סופי של מילים כאלה.
פתרון: רגולרית. L̅₃ = { w | |w| ≤ 11 }, ויש בה מספר סופי של מילים (2¹² − 1). לכן גם L₁ ∩ L̅₃ ⊆ L̅₃ סופית (אלה המילים באורך זוגי עד 10 עם מספר שווה של a ושל b, כולל המילה הריקה ε), וכל שפה סופית היא רגולרית.
בדיקה: ספירת המילים: יש 351 מילים ב־L₁ ∩ L̅₃ (1 + 2 + 6 + 20 + 70 + 252 לפי האורכים 0, 2, …, 10).
סעיף א(4)
לא. L₁ ⊆ L₂, ולכן L₁ = L₂ ∩ (משלים של (L₂ ∩ L̅₁)). אילו L₂ ∩ L̅₁ הייתה רגולרית, L₁ הייתה רגולרית (משלים וחיתוך) – סתירה.
מה נדרש: לקבוע אם השפה של המילים שבהן ההפרש n_a − n_b מתחלק ב־3 אבל אינו 0 – רגולרית.
רעיון: ננסה "לבנות" את L₁ מ־L₂ ומהשפה הנתונה בעזרת פעולות שהשפות הרגולריות סגורות להן. אם זה מצליח – השפה הנתונה לא יכולה להיות רגולרית.
פתרון: לא רגולרית. אם n_a(w) = n_b(w) אז ההפרש הוא 0, שמתחלק ב־3, ולכן L₁ ⊆ L₂. מכאן ש־L₂ מתחלקת לשני חלקים זרים: L₁ (הפרש 0) ו־M = L₂ ∩ L̅₁ (הפרש 3, −3, 6, …). לכן L₁ = L₂ ∩ M̅ (המילים של L₂ שאינן ב־M).
נניח בשלילה ש־M רגולרית. אז גם M̅ רגולרית (משלים), ו־L₂ ∩ M̅ רגולרית (חיתוך של רגולריות). כלומר L₁ רגולרית – סתירה לסעיף (1).
במילים: כדי להבחין בין הפרש 0 להפרש 3 או 6 האוטומט היה צריך לזכור את ההפרש עצמו, ולא רק את השארית שלו.
בדיקה: L₁ ⊆ L₂, והשוויון L₁ = L₂ ∩ M̅ מתקיים. דוגמאות למילים ב־M: aaa, bbb, aaaab.
טעות נפוצה: לענות "רגולרית, כי L₂ רגולרית". L̅₁ אינה רגולרית, ולכן כלל הסגירות לחיתוך לא חל כאן.
סעיף ב(1)
ba#b – שייכת; ab#aab – שייכת; ab#ab – לא שייכת; a# – לא שייכת; aa#b#a – לא שייכת.
מה נדרש: לקבוע לכל מילה אם היא ב־L.
רעיון: בודקים קודם את הצורה (בדיוק # אחד, ומשני צדדיו חלק לא ריק של a/b), ואז את התנאי: u מסתיימת ב־a או |v| מתחלק ב־3.
פתרון:
| מילה | צורה u#v? | u , v | התנאי | שייכת? |
|---|---|---|---|---|
| ba#b | כן | ba , b | u מסתיימת ב־a | כן |
| ab#aab | כן | ab , aab | u מסתיימת ב־b, אבל אורך v הוא 3 | כן |
| ab#ab | כן | ab , ab | u מסתיימת ב־b ואורך v הוא 2 | לא |
| a# | לא – v ריקה | – | – | לא |
| aa#b#a | לא – יש שני סימני # | – | – | לא |
בדיקה: חמש המילים מול הגדרת L – התוצאות זהות לטבלה.
טעות נפוצה: לקבל את a# כי "u מסתיימת ב־a". התנאי על u או על v נבדק רק אחרי שהצורה תקינה, ו־v חייבת להיות לא ריקה.
סעיף ב(2)
p₀→p₂: b ; p₁→p₁: a ; p₁→p₂: b ; p₂→p₁: a ; p₃→p₄: a,b ; p₂→p₅: # ; p₆→p₇: a,b ; p₇→p₈: a,b ; p₈→p₆: a,b. מצבים מקבלים: p₄, p₈.
מה נדרש: להשלים את התוויות החסרות ולסמן מצבים מקבלים, בלי לשנות שום דבר אחר.
רעיון: נבין מה כל מצב "זוכר":
פתרון: האוטומט המושלם (התוויות שהוספנו באדום, המצבים המקבלים מסומנים בעיגול כפול אדום):

טבלת המעברים המלאה (מעבר שאינו בטבלה – האוטומט נתקע ודוחה; בהדגשה – התוויות שהוספנו):
| מצב | a | b | # | מקבל? | משמעות |
|---|---|---|---|---|---|
| p₀ (התחלה) | p₁ | p₂ | – | לא | התחלה |
| p₁ | p₁ | p₂ | p₃ | לא | בתוך u, האות האחרונה שנקראה – a |
| p₂ | p₁ | p₂ | p₅ | לא | בתוך u, האות האחרונה שנקראה – b |
| p₃ | p₄ | p₄ | – | לא | u מסתיימת ב־a, נקרא # (v עדיין ריקה) |
| p₄ | p₄ | p₄ | – | כן | u מסתיימת ב־a, v לא ריקה – כל המשך של a/b תקין |
| p₅ | p₆ | p₆ | – | לא | u מסתיימת ב־b, נקרא # (אורך v הוא 0) |
| p₆ | p₇ | p₇ | – | לא | u מסתיימת ב־b, אורך v % 3 = 1 |
| p₇ | p₈ | p₈ | – | לא | u מסתיימת ב־b, אורך v % 3 = 2 |
| p₈ | p₆ | p₆ | – | כן | u מסתיימת ב־b, אורך v % 3 = 0 ו־v לא ריקה |
מעקב לדוגמה:
בדיקה: האוטומט המושלם מקבל בדיוק את המילים של L. בנוסף נבדקו בחיפוש ממצה כל ההשלמות האפשריות של התוויות החסרות (כל קבוצה לא ריקה של סימנים לכל מעבר, וכל בחירה של מצבים מקבלים): ההשלמה שלמעלה היא היחידה שמקבלת את L.
שאלה 8 · פעולות על שפות ואוטומט מחסנית דטרמיניסטי – בלוקים מקוננים (25 נקודות)
בשאלה זו שני סעיפים, א–ב, שאין ביניהם קשר. ענו על שני הסעיפים.
לפניכם שש שפות מעל הא"ב {x , y} :
Σ* – מציין את שפת כל המילים מעל הא"ב {x , y} .
Lᴿ – מציין את השפה שמילותיה הן היפוכי המילים של L .
| L₁ = Σ* | L₄ = {xy , yyx} |
| L₂ = {ε} | L₅ = {ε , x , xy} |
| L₃ = Ø | L₆ = {y , yx} |
כתבו את השפה המתקבלת מכל אחת מחמש הפעולות שלהלן:
(1) L₄ ∩ L₅
L₆ • L₆
L₁ • L₂
L₃ᴿ
L₅ • L₆
נתונה השפה L מעל הא"ב {a, b, c} :
L = {aⁿ bᵐ cᵐ aⁿ | n >= 0 , m >= 1}
בנו אוטומט מחסנית דטרמיניסטי המקבל את השפה L .
◂ פתרון מלא לשאלה 8
סעיף א(1)
L₄ ∩ L₅ = {xy}
מה נדרש: המילים המשותפות ל־ L₄ ול־ L₅.
פתרון: L₄ = {xy, yyx}, L₅ = {ε, x, xy}. רק xy מופיעה בשתיהן: L₄ ∩ L₅ = {xy}.
סעיף א(2)
L₆ • L₆ = {yy, yyx, yxy, yxyx}
מה נדרש: כל השרשורים uv כאשר u, v ∈ L₆ (אפשר גם u = v).
פתרון:
| u | v = y | v = yx |
|---|---|---|
| y | yy | yyx |
| yx | yxy | yxyx |
L₆ • L₆ = {yy, yyx, yxy, yxyx}.
טעות נפוצה: לשכוח את השרשור של מילה עם עצמה (yy, yxyx), או לחשוב ש־ yyx ו־ yxy הן אותה מילה.
סעיף א(3)
L₁ • L₂ = Σ* = L₁
מה נדרש: שרשור של Σ* עם {ε}.
פתרון: המילה היחידה ב־ L₂ היא ε, ו־ w • ε = w לכל w. לכן כל מילה של Σ* מתקבלת כמו שהיא, ולא מתקבל שום דבר אחר: L₁ • L₂ = Σ*.
סעיף א(4)
L₃ᴿ = Ø
מה נדרש: ההיפוך של השפה הריקה.
פתרון: Lᴿ מכילה את ההיפוך של כל מילה של L. ב־ L₃ = Ø אין מילים, ולכן אין מה להפוך: L₃ᴿ = Ø.
טעות נפוצה: לכתוב {ε}. ההיפוך של {ε} הוא {ε}, אבל ההיפוך של Ø הוא Ø.
סעיף א(5)
L₅ • L₆ = {y, yx, xy, xyx, xyy, xyyx}
מה נדרש: כל השרשורים uv כאשר u ∈ L₅ ו־ v ∈ L₆.
פתרון:
| u ∈ L₅ | v = y | v = yx |
|---|---|---|
| ε | y | yx |
| x | xy | xyx |
| xy | xyy | xyyx |
כל שש המילים שונות: L₅ • L₆ = {y, yx, xy, xyx, xyy, xyyx}.
סעיף ב
דוחפים סימן לכל a ראשון ולכל b; על כל c שולפים סימן של b, ואחר כך על כל a שבסוף שולפים סימן של a. הסימנים התחתונים של כל בלוק מסומנים (A – ה־ a הראשון; Z/Y – ה־ b הראשון כש־ n = 0 / n ≥ 1) כדי לדעת בלי מעברי ε מתי בלוק נגמר. מצב מקבל יחיד: q5.
מה נדרש: אוטומט מחסנית דטרמיניסטי ל־ L = {aⁿ bᵐ cᵐ aⁿ | n ≥ 0, m ≥ 1}.
רעיון: המבנה "מקונן": ה־ b מתאימים ל־ c שמיד אחריהם, וה־ a הראשונים מתאימים ל־ a שבסוף. מחסנית מתאימה בדיוק לזה – מה שנדחף אחרון נשלף ראשון:
כדי לדעת בלי מעברי ε מתי נגמר כל בלוק, מסמנים את הסימן הראשון של כל בלוק:
התנאי m ≥ 1 חשוב: בזכותו ה־ b מפריד בין שני בלוקי ה־ a, והאוטומט יודע בכל רגע אם הוא בבלוק הראשון או האחרון.
פתרון:
| מצב | קלט | ראש המחסנית | מצב חדש | מחליפים את הראש ב־ | משמעות |
|---|---|---|---|---|---|
| q0 | a | ⊥ | q1 | A⊥ | ה־ a הראשון: דוחפים A (מסמן את תחתית ה־ a) |
| q1 | a | A | q1 | XA | a נוסף: דוחפים X |
| q1 | a | X | q1 | XX | a נוסף: דוחפים X |
| q0 | b | ⊥ | q2 | Z⊥ | n = 0, ה־ b הראשון: דוחפים Z |
| q1 | b | A | q2 | YA | n ≥ 1, ה־ b הראשון: דוחפים Y |
| q1 | b | X | q2 | YX | n ≥ 1, ה־ b הראשון: דוחפים Y |
| q2 | b | Z | q2 | BZ | b נוסף: דוחפים B |
| q2 | b | Y | q2 | BY | b נוסף: דוחפים B |
| q2 | b | B | q2 | BB | b נוסף: דוחפים B |
| q2 | c | B | q3 | ε | ה־ c הראשון: שולפים B |
| q2 | c | Z | q5 | ε | n = 0, m = 1: המחסנית מתרוקנת |
| q2 | c | Y | q4 | ε | n ≥ 1, m = 1: עוברים לקריאת ה־ a שבסוף |
| q3 | c | B | q3 | ε | c נוסף: שולפים |
| q3 | c | Z | q5 | ε | n = 0: ה־ c האחרון – המחסנית מתרוקנת |
| q3 | c | Y | q4 | ε | n ≥ 1: ה־ c האחרון – עוברים לקריאת ה־ a |
| q4 | a | X | q4 | ε | a שבסוף: שולפים |
| q4 | a | A | q5 | ε | ה־ a האחרון – המחסנית מתרוקנת |
בכל צירוף (מצב, אות, ראש מחסנית) יש לכל היותר מעבר אחד ואין מעברי ε – האוטומט דטרמיניסטי. כל צירוף שאינו בטבלה = דחייה (למשל c בלי b לפניו, b אחרי c, או אות נוספת ב־ q5).
בדיקה – מעקב על aabbccaa (n = 2, m = 2):
| נקרא | מצב | מחסנית (ראש משמאל) |
|---|---|---|
| – | q0 | ⊥ |
| a | q1 | A⊥ |
| a | q1 | XA⊥ |
| b | q2 | YXA⊥ |
| b | q2 | BYXA⊥ |
| c | q3 | YXA⊥ |
| c | q4 | XA⊥ |
| a | q4 | A⊥ |
| a | q5 | ⊥ |
הקלט נגמר ב־ q5 ⇒ מתקבלת. מעקב על bbcc (n = 0): q0 → q2 (Z⊥) → q2 (BZ⊥) → q3 (Z⊥) → q5 (⊥) ⇒ מתקבלת. לעומת זאת abcaa נתקעת ב־ q5 (a מיותר), ו־ abcca נתקעת ב־ q4 (c במקום a) – נדחות.
האוטומט מקבל בדיוק את המילים של השפה – כך עולה גם מבדיקה של כל המילים הקצרות מעל {a, b, c}. מתקבלות למשל: bc, bbcc, abca, aabcaa, abbcca.
טעות נפוצה: לסמן את q0 כמקבל – ε אינה בשפה, כי m ≥ 1.
שאלה 9 · מכונת טיורינג – מעקב ותיאור (25 נקודות)
לפניכם מכונת טיורינג המקבלת מילים מעל הא"ב {a, b} (מקרא: R – ימין, L – שמאל).

בסעיף א שלפניכם תתבקשו לבצע מעקב אחר פעולת המכונה. במעקב יש להראות בכל שלב את הסרט, מעל איזה תא ממוקם הראש הקורא, ובאיזה מצב q₀–q₆ נמצאים.
להלן דוגמה לאופן שבו יש לבצע מעקב בעבור המכונה הנתונה והמילה a.

עקבו אחרי פעולת המכונה בעבור המילה ba . יש להציג את המעקב.
עקבו אחרי פעולת המכונה בעבור המילה abb . יש להציג את המעקב.
הציגו את הסרט המתקבל בגמר הפעולה בעבור המילה babaa . אין צורך להציג את המעקב.
כמה מילים באורך 4 מעל הא"ב {a, b} אינן משתנות לאחר הרצת המכונה עליהן? נמקו את תשובתכם.
כתבו בקצרה מה המכונה עושה.
◂ פתרון מלא לשאלה 9
סעיף א(1)
7 שלבים (0–6), מסתיים ב־q₆ ; המילה מתקבלת והסרט בסוף: ⊣ b b Δ …
מה נדרש: מעקב מלא על המילה ba.
רעיון: מריצים את המעברים אחד אחד. מעברי המכונה: q₀: a/a,R → q₁ ; b/b,R → q₂ . q₁: a/a,R → q₁ ; b/b,R → q₁ ; Δ/Δ,L → q₃ . q₂: a/a,R → q₂ ; b/b,R → q₂ ; Δ/Δ,L → q₄ . q₃: a/a,L → q₅ ; b/a,L → q₅ . q₄: a/b,L → q₅ ; b/b,L → q₅ . q₅: a/a,L → q₅ ; b/b,L → q₅ ; ⊣/⊣,R → q₆ . q₆ – מצב מקבל.
בטבלה: תא 0 הוא הסמן ⊣ , הראש הקורא מסומן ב־[ ].
פתרון:
| שלב | מצב | תא 0 | תא 1 | תא 2 | תא 3 | תא 4 | תא 5 | |
|---|---|---|---|---|---|---|---|---|
| 0 | q₀ | ⊣ | [b] | a | Δ | Δ | Δ | ... |
| 1 | q₂ | ⊣ | b | [a] | Δ | Δ | Δ | ... |
| 2 | q₂ | ⊣ | b | a | [Δ] | Δ | Δ | ... |
| 3 | q₄ | ⊣ | b | [a] | Δ | Δ | Δ | ... |
| 4 | q₅ | ⊣ | [b] | b | Δ | Δ | Δ | ... |
| 5 | q₅ | [⊣] | b | b | Δ | Δ | Δ | ... |
| 6 | q₆ | ⊣ | [b] | b | Δ | Δ | Δ | ... |
שלב 0→1: ב־q₀ קוראים b ⇐ משאירים, זזים ימינה ל־q₂ (המכונה "זוכרת" שהאות הראשונה היא b). שלב 1→2: q₂ ממשיך ימינה. שלב 2→3: ב־Δ חוזרים צעד שמאלה – אל האות האחרונה – ב־q₄. שלב 3→4: q₄ כותב b במקום ה־a האחרון. שלבים 4→6: q₅ חוזר שמאלה עד ⊣ , ואז ימינה ל־q₆. המילה מתקבלת, ועל הסרט bb.
בדיקה: המעקב עובר בדיוק בשלבים האלה.
סעיף א(2)
9 שלבים (0–8), מסתיים ב־q₆ ; המילה מתקבלת והסרט בסוף: ⊣ a b a Δ …
מה נדרש: מעקב מלא על abb.
פתרון:
| שלב | מצב | תא 0 | תא 1 | תא 2 | תא 3 | תא 4 | תא 5 | תא 6 | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | q₀ | ⊣ | [a] | b | b | Δ | Δ | Δ | ... |
| 1 | q₁ | ⊣ | a | [b] | b | Δ | Δ | Δ | ... |
| 2 | q₁ | ⊣ | a | b | [b] | Δ | Δ | Δ | ... |
| 3 | q₁ | ⊣ | a | b | b | [Δ] | Δ | Δ | ... |
| 4 | q₃ | ⊣ | a | b | [b] | Δ | Δ | Δ | ... |
| 5 | q₅ | ⊣ | a | [b] | a | Δ | Δ | Δ | ... |
| 6 | q₅ | ⊣ | [a] | b | a | Δ | Δ | Δ | ... |
| 7 | q₅ | [⊣] | a | b | a | Δ | Δ | Δ | ... |
| 8 | q₆ | ⊣ | [a] | b | a | Δ | Δ | Δ | ... |
הסבר: האות הראשונה היא a ⇐ q₁ (0→1). q₁ עובר ימינה עד Δ (1→3), חוזר אל האות האחרונה ב־q₃ (3→4), וכותב שם a במקום b (4→5). q₅ חוזר שמאלה עד ⊣ (5→7), ו־q₆ (7→8). המילה מתקבלת, ועל הסרט aba.
בדיקה: המעקב עובר באותם 9 שלבים, ובסוף: מתקבלת, תוכן הסרט aba.
סעיף א(3)
⊣ b a b a b Δ … (על הסרט: babab)
מה נדרש: תוכן הסרט בסוף הריצה על babaa.
רעיון: כפי שראינו בסעיפים (1) ו־(2), המכונה זוכרת את האות הראשונה (q₁ – a , q₂ – b), עוברת לסוף המילה וכותבת את האות הזו במקום האות האחרונה.
פתרון: האות הראשונה של babaa היא b , והאחרונה (תא 5) היא a – היא מוחלפת ב־b. הסרט בסוף:
| תא 0 | תא 1 | תא 2 | תא 3 | תא 4 | תא 5 | תא 6 | |
|---|---|---|---|---|---|---|---|
| ⊣ | b | a | b | a | b | Δ | ... |
כלומר על הסרט babab, והמכונה עוצרת ב־q₆ (מקבלת).
בדיקה: עבור babaa: מתקבלת, תוכן הסרט babab.
סעיף ב(1)
8 מילים – בדיוק המילים שהאות האחרונה בהן שווה לאות הראשונה (2 · 2 · 2 = 8).
מה נדרש: לספור את המילים באורך 4 שהמכונה משאירה כמו שהן.
רעיון: המכונה משנה רק את האות האחרונה – כותבת בה את האות הראשונה. המילה לא משתנה בדיוק כשהאות האחרונה כבר שווה לראשונה.
פתרון: במילה x₁x₂x₃x₄ יש לבחור את x₁ (2 אפשרויות), את x₂ ו־x₃ (2 אפשרויות לכל אחת), ו־x₄ חייבת להיות שווה ל־x₁ (אפשרות אחת). סך הכול 2 · 2 · 2 = 8 מילים: aaaa , aaba , abaa , abba , baab , babb , bbab , bbbb.
בדיקה: מבין 16 המילים באורך 4 בדיוק 8 נשארות ללא שינוי, אלה שברשימה.
סעיף ב(2)
המכונה מחליפה את האות האחרונה במילה בעותק של האות הראשונה, וחוזרת לתחילת הסרט. היא מקבלת כל מילה לא ריקה ודוחה את המילה הריקה.
מה נדרש: תיאור קצר של פעולת המכונה.
פתרון: המכונה כותבת במקום האות האחרונה של המילה את האות הראשונה שלה: ב־q₀ היא קוראת את האות הראשונה ו"זוכרת" אותה במצב (q₁ – a , q₂ – b), עוברת ימינה עד סוף המילה, חוזרת צעד אחד אל האות האחרונה (q₃ / q₄) וכותבת שם את האות שזכרה. אחר כך q₅ חוזר שמאלה עד הסמן ⊣ , והמכונה עוצרת ב־q₆ מעל התא הראשון. במילה בת אות אחת האות הראשונה היא גם האחרונה, והמילה לא משתנה. המכונה מקבלת כל מילה לא ריקה; במילה הריקה q₀ קורא Δ , אין מעבר – והמילה נדחית.
בדיקה: ba → bb , abb → aba , babaa → babab , a → a , המילה הריקה – נדחית. פלט המכונה הוא בדיוק הפונקציה הזו לכל מילה.
תכנות מונחה עצמים
יש לענות על שתי שאלות במסלול שלמדתם (לכל שאלה – 25 נקודות).
שאלה 10 · פנסיון לחיות מחמד – ירושה, הכלה ופעולת עלות (25 נקודות)
בפנסיון לחיות מחמד "זנב שמח" פותחה מערכת ממוחשבת שבה המחלקות האלה:
Stay – שהייה, Pet – חיית מחמד, Cat – חתול, Dog – כלב, Rabbit – ארנב.
להלן פירוט תכונות המחלקות:
meals – מספר הארוחות המיוחדות שהוזמנו (מטיפוס שלם).
חתול בית – indoor (בוליאני. אם החתול אינו יוצא מהבית התכונה היא אמת ואם לא, היא שקר).
משקל הכלב בקילוגרמים – weight (מטיפוס שלם).
גיל הארנב בשנים – age (מטיפוס שלם).
סרטטו תרשים הייררכייה המתאר את הקשר בין המחלקות של המערכת הממוחשבת.
יש לסמן ירושה באמצעות החץ
והכלה באמצעות הסימן
.
כתבו את כותרות המחלקות ואת התכונות שלהן. הניחו שהפעולות get ו־ set קיימות בכל התכונות של המחלקות,
ואין צורך לממש אותן.
נתונה הפעולה הבונה של המחלקה Stay :
public Stay (String owner, int nights, int meals)אין צורך לממש את הפעולה.
(1) לפניכם כותרת הפעולה הבונה של המחלקה Pet . הפעולה מקבלת שם בעלים, מספר לילות, מספר ארוחות
מיוחדות ומספר שבב.
public Pet (String owner, int nights, int meals, String chip)ממשו את הפעולה הבונה.
לפניכם כותרת הפעולה הבונה של המחלקה Dog . הפעולה מקבלת שם בעלים, מספר לילות, מספר ארוחות מיוחדות,
מספר שבב ומשקל.
public Dog (String owner, int nights, int meals, String chip, int weight)ממשו את הפעולה הבונה.
העלות הבסיסית של שהיית חיית מחמד – Pet בפנסיון היא 90 שקלים ללילה ו־ 12 שקלים לכל ארוחה מיוחדת.
העלות של שהיית חתול (Cat) היא לפי העלות הבסיסית.
העלות של שהיית כלב (Dog) שמשקלו יותר מ־ 20 קילוגרמים היא העלות הבסיסית ונוסף על כך 15 שקלים לכל לילה.
העלות של שהיית כלב שמשקלו 20 קילוגרמים או פחות היא לפי העלות הבסיסית.
העלות של שהיית ארנב (Rabbit) היא 75% מן העלות הבסיסית.
ג. הפעולה cost מחזירה מספר ממשי השווה לסכום שהבעלים נדרשים לשלם בעבור כל אחד מסוגי החיות שבפנסיון
(בהתאם לעצם שזימן את הפעולה).
(1) כתבו את הפעולה cost במחלקה Pet (כאמור לעיל, הסכום לתשלום הוא לפי העלות הבסיסית).
הוסיפו את הפעולה cost במחלקה/ות האחרות כדי לבצע את הנדרש (רק במחלקות שיש בהן צורך), לפי
העקרונות של תכנות מונחה עצמים.
הערה: אין להשתמש בפעולה instanceOf בסעיף זה ובפעולות של המחלקה Object ואין לשנות את תכונות המחלקות.
פתרון הכולל שימושים כאלה לא יזוכה בנקודות.
◂ פתרון מלא לשאלה 10
סעיף א(1)
Cat, Dog ו־Rabbit יורשות מ־Pet (חץ ירושה אל Pet); Pet מכילה עצם מטיפוס Stay (הכלה, המעוין בצד של Pet).
מה נדרש – לסרטט את הקשרים בין חמש המחלקות: מי יורש ממי ומי מכיל את מי.
רעיון – חתול, כלב וארנב הם חיות מחמד ("is-a") ⇒ הם יורשים מ־Pet. לכל חיית מחמד בפנסיון יש שהייה ("has-a") ⇒ הכלה: ב־Pet יש תכונה stay מטיפוס Stay. התכונות chip ו־stay שמופיעות גם בפירוט של Cat, Dog ו־Rabbit עוברות אליהן בירושה מ־Pet – לכן לא מגדירים אותן שוב, ולא מחברים את Stay ישירות לכל אחת מהן.
פתרון
Stay ───────────◆ Pet
△
┌──────────┼──────────┐
│ │ │
Cat Dog Rabbitבדיקה – Cat "היא" Pet ✓, Dog "היא" Pet ✓, Rabbit "היא" Pet ✓; ל־Pet "יש" Stay ✓; שהייה אינה חיית מחמד, לכן אין ירושה בין Stay ל־Pet.
טעות נפוצה: לסמן את Stay כמחלקת־על של Pet ("חיה היא שהייה") – הקשר ביניהן הוא הכלה, לא ירושה.
סעיף א(2)
Stay: owner, nights, meals; Pet: chip, stay; Cat extends Pet: indoor; Dog extends Pet: weight; Rabbit extends Pet: age.
מה נדרש – לכתוב את כותרות חמש המחלקות ואת התכונות שלהן (בלי פעולות).
רעיון – התכונות פרטיות (private). במחלקות היורשות כותבים רק את התכונה החדשה – chip ו־stay כבר נמצאות ב־Pet ועוברות בירושה. ב־Java הירושה נכתבת extends Pet, וב־C# : Pet. התכונה stay היא מטיפוס המחלקה Stay (הכלה); indoor בוליאנית – boolean ב־Java ו־bool ב־C#.
פתרון
public class Stay {
private String owner;
private int nights;
private int meals;
}
public class Pet {
private String chip;
private Stay stay;
}
public class Cat extends Pet {
private boolean indoor;
}
public class Dog extends Pet {
private int weight;
}
public class Rabbit extends Pet {
private int age;
}בדיקה – שני הקבצים עוברים הידור (Java ו־C#).
טעות נפוצה: להגדיר שוב chip ו־stay במחלקות היורשות – כך נוצרות תכונות כפולות שמסתירות את אלה של Pet.
public class Stay {
private String owner;
private int nights;
private int meals;
}
public class Pet {
private String chip;
private Stay stay;
}
public class Cat extends Pet {
private boolean indoor;
}
public class Dog extends Pet {
private int weight;
}
public class Rabbit extends Pet {
private int age;
}סעיף ב(1)
this.stay = new Stay(owner, nights, meals); this.chip = chip;
מה נדרש – לממש את הפעולה הבונה של Pet שמקבלת את נתוני השהייה ואת מספר השבב.
רעיון – Pet מכילה שהייה, ולכן הפעולה הבונה יוצרת עצם Stay חדש מהנתונים שקיבלה (owner, nights, meals) בעזרת הפעולה הבונה הנתונה של Stay, ושומרת אותו בתכונה stay. את chip שומרים ישירות.
פתרון
(בתוך המחלקה Pet):
public Pet(String owner, int nights, int meals, String chip) {
this.stay = new Stay(owner, nights, meals);
this.chip = chip;
}בדיקה – new Pet("Gal", 4, 8, "P-100") יוצר שהייה של 4 לילות ו־8 ארוחות, והעלות שלה 456 (ראו סעיף ג).
טעות נפוצה: לשמור את owner, nights ו־meals בתכונות של Pet – אין לה תכונות כאלה; הנתונים שייכים לעצם ה־Stay.
public Pet(String owner, int nights, int meals, String chip) {
this.stay = new Stay(owner, nights, meals);
this.chip = chip;
}סעיף ב(2)
Java: super(owner, nights, meals, chip); this.weight = weight; C#: : base(owner, nights, meals, chip) { this.weight = weight; }
מה נדרש – לממש את הפעולה הבונה של Dog.
רעיון – התכונות chip ו־stay פרטיות ב־Pet, ולכן Dog מעבירה את ארבעת הנתונים הראשונים לפעולה הבונה של מחלקת־העל (super(...) ב־Java – בשורה הראשונה, : base(...) ב־C#), ואחר כך מאתחלת את התכונה שלה – weight. (בקוד המלא שלפניכם מופיעה גם הפעולה cost מסעיף ג2.)
פתרון
public class Dog extends Pet {
private int weight;
public Dog(String owner, int nights, int meals, String chip, int weight) {
super(owner, nights, meals, chip);
this.weight = weight;
}
public double cost() {
if (this.weight > 20)
return super.cost() + 15 * getStay().getNights();
return super.cost();
}
}בדיקה – new Dog("Gal", 4, 8, "P-102", 30) תקין בשתי השפות; השהייה שנוצרת בתוכו היא של 4 לילות ו־8 ארוחות.
public class Dog extends Pet {
private int weight;
public Dog(String owner, int nights, int meals, String chip, int weight) {
super(owner, nights, meals, chip);
this.weight = weight;
}
public double cost() {
if (this.weight > 20)
return super.cost() + 15 * getStay().getNights();
return super.cost();
}
}סעיף ג(1)
Java: return 90 * this.stay.getNights() + 12 * this.stay.getMeals(); (C#: public virtual double Cost() עם GetNights/GetMeals)
מה נדרש – לכתוב ב־Pet פעולה שמחזירה את העלות הבסיסית: 90 ש"ח ללילה ועוד 12 ש"ח לארוחה מיוחדת.
רעיון – מספר הלילות ומספר הארוחות שמורים בעצם השהייה, והתכונות שלו פרטיות – לכן ניגשים אליהן דרך פעולות ה־get של Stay. ב־C# מגדירים את הפעולה virtual כדי שאפשר יהיה לדרוס אותה במחלקות היורשות.
פתרון
(בתוך Pet):
public double cost() {
return 90 * this.stay.getNights() + 12 * this.stay.getMeals();
}בדיקה – 4 לילות ו־8 ארוחות: 90·4 + 12·8 = 360 + 96 = 456. התוצאה: 456 בשתי השפות.
טעות נפוצה: לגשת ל־stay.nights ישירות – התכונה פרטית במחלקה Stay, ולכן זו שגיאת הידור.
public double cost() {
return 90 * this.stay.getNights() + 12 * this.stay.getMeals();
}סעיף ג(2)
Cat – אין צורך. Dog: אם weight > 20 מחזירה super.cost() + 15 * getStay().getNights(), אחרת super.cost(); Rabbit: return super.cost() * 0.75; (C#: override ו־base.Cost())
מה נדרש – להוסיף cost רק במחלקות שבהן העלות שונה מהעלות הבסיסית, בלי לבדוק את סוג העצם.
רעיון – פולימורפיזם: Dog ו־Rabbit דורסות את cost, והפעולה המתאימה נבחרת בזמן ריצה לפי סוג העצם. חתול משלם לפי העלות הבסיסית – לכן לא כותבים cost ב־Cat. בכלב התוספת תלויה בתכונה של הכלב עצמו (weight), ולכן הבדיקה weight > 20 נמצאת בתוך הפעולה הדורסת של Dog – זו לא בדיקת סוג, אלא בדיקת ערך של תכונה. את מספר הלילות מקבלים דרך getStay().getNights() (stay פרטית ב־Pet). בשני המקרים משתמשים בחישוב הבסיסי דרך super.cost() (ב־C# base.Cost()). ב־C# הפעולות מסומנות override.
פתרון
public class Dog extends Pet {
private int weight;
public Dog(String owner, int nights, int meals, String chip, int weight) {
super(owner, nights, meals, chip);
this.weight = weight;
}
public double cost() {
if (this.weight > 20)
return super.cost() + 15 * getStay().getNights();
return super.cost();
}
}
public class Rabbit extends Pet {
private int age;
public Rabbit(String owner, int nights, int meals, String chip, int age) {
super(owner, nights, meals, chip);
this.age = age;
}
public double cost() {
return super.cost() * 0.75;
}
}בדיקה – מערך Pet[] (4 לילות, 8 ארוחות): Pet 456, Cat 456, Dog של 30 ק"ג 516 (=456+15·4), Dog של 20 ק"ג בדיוק 456 (אין תוספת – "יותר מ־20"), Rabbit 342 (=456·0.75). כלב של 21 ק"ג ל־2 לילות ו־3 ארוחות: 180+36+30 = 246; ארנב ללילה אחד וארוחה אחת: 102·0.75 = 76.5. התוצאות זהות ב־Java וב־C#.
טעות נפוצה: לכתוב weight >= 20 – כלב של 20 ק"ג בדיוק לא משלם תוספת. וגם: לכתוב cost מיותרת ב־Cat, או לבדוק את סוג העצם (instanceof / is) ב־Pet במקום לדרוס.
public class Dog extends Pet {
private int weight;
public Dog(String owner, int nights, int meals, String chip, int weight) {
super(owner, nights, meals, chip);
this.weight = weight;
}
public double cost() {
if (this.weight > 20)
return super.cost() + 15 * getStay().getNights();
return super.cost();
}
}
public class Rabbit extends Pet {
private int age;
public Rabbit(String owner, int nights, int meals, String chip, int age) {
super(owner, nights, meals, chip);
this.age = age;
}
public double cost() {
return super.cost() * 0.75;
}
}שאלה 11 · פעולות בונות, תכונת מחלקה (static), תרשימי הייררכייה והשלמת מחלקת־על (Box, Lamp) (25 נקודות)
לפניכם שלושה סעיפים א–ג שאין קשר ביניהם. ענו על שלושת הסעיפים.
לפניכם המחלקה Box (קופסה) ללא מימוש הפעולות הבונות, והמחלקה GiftBox (קופסת מתנה) ללא מימוש הפעולות הבונות ופעולת toString .
public class Box {
private static int made = 0;
protected int size;
public Box () {…}
public Box (int size) {…}
public String toString() {return "made=" + made + ", size=" + size;}
}
------------------------------------------------------------------
public class GiftBox extends Box {
private String color;
public GiftBox (String color) {…}
public GiftBox (int size, String color) {…}
public String toString() {…}
}לפניכם קטע קוד והפלט שהתקבל בעקבות הרצת קטע הקוד:
הקוד
Box b1 = new Box ();
System.out.println (b1); //1
Box b2 = new Box (8);
System.out.println (b2); //2
Box b3 = new GiftBox ("red");
System.out.println (b3); //3
Box b4 = new GiftBox (6, "green");
System.out.println (b4); //4
Box b5 = new GiftBox ("yellow");
System.out.println (b5); //5הפלט
made=0, size=1 //1
made=1, size=8 //2
made=2, size=3, color=red //3
made=2, size=6, color=green //4
made=3, size=6, color=yellow //5ממשו את הפעולות הבונות במחלקה Box ובמחלקה GiftBox , ואת הפעולה toString במחלקה GiftBox , בהתאם לעקרונות תכנות מונחה עצמים, כך שיתקבל הפלט הנתון.
אין להוסיף פעולות נוספות למחלקות. תשובה הכוללת פעולות נוספות במחלקות, לא תזוכה בנקודות.
נתונות המחלקות K, L, M, N והפעולה main במחלקה Tester , שרצה באופן תקין.
public class Tester {
public static void main(String[] args) {
L l1 = new N();
K k1 = new M();
K k2 = new L();
}
}לפניכם שישה תרשימי הייררכייה 1–6 (החץ מסמן יחס ירושה). בנוגע לכל אחד מהם, ציינו אם ייתכן או לא ייתכן שהוא מתאר את יחס ההייררכייה בין המחלקות K, L, M, N . אם ציינתם שלא ייתכן, נמקו מדוע.

בסוף הפעולה main נוספו שורות הקוד שלהלן, שרצות באופן תקין:
K k3 = new M();
L l2 = (L)k3;בעקבות זאת, האם תשתנה תשובתכם בתת־סעיף ב(1)? נמקו את תשובתכם.
נתונה המחלקה Lamp (מנורה), היורשת מהמחלקה Device (מכשיר):
public class Lamp extends Device {
private int watts;
public Lamp (String code, int watts) {
super (code, watts * 2);
this.watts = watts;
}
public Lamp (int watts) {this.watts = watts; }
public String info () {return super.info() + "/" + this.watts; }
public double cost (int hours) {return hours * this.watts * Device.rate; }
public boolean isOld () {return this.isOld (2020) || this.broken; }
}כתבו במחלקה Device את תכונות המחלקה (כולל הרשאות הגישה) ואת הכותרות של הפעולות הנדרשות כדי שהמחלקה Lamp תהיה תקינה, בהתאם לעקרונות תכנות מונחה עצמים.
אין צורך לממש את הפעולות שאת הכותרות שלהן הוספתם במחלקה Device . אין לשנות את הקוד של המחלקה Lamp.
◂ פתרון מלא לשאלה 11
סעיף א
Box(): size=1 · Box(int size): made++, size=size · GiftBox(String color): super(color.length()); color=color · GiftBox(int size, String color): super(); this.size=size; color=color · toString: super.toString() + ", color=" + color
מה נדרש – לממש את הפעולות הבונות של Box ושל GiftBox ואת toString של GiftBox כך שיתקבל הפלט הנתון.
רעיון – מסיקים מהפלט, שורה אחר שורה. made היא תכונת מחלקה (static) פרטית של Box: יש עותק אחד שלה לכל העצמים, ו־GiftBox לא יכולה לגשת אליה. לכן רק פעולה בונה של Box יכולה לשנות אותה.
new Box() הדפיס made=0, size=1 ⇐ הבונה בלי פרמטרים לא משנה את made ומציבה size=1.new Box(8) הדפיס made=1, size=8 ⇐ הבונה עם פרמטר מגדילה את made ב־1 ומציבה את size.new GiftBox("red") הדפיס made=2 ⇐ made גדל, ולכן הבונה של GiftBox חייבת לזמן את Box(int). הגודל 3 הוא אורך המחרוזת "red", וזה מתאשר ב־//5: ל־"yellow" (6 תווים) התקבל size=6 ⇐ super(color.length()) / : base(color.Length).new GiftBox(6, "green") הדפיס made=2 (לא השתנה) ⇐ הבונה הזו מזמנת את Box() (במפורש או אוטומטית), שלא משנה את made, ואחר כך מציבה size=6 בעצמה. זה מותר, כי size מוגדרת protected.super.toString() / base.ToString()) ומוסיפה ", color=" + color. כך אין שכפול קוד ואין צורך בגישה ל־made, שהיא private.פתרון
public class Box {
private static int made = 0;
protected int size;
public Box() {
this.size = 1; // לא משנה את made
}
public Box(int size) {
made++; // הפעולה הבונה היחידה שמגדילה את made
this.size = size;
}
public String toString() {return "made=" + made + ", size=" + size;}
}
public class GiftBox extends Box {
private String color;
public GiftBox(String color) {
super(color.length()); // הגודל הוא אורך שם הצבע, ו-made גדל
this.color = color;
}
public GiftBox(int size, String color) {
super(); // לא מגדילה את made
this.size = size; // size מוגדרת protected ולכן נגישה במחלקה היורשת
this.color = color;
}
public String toString() {
return super.toString() + ", color=" + this.color;
}
}בדיקה – קטע הקוד מהשאלה (Java ו־C#) מדפיס בדיוק:
made=0, size=1
made=1, size=8
made=2, size=3, color=red
made=2, size=6, color=green
made=3, size=6, color=yellowטעות נפוצה: לממש את GiftBox(int size, String color) עם super(size). כך made גדל, ובשורה //4 מודפס made=3, size=6, color=green (ובשורה //5 made=4). גם מימוש של Box() כ־this(1) שגוי: הוא מגדיל את made, ובשורה //1 מתקבל made=1.
public class Box {
private static int made = 0;
protected int size;
public Box() {
this.size = 1; // לא משנה את made
}
public Box(int size) {
made++; // הפעולה הבונה היחידה שמגדילה את made
this.size = size;
}
public String toString() {return "made=" + made + ", size=" + size;}
}
public class GiftBox extends Box {
private String color;
public GiftBox(String color) {
super(color.length()); // הגודל הוא אורך שם הצבע, ו-made גדל
this.color = color;
}
public GiftBox(int size, String color) {
super(); // לא מגדילה את made
this.size = size; // size מוגדרת protected ולכן נגישה במחלקה היורשת
this.color = color;
}
public String toString() {
return super.toString() + ", color=" + this.color;
}
}סעיף ב(1)
1 – לא ייתכן; 2 – ייתכן; 3 – לא ייתכן; 4 – ייתכן; 5 – לא ייתכן; 6 – ייתכן.
מה נדרש – לקבוע לכל אחד מששת התרשימים אם הוא מתאים לקוד שרץ באופן תקין.
רעיון – השמה X ref = new Y(); עוברת הידור רק אם Y היא X או תת־מחלקה של X (ישירה או עקיפה). מכל שורה ב־main מקבלים דרישה:
L l1 = new N(); ⇐ N יורשת (ישירות או בעקיפין) מ־L.K k1 = new M(); ⇐ M יורשת מ־K.K k2 = new L(); ⇐ L יורשת מ־K.פתרון
| תרשים | מבנה (בן → אב) | ייתכן? | נימוק |
|---|---|---|---|
| 1 | L→K, N→K, M→L | לא ייתכן | N ו־L הן "אחיות" (שתיהן יורשות מ־K), ו־N אינה יורשת מ־L ⇐ L l1 = new N(); שגיאת הידור. |
| 2 | L→K, M→L, N→M | ייתכן | N יורשת מ־L בעקיפין (דרך M), M יורשת מ־K בעקיפין (דרך L), ו־L יורשת מ־K. |
| 3 | K→L, N→L, M→K | לא ייתכן | L היא מחלקת־על של K, ולא תת־מחלקה שלה ⇐ K k2 = new L(); שגיאת הידור. |
| 4 | L→K, M→K, N→L | ייתכן | N יורשת מ־L, ו־L ו־M יורשות מ־K. |
| 5 | M→K, N→K, L→N | לא ייתכן | L יורשת מ־N, ולא להפך ⇐ L l1 = new N(); שגיאת הידור. |
| 6 | M→K, L→M, N→L | ייתכן | N יורשת מ־L, M מ־K, ו־L יורשת מ־K בעקיפין (דרך M). |
בדיקה – בנינו את המחלקות K, L, M, N לפי כל אחד מהתרשימים והידרנו את Tester (ב־Java וב־C#). בתרשימים 1, 3, 5 הייתה שגיאת הידור, ובתרשימים 2, 4, 6 התוכנית רצה תקין.
סעיף ב(2)
כן. ההמרה (L)k3 של עצם M מצליחה רק אם M יורשת מ־L, ולכן רק תרשים 2 נשאר אפשרי (ב־4 וב־6 תהיה שגיאת ריצה).
מה נדרש – לבדוק אילו מהתרשימים שנמצאו אפשריים ב־ב(1) (2, 4, 6) נשארים אפשריים אחרי השורות החדשות.
רעיון – k3 מפנה לעצם מטיפוס M. ההמרה (L)k3 עוברת הידור בכל שלושת התרשימים, כי L היא תת־מחלקה של K (טיפוס ההפניה). אבל בזמן ריצה היא מצליחה רק אם העצם (M) הוא באמת L, כלומר רק אם M יורשת (ישירות או בעקיפין) מ־L. אחרת נזרקת חריגה: ClassCastException ב־Java, InvalidCastException ב־C#.
פתרון – כן, התשובה משתנה: רק תרשים 2 נשאר אפשרי.
בדיקה – עם השורות הנוספות (Java ו־C#): בתרשימים 4 ו־6 מתקבלת שגיאת ריצה (ClassCastException / InvalidCastException), ובתרשים 2 התוכנית רצה תקין.
טעות נפוצה: לחשוב שאם ההמרה עוברת הידור היא תקינה. המהדר בודק רק שההמרה אפשרית לפי טיפוס ההפניה (K ⇐ L). את סוג העצם בפועל בודקים רק בזמן ריצה.
סעיף ג
protected boolean broken; protected static double rate; public Device(); public Device(String code, int power); public String info() (ב־C#: public virtual string Info()); public boolean isOld(int year) (ב־C#: public bool IsOld(int year))
מה נדרש – לכתוב את התכונות ואת כותרות הפעולות של Device שבלעדיהן Lamp לא תעבור הידור.
רעיון – עוברים על Lamp שורה אחר שורה ושואלים מה היא צריכה מ־Device:
| שורה ב־Lamp | מה נדרש ב־Device |
|---|---|
super(code, watts * 2) / : base(code, watts * 2) | פעולה בונה Device(String code, int power) |
Lamp(int watts) – אין קריאה מפורשת ל־super/base | מופעלת אוטומטית הבונה בלי פרמטרים ⇐ Device() |
super.info() + "/" + this.watts שמוחזר כמחרוזת | public String info(). ב־C# Lamp כותבת override, ולכן ב־Device: public virtual string Info() (הרשאת הגישה חייבת להיות זהה, ולכן protected virtual תגרום לשגיאת הידור) |
Device.rate – גישה דרך שם המחלקה, בתוך ביטוי מטיפוס ממשי | תכונת מחלקה (static) מטיפוס מספרי שנגישה ליורשת ⇐ protected static double rate (גם int מתאים) |
this.isOld(2020) || ... – ב־Lamp יש רק isOld() בלי פרמטרים | העמסה: public boolean isOld(int year) (ב־C#: public bool IsOld(int year)) |
... || this.broken | תכונה בוליאנית שנגישה ליורשת ⇐ protected boolean broken (עם private תהיה שגיאת הידור) |
פתרון (גופי הפעולות כאן ריקים או מחזירים ערך זמני, רק כדי שהקוד יעבור הידור; בבחינה מספיקות הכותרות)
public class Device {
protected boolean broken;
protected static double rate;
public Device() { }
public Device(String code, int power) { }
public String info() { return ""; }
public boolean isOld(int year) { return false; }
}אפשר להוסיף ל־Device תכונות פרטיות נוספות (למשל code ו־power, לשמירת הפרמטרים של הפעולה הבונה), אבל הן אינן נדרשות כדי ש־Lamp תהיה תקינה.
בדיקה – Device יחד עם Lamp ללא שינוי תקינות ב־Java וב־C#. בדקנו גם שכל רכיב נחוץ: בלי Device() או בלי Device(String, int), עם private boolean broken, עם rate שאינה static או שהיא private, בלי info() (וב־C# בלי virtual), בלי isOld(int) או כש־isOld(int) מחזירה int – בכל אחד מהמקרים התקבלה שגיאת הידור.
טעות נפוצה: לחשוב ש־this.isOld(2020) היא קריאה רקורסיבית ל־isOld() של Lamp. ל־isOld של Lamp אין פרמטרים, ולכן נדרשת ב־Device פעולה נוספת בשם isOld שמקבלת מספר ומחזירה boolean (העמסה).
public class Device {
protected boolean broken;
protected static double rate;
public Device() { }
public Device(String code, int power) { }
public String info() { return ""; }
public boolean isOld(int year) { return false; }
}שאלה 12 · רובוט ורובוט ניקוי – עצמים, פלט ותקינות פקודות (25 נקודות)
לפניכם המחלקות Robot (רובוט) ו־ Cleaner (רובוט ניקוי):
public class Robot
{
private static int made = 0;
protected String name;
protected int energy;
public Robot (String name) {
this.name = name;
this.energy = 100;
made++;
System.out.println ("Robot " + name);
}
public static int getMade() {
return made;
}
public int getEnergy() {
return energy;
}
public void work (int hours) {
this.energy -= 10 * hours;
System.out.println (this.name + " worked, energy = " + this.energy);
}
public String describe() {
return this.name + "(" + this.energy + ")";
}
}
public class Cleaner extends Robot
{
private int rooms;
public Cleaner (String name) {
super (name);
this.rooms = 0;
System.out.println ("Cleaner");
}
public Cleaner (String name, int energy) {
super (name);
this.energy = energy;
this.rooms = 0;
System.out.println ("Cleaner " + energy);
}
public void work (int hours) {
super.work (hours);
this.rooms += 2 * hours;
}
public void work (int hours, int extra) {
work (hours + extra);
System.out.println ("rooms = " + this.rooms);
}
public String describe() {
return super.describe() + " rooms: " + this.rooms;
}
}נתונה המחלקה Tester :
public class Tester
{
public static void main(String[] args)
{
Robot r1 = new Robot ("Alfa");
Robot r2 = new Cleaner ("Beta");
Cleaner c1 = new Cleaner ("Gama", 60);
Cleaner c2 = new Cleaner ("Delta");
// ***
}
}ציירו את העצמים שנוצרו בפעולה main , וכתבו את הפלט של הפעולה.
הציבו כל אחת מן הפקודות 1–10 שלהלן בפעולה main במקום המצוין לעיל ב־ * .
כתבו במחברת את מספר הפקודה וציינו אם הקוד תקין או לא תקין. אם הקוד תקין – כתבו את הפלט (אין צורך לחזור על הפלט של הפעולות הבונות מסעיף א), ואם הוא אינו תקין – הסבירו מדוע.
הערה: אין קשר בין הפקודות. כלומר, יש להתייחס לכל פקודה כאילו היא היחידה בפעולה main .
1. System.out.println (r1.describe() + " " + Robot.getMade());
2. r2.work (1);
3. r2.work (1, 2);
4. c1.work (1, 2);
5. System.out.println (r2.describe());
6. c2 = r2;
7. ((Cleaner)r1).work (1, 1);
8. Robot r3 = new Cleaner ("Eps", 50, 2);
9. Cleaner c3 = (Cleaner)r2; c3.work (0, 1);
10. ((Robot)c2).work (2);◂ פתרון מלא לשאלה 12
סעיף א
Robot Alfa / Robot Beta / Cleaner / Robot Gama / Cleaner 60 / Robot Delta / Cleaner. עצמים: r1 Robot(Alfa,100), r2 Cleaner(Beta,100,0), c1 Cleaner(Gama,60,0), c2 Cleaner(Delta,100,0); made = 4.
מה נדרש – לצייר את ארבעת העצמים ולכתוב את הפלט של הפעולות הבונות.
רעיון
Robot(name) מציבה energy = 100, מגדילה את made ומדפיסה Robot <name>.super(name) (ב־C#: : base(name)), ורק אחר כך ממשיכות. Cleaner(name, energy) משנה אחר כך את energy (התכונה protected, ולכן נגישה במחלקה היורשת) – אצל Gama היא 60.פתרון
תרשים העצמים:

הפלט:
Robot Alfa
Robot Beta
Cleaner
Robot Gama
Cleaner 60
Robot Delta
Cleanerבדיקה – הפלט ב־Java וב־C# הוא בדיוק הפלט שלמעלה.
טעות נפוצה: לחשוב שאצל Gama האנרגיה היא 100 – הפעולה הבונה של Robot מציבה 100, אבל מיד אחריה הפעולה הבונה של Cleaner מציבה 60.
סעיף ב
1 תקין – Alfa(100) 4 | 2 תקין – Beta worked, energy = 90 | 3 לא תקין (הידור: אין work(int,int) ב־Robot) | 4 תקין – Gama worked, energy = 30 / rooms = 6 | 5 תקין – Beta(100) rooms: 0 | 6 לא תקין (הידור: Robot ל־Cleaner בלי המרה) | 7 לא תקין (שגיאת ריצה – המרה לא חוקית) | 8 לא תקין (הידור: אין פעולה בונה עם 3 פרמטרים) | 9 תקין – Beta worked, energy = 90 / rooms = 2 | 10 תקין – Delta worked, energy = 80
מה נדרש – לכל פקודה: האם היא עוברת הידור, האם היא רצה בלי שגיאה, ומה היא מדפיסה.
רעיון – שלושה כללים:
פתרון
| # | תקין? | פלט / הסבר |
|---|---|---|
| 1 | תקין | Alfa(100) 4 – describe של Robot (r1 הוא Robot) ו־getMade סטטית: נוצרו 4 רובוטים. |
| 2 | תקין | Beta worked, energy = 90 – r2 מפנה לעצם Cleaner, ולכן מופעלת work(int) של Cleaner: היא קוראת ל־super.work(1) (מדפיסה energy = 90) ומוסיפה 2 ל־rooms (לא מודפס). |
| 3 | לא תקין | שגיאת הידור: r2 היא הפניה מטיפוס Robot , ובמחלקה Robot אין פעולה work עם שני פרמטרים (העמסה זו קיימת רק ב־Cleaner). |
| 4 | תקין | Gama worked, energy = 30 ובשורה הבאה rooms = 6 – work(1, 2) של Cleaner מזמנת work(3): Gama מתחיל עם 60 אנרגיה, 60 − 30 = 30, ו־rooms = 2·3 = 6. |
| 5 | תקין | Beta(100) rooms: 0 – מופעלת describe של Cleaner (הטיפוס בפועל), שמשרשרת את התוצאה של describe של Robot. |
| 6 | לא תקין | שגיאת הידור: אי אפשר להציב הפניה מטיפוס Robot במשתנה מטיפוס Cleaner בלי המרה מפורשת. |
| 7 | לא תקין | שגיאת ריצה: הפקודה עוברת הידור (יש המרה מפורשת), אבל r1 מפנה לעצם Robot ולא Cleaner – ההמרה נכשלת (Java: ClassCastException , C#: InvalidCastException). |
| 8 | לא תקין | שגיאת הידור: ל־Cleaner אין פעולה בונה עם שלושה פרמטרים. |
| 9 | תקין | Beta worked, energy = 90 ובשורה הבאה rooms = 2 – r2 מפנה באמת לעצם Cleaner, ולכן ההמרה מצליחה; work(0, 1) מזמנת work(1): energy = 90, rooms = 2. |
| 10 | תקין | Delta worked, energy = 80 – ההמרה ל־Robot אינה משנה את העצם: מופעלת work של Cleaner: 100 − 20 = 80 (rooms גדל ל־4, לא מודפס). |
בדיקה – כל פקודה לבדה בפעולה הראשית תקינה ב־Java וב־C#. הפקודות הלא תקינות נכשלו בהידור בשתי השפות, פרט לפקודה שנכשלה בזמן ריצה (ב־Java: ClassCastException, ב־C#: InvalidCastException). הפלט של הפקודות התקינות זהה בשתי השפות.
טבלת התשובות
| שאלה | סעיף | תשובה |
|---|---|---|
| 1 | א(1) | ppp(q, 4) מחזירה false (c = 3: האיברים 4, 7, 9 גדולים מכל האיברים שלפניהם); התור חוזר למצבו המקורי. |
| א(2) | מחזירה true אם בתור יש לפחות k איברים שכל אחד מהם גדול מכל האיברים שלפניו בתור ("שיאים"; האיבר שבראש התור תמיד נספר), אחרת false; התור נשאר ללא שינוי. | |
| א(3) | O(n) – סיבוב אחד של התור: n סיבובי לולאה, O(1) בכל סיבוב. | |
| ב(1) | find(q, 1) מחזירה 3 (k = 1, 2, 3 – true; k = 4 – false ⇒ מוחזר 4 − 1 = 3). | |
| ב(2) | find(q, 1) מחזירה את מספר האיברים בתור שגדולים מכל האיברים שלפניהם (מספר ה"שיאים"); התור לא משתנה. | |
| ב(3) | O(n²) במקרה הגרוע: עד n + 1 קריאות רקורסיביות, בכל אחת ppp ב־O(n). | |
| 2 | א | pv, pj על החוליה הראשונה; כל עוד count < steps וגם pv != null: sum += pv.getValue(), count++, ואז מקדמים את pv ו־pj במקביל jump = pj.getValue() פעמים (עוצרים אם pv נהיה null). |
| ב | pv, pj עוברים במקביל על כל המיקומים; לכל מיקום: אם collect(pv, pj, steps) == target מחזירים true; בסוף false. | |
| 3 | - | עלה ← true. בצומת אחר: אם ערכו שונה מ־ sumTree(left) + sumTree(right) ← false; אחרת בודקים ברקורסיה כל בן קיים. בדוגמה הראשונה מתקבל true, ובשנייה false (בצומת 5: 2 + 4 = 6). |
| 4 | א | BFS מ־ vₛ שבו לכל קודקוד שמתגלה שומרים את ה"הורה" π[v] (הקודקוד שממנו התגלה). אם vₜ לא התגלה – מחזירים רשימה ריקה; אחרת הולכים מ־ vₜ אחורה לפי π עד vₛ ומחזירים את המסלול בסדר הפוך. זמן ריצה O(|V| + |E|). בדוגמה: 2, 4, 9. |
| ב | עוברים על הקודקודים לפי הסדר; מכל קודקוד שעוד לא סומן מריצים BFS שאוסף את הרכיב ומסכם את דרגות הקודקודים שלו. מספר הקשתות ברכיב = סכום הדרגות / 2. שומרים את הרכיב עם המספר הגדול ביותר. זמן ריצה O(|V| + |E|). בדוגמה: {2, 4, 6, 9, 10} עם 6 קשתות. | |
| 5 | א | בונים את הגרף ההפוך (כל קשת u → v הופכת ל־ v → u) ב־O(|V| + |E|), ומריצים בו BFS (או DFS) מ־ vⱼ . כל קודקוד שמתגלה (חוץ מ־ vⱼ) מוחזר. זמן ריצה O(|V| + |E|). בדוגמה מתגלים 2 , 3 , 8 ואחר כך 0 . |
| ב | מיון טופולוגי, ואז מעבר על הקודקודים לפי הסדר: L[v] = האורך המקסימלי של שרשרת שמסתיימת ב־ v (L[v] = max(L[u] + 1) על כל קשת u → v), עם שמירת הקודם P[v]. מחזירים את השרשרת מהקודקוד שבו L מקסימלי אחורה לפי P. זמן ריצה O(|V| + |E|). בדוגמה: 0 , 2 , 5 , 1 , 7 , 4 . | |
| 6 | א | למשל: קודקודים 1–6, קשתות 1–2 , 2–3 , 4–5 , 4–6. אין קשת בין 3 ל־4, והוספת הקשת 3–4 נותנת עץ. |
| ב(1) | כן. מספר הקשתות הוא n−2. | |
| ב(2) | כן. יש בדיוק 2 רכיבי קשירות. | |
| ג(1) | G הוא גרף־עץ־שבור ⇔ יש בו בדיוק n−2 קשתות ובדיוק 2 רכיבי קשירות. האלגוריתם: סופרים קשתות (עוצרים אם יש יותר מ־n−2); אם ≠ n−2 – שקר; אחרת סופרים רכיבי קשירות בעזרת BFS מכל קודקוד שעוד לא התגלה: אם התקבלו בדיוק 2 – אמת, אחרת שקר. | |
| ג(2) | O(n) (ובאופן כללי O(V+E), אבל בזכות העצירה המוקדמת E ≤ n−2). | |
| 7 | א(1) | L₁ – לא רגולרית; L₂ – רגולרית (3 מצבים); L₃ – רגולרית. |
| א(2) | לא. L₁ ∩ L̅₃ סופית, ולכן אילו L₁ ∩ L₃ הייתה רגולרית, L₁ = (L₁ ∩ L₃) ∪ (L₁ ∩ L̅₃) הייתה רגולרית – סתירה. | |
| א(3) | כן. L̅₃ = כל המילים באורך קטן מ־12, ולכן L₁ ∩ L̅₃ סופית – וכל שפה סופית רגולרית. | |
| א(4) | לא. L₁ ⊆ L₂, ולכן L₁ = L₂ ∩ (משלים של (L₂ ∩ L̅₁)). אילו L₂ ∩ L̅₁ הייתה רגולרית, L₁ הייתה רגולרית (משלים וחיתוך) – סתירה. | |
| ב(1) | ba#b – שייכת; ab#aab – שייכת; ab#ab – לא שייכת; a# – לא שייכת; aa#b#a – לא שייכת. | |
| ב(2) | p₀→p₂: b ; p₁→p₁: a ; p₁→p₂: b ; p₂→p₁: a ; p₃→p₄: a,b ; p₂→p₅: # ; p₆→p₇: a,b ; p₇→p₈: a,b ; p₈→p₆: a,b. מצבים מקבלים: p₄, p₈. | |
| 8 | א(1) | L₄ ∩ L₅ = {xy} |
| א(2) | L₆ • L₆ = {yy, yyx, yxy, yxyx} | |
| א(3) | L₁ • L₂ = Σ* = L₁ | |
| א(4) | L₃ᴿ = Ø | |
| א(5) | L₅ • L₆ = {y, yx, xy, xyx, xyy, xyyx} | |
| ב | דוחפים סימן לכל a ראשון ולכל b; על כל c שולפים סימן של b, ואחר כך על כל a שבסוף שולפים סימן של a. הסימנים התחתונים של כל בלוק מסומנים (A – ה־ a הראשון; Z/Y – ה־ b הראשון כש־ n = 0 / n ≥ 1) כדי לדעת בלי מעברי ε מתי בלוק נגמר. מצב מקבל יחיד: q5. | |
| 9 | א(1) | 7 שלבים (0–6), מסתיים ב־q₆ ; המילה מתקבלת והסרט בסוף: ⊣ b b Δ … |
| א(2) | 9 שלבים (0–8), מסתיים ב־q₆ ; המילה מתקבלת והסרט בסוף: ⊣ a b a Δ … | |
| א(3) | ⊣ b a b a b Δ … (על הסרט: babab) | |
| ב(1) | 8 מילים – בדיוק המילים שהאות האחרונה בהן שווה לאות הראשונה (2 · 2 · 2 = 8). | |
| ב(2) | המכונה מחליפה את האות האחרונה במילה בעותק של האות הראשונה, וחוזרת לתחילת הסרט. היא מקבלת כל מילה לא ריקה ודוחה את המילה הריקה. | |
| 10 | א(1) | Cat, Dog ו־Rabbit יורשות מ־Pet (חץ ירושה אל Pet); Pet מכילה עצם מטיפוס Stay (הכלה, המעוין בצד של Pet). |
| א(2) | Stay: owner, nights, meals; Pet: chip, stay; Cat extends Pet: indoor; Dog extends Pet: weight; Rabbit extends Pet: age. | |
| ב(1) | this.stay = new Stay(owner, nights, meals); this.chip = chip; | |
| ב(2) | Java: super(owner, nights, meals, chip); this.weight = weight; C#: : base(owner, nights, meals, chip) { this.weight = weight; } | |
| ג(1) | Java: return 90 * this.stay.getNights() + 12 * this.stay.getMeals(); (C#: public virtual double Cost() עם GetNights/GetMeals) | |
| ג(2) | Cat – אין צורך. Dog: אם weight > 20 מחזירה super.cost() + 15 * getStay().getNights(), אחרת super.cost(); Rabbit: return super.cost() * 0.75; (C#: override ו־base.Cost()) | |
| 11 | א | Box(): size=1 · Box(int size): made++, size=size · GiftBox(String color): super(color.length()); color=color · GiftBox(int size, String color): super(); this.size=size; color=color · toString: super.toString() + ", color=" + color |
| ב(1) | 1 – לא ייתכן; 2 – ייתכן; 3 – לא ייתכן; 4 – ייתכן; 5 – לא ייתכן; 6 – ייתכן. | |
| ב(2) | כן. ההמרה (L)k3 של עצם M מצליחה רק אם M יורשת מ־L, ולכן רק תרשים 2 נשאר אפשרי (ב־4 וב־6 תהיה שגיאת ריצה). | |
| ג | protected boolean broken; protected static double rate; public Device(); public Device(String code, int power); public String info() (ב־C#: public virtual string Info()); public boolean isOld(int year) (ב־C#: public bool IsOld(int year)) | |
| 12 | א | Robot Alfa / Robot Beta / Cleaner / Robot Gama / Cleaner 60 / Robot Delta / Cleaner. עצמים: r1 Robot(Alfa,100), r2 Cleaner(Beta,100,0), c1 Cleaner(Gama,60,0), c2 Cleaner(Delta,100,0); made = 4. |
| ב | 1 תקין – Alfa(100) 4 | 2 תקין – Beta worked, energy = 90 | 3 לא תקין (הידור: אין work(int,int) ב־Robot) | 4 תקין – Gama worked, energy = 30 / rooms = 6 | 5 תקין – Beta(100) rooms: 0 | 6 לא תקין (הידור: Robot ל־Cleaner בלי המרה) | 7 לא תקין (שגיאת ריצה – המרה לא חוקית) | 8 לא תקין (הידור: אין פעולה בונה עם 3 פרמטרים) | 9 תקין – Beta worked, energy = 90 / rooms = 2 | 10 תקין – Delta worked, energy = 80 |