דילוג לתוכן המרכזי

מתכונת 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

א.(1)

לפניכם הפעולה ppp , המקבלת תור – q ובו מספרים הגדולים מ־ 0 , ומספר שלם – k הגדול מ־ 0 .

Java
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) , וכתבו מה הפעולה מחזירה.

התור qxx > maxmaxc
(2)

הסבירו מה הפעולה ppp עושה.

(3)

מהי סיבוכיות זמן הריצה של הפעולה ppp ? נמקו את תשובתכם.

ב.(1)

לפניכם הפעולה find , המקבלת תור – q ובו מספרים הגדולים מ־ 0 , ומספר שלם – k הגדול מ־ 0 . הפעולה find משתמשת בפעולה ppp מסעיף א.

Java
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ערך מוחזר
(2)

הסבירו מה הפעולה find עושה כאשר מזמנים אותה עם k = 1 .

(3)

מהי סיבוכיות זמן הריצה של הפעולה 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 (מראש התור לסופו)xx > maxmaxc
4, 2, 7, 7, 5, 9, 3, −1 (לפני הלולאה)00
4, 2, 7, 7, 5, 9, 3, −14true41
2, 7, 7, 5, 9, 3, −1, 42false41
7, 7, 5, 9, 3, −1, 4, 27true72
7, 5, 9, 3, −1, 4, 2, 77false72
5, 9, 3, −1, 4, 2, 7, 75false72
9, 3, −1, 4, 2, 7, 7, 59true93
3, −1, 4, 2, 7, 7, 5, 93false93
−1, 4, 2, 7, 7, 5, 9, 3 (סוף הלולאה)93

בשורה האחרונה בראש התור −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, 31true3 (מהקריאה find(q, 2))
4, 2, 7, 7, 5, 9, 32true3 (מהקריאה find(q, 3))
4, 2, 7, 7, 5, 9, 33true3 (מהקריאה find(q, 4))
4, 2, 7, 7, 5, 9, 34false4 − 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קפיצה ← משבצת הבאה
11442 ← 3
23153 ← 6
365101 ← 7
472124 ← מעבר לסוף (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 לכל משבצת התחלה:

התחלההמשחקסכום
14 ← משבצת 3: 15
27 ← משבצת 3: 18
31 ← משבצת 6: 56
46 ← משבצת 6: 511
53 ← משבצת 6: 58
65 ← משבצת 7: 27
72 ← מעבר לסוף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).

מה נדרש: לבדוק שבכל צומת שאינו עלה, הערך שווה לסכום הערכים של כל הצאצאים שלו.

רעיון (רקורסיה):

•מקרה בסיס – tree הוא עלה: אין לו צאצאים ואין עליו דרישה ⇒ מחזירים true.
•אחרת: סכום הצאצאים של tree הוא בדיוק סכום תת־העץ השמאלי ועוד סכום תת־העץ הימני, ולכן מחשבים אותו בעזרת הפעולה הנתונה: sumTree(left) + sumTree(right) (הפעולה מחזירה 0 בעבור בן שאינו קיים). אם הערך של tree שונה מהסכום – מחזירים false.
•אם התנאי מתקיים בצומת עצמו, צריך שיתקיים גם בכל הצמתים שמתחתיו: בודקים ברקורסיה כל בן קיים (רק בן שאינו null, כי הפעולה מניחה ש־ tree אינו null). אם אחד מהם אינו "עץ סכומים" – מחזירים false, אחרת true.

פתרון:

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כן
102 + 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 כבר התגלה, והלולאה נעצרת:

צעדהוצא מהתורשכנים חדשים (הורה)התור אחרי הצעד
124 (2), 6 (2)[4, 6]
249 (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}5844
1{1, 8}2214
2{2, 4, 6, 9, 10}51266

מוחזר הרכיב {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]{}
162, 3, 82, 3, 8[2, 3, 8]{2, 3, 8}
2200[3, 8, 0]{2, 3, 8, 0}
33––[8, 0]{2, 3, 8, 0}
48––[0]{2, 3, 8, 0}
50––[]{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]
00L[2]=1 (P=0)0010000000
30L[5]=1 (P=3), L[6]=1 (P=3)0010011000
80L[9]=1 (P=8)0010011001
21L[5]=2 (P=2), L[6]=2 (P=2)0010022001
91L[4]=2 (P=9)0010222001
52L[1]=3 (P=5)0310222001
62L[4]=3 (P=6)0310322001
13L[7]=4 (P=1)0310322401
74L[4]=5 (P=7)0310522401
45–0310522401

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 קודקודים והוא "גרף־עץ־שבור".

ב.(1)

נתון "גרף־עץ־שבור" ובו n קודקודים.

(1) האם אפשר לדעת את מספר הקשתות בגרף זה? נמקו את תשובתכם. אם עניתם שאפשר, ציינו את מספר הקשתות.

(2)

האם אפשר לדעת את מספר רכיבי הקשירות בגרף זה? נמקו את תשובתכם. אם עניתם שאפשר, ציינו את מספר רכיבי הקשירות.

ג.(1)

נתון גרף לא מכוון G ובו n קודקודים (n ≥ 2), המיוצג על ידי רשימת סמיכויות.

(1) כתבו אלגוריתם יעיל המחזיר "אמת" אם הגרף הוא "גרף־עץ־שבור" ואחרת הוא מחזיר "שקר".

(2)

מהי סיבוכיות זמן הריצה של האלגוריתם שכתבתם? נמקו את תשובתכם.

◂ פתרון מלא לשאלה 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 מסלול פשוט יחיד – הקשת u–v עצמה (אילו היה מסלול נוסף, יחד עם הקשת הוא היה סוגר מעגל בעץ). לכן אחרי הורדת הקשת אין מסלול בין u ל־v, והם ברכיבים שונים.
•לכל היותר 2 רכיבים: לכל קודקוד x יש ב־T מסלול פשוט ל־u. אם המסלול לא עובר בקשת u–v – x מחובר ל־u גם בגרף שלנו. אם הוא עובר בה – זו הקשת האחרונה במסלול, והחלק שלפניה מחבר את x ל־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 רכיבי קשירות.

•⇐ (כיוון ראשון): הוכח בסעיף ב.
•⇒ (כיוון שני): נניח שיש ב־G שני רכיבים, עם k₁ ו־k₂ קודקודים (k₁ + k₂ = n), ו־n−2 קשתות. כל רכיב קשיר, ולכן יש בו לפחות kᵢ−1 קשתות; יחד לפחות (k₁−1) + (k₂−1) = n−2. אבל יש בדיוק n−2 קשתות, ולכן בכל רכיב יש בדיוק kᵢ−1 קשתות – כל רכיב הוא עץ. ניקח קודקוד u מהרכיב הראשון ו־v מהשני (אין ביניהם קשת) ונוסיף את הקשת u–v: הגרף נעשה קשיר ויש בו n−1 קשתות – כלומר עץ. לכן G הוא גרף־עץ־שבור.

אז מספיק לספור קשתות ורכיבי קשירות.

פתרון – האלגוריתם (הגרף פשוט – בלי קשתות מקבילות ולולאות; כל קשת מופיעה פעמיים ברשימת הסמיכויות – פעם אצל כל אחד מקצותיה):

1.count ← 0
2.לכל קודקוד v בגרף: לכל שכן u ברשימת הסמיכויות של v:
•count ← count + 1
•אם count > 2(n−2): החזר "שקר" (יותר מ־n−2 קשתות – אפשר לעצור מיד)
3.אם count ≠ 2(n−2): החזר "שקר" (מספר הקשתות count/2 שונה מ־n−2)
4.סמן את כל הקודקודים "לא התגלה". comps ← 0
5.לכל קודקוד s בגרף: אם s "לא התגלה":
•comps ← comps + 1
•אם comps > 2: החזר "שקר"
•הרץ BFS מ־s וסמן "התגלה" את כל הקודקודים שהוא מגיע אליהם
6.אם comps = 2: החזר "אמת", אחרת החזר "שקר".

בדיקה: האלגוריתם מחזיר תמיד אותה תשובה כמו ההגדרה עצמה ("יש זוג קודקודים לא שכנים שהוספת קשת ביניהם נותנת עץ"). על הדוגמה מסעיף א מתקבל "אמת".

טעות נפוצה: לבדוק רק "יש n−2 קשתות". גרף עם n−2 קשתות יכול להיות בעל יותר משני רכיבים – למשל משולש ועוד שני קודקודים מבודדים (5 קודקודים, 3 קשתות, 3 רכיבים) – ואז אין קשת אחת שהוספתה נותנת עץ. חייבים לבדוק גם את מספר הרכיבים.

סעיף ג(2)

תשובה

O(n) (ובאופן כללי O(V+E), אבל בזכות העצירה המוקדמת E ≤ n−2).

מה נדרש: סיבוכיות האלגוריתם מסעיף ג(1), עם נימוק.

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

פתרון:

•שלב 2 (ספירת קשתות): עוברים על n רשימות. בגלל העצירה כש־count > 2(n−2), סופרים לכל היותר 2n−3 איברים. לכן הזמן הוא O(n).
•שלב 3: השוואה אחת – O(1).
•שלבים 4–6 (ספירת רכיבים): כל ריצות ה־BFS יחד עוברות על כל קודקוד פעם אחת ועל כל רשימת סמיכויות פעם אחת – O(V+E). מגיעים לשלב הזה רק אם E = n−2, ולכן O(n + n) = O(n).

סך הכול: O(n). (בלי העצירה המוקדמת הסיבוכיות היא O(n + E), שהיא עדיין לינארית בגודל הקלט – רשימת הסמיכויות.)

פרק שני (50 נקודות) – מודלים חישוביים

יש לענות על שתי שאלות במסלול שלמדתם (לכל שאלה – 25 נקודות).

שאלה 7 · שפות רגולריות, תכונות סגירות והשלמת אוטומט עם מפריד (25 נקודות)

בשאלה זו שני סעיפים, א–ב, שאין קשר ביניהם. ענו על שני הסעיפים.

א.(1)

לפניכם שלוש שפות מעל הא"ב {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₃ , כתבו אם היא רגולרית או לא רגולרית. נמקו את תשובתכם.

(2)

האם השפה L₁ ∩ L₃ רגולרית? נמקו את תשובתכם.

(3)

האם השפה L₁ ∩ L̅₃ רגולרית? נמקו את תשובתכם.

(4)

האם השפה L₂ ∩ L̅₁ רגולרית? נמקו את תשובתכם.

ב.(1)

נתונה השפה 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 . נמקו את קביעותיכם.

(2)

נתון לפניכם אוטומט סופי דטרמיניסטי שאינו מלא המקבל את השפה 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:

מצבabמקבל?
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 , bu מסתיימת ב־aכן
ab#aabכןab , aabu מסתיימת ב־b, אבל אורך v הוא 3כן
ab#abכןab , abu מסתיימת ב־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₈.

מה נדרש: להשלים את התוויות החסרות ולסמן מצבים מקבלים, בלי לשנות שום דבר אחר.

רעיון: נבין מה כל מצב "זוכר":

•p₀ –a→ p₁ ולולאת b על p₂ נתונות: p₁ = "בתוך u, האות האחרונה a", p₂ = "בתוך u, האות האחרונה b". לכן p₀→p₂ ב־b, לולאת a על p₁, p₁→p₂ ב־b, p₂→p₁ ב־a.
•p₁ –#→ p₃ נתון: u מסתיימת ב־a, והתנאי כבר מתקיים. צריך רק ש־v לא תהיה ריקה: p₃→p₄ ב־a,b, ו־p₄ (עם הלולאה a,b הנתונה) מקבל. p₃ לא מקבל (v ריקה).
•המעבר היחיד שנשאר מ־p₂ הוא p₂→p₅, והוא חייב להיות #. כאן u מסתיימת ב־b, ולכן צריך ש־|v| יתחלק ב־3: p₅ = אורך 0, p₆ = שארית 1 (p₅→p₆ ב־a,b נתון), p₇ = שארית 2, p₈ = שארית 0 ו־v לא ריקה. לכן p₆→p₇, p₇→p₈, p₈→p₆ – כולם a,b, ו־p₈ מקבל. p₅ לא מקבל (v ריקה).

פתרון: האוטומט המושלם (התוויות שהוספנו באדום, המצבים המקבלים מסומנים בעיגול כפול אדום):

טבלת המעברים המלאה (מעבר שאינו בטבלה – האוטומט נתקע ודוחה; בהדגשה – התוויות שהוספנו):

מצבab#מקבל?משמעות
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 לא ריקה

מעקב לדוגמה:

•bba#ab: p₀ –b→ p₂ –b→ p₂ –a→ p₁ –#→ p₃ –a→ p₄ –b→ p₄ ✔ (מצב מקבל)
•ab#bab: p₀ –a→ p₁ –b→ p₂ –#→ p₅ –b→ p₆ –a→ p₇ –b→ p₈ ✔ (מצב מקבל)
•abb#ab: p₀ –a→ p₁ –b→ p₂ –b→ p₂ –#→ p₅ –a→ p₆ –b→ p₇ ✘ (מצב לא מקבל)

בדיקה: האוטומט המושלם מקבל בדיוק את המילים של L. בנוסף נבדקו בחיפוש ממצה כל ההשלמות האפשריות של התוויות החסרות (כל קבוצה לא ריקה של סימנים לכל מעבר, וכל בחירה של מצבים מקבלים): ההשלמה שלמעלה היא היחידה שמקבלת את L.

שאלה 8 · פעולות על שפות ואוטומט מחסנית דטרמיניסטי – בלוקים מקוננים (25 נקודות)

בשאלה זו שני סעיפים, א–ב, שאין ביניהם קשר. ענו על שני הסעיפים.

א.(1)

לפניכם שש שפות מעל הא"ב {x , y} :
Σ* – מציין את שפת כל המילים מעל הא"ב {x , y} .
Lᴿ – מציין את השפה שמילותיה הן היפוכי המילים של L .

L₁ = Σ*L₄ = {xy , yyx}
L₂ = {ε}L₅ = {ε , x , xy}
L₃ = ØL₆ = {y , yx}

כתבו את השפה המתקבלת מכל אחת מחמש הפעולות שלהלן:

(1) L₄ ∩ L₅

(2)

L₆ • L₆

(3)

L₁ • L₂

(4)

L₃ᴿ

(5)

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).

פתרון:

uv = yv = yx
yyyyyx
yxyxyyxyx

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 = yv = yx
εyyx
xxyxyx
xyxyyxyyx

כל שש המילים שונות: 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 שבסוף. מחסנית מתאימה בדיוק לזה – מה שנדחף אחרון נשלף ראשון:

•על כל a (בבלוק הראשון) דוחפים סימן של a; על כל b דוחפים סימן של b.
•על כל c שולפים סימן של b; כשנגמרים סימני ה־ b עוברים לשלוף סימן של a על כל a שבסוף.

כדי לדעת בלי מעברי ε מתי נגמר כל בלוק, מסמנים את הסימן הראשון של כל בלוק:

•A – סימן ה־ a הראשון (התחתון ביותר כש־ n ≥ 1); X – שאר ה־ a.
•ה־ b הראשון: Z אם n = 0 (אחרי ששולפים אותו המילה הסתיימה), Y אם n ≥ 1 (אחרי ששולפים אותו צריך לקרוא a); B – שאר ה־ b.

התנאי m ≥ 1 חשוב: בזכותו ה־ b מפריד בין שני בלוקי ה־ a, והאוטומט יודע בכל רגע אם הוא בבלוק הראשון או האחרון.

פתרון:

•מצבים: q0 (התחלתי), q1, q2, q3, q4, q5. מצב מקבל: q5 (ε אינה בשפה, כי m ≥ 1).
•א"ב הקלט: {a, b, c}. א"ב המחסנית: {A, X, Z, Y, B, ⊥}, כאשר ⊥ הוא סימן תחתית המחסנית.
•סימון מעבר: (מצב, אות קלט, ראש המחסנית) → (מצב חדש, מחרוזת שמחליפה את ראש המחסנית). התו השמאלי במחרוזת הוא ראש המחסנית החדש; ε = שליפה.
•משמעות המצבים: q1 – קוראים a ראשונים; q2 – קוראים b; q3 – קוראים c; q4 – קוראים a שבסוף; q5 – המחסנית התרוקנה.
מצבקלטראש המחסניתמצב חדשמחליפים את הראש ב־משמעות
q0a⊥q1A⊥ה־ a הראשון: דוחפים A (מסמן את תחתית ה־ a)
q1aAq1XAa נוסף: דוחפים X
q1aXq1XXa נוסף: דוחפים X
q0b⊥q2Z⊥n = 0, ה־ b הראשון: דוחפים Z
q1bAq2YAn ≥ 1, ה־ b הראשון: דוחפים Y
q1bXq2YXn ≥ 1, ה־ b הראשון: דוחפים Y
q2bZq2BZb נוסף: דוחפים B
q2bYq2BYb נוסף: דוחפים B
q2bBq2BBb נוסף: דוחפים B
q2cBq3εה־ c הראשון: שולפים B
q2cZq5εn = 0, m = 1: המחסנית מתרוקנת
q2cYq4εn ≥ 1, m = 1: עוברים לקריאת ה־ a שבסוף
q3cBq3εc נוסף: שולפים
q3cZq5εn = 0: ה־ c האחרון – המחסנית מתרוקנת
q3cYq4εn ≥ 1: ה־ c האחרון – עוברים לקריאת ה־ a
q4aXq4εa שבסוף: שולפים
q4aAq5εה־ a האחרון – המחסנית מתרוקנת

בכל צירוף (מצב, אות, ראש מחסנית) יש לכל היותר מעבר אחד ואין מעברי ε – האוטומט דטרמיניסטי. כל צירוף שאינו בטבלה = דחייה (למשל c בלי b לפניו, b אחרי c, או אות נוספת ב־ q5).

בדיקה – מעקב על aabbccaa (n = 2, m = 2):

נקראמצבמחסנית (ראש משמאל)
–q0⊥
aq1A⊥
aq1XA⊥
bq2YXA⊥
bq2BYXA⊥
cq3YXA⊥
cq4XA⊥
aq4A⊥
aq5⊥

הקלט נגמר ב־ 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.

א.(1)

עקבו אחרי פעולת המכונה בעבור המילה ba . יש להציג את המעקב.

(2)

עקבו אחרי פעולת המכונה בעבור המילה abb . יש להציג את המעקב.

(3)

הציגו את הסרט המתקבל בגמר הפעולה בעבור המילה babaa . אין צורך להציג את המעקב.

ב.(1)

כמה מילים באורך 4 מעל הא"ב {a, b} אינן משתנות לאחר הרצת המכונה עליהן? נמקו את תשובתכם.

(2)

כתבו בקצרה מה המכונה עושה.

◂ פתרון מלא לשאלה 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
0q₀⊣[b]aΔΔΔ...
1q₂⊣b[a]ΔΔΔ...
2q₂⊣ba[Δ]ΔΔ...
3q₄⊣b[a]ΔΔΔ...
4q₅⊣[b]bΔΔΔ...
5q₅[⊣]bbΔΔΔ...
6q₆⊣[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
0q₀⊣[a]bbΔΔΔ...
1q₁⊣a[b]bΔΔΔ...
2q₁⊣ab[b]ΔΔΔ...
3q₁⊣abb[Δ]ΔΔ...
4q₃⊣ab[b]ΔΔΔ...
5q₅⊣a[b]aΔΔΔ...
6q₅⊣[a]baΔΔΔ...
7q₅[⊣]abaΔΔΔ...
8q₆⊣[a]baΔΔΔ...

הסבר: האות הראשונה היא 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
⊣bababΔ...

כלומר על הסרט 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 – ארנב.

להלן פירוט תכונות המחלקות:

•למחלקה שהייה (Stay) שלוש תכונות: owner – שם הבעלים (מחרוזת), nights – מספר הלילות בפנסיון (מטיפוס שלם),

meals – מספר הארוחות המיוחדות שהוזמנו (מטיפוס שלם).

•למחלקה חיית מחמד (Pet) שתי תכונות: מספר השבב – chip (מחרוזת), שהייה – stay (Stay).
•למחלקה חתול (Cat) שלוש תכונות: מספר השבב – chip (מחרוזת), שהייה – stay (Stay),

חתול בית – indoor (בוליאני. אם החתול אינו יוצא מהבית התכונה היא אמת ואם לא, היא שקר).

•למחלקה כלב (Dog) שלוש תכונות: מספר השבב – chip (מחרוזת), שהייה – stay (Stay),

משקל הכלב בקילוגרמים – weight (מטיפוס שלם).

•למחלקה ארנב (Rabbit) שלוש תכונות: מספר השבב – chip (מחרוזת), שהייה – stay (Stay),

גיל הארנב בשנים – age (מטיפוס שלם).

א.(1)

סרטטו תרשים הייררכייה המתאר את הקשר בין המחלקות של המערכת הממוחשבת.
יש לסמן ירושה באמצעות החץ

והכלה באמצעות הסימן

.

(2)

כתבו את כותרות המחלקות ואת התכונות שלהן. הניחו שהפעולות get ו־ set קיימות בכל התכונות של המחלקות,
ואין צורך לממש אותן.

ב.(1)

נתונה הפעולה הבונה של המחלקה Stay :

Java
public Stay (String owner, int nights, int meals)

אין צורך לממש את הפעולה.

(1) לפניכם כותרת הפעולה הבונה של המחלקה Pet . הפעולה מקבלת שם בעלים, מספר לילות, מספר ארוחות
מיוחדות ומספר שבב.

Java
public Pet (String owner, int nights, int meals, String chip)

ממשו את הפעולה הבונה.

(2)

לפניכם כותרת הפעולה הבונה של המחלקה Dog . הפעולה מקבלת שם בעלים, מספר לילות, מספר ארוחות מיוחדות,
מספר שבב ומשקל.

Java
public Dog (String owner, int nights, int meals, String chip, int weight)

ממשו את הפעולה הבונה.

ג.(1)

העלות הבסיסית של שהיית חיית מחמד – Pet בפנסיון היא 90 שקלים ללילה ו־ 12 שקלים לכל ארוחה מיוחדת.
העלות של שהיית חתול (Cat) היא לפי העלות הבסיסית.
העלות של שהיית כלב (Dog) שמשקלו יותר מ־ 20 קילוגרמים היא העלות הבסיסית ונוסף על כך 15 שקלים לכל לילה.
העלות של שהיית כלב שמשקלו 20 קילוגרמים או פחות היא לפי העלות הבסיסית.
העלות של שהיית ארנב (Rabbit) היא 75% מן העלות הבסיסית.

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

(1) כתבו את הפעולה cost במחלקה Pet (כאמור לעיל, הסכום לתשלום הוא לפי העלות הבסיסית).

(2)

הוסיפו את הפעולה 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, Dog, Rabbit ומצביע אל Pet.
•קו הכלה בין Pet ל־Stay, והמעוין המלא בצד של המחלקה המכילה – Pet.

בדיקה – 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 .

Java
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() {…}
}

לפניכם קטע קוד והפלט שהתקבל בעקבות הרצת קטע הקוד:

הקוד

Java
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 , בהתאם לעקרונות תכנות מונחה עצמים, כך שיתקבל הפלט הנתון.

אין להוסיף פעולות נוספות למחלקות. תשובה הכוללת פעולות נוספות במחלקות, לא תזוכה בנקודות.

ב.(1)

נתונות המחלקות K, L, M, N והפעולה main במחלקה Tester , שרצה באופן תקין.

Java
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 . אם ציינתם שלא ייתכן, נמקו מדוע.

(2)

בסוף הפעולה main נוספו שורות הקוד שלהלן, שרצות באופן תקין:

Java
K k3 = new M();
L l2 = (L)k3;

בעקבות זאת, האם תשתנה תשובתכם בתת־סעיף ב(1)? נמקו את תשובתכם.

ג.

נתונה המחלקה Lamp (מנורה), היורשת מהמחלקה Device (מכשיר):

Java
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 יכולה לשנות אותה.

•//1 new Box() הדפיס made=0, size=1 ⇐ הבונה בלי פרמטרים לא משנה את made ומציבה size=1.
•//2 new Box(8) הדפיס made=1, size=8 ⇐ הבונה עם פרמטר מגדילה את made ב־1 ומציבה את size.
•//3 new GiftBox("red") הדפיס made=2 ⇐ made גדל, ולכן הבונה של GiftBox חייבת לזמן את Box(int). הגודל 3 הוא אורך המחרוזת "red", וזה מתאשר ב־//5: ל־"yellow" (6 תווים) התקבל size=6 ⇐ super(color.length()) / : base(color.Length).
•//4 new GiftBox(6, "green") הדפיס made=2 (לא השתנה) ⇐ הבונה הזו מזמנת את Box() (במפורש או אוטומטית), שלא משנה את made, ואחר כך מציבה size=6 בעצמה. זה מותר, כי size מוגדרת protected.
•toString של GiftBox משתמשת ב־toString של Box (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.

פתרון

תרשיםמבנה (בן → אב)ייתכן?נימוק
1L→K, N→K, M→Lלא ייתכןN ו־L הן "אחיות" (שתיהן יורשות מ־K), ו־N אינה יורשת מ־L ⇐ L l1 = new N(); שגיאת הידור.
2L→K, M→L, N→MייתכןN יורשת מ־L בעקיפין (דרך M), M יורשת מ־K בעקיפין (דרך L), ו־L יורשת מ־K.
3K→L, N→L, M→Kלא ייתכןL היא מחלקת־על של K, ולא תת־מחלקה שלה ⇐ K k2 = new L(); שגיאת הידור.
4L→K, M→K, N→LייתכןN יורשת מ־L, ו־L ו־M יורשות מ־K.
5M→K, N→K, L→Nלא ייתכןL יורשת מ־N, ולא להפך ⇐ L l1 = new N(); שגיאת הידור.
6M→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 נשאר אפשרי.

•תרשים 2: M יורשת מ־L ⇐ ההמרה תקינה ⇐ ייתכן.
•תרשים 4: M ו־L הן "אחיות" (שתיהן יורשות מ־K). עצם M אינו L ⇐ שגיאת ריצה ⇐ לא ייתכן.
•תרשים 6: L יורשת מ־M (ולא להפך). עצם M אינו L ⇐ שגיאת ריצה ⇐ לא ייתכן.
•תרשימים 1, 3, 5 נשארים לא אפשריים (מהנימוקים של ב(1)).

בדיקה – עם השורות הנוספות (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 (רובוט ניקוי):

Java
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 :

Java
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>.
•שתי הפעולות הבונות של Cleaner קוראות קודם ל־super(name) (ב־C#: : base(name)), ורק אחר כך ממשיכות. Cleaner(name, energy) משנה אחר כך את energy (התכונה protected, ולכן נגישה במחלקה היורשת) – אצל Gama היא 60.
•made היא תכונה סטטית המשותפת לכל העצמים: 4 בסוף הפעולה.
•r2 הוא משתנה מטיפוס Robot, אבל העצם הוא Cleaner.

פתרון

תרשים העצמים:

הפלט:

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.בזמן הידור קובע הטיפוס של ההפניה (המשתנה): אפשר לזמן רק פעולות שקיימות בטיפוס ההפניה, ואי אפשר להציב הפניה ממחלקת־על במשתנה של מחלקה יורשת בלי המרה מפורשת. גם פעולה בונה חייבת להתאים בדיוק לאחת הקיימות.
2.בזמן ריצה קובע הטיפוס של העצם בפועל: פעולה שנדרסה מופעלת לפי הטיפוס בפועל (גם אחרי המרה למחלקת־העל). המרה מפורשת לטיפוס שהעצם אינו שייך אליו עוברת הידור אבל נכשלת בזמן ריצה.
3.העמסה (פעולות באותו שם עם פרמטרים שונים) נבחרת בזמן הידור לפי טיפוס ההפניה ומספר הפרמטרים; תכונה סטטית משותפת לכל העצמים, ומזמנים פעולה סטטית דרך שם המחלקה.

פתרון

#תקין?פלט / הסבר
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

כל האתר במבט אחד

קיצורי דרך לכל הכלים, הבגרויות והקורסים

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