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

מתכונת 3 במדעי המחשב - מבני נתונים ומסלול 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 נקודות)

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

בחנות נרשם קוד המוצר של כל מכירה במהלך היום, לפי סדר המכירות, בתור q של מספרים שלמים. מוצר שנמכר כמה פעמים מופיע בתור כמה פעמים.
"מוצר בודד" הוא מוצר שהקוד שלו מופיע בתור פעם אחת בדיוק (המוצר נמכר פעם אחת בלבד).

ממשו את הפעולה החיצונית שלפניכם:

Java – public static int countSingles (Queue<Integer> q)

C# – public static int CountSingles (Queue<int> q)

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

דוגמה: עבור התור q שלפניכם הפעולה תחזיר 3, כי המוצרים הבודדים הם 5, 9, 4 (המוצר 7 נמכר שלוש פעמים, והמוצר 2 נמכר פעמיים).

הערות:
– הניחו שבתור q יש שני איברים לפחות.
– אין צורך לשמור על התור q.
– אין להשתמש בשאלה זו במערך וברשימה מקושרת.

ממשו את הפעולה countSingles/CountSingles המתוארת לעיל.

◂ פתרון מלא לשאלה 1

פתרון

תשובה

כל עוד התור אינו ריק: מוציאים v, מעבירים את שאר האיברים דרך תור עזר – מופעים נוספים של v נספרים ואינם מוחזרים; אם לא נמצא אף מופע נוסף – מגדילים את המונה. עבור הדוגמה מתקבל 3. סיבוכיות O(n²).

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

רעיון: כל עוד התור אינו ריק, מוציאים את האיבר הראשון v, ומעבירים את כל שאר האיברים דרך תור עזר temp: איבר x השווה ל־ v נספר (same++) ואינו מוחזר, ואיבר אחר מוכנס ל־ temp. אם לא נמצא אף איבר אחר השווה ל־ v (same == 0) – זה מוצר בודד, ומגדילים את המונה. בסוף הסבב מחזירים את האיברים מ־ temp אל q. כך בכל סבב יוצאים מהתור כל המופעים של מוצר אחד, וכל קוד מוצר נבדק פעם אחת בדיוק. התור מתרוקן בסוף, וזה מותר כי אין צורך לשמור עליו.

פתרון: הקוד המלא – בלשונית Java/C#. רק פעולות התור (insert, remove, isEmpty), תור עזר ומשתנים פשוטים.

יעילות (n – מספר האיברים בתור): בכל סבב יוצא מהתור לפחות איבר אחד, ולכן יש לכל היותר n סבבים. בסבב שבו נשארו בתור k איברים מתבצעות לכל היותר 2k הוצאות והכנסות (ל־ temp וחזרה). במקרה הגרוע (כל הקודים שונים) k = n − 1, n − 2, ..., 0, וסך הפעולות הוא n·(n − 1) ⇒ O(n²).

בדיקה (התור 7, 2, 7, 5, 9, 2, 7, 4):

סבבvמופעים נוספים של vמוצר בודד?מונההתור q בסוף הסבב
172לא02, 5, 9, 2, 4
221לא05, 9, 4
350כן19, 4
490כן24
540כן3(ריק)

הפעולה מחזירה 3. מקרים נוספים: 3, 3 ← 0; 1, 2 ← 2; 4, 6, 4, 6, 4 ← 0; 10, 20, 30, 20 ← 2.

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

הקוד המלא
public static int countSingles(Queue<Integer> q) {
    int singles = 0;
    while (!q.isEmpty()) {
        int v = q.remove();                 // קוד המוצר הנבדק
        int same = 0;
        Queue<Integer> temp = new Queue<Integer>();
        while (!q.isEmpty()) {              // מעבר על כל שאר המכירות
            int x = q.remove();
            if (x == v)
                same++;                     // עוד מכירה של אותו מוצר – לא מחזירים אותה
            else
                temp.insert(x);
        }
        if (same == 0)                      // אין עוד מכירה עם אותו קוד
            singles++;
        while (!temp.isEmpty())             // החזרת המכירות של שאר המוצרים לתור
            q.insert(temp.remove());
    }
    return singles;
}

שאלה 2 · מרחק בין ערכים חוזרים בשרשרת חוליות (25 נקודות)

נוסח השאלה:

שימו לב: לשאלה זו שני נוסחים: בשפת Java ובשפת C#.

לפותרים בשפת Java

א.(1)

נתונה הפעולה what :

Java
public static int what (Node<Integer> lst, int x)
{
    if (lst == null)
        return 0;
    if (lst.getValue() == x)
        return 1;
    int r = what (lst.getNext(), x);
    if (r == 0)
        return 0;
    return r + 1;
}

נתונה שרשרת חוליות – lst מטיפוס שלם:

(1) עקבו אחר הפעולה what (lst, 6) , ורשמו את הערך שהפעולה מחזירה.

(2)

מה עושה הפעולה what ? הסבירו את תשובתכם.

(3)

מהי סיבוכיות הפעולה what ? נמקו את תשובתכם.

ב.(1)

נתונה הפעולה guess :

Java
public static int guess (Node<Integer> lst)
{
    if (lst == null)
        return 0;
    int d = what (lst.getNext(), lst.getValue());
    int r = guess (lst.getNext());
    if (d != 0 && (r == 0 || d < r))
        return d;
    return r;
}

נתונה שרשרת חוליות – lst מטיפוס שלם:

(1) עקבו אחר הפעולה guess (lst) , ורשמו את הערך שהיא מחזירה.
בסעיף זה אין צורך לעקוב אחר הפעולה what .

(2)

מה עושה הפעולה guess ? הסבירו את תשובתכם.

(3)

מהי סיבוכיות הפעולה guess ? נמקו את תשובתכם.

◂ פתרון מלא לשאלה 2

סעיף א(1)

תשובה

הפעולה מחזירה 5 (הערך 6 מופיע לראשונה בחוליה החמישית).

מה נדרש: לעקוב אחר הקריאה what(lst, 6) ולרשום את הערך המוחזר.

רעיון: כאן הבדיקה של החוליה מתבצעת לפני הקריאה הרקורסיבית: אם ערך החוליה שווה ל־x מחזירים 1 מיד, בלי להמשיך בשרשרת. אחרת ממשיכים לחוליה הבאה, ובדרך חזרה מוסיפים 1 לתוצאה – אלא אם התוצאה 0, ואז מחזירים 0.

פתרון – מעקב (השרשרת 3, 8, 5, 3, 6, 8, 6, 5; x = 6):
הקריאות יורדות: 3 ≠ 6 → 8 ≠ 6 → 5 ≠ 6 → 3 ≠ 6 → 6 == 6, והקריאה על החוליה החמישית מחזירה 1. החוליות 8, 6, 5 שאחריה לא נבדקות כלל.
החזרה (מהקריאה העמוקה החוצה):

קריאה על חוליה (מקום)ערך == 6 ?r שהתקבלמוחזר
6 (מקום 5)כןאין קריאה רקורסיבית1
3 (מקום 4)לא12
5 (מקום 3)לא23
8 (מקום 2)לא34
3 (מקום 1)לא45

הפעולה מחזירה 5.

בדיקה: הקוד מדפיס what(lst, 6) = 5.

סעיף א(2)

תשובה

הפעולה מחזירה את המקום (מספור מ־1) של החוליה הראשונה בשרשרת lst שערכה x. אם x אינו מופיע בשרשרת – מחזירה 0.

מה נדרש: לתאר במילים מה הפעולה מחשבת.

רעיון: נסתכל על הפעולה כעל הגדרה רקורסיבית:

•שרשרת ריקה – x אינו מופיע בה, ומחזירים 0.
•אם החוליה הראשונה היא x – x נמצא במקום 1, ומחזירים 1.
•אחרת, r = what(lst.getNext(), x) הוא המקום של x בהמשך השרשרת (הנחת האינדוקציה). אם r = 0, x אינו מופיע גם בהמשך, ומחזירים 0. אחרת, המקום ב־lst גדול ב־1 מהמקום בהמשך, ומחזירים r + 1.

פתרון: הפעולה מחזירה את המקום של ההופעה הראשונה של x בשרשרת (החוליה הראשונה היא מקום 1), או 0 אם x אינו מופיע בשרשרת. הבדיקה r == 0 נחוצה כדי שתשובת "לא נמצא" תעבור כמו שהיא ולא תהפוך ל־1, 2, …. הפעולה אינה משנה את השרשרת.

בדיקה: על השרשרת 3, 8, 5, 3, 6, 8, 6, 5:

•what(lst, 3) ⇒ 1;
•what(lst, 5) ⇒ 3;
•what(lst, 7) ⇒ 0.

סעיף א(3)

תשובה

O(n) במקרה הגרוע, כאשר n הוא מספר החוליות: כש־x אינו מופיע (או מופיע רק בסוף) יש קריאה אחת לכל חוליה ועוד אחת ל־null, ובכל קריאה עבודה קבועה.

מה נדרש: סיבוכיות זמן, כאשר n הוא מספר החוליות ב־lst.

רעיון: סופרים את הקריאות הרקורסיביות ואת העבודה בכל קריאה.

פתרון: כל קריאה מבצעת לכל היותר קריאה רקורסיבית אחת, על החוליה הבאה, ועוד מספר קבוע של פעולות (השוואות, getValue, חיבור), כלומר O(1).

•המקרה הגרוע: x אינו מופיע בשרשרת (או מופיע רק בחוליה האחרונה). אז הקריאות ממשיכות עד סוף השרשרת: n + 1 קריאות ⇒ O(n).
•המקרה הטוב: x בחוליה הראשונה – קריאה אחת, O(1).

הסיבוכיות (המקרה הגרוע) היא O(n).

בדיקה: כש־x אינו בשרשרת, באורך 100, 200, 400, 800, 1600 יש 101, 201, 401, 801, 1601 קריאות, כלומר n + 1.

סעיף ב(1)

תשובה

הפעולה מחזירה 2.

מה נדרש: לעקוב אחר guess(lst) ולרשום את הערך המוחזר.

רעיון: לפי סעיף א, d = what(lst.getNext(), lst.getValue()) הוא המקום של ההופעה הבאה של ערך החוליה בהמשך השרשרת – כלומר המרחק (במספר צעדים) מהחוליה הנוכחית לחוליה הבאה עם אותו ערך, או 0 אם אין כזו. r = guess(lst.getNext()) היא התוצאה על המשך השרשרת. הפעולה מחזירה את d אם d ≠ 0 ו־d קטן מ־r (או ש־r = 0), אחרת את r.
הקריאה הרקורסיבית על ההמשך מסתיימת לפני ההשוואה, ולכן התוצאות נקבעות מהחוליה האחרונה לראשונה.

פתרון – מעקב (השרשרת 3, 8, 5, 3, 6, 8, 6, 5; השורות לפי סדר החזרה מהרקורסיה):

חוליה (מקום)drמוחזר
null––0
5 (מקום 8)000
6 (מקום 7)000
8 (מקום 6)000
6 (מקום 5)2 (ה־6 הבא במקום 7)02
3 (מקום 4)022
5 (מקום 3)5 (ה־5 הבא במקום 8)22
8 (מקום 2)4 (ה־8 הבא במקום 6)22
3 (מקום 1)3 (ה־3 הבא במקום 4)22

הפעולה מחזירה 2.

בדיקה: הקוד מדפיס guess(lst) = 2.

סעיף ב(2)

תשובה

הפעולה מחזירה את המרחק הקטן ביותר (בהפרש מקומות) בין שתי חוליות בשרשרת שיש להן אותו ערך. אם כל הערכים בשרשרת שונים – מחזירה 0.

מה נדרש: לתאר במילים מה guess מחשבת.

רעיון: לכל חוליה, d הוא המרחק ממנה להופעה הבאה של אותו ערך (0 אם אין). guess מחזירה את הערך הקטן ביותר מבין כל ה־d השונים מ־0, כשהערך 0 משמש כסימן ל"עוד לא נמצא מרחק": התנאי d != 0 מדלג על חוליות שאין להן חוזרת, והתנאי r == 0 מאפשר ל־d הראשון שנמצא להיכנס.

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

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

בדיקה: 1, 2, 3, 4 ⇒ 0; ‏7, 7 ⇒ 1; ‏2, 9, 4, 2, 9 ⇒ 3.

סעיף ב(3)

תשובה

O(n²), כאשר n הוא מספר החוליות. במקרה הגרוע (כל הערכים שונים) what עוברת לכל חוליה על כל ההמשך: (n−1) + (n−2) + … + 0 = n(n−1)/2.

מה נדרש: סיבוכיות זמן של guess, כאשר n הוא מספר החוליות ב־lst.

רעיון: guess מופעלת פעם אחת על כל חוליה (ועוד פעם על null), ובכל הפעלה יש קריאה אחת ל־what על המשך השרשרת ועוד עבודה קבועה. לפי סעיף א(3), what עולה לכל היותר O(אורך ההמשך).

פתרון: המקרה הגרוע הוא שכל הערכים שונים: אז what אף פעם לא מוצאת את הערך ועוברת על כל ההמשך. לחוליה הראשונה n−1 חוליות, לשנייה n−2, וכן הלאה:
(n−1) + (n−2) + … + 1 + 0 = n(n−1)/2 ⇒ O(n²).
(במקרה הטוב, כשכל הערכים שווים, what מוצאת את הערך כבר בחוליה הבאה, וכל הפעלה עולה O(1), כלומר O(n) בסך הכול. הסיבוכיות שמבקשים היא של המקרה הגרוע: O(n²).)

בדיקה: כש־guess פועלת על שרשרות עם ערכים שונים באורך n = 100, 200, 400, 800, 1600 יש 5050, 20100, 80200, 320400, 1280800 קריאות ל־what, כלומר n(n+1)/2 – גידול פי 4 כשמכפילים את n. על שרשרות שכל ערכיהן שווים היו בדיוק n קריאות.

שאלה 3 · עץ K־זיגזג (25 נקודות)

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

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

דוגמה:

עץ זה אינו "עץ 1־זיגזג" כי בעץ יש מסלול שבו יותר מפנייה אחת: המסלול המודגש (צמתיו צבועים באפור) יורד ימינה, אחר כך שמאלה ואחר כך שוב ימינה – יש בו שתי פניות.
לעומת זאת, עץ זה הוא "עץ 2־זיגזג" כי אין בו מסלול שבו יותר משתי פניות (ובאותה מידה הוא גם "עץ 3־זיגזג", וגם "עץ 4־זיגזג" וכן הלאה).

א.

סרטטו "עץ 1־זיגזג" (כלומר שבכל מסלול של העץ יש לכל היותר פנייה אחת), ובו 9 צמתים ומקסימום 4 רמות.

ב.

כתבו פעולה חיצונית ששמה isZigzagK בשפת Java או IsZigzagK בשפת C# , המקבלת עץ בינרי – root מטיפוס שלם שאינו ריק, וערך K שאינו שלילי, ומחזירה true אם העץ הוא "עץ K־זיגזג", ואם לא – היא מחזירה false .
אפשר להיעזר בפעולת עזר.

◂ פתרון מלא לשאלה 3

סעיף א

תשובה

לדוגמה: שורש 1 עם הבנים 2 (שמאלי), 3 (ימני); ל־2 הבנים 4, 5; ל־3 הבנים 6, 7; ל־4 רק בן שמאלי 8; ל־6 רק בן שמאלי 9. (9 צמתים, 4 רמות, בכל מסלול ≤ פנייה אחת.)

מה נדרש: עץ בינרי עם 9 צמתים ולכל היותר 4 רמות, שבו בכל מסלול מהשורש לעלה יש לכל היותר פנייה אחת.

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

הבחנה: "עץ 0־זיגזג" בן 4 רמות לכל היותר יכול להכיל רק 7 צמתים (השורש ועוד שתי "שרשראות" ישרות – שמאלה וימינה – של 3 צמתים כל אחת). לכן ב־9 צמתים חייבות להיות פניות, ויש לפזר אותן כך שבכל מסלול תהיה לכל היותר אחת.

פתרון – עץ לדוגמה (המספרים בצמתים רק למעקב):

בכתיב מילולי: לשורש 1 – בן שמאלי 2 ובן ימני 3. ל־2 – בן שמאלי 4 ובן ימני 5. ל־3 – בן שמאלי 6 ובן ימני 7. ל־4 – רק בן שמאלי 8. ל־6 – רק בן שמאלי 9. הצמתים 5, 7, 8, 9 הם עלים.

בדיקה – כל המסלולים מהשורש לעלים:

מסלולכיווני הקשתותפניות
1 → 2 → 4 → 8שמאל, שמאל, שמאל0
1 → 2 → 5שמאל, ימין1
1 → 3 → 6 → 9ימין, שמאל, שמאל1
1 → 3 → 7ימין, ימין0

9 צמתים, 4 רמות, ובכל מסלול לכל היותר פנייה אחת ⇒ "עץ 1־זיגזג". (ובדוגמה: isZigzagK(עץ, 1) = true, isZigzagK(עץ, 0) = false. בסריקה של כל צורות העצים בנות 4 רמות לכל היותר נמצאו 44 עצים כאלה.)
יש עוד תשובות נכונות רבות.

טעות נפוצה: לתת לצומת 6 בן ימני – במסלול 1 → 3 → 6 כבר יש פנייה (ימין ואז שמאל), והבן הימני היה יוצר פנייה שנייה.

סעיף ב

תשובה

פעולת עזר zigzag(node, k, fromLeft): לבן השמאלי התקציב הוא k אם fromLeft, אחרת k − 1; לבן הימני – k אם !fromLeft, אחרת k − 1; תקציב שלילי ⇒ false. isZigzagK קוראת לה עם הבן השמאלי (fromLeft = true) והימני (false) של השורש עם K. יעילות O(n).

מה נדרש: פעולה שבודקת שבכל מסלול מהשורש לעלה יש לכל היותר K פניות.

רעיון: כדי לדעת אם ירידה לבן היא פנייה, צריך לדעת מאיזה כיוון הגענו לצומת הנוכחי. לכן כותבים פעולת עזר רקורסיבית zigzag(node, k, fromLeft):

•node – הצומת הנוכחי, fromLeft – true אם הגענו אליו בקשת שמאלית, ו־k – "התקציב": כמה פניות עוד מותר לבצע בהמשך המסלול.
•ירידה לבן השמאלי היא פנייה אם הגענו בקשת ימנית (!fromLeft) – ואז התקציב יורד ב־1; ירידה לבן הימני היא פנייה אם הגענו בקשת שמאלית.
•אם התקציב נעשה שלילי – יש מסלול עם יותר מ־K פניות ⇒ false. עלה ⇒ true.

השורש עצמו לא הגיע מאף כיוון, ולכן הקשתות היוצאות ממנו אינן פניות: isZigzagK מזמנת את פעולת העזר על הבן השמאלי (fromLeft = true) ועל הבן הימני (fromLeft = false) עם אותו K.

פתרון:

public static boolean isZigzagK(BinNode<Integer> root, int k)
{
    // הקשת הראשונה במסלול אינה יכולה ליצור פנייה
    if (root.hasLeft() && !zigzag(root.getLeft(), k, true))
        return false;
    if (root.hasRight() && !zigzag(root.getRight(), k, false))
        return false;
    return true;
}

// node – צומת שהגענו אליו מאביו; fromLeft – האם הגענו אליו בקשת שמאלית;
// k – כמה פניות עוד מותר לבצע בהמשך המסלול
public static boolean zigzag(BinNode<Integer> node, int k, boolean fromLeft)
{
    if (node.hasLeft())
    {
        int kLeft = k;
        if (!fromLeft)
            kLeft = k - 1;   // ימינה ואז שמאלה – פנייה
        if (kLeft < 0 || !zigzag(node.getLeft(), kLeft, true))
            return false;
    }
    if (node.hasRight())
    {
        int kRight = k;
        if (fromLeft)
            kRight = k - 1;  // שמאלה ואז ימינה – פנייה
        if (kRight < 0 || !zigzag(node.getRight(), kRight, false))
            return false;
    }
    return true;
}

בדיקה: על העץ מהדוגמה בשאלה: isZigzagK(root, 0) = false, isZigzagK(root, 1) = false (המסלול המודגש: ימין, שמאל, ימין – 2 פניות), isZigzagK(root, 2) = true, isZigzagK(root, 3) = true. על העץ מסעיף א: K = 1 ⇒ true, K = 0 ⇒ false. עץ שהוא עלה יחיד עם K = 0 ⇒ true.

יעילות: כל צומת מבוקר פעם אחת לכל היותר, ובכל ביקור מספר קבוע של פעולות ⇒ O(n), כאשר n מספר הצמתים בעץ.

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

הקוד המלא
public static boolean isZigzagK(BinNode<Integer> root, int k)
{
    // הקשת הראשונה במסלול אינה יכולה ליצור פנייה
    if (root.hasLeft() && !zigzag(root.getLeft(), k, true))
        return false;
    if (root.hasRight() && !zigzag(root.getRight(), k, false))
        return false;
    return true;
}

// node – צומת שהגענו אליו מאביו; fromLeft – האם הגענו אליו בקשת שמאלית;
// k – כמה פניות עוד מותר לבצע בהמשך המסלול
public static boolean zigzag(BinNode<Integer> node, int k, boolean fromLeft)
{
    if (node.hasLeft())
    {
        int kLeft = k;
        if (!fromLeft)
            kLeft = k - 1;   // ימינה ואז שמאלה – פנייה
        if (kLeft < 0 || !zigzag(node.getLeft(), kLeft, true))
            return false;
    }
    if (node.hasRight())
    {
        int kRight = k;
        if (fromLeft)
            kRight = k - 1;  // שמאלה ואז ימינה – פנייה
        if (kRight < 0 || !zigzag(node.getRight(), kRight, false))
            return false;
    }
    return true;
}

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

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

שאלה 4 · קודקודים במרחק חסום והשלמת הגרף לגרף קשיר (25 נקודות)

נתון גרף לא מכוון ולא קשיר G(V, E) שבו n קודקודים, מ־ v₀ עד vₙ₋₁ . הגרף מתאר רשת מחשבים: כל קודקוד הוא מחשב, וכל קשת היא כבל המחבר שני מחשבים.

א.

כתבו אלגוריתם שמקבל קודקוד בגרף – vⱼ , ומספר שלם k ≥ 1 , ומחזיר את כל הקודקודים (חוץ מ־ vⱼ עצמו) שהמרחק שלהם מ־ vⱼ הוא לכל היותר k , כלומר את כל הקודקודים שיש מסלול בינם ובין vⱼ שיש בו k קשתות או פחות.
הערה: יש לכתוב אלגוריתם יעיל שאינו עובר על כל המסלולים האפשריים.
דוגמה: עבור הגרף שלפניכם, הקודקוד 4 ו־ k = 2 , האלגוריתם יחזיר את הקודקודים 0 , 2 , 9 .

הסבר: המרחק מקודקוד 4 אל הקודקודים 2 ו־ 9 הוא קשת אחת, והמרחק אל קודקוד 0 הוא 2 קשתות. המרחק אל קודקוד 6 הוא 3 קשתות, ולכן הוא אינו מוחזר, ואל שאר הקודקודים אין מסלול מקודקוד 4 .

ב.

"רכיב קשירות" בגרף לא מכוון G(V, E) הוא קבוצת קודקודים שבה בין כל שני קודקודים יש מסלול, ואין שום קשת היוצאת מקודקוד בקבוצה לקודקוד שאינו בקבוצה. קודקוד שאין קשת בינו ובין שום קודקוד אחר יהיה בקבוצה משלו.
דוגמה: בגרף שבדוגמה לעיל יש שלושה רכיבי קשירות, והם מסומנים בקו מקווקו.

מנהל הרשת רוצה להוסיף כבלים (קשתות) כך שבין כל שני מחשבים יהיה מסלול, כלומר שהגרף יהיה קשיר.
כתבו אלגוריתם המחזיר את המספר המינימלי של קשתות שיש להוסיף לגרף G(V, E) כדי שיהיה קשיר, ורשימה של קשתות כאלה שאפשר להוסיף.
למשל עבור הדוגמה שלעיל, האלגוריתם יחזיר את המספר 2 ואת הקשתות (0, 1) ו־ (0, 8) .
הערה: יש לכתוב אלגוריתם יעיל שאינו עובר על כל המסלולים האפשריים.

◂ פתרון מלא לשאלה 4

סעיף א

תשובה

סריקת BFS מ־ vⱼ שמחשבת לכל קודקוד את מרחקו dist (מספר הקשתות): קודקוד שמתגלה נכנס לתור ולרשימת התוצאה; קודקוד שמרחקו כבר k אינו מרחיב את שכניו. מוחזרים כל הקודקודים שהתגלו (חוץ מ־ vⱼ). זמן ריצה O(|V| + |E|). בדוגמה (4, k = 2): 2, 9, 0.

מה נדרש: למצוא את כל הקודקודים שהמרחק שלהם מ־ vⱼ (מספר הקשתות במסלול הקצר ביותר) הוא בין 1 ל־ k .

רעיון: סריקה לרוחב (BFS) מגלה את הקודקודים לפי סדר המרחק מ־ vⱼ : קודם כל הקודקודים במרחק 1, אחר כך במרחק 2, וכן הלאה, וכשקודקוד v מתגלה מהקודקוד u מתקיים dist[v] = dist[u] + 1 – זה המרחק הקצר ביותר. לכן מספיק לשמור לכל קודקוד את dist שלו, ולא להרחיב (לא לעבור על השכנים של) קודקוד שמרחקו כבר k – השכנים החדשים שלו היו במרחק k + 1 . כל קודקוד נכנס לתור פעם אחת בלבד, ואין מעבר על מסלולים.

פתרון (הגרף מיוצג ברשימות שכנויות):

WithinK(G, vⱼ, k):
    לכל קודקוד u ב־V:   dist[u] ← −1          // −1 = עוד לא התגלה
    dist[vⱼ] ← 0
    Q ← תור ריק ;  Q.insert(vⱼ)
    R ← רשימה ריקה                          // הקודקודים שיוחזרו
    כל עוד Q אינו ריק:
        u ← Q.remove()
        אם dist[u] < k:                       // רק אז השכנים במרחק ≤ k
            לכל שכן v של u:
                אם dist[v] = −1:
                    dist[v] ← dist[u] + 1
                    הוסף את v ל־R
                    Q.insert(v)
    החזר את R

זמן ריצה: האתחול O(|V|). כל קודקוד נכנס לתור לכל היותר פעם אחת, ורשימת השכנים של כל קודקוד נסרקת לכל היותר פעם אחת – סך הכול O(|V| + |E|).

בדיקה – על הגרף שבדוגמה מקודקוד 4 עם k = 2 (שכנים בסדר עולה; בסוגריים – המרחק):

צעדהוצא מהתור (מרחק)שכנים חדשים (מרחק)התור אחרי הצעדR
14 (0)2 (1), 9 (1)[2, 9][2, 9]
22 (1)0 (2)[9, 0][2, 9, 0]
39 (1)–[0][2, 9, 0]
40 (2)– (מרחק 2 – לא ממשיכים)[][2, 9, 0]

התוצאה: 0, 2, 9 – כמו בדוגמה.

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

סעיף ב

תשובה

סופרים את רכיבי הקשירות בעזרת BFS מכל קודקוד שעוד לא סומן, ושומרים את הקודקוד שפתח כל רכיב (נציג). אם יש c רכיבים – המספר המינימלי הוא c − 1, והקשתות: מהנציג של הרכיב הראשון לנציג של כל רכיב אחר. זמן ריצה O(|V| + |E|). בדוגמה: c = 3, מוחזר 2 עם הקשתות (0, 1), (0, 8).

מה נדרש: כמה קשתות לכל הפחות צריך להוסיף כדי שהגרף יהיה קשיר, ואילו קשתות.

רעיון: נסמן ב־ c את מספר רכיבי הקשירות.

•צריך לפחות c − 1 קשתות: כל קשת חדשה מחברת שני קודקודים, ולכן היא מאחדת לכל היותר שני רכיבים לרכיב אחד – מספר הרכיבים יורד בכל הוספה לכל היותר ב־1. כדי לרדת מ־ c רכיבים לרכיב אחד צריך לפחות c − 1 הוספות.
•c − 1 קשתות מספיקות: בוחרים מכל רכיב קודקוד אחד (נציג), ומחברים את הנציג של הרכיב הראשון לנציג של כל אחד משאר הרכיבים.

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

פתרון:

ConnectEdges(G):
    לכל קודקוד u ב־V:   visited[u] ← false
    reps ← רשימה ריקה                      // נציג אחד מכל רכיב
    לכל קודקוד s מ־ v₀ עד vₙ₋₁:
        אם visited[s] = false:               // s פותח רכיב קשירות חדש
            הוסף את s ל־reps
            visited[s] ← true ;  Q ← תור ריק ;  Q.insert(s)
            כל עוד Q אינו ריק:
                u ← Q.remove()
                לכל שכן v של u:
                    אם visited[v] = false:
                        visited[v] ← true ;  Q.insert(v)
    c ← מספר האיברים ב־reps
    edges ← רשימה ריקה
    לכל i מ־2 עד c:
        הוסף ל־edges את הקשת (reps[1], reps[i])
    החזר את c − 1 ואת edges

זמן ריצה: בכל הסריקות יחד כל קודקוד נכנס לתור פעם אחת וכל רשימת שכנים נסרקת פעם אחת ⇒ O(|V| + |E|); בניית רשימת הקשתות O(c) ⊆ O(|V|). סך הכול O(|V| + |E|).

בדיקה – על הגרף שבדוגמה:

קודקוד שפותח רכיבהרכיב שנמצא
0{0, 2, 4, 6, 9, 10}
1{1, 3, 5, 7}
8{8}

reps = [0, 1, 8], c = 3 ⇒ מוחזר 2 עם הקשתות (0, 1), (0, 8) – כמו בדוגמה. אחרי הוספתן הגרף קשיר, ואף קשת אחת שתתווסף לא הופכת את הגרף לקשיר.

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

שאלה 5 · סדר ביצוע משימות – האלגוריתם Order (25 נקודות)

בפרויקט יש שבע משימות: A, B, C, D, E, F, G . יש משימות שאפשר להתחיל רק אחרי שמשימות אחרות הסתיימו. את הדרישות מציג הגרף המכוון G = (V, E) שלפניכם: קשת X → Y פירושה שאת המשימה Y אפשר להתחיל רק אחרי שהמשימה X הסתיימה.

צמתי הגרףV = {A, B, C, D, E, F, G}
קשתות הגרףE = {(A → C), (B → A), (B → D), (C → F), (D → C), (D → F), (E → D), (E → G), (G → A), (G → F)}
א.

הצומת M מוגדר "שכן יוצא" של הצומת U , אם יש בגרף קשת U → M .
לפניכם טבלה המציגה את השכנים היוצאים של כל צומת בגרף G , מסודרים לפי סדר האלף־בית. השכנים היוצאים של B ושל E נתונים בטבלה.
השלימו את הטבלה. (אם לצומת אין שכנים יוצאים, כתבו – .)

UABCDEFG
MA , DD , G
ב.

נתון האלגוריתם Order (G) , שמחשב סדר לביצוע המשימות.

שלב אתחול נתונים באלגוריתם:

1.נגדיר מערך – indeg בגודל 7 , ובו נשמור בעבור כל צומת את מספר הקשתות שנכנסות אליו (דרגת הכניסה שלו). בהתחלה:
indeg:ABCDEFG
2022031
2.נגדיר מערך – level בגודל 7 (בהתחלה בכל התאים במערך מופיע הערך 0).
level:ABCDEFG
0000000
3.נגדיר תור – q , שאליו נכניס, לפי סדר האלף־בית, את כל הצמתים שבתא שלהם במערך indeg מופיע הערך 0 (כלומר את B ואחריו את E).
4.נגדיר רשימה – R , שבהתחלה היא ריקה.

שלב הרצת האלגוריתם:

1.כל עוד התור – q אינו ריק:
•נוציא את הצומת הנמצא בראש התור, ונקרא לו U . נוסיף את U לסוף הרשימה R .
•בעבור כל שכן יוצא – M של הצומת U , לפי הסדר שבטבלה מסעיף א:

א. נקטין ב־ 1 את הערך בתא של M במערך indeg .
ב. אם level[U] + 1 גדול מן הערך level[M] , נכתוב בתא של M במערך level את הערך level[U] + 1 .
ג. אם הערך בתא של M במערך indeg הוא עכשיו 0 , נכניס את M לתור.

2.כשהתור ריק: אם ברשימה R יש 7 צמתים, נחזיר את R ; אחרת נחזיר null . האלגוריתם מסתיים.

עקבו בטבלת מעקב אחרי ריצת האלגוריתם Order על הגרף הנתון G .
המעקב צריך לכלול בכל איטרציה את הפריטים שלהלן:

•מצב התור
•הצומת U שהוצאנו מן התור
•מערך indeg
•מערך level
•הרשימה R
התורUindeg ABCDEFGlevel ABCDEFGR
←[ ]
←[ ]
ג.

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

◂ פתרון מלא לשאלה 5

סעיף א

תשובה

A: C ; C: F ; D: C, F ; F: – ; G: A, F

מה נדרש: לכל צומת – רשימת הצמתים שיש אליהם קשת ממנו, לפי סדר האלף־בית.

רעיון: עוברים על רשימת הקשתות E . כל קשת (X → Y) מוסיפה את Y לשכנים היוצאים של X בלבד (הגרף מכוון).

פתרון:

UABCDEFG
MCA , DFC , FD , G–A , F

בדיקה: בטבלה 1 + 2 + 1 + 2 + 2 + 0 + 2 = 10 שכנים יוצאים – כמספר הקשתות.

סעיף ב

תשובה

הוצאות מן התור לפי הסדר: B, E, D, G, A, C, F. בסוף: כל ערכי indeg הם 0 ; level = (A 2, B 0, C 3, D 1, E 0, F 4, G 1).

מה נדרש: להריץ את Order צעד אחר צעד ולרשום אחרי כל איטרציה את התור, את U , את שני המערכים ואת R .

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

פתרון:

•איטרציה 1: U = B (level 0). A: indeg 2→1 , level 1 . D: indeg 2→1 , level 1 .
•איטרציה 2: U = E (level 0). D: indeg 1→0 , level נשאר 1 ⇐ D נכנס לתור. G: indeg 1→0 , level 1 ⇐ G נכנס לתור.
•איטרציה 3: U = D (level 1). C: indeg 2→1 , level 2 . F: indeg 3→2 , level 2 .
•איטרציה 4: U = G (level 1). A: indeg 1→0 , level 2 (1 + 1 > 1) ⇐ A נכנס לתור. F: indeg 2→1 , level נשאר 2 .
•איטרציה 5: U = A (level 2). C: indeg 1→0 , level 3 ⇐ C נכנס לתור.
•איטרציה 6: U = C (level 3). F: indeg 1→0 , level 4 ⇐ F נכנס לתור.
•איטרציה 7: U = F – אין לו שכנים יוצאים. התור ריק, ב־ R יש 7 צמתים ⇐ האלגוריתם מחזיר את R .
התור (אחרי האיטרציה)Uindeg[A]indeg[B]indeg[C]indeg[D]indeg[E]indeg[F]indeg[G]level[A]level[B]level[C]level[D]level[E]level[F]level[G]R
[B, E] (אתחול)20220310000000
[E]B10210311001000B
[D, G]E10200301001001B, E
[G]D10100201021021B, E, D
[A]G00100102021021B, E, D, G
[C]A00000102031021B, E, D, G, A
[F]C00000002031041B, E, D, G, A, C
[]F00000002031041B, E, D, G, A, C, F

בדיקה: מעקב אחרי Order בדיוק כמתואר נותן את אותה טבלה. כל קשת בגרף הולכת "קדימה" ברשימה R , והערך הסופי של level[v] שווה למספר הקשתות במסלול הארוך ביותר שמסתיים ב־ v (למשל F : E→G→A→C→F , 4 קשתות).

טעות נפוצה: להכניס צומת לתור כבר כשמקטינים את הערך שלו במערך indeg , לפני שהגיע ל־ 0 – למשל להכניס את A לתור כבר באיטרציה 1 . A נכנס לתור רק באיטרציה 4 , אחרי שגם G (שיש ממנו קשת אל A) הוצא מן התור.

סעיף ג

תשובה

B , E , D , G , A , C , F

מה נדרש: הרשימה R שמוחזרת בסוף הריצה.

רעיון: R מכילה את הצמתים לפי הסדר שבו הוצאו מן התור.

פתרון: B , E , D , G , A , C , F .
זהו מיון טופולוגי של הגרף – סדר אפשרי לביצוע המשימות: כל קשת X → Y מקיימת ש־ X מופיע ברשימה לפני Y (למשל G → A : G במקום הרביעי ו־ A במקום החמישי).

בדיקה: התוצאה: ['B', 'E', 'D', 'G', 'A', 'C', 'F'], וכל 10 הקשתות הולכות קדימה ברשימה.

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

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

א.(1)

כתבו אלגוריתם המקבל גרף ממושקל (אי־שלילי) לא מכוון כלשהו, שבו n קודקודים מ־ v₀ עד vₙ₋₁ , ושני קודקודים בגרף – vⱼ ו־ vₜ . האלגוריתם ימצא את המסלולים הקלים ביותר מקודקוד vⱼ אל שאר הקודקודים שבגרף, ואחר כך ידפיס את המסלול הקל ביותר מ־ vⱼ אל vₜ – את הקודקודים שבמסלול לפי הסדר, מ־ vⱼ ועד vₜ . אם אין מסלול מ־ vⱼ אל vₜ , האלגוריתם ידפיס "אין מסלול".

(2)

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

ב.(1)

בעבור גרף G הנתון, מצאו בעזרת האלגוריתם שכתבתם את המסלול הקל ביותר מקודקוד K לכל אחד מן הקודקודים, וסרטטו טבלת מעקב כמפורט:
המעקב יכלול בכל איטרציה את קבוצת הקודקודים הקבועים (שכבר ביקרנו בהם) – P ואת קבוצת הקודקודים הזמניים (שבהם עדיין לא ביקרנו) – T . נוסף על כך, בעבור כל קודקוד יצוין אורך המסלול עד אליו וזהות הקודקוד הקודם לו (ה"הורה" שלו).
מה ידפיס האלגוריתם עבור vₜ = W ?

(2)

בעבור גרף G הנתון, סרטטו את עץ המסלולים הקלים ביותר (מקודקוד K).

◂ פתרון מלא לשאלה 6

סעיף א(1)

תשובה

שלב 1 – דייקסטרה מ־vⱼ (d, π; בכל איטרציה u בעל d מינימלי ב־T עובר ל־P, ומרפים את קשתותיו). שלב 2 – אם d[vₜ] = ∞ מדפיסים "אין מסלול"; אחרת PrintPath(vₜ): אם v = vⱼ מדפיסים אותו, אחרת קוראים ל־PrintPath(π[v]) ואז מדפיסים את v.

מה נדרש: דייקסטרה ממקור vⱼ , ואחר כך הדפסת המסלול אל vₜ בסדר הנכון (מההתחלה לסוף).

רעיון: אחרי דייקסטרה, π[v] הוא הקודקוד שלפני v במסלול הקל ביותר. הליכה v, π[v], π[π[v]], … נותנת את המסלול מהסוף להתחלה. כדי להדפיס מההתחלה משתמשים ברקורסיה: קודם מדפיסים (רקורסיבית) את המסלול אל π[v] , ורק אחר כך את v . (אפשר גם לשמור את הקודקודים ברשימה ולהפוך אותה.)

פתרון:

Dijkstra(G, w, vⱼ):
    לכל קודקוד v ב־V:   d[v] ← ∞ ;  π[v] ← NIL
    d[vⱼ] ← 0
    P ← ∅ ;  T ← V
    כל עוד T ≠ ∅:
        u ← הקודקוד ב־T שעבורו d[u] מינימלי
        T ← T − {u} ;  P ← P ∪ {u}
        לכל שכן v של u כך ש־ v ∈ T:
            אם d[u] + w(u, v) < d[v]:
                d[v] ← d[u] + w(u, v)
                π[v] ← u

PrintPath(v):                      // מדפיס את המסלול מ־ vⱼ אל v
    אם v = vⱼ:
        הדפס v
    אחרת:
        PrintPath(π[v])
        הדפס v

ShortestRoute(G, w, vⱼ, vₜ):
    Dijkstra(G, w, vⱼ)
    אם d[vₜ] = ∞:
        הדפס "אין מסלול"
    אחרת:
        PrintPath(vₜ)

בדיקה: המעקב על G מ־K מופיע בסעיף ב(1). עבור vₜ = W: PrintPath(W) קורא ל־PrintPath(T), שקורא ל־PrintPath(Q), וכן הלאה דרך P ו־M עד PrintPath(K), שמדפיס את K. בחזרה מהרקורסיה מודפס: K M P Q T W.

טעות נפוצה: להדפיס את v לפני הקריאה הרקורסיבית – אז המסלול מודפס הפוך (W T Q P M K).

סעיף א(2)

תשובה

O(n²): דייקסטרה במערכים O(n²) + הדפסת המסלול O(n).

מה נדרש: זמן הריצה של האלגוריתם מסעיף א(1) כפונקציה של n (ומספר הקשתות |E|).

פתרון (מימוש פשוט – d ו־π במערכים, הגרף ברשימות שכנויות):

•אתחול: O(n).
•הלולאה הראשית רצה n פעמים (בכל איטרציה קודקוד אחד עובר מ־T ל־P).
•בכל איטרציה מחפשים את המינימום ב־T במעבר על עד n קודקודים: O(n). סך הכול לחיפושים: n · O(n) = O(n²).
•ההרפיות: רשימת השכנים של כל קודקוד נסרקת פעם אחת (כשהוא נקבע) ⇒ סך הכול O(|E|), ו־ |E| ≤ n² .
•הדפסת המסלול: במסלול יש לכל היותר n קודקודים, ו־PrintPath מבצעת קריאה אחת לכל קודקוד במסלול ⇒ O(n).

לכן זמן הריצה הוא O(n²) + O(n) = O(n²).

(הערה: עם ערימת מינימום – O((n + |E|)·log n).)

סעיף ב(1)

תשובה

d/π: K 0/– , M 3/K , L 7/K , P 9/M , Q 13/P , N 14/P , T 15/Q , R 20/N , W 22/T , Z 24/T. סדר הקביעה: K, M, L, P, Q, N, T, R, W, Z. עבור W יודפס: K M P Q T W (22 דקות).

מה נדרש: להריץ את דייקסטרה מ־K על G, להראות בכל איטרציה את P, T ואת d/π של כל קודקוד, ולמצוא מה יודפס עבור W.

רעיון: בכל איטרציה בוחרים מ־T את הקודקוד בעל d מינימלי (בגרף הזה לא היה שוויון בשום שלב), מעבירים אותו ל־P ומרפים את הקשתות לשכניו שב־T.

פתרון – טבלת המעקב (בכל תא: d/π; מודגש – הקודקוד שנקבע באיטרציה):

איטרציההקודקוד שנבחרP (קבועים)T (זמניים)KLMNPQRTWZ
0 (אתחול)–{ }{K, L, M, N, P, Q, R, T, W, Z}0/–∞/–∞/–∞/–∞/–∞/–∞/–∞/–∞/–∞/–
1K{K}{L, M, N, P, Q, R, T, W, Z}0/–7/K3/K∞/–12/K∞/–∞/–∞/–∞/–∞/–
2M{K, M}{L, N, P, Q, R, T, W, Z}0/–7/K3/K∞/–9/M17/M∞/–∞/–∞/–∞/–
3L{K, M, L}{N, P, Q, R, T, W, Z}0/–7/K3/K16/L9/M17/M∞/–∞/–∞/–∞/–
4P{K, M, L, P}{N, Q, R, T, W, Z}0/–7/K3/K14/P9/M13/P∞/–20/P∞/–∞/–
5Q{K, M, L, P, Q}{N, R, T, W, Z}0/–7/K3/K14/P9/M13/P∞/–15/Q23/Q∞/–
6N{K, M, L, P, Q, N}{R, T, W, Z}0/–7/K3/K14/P9/M13/P20/N15/Q23/Q∞/–
7T{K, M, L, P, Q, N, T}{R, W, Z}0/–7/K3/K14/P9/M13/P20/N15/Q22/T24/T
8R{K, M, L, P, Q, N, T, R}{W, Z}0/–7/K3/K14/P9/M13/P20/N15/Q22/T24/T
9W{K, M, L, P, Q, N, T, R, W}{Z}0/–7/K3/K14/P9/M13/P20/N15/Q22/T24/T
10Z{K, M, L, P, Q, N, T, R, W, Z}{ }0/–7/K3/K14/P9/M13/P20/N15/Q22/T24/T

פירוט ההרפיות:

1.K (0): L: 0+7 = 7 < ∞ ⇒ 7/K ; M: 0+3 = 3 < ∞ ⇒ 3/K ; P: 0+12 = 12 < ∞ ⇒ 12/K
2.M (3): K: כבר קבוע ; P: 3+6 = 9 < 12 ⇒ 9/M ; Q: 3+14 = 17 < ∞ ⇒ 17/M
3.L (7): K: כבר קבוע ; N: 7+9 = 16 < ∞ ⇒ 16/L ; P: 7+3 = 10 ≥ 9 – ללא שינוי
4.P (9): K: כבר קבוע ; L: כבר קבוע ; M: כבר קבוע ; N: 9+5 = 14 < 16 ⇒ 14/P ; Q: 9+4 = 13 < 17 ⇒ 13/P ; T: 9+11 = 20 < ∞ ⇒ 20/P
5.Q (13): M: כבר קבוע ; P: כבר קבוע ; T: 13+2 = 15 < 20 ⇒ 15/Q ; W: 13+10 = 23 < ∞ ⇒ 23/Q
6.N (14): L: כבר קבוע ; P: כבר קבוע ; R: 14+6 = 20 < ∞ ⇒ 20/N ; T: 14+3 = 17 ≥ 15 – ללא שינוי
7.T (15): P: כבר קבוע ; N: כבר קבוע ; Q: כבר קבוע ; R: 15+8 = 23 ≥ 20 – ללא שינוי ; W: 15+7 = 22 < 23 ⇒ 22/T ; Z: 15+9 = 24 < ∞ ⇒ 24/T
8.R (20): N: כבר קבוע ; T: כבר קבוע ; Z: 20+5 = 25 ≥ 24 – ללא שינוי
9.W (22): Q: כבר קבוע ; T: כבר קבוע ; Z: 22+5 = 27 ≥ 24 – ללא שינוי
10.Z (24): T: כבר קבוע ; R: כבר קבוע ; W: כבר קבוע

המסלולים הקלים ביותר מ־K:

קודקודאורךמסלול
M3K → M
L7K → L
P9K → M → P
Q13K → M → P → Q
N14K → M → P → N
T15K → M → P → Q → T
R20K → M → P → N → R
W22K → M → P → Q → T → W
Z24K → M → P → Q → T → Z

עבור vₜ = W האלגוריתם ידפיס: K M P Q T W (זמן נסיעה 22 דקות).

בדיקה: הרפיה חוזרת של כל הקשתות (בלמן־פורד) נותנת אותם מרחקים, ולכל קודקוד יש מסלול קל ביותר יחיד.

טעות נפוצה: לקבוע את P עם 12 (הקשת הישירה K–P) – המסלול K → M → P קל יותר (9).

סעיף ב(2)

תשובה

קשתות העץ (π[v] – v): K–L, K–M, M–P, P–N, P–Q, Q–T, N–R, T–W, T–Z.

מה נדרש: לשרטט את העץ שנוצר מקשתות ההורה π.

רעיון: לכל קודקוד v ≠ K מוסיפים את הקשת (π[v], v). מתקבלות n − 1 = 9 קשתות היוצרות עץ ששורשו K.

פתרון: לפי השורה האחרונה בטבלה מסעיף ב(1): π[L] = K, π[M] = K, π[P] = M, π[N] = P, π[Q] = P, π[T] = Q, π[R] = N, π[W] = T, π[Z] = T.

ובצורת עץ:

K
├── M   (3)
│   └── P   (9)
│       ├── Q   (13)
│       │   └── T   (15)
│       │       ├── W   (22)
│       │       └── Z   (24)
│       └── N   (14)
│           └── R   (20)
└── L   (7)

בדיקה: סכום המשקלים על המסלול מהשורש לכל קודקוד בעץ שווה ל־ d שלו (למשל K–M–P–Q–T–Z: 3 + 6 + 4 + 2 + 9 = 24). המסלול ש־PrintPath מדפיס עבור W הוא בדיוק המסלול מהשורש אל W בעץ.

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

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

שאלה 7 · שפה המוגדרת בארבע דרישות – דוגמאות והוכחת רגולריות (25 נקודות)

L היא שפת כל המילים מעל הא"ב {a, b, c} המקיימות את כל הדרישות שלפניכם:

— המילה אינה ריקה, והאות הראשונה במילה זהה לאות האחרונה במילה (במילה בת אות אחת, אות זו היא גם הראשונה וגם האחרונה).

— מספר המופעים של האות c במילה מתחלק ב־3 בלי שארית.

— המילה אינה מכילה את הרצף ba .

— אורך המילה הוא מספר אי־זוגי.

א.(1)

הביאו דוגמה אחת למילה השייכת לשפה L , שמופיעות בה כל שלוש האותיות a , b , c .

(2)

הביאו דוגמה אחת למילה שאינה שייכת לשפה L , אף שהיא מקיימת שלוש מן הדרישות. ציינו איזו דרישה אינה מתקיימת בה.

ב.

הוכיחו שהשפה L רגולרית.

הערה: אפשר להיעזר בתכונות סגירות.

◂ פתרון מלא לשאלה 7

סעיף א(1)

תשובה

לדוגמה: aabccca (יש עוד אפשרויות, למשל cabcc).

מה נדרש: מילה אחת ב־L שיש בה a, b ו־c.

רעיון:
בונים מילה שמתחילה ומסתיימת באותה אות, עם 3 אותיות c (או 0 או 6...), באורך אי־זוגי, ומקפידים שאחרי b לא תבוא a מיד.

פתרון:
המילה aabccca:

דרישהבדיקהמתקיימת?
ראשונה = אחרונהa ... a✔
מספר ה־c מתחלק ב־33 אותיות c✔
אין baאחרי ה־b בא c✔
אורך אי־זוגי7✔

בדיקה: לפי הגדרת השפה, המילה שייכת ל־L. המילים הקצרות ביותר ב־L שמכילות את שלוש האותיות הן באורך 5, למשל cabcc, cacbc, ccabc.

טעות נפוצה: לשכוח ש־b ו־a צמודות יוצרות ba גם באמצע המילה (למשל abccaba – לא בשפה).

סעיף א(2)

תשובה

לדוגמה: aba – מקיימת את שלוש הדרישות האחרות, אבל מכילה את הרצף ba.

מה נדרש: מילה שנכשלת בדרישה אחת בדיוק.

רעיון:
לוקחים מילה "כמעט טובה" ומקלקלים בה דרישה אחת.

פתרון:
המילה aba:

דרישהבדיקהמתקיימת?
ראשונה = אחרונהa ... a✔
מספר ה־c מתחלק ב־30 אותיות c (0 מתחלק ב־3)✔
אין baמופיע ba (אותיות 2–3)✘
אורך אי־זוגי3✔

לכן aba אינה שייכת ל־L. (דוגמאות נוספות: cbc – מספר ה־c הוא 2; aa – אורך זוגי.)

בדיקה: aba – שלוש דרישות מתקיימות, "אין ba" לא מתקיימת ⇒ לא ב־L.

טעות נפוצה: לחשוב ש־0 אותיות c לא מקיים "מתחלק ב־3". 0 מתחלק ב־3 בלי שארית.

סעיף ב

תשובה

L = L₁ ∩ L₂ ∩ L₃ ∩ L₄, כאשר Lᵢ היא שפת המילים המקיימות את הדרישה ה־i. לכל Lᵢ יש אוטומט סופי דטרמיניסטי (בפתרון), ולכן כולן רגולריות. השפות הרגולריות סגורות לחיתוך ⇒ L רגולרית.

מה נדרש: להראות ש־L רגולרית.

רעיון:
מפרקים את L לארבע שפות פשוטות – אחת לכל דרישה:

•L₁ – מילים לא ריקות שהאות הראשונה בהן זהה לאחרונה.
•L₂ – מילים שמספר ה־c בהן מתחלק ב־3.
•L₃ – מילים שאינן מכילות ba.
•L₄ – מילים באורך אי־זוגי.

מילה שייכת ל־L בדיוק כשהיא מקיימת את כל הדרישות, ולכן L = L₁ ∩ L₂ ∩ L₃ ∩ L₄. מספיק להראות שכל Lᵢ רגולרית (בונים לה אוטומט סופי), ואז להשתמש בכך שהשפות הרגולריות סגורות לחיתוך.

פתרון:
אוטומט סופי דטרמיניסטי לכל אחת מן השפות (→ = מצב התחלה; "מקבל" = מצב מקבל):

L₁ – האות הראשונה זהה לאחרונה (והמילה לא ריקה). זוכרים את האות הראשונה, ובכל צעד בודקים אם האות האחרונה שנקראה זהה לה:

מצבabcמשמעות
→ sEaEbEcעוד לא נקראה אף אות
Ea (מקבל)EaNaNaראשונה a, אחרונה a
NaEaNaNaראשונה a, אחרונה לא a
Eb (מקבל)NbEbNbראשונה b, אחרונה b
NbNbEbNbראשונה b, אחרונה לא b
Ec (מקבל)NcNcEcראשונה c, אחרונה c
NcNcNcEcראשונה c, אחרונה לא c

L₂ – מספר ה־c מתחלק ב־3. סופרים את ה־c מודולו 3 (a ו־b לא משנים את המצב):

מצבabcמשמעות
→ r0 (מקבל)r0r0r1מספר ה־c שנקראו %3 = 0
r1r1r1r2%3 = 1
r2r2r2r0%3 = 2

L₃ – אין רצף ba. זוכרים אם האות האחרונה הייתה b; a אחרי b – אין מעבר (דחייה):

מצבabcמשמעות
→ p (מקבל)ptpהאות האחרונה אינה b (או שלא נקראה אות)
t (מקבל)–tpהאות האחרונה b

L₄ – אורך אי־זוגי. מצב לזוגי ומצב לאי־זוגי:

מצבabcמשמעות
→ eoooנקרא מספר זוגי של אותיות
o (מקבל)eeeנקרא מספר אי־זוגי של אותיות

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

המסקנה: לכל אחת מן השפות L₁–L₄ יש אוטומט סופי דטרמיניסטי, ולכן כולן רגולריות. חיתוך של שתי שפות רגולריות הוא שפה רגולרית (אוטומט המכפלה: מריצים את שני האוטומטים "במקביל" ומקבלים כששניהם מקבלים). לכן L₁ ∩ L₂ רגולרית, ומכאן (L₁ ∩ L₂) ∩ L₃ רגולרית, ולבסוף L = (L₁ ∩ L₂ ∩ L₃) ∩ L₄ רגולרית. ∎

בדיקה: כל אחד מארבעת האוטומטים מקבל בדיוק את המילים שמקיימות את הדרישה שלו. אוטומט המכפלה של הארבעה (49 מצבים ישיגים) מקבל בדיוק את L (למשל a, aabccca, cabcc מתקבלות; aba, cbc, aa נדחות).

טעות נפוצה: לנסות לבנות מיד אוטומט אחד לכל L – זה אפשרי אבל ארוך ומועד לטעויות. ההוכחה בעזרת תכונת הסגירות לחיתוך קצרה ובטוחה יותר.

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

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

א.(1)

נתונות שלוש שפות L₁ , L₂ , L₃ מעל הא"ב {a, b} . הסימון R(L) מציין את השפה ההפוכה של L , והסימון L* מציין את סגור קלייני של L (פעולת הכוכב) .

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

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

(1) (L₁ · L₂)* = L₁* · L₂*

(2)

R(L₁ ∪ L₂) = R(L₁) ∪ R(L₂)

(3)

(L₁ ∩ L₂) · L₃ = (L₁ · L₃) ∩ (L₂ · L₃)

ב.

נתונה השפה L מעל הא"ב {a, b, c} :

L = {aⁿbᵐcⁿ⁺²ᵐ | n ≥ 2, m ≥ 0}

בנו אוטומט מחסנית דטרמיניסטי המקבל את השפה L .

◂ פתרון מלא לשאלה 8

סעיף א(1)

תשובה

לא נכונה. דוגמה: L₁ = {a}, L₂ = {b}: abab ∈ (L₁·L₂)* אבל abab ∉ L₁*·L₂* = { aⁱbʲ | i, j ≥ 0 }.

מה נדרש: לקבוע אם הסגור של השרשור שווה לשרשור הסגורים.

רעיון: ב־(L₁·L₂)* החלקים מ־L₁ ומ־L₂ מתחלפים (L₁ L₂ L₁ L₂ …), וב־L₁*·L₂* קודם כל החלקים מ־L₁ ורק אחריהם כל החלקים מ־L₂.

פתרון: לא נכונה. דוגמה נגדית: L₁ = {a} , L₂ = {b}.

•L₁·L₂ = {ab}, ולכן (L₁·L₂)* = {ε, ab, abab, ababab, …}. בפרט abab ∈ (L₁·L₂)*.
•L₁* · L₂* = { aⁱbʲ | i, j ≥ 0 } – כל ה־a לפני כל ה־b. ב־abab יש a אחרי b, ולכן abab ∉ L₁*·L₂*.

השפות שונות – הטענה לא מתקיימת. (גם בכיוון השני: aab ∈ L₁*·L₂* אבל aab ∉ (L₁·L₂)*.)

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

סעיף א(2)

תשובה

נכונה – היפוך פועל על כל מילה בנפרד, ולכן מתפלג מעל איחוד.

מה נדרש: לקבוע אם ההיפוך של איחוד שווה לאיחוד ההיפוכים.

רעיון: R(L) = { R(w) | w ∈ L } – הופכים כל מילה בנפרד. נראה הכלה בשני הכיוונים.

פתרון: נכונה.

•⊆: תהי x ∈ R(L₁ ∪ L₂). אז x = R(w) עבור w ∈ L₁ ∪ L₂. אם w ∈ L₁ אז x ∈ R(L₁), ואם w ∈ L₂ אז x ∈ R(L₂). בכל מקרה x ∈ R(L₁) ∪ R(L₂).
•⊇: תהי x ∈ R(L₁) ∪ R(L₂). אם x ∈ R(L₁) אז x = R(w) עם w ∈ L₁ ⊆ L₁ ∪ L₂, ולכן x ∈ R(L₁ ∪ L₂). אותו דבר אם x ∈ R(L₂).

שתי ההכלות מתקיימות, ולכן השפות שוות.

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

סעיף א(3)

תשובה

לא נכונה. דוגמה: L₁ = {a}, L₂ = {ab}, L₃ = {b, ε}: אגף שמאל ∅·L₃ = ∅, אגף ימין {ab, a} ∩ {abb, ab} = {ab}.

מה נדרש: לקבוע אם שרשור מימין "מתפלג" מעל חיתוך.

רעיון: באגף ימין מספיק שהמילה כולה תתקבל משני שרשורים, ובכל אחד מהם ייתכן חיתוך אחר של המילה לשני חלקים. באגף שמאל דורשים חלק ראשון אחד ששייך גם ל־L₁ וגם ל־L₂.

פתרון: לא נכונה. דוגמה נגדית: L₁ = {a} , L₂ = {ab} , L₃ = {b, ε}.

•אגף שמאל: L₁ ∩ L₂ = ∅, ושרשור עם הקבוצה הריקה הוא ריק: ∅.
•אגף ימין: L₁·L₃ = {ab, a}, L₂·L₃ = {abb, ab}, והחיתוך הוא {ab} (ab = a·b = ab·ε).

∅ ≠ {ab} – הטענה לא מתקיימת. (הכיוון ⊆ דווקא תמיד נכון: מילה מאגף שמאל שייכת לשני השרשורים.)

בדיקה: שני האגפים לדוגמה הזאת: ∅ מול {ab}. באופן כללי אגף שמאל תמיד מוכל באגף ימין, אבל לא תמיד שווה לו.

טעות נפוצה: לבלבל בין ∅ (שפה ריקה) לבין {ε} (שפה שמכילה את המילה הריקה). ∅·A = ∅, אבל {ε}·A = A.

סעיף ב

תשובה

ה־a הראשון דוחף X, כל a נוסף דוחף A, כל b דוחף שני B; כל c שולף סימן אחד; ה־c ששולף את X עובר למצב המקבל f. b ו־c מותרים רק אחרי שתי a לפחות. טבלת המעברים המלאה בפתרון.

מה נדרש: אוטומט מחסנית דטרמיניסטי (אמ"ד) שמקבל בדיוק את המילים aⁿbᵐcᵏ עם n ≥ 2, m ≥ 0 ו־k = n + 2m.

רעיון:

•מספר ה־c צריך להיות n + 2m: לכל a דוחפים סימן אחד, ולכל b דוחפים שני סימנים. כל c שולף סימן אחד. המילה תקינה אם המחסנית מתרוקנת בדיוק עם ה־c האחרון.
•כדי לדעת מתי הוצא הסימן האחרון (בלי מעברי ε), ה־a הראשון דוחף סימן מיוחד X ושאר ה־a דוחפים A. ה־c ששולף את X הוא בהכרח האחרון – עוברים איתו למצב המקבל f, שאין ממנו מעברים (תו נוסף ⇒ דחייה).
•התנאי n ≥ 2: אחרי a אחד בלבד (מצב q₁) אין מעבר ב־b או ב־c. m = 0 מותר: מ־q₂ אפשר לעבור ישר ל־c.

פתרון:

•א"ב הקלט: {a, b, c}. א"ב המחסנית: {⊥, X, A, B}, בתחילה במחסנית רק ⊥ (סימן תחתית).
•מצב התחלה: q₀. מצב מקבל: f.
•המילה מתקבלת אם בסוף הקלט האוטומט נמצא במצב מקבל.
•הסימון "x, T / γ": קוראים את התו x כשבראש המחסנית T; מוציאים את T ודוחפים במקומו את המחרוזת γ (התו השמאלי של γ הוא הראש החדש). γ = ε – רק שליפה.
•אין מעברי ε, ולכל שלשה (מצב, תו, ראש המחסנית) יש לכל היותר מעבר אחד ⇒ האוטומט דטרמיניסטי. צירוף שאינו בטבלה ⇒ האוטומט נתקע והמילה נדחית.
מצבקלטראש המחסניתמעבר (קלט, ראש / נדחף)מצב חדשהסבר
q₀a⊥a, ⊥ / X⊥q₁ה־a הראשון: מסמן X מעל ⊥
q₁aXa, X / AXq₂ה־a השני
q₂aAa, A / AAq₂עוד a
q₂bAb, A / BBAq₃b ראשון: שני B
q₃bBb, B / BBBq₃עוד b: עוד שני B
q₂cAc, A / εq₄c ראשון, בלי b
q₃cBc, B / εq₄c ראשון אחרי b
q₄cBc, B / εq₄שולפים B
q₄cAc, A / εq₄שולפים A
q₄cXc, X / εfה־c האחרון: שולף את X ⇒ f

מעקב על aaabccccc (n = 3, m = 1, k = 5 = 3 + 2·1 ⇒ בשפה):

נקראמצב אחרימחסנית (הראש מימין)
–q₀⊥
aq₁⊥X
aq₂⊥XA
aq₂⊥XAA
bq₃⊥XAABB
cq₄⊥XAAB
cq₄⊥XAA
cq₄⊥XA
cq₄⊥X
cf⊥

סוף הקלט במצב f ⇒ מתקבלת.

דוגמאות דחייה: acc (n = 1: ב־q₁ נקרא c – אין מעבר); aabccc (רק 3 c במקום 4: הקלט נגמר במצב q₄, לא מקבל); aabcccccc (5 c: ה־c הרביעי מביא ל־f, ה־c החמישי – אין מעבר מ־f); aacca (a אחרי c – אין מעבר).

בדיקה: האוטומט מקבל בדיוק את המילים של L – כך עולה גם מבדיקה של כל המילים הקצרות מעל {a,b,c}. בין השאר: aacc, aaaccc, aabcccc, aabbcccccc מתקבלות; ε, acc, abccc, aaccc, aabccc, aacca נדחות.

טעות נפוצה: לדחוף את אותו סימן גם עבור ה־a הראשון. אז אי אפשר לדעת, בלי מעבר ε, מתי המחסנית "התרוקנה", והאוטומט לא יודע מתי לעבור למצב המקבל.

שאלה 9 · מכונת טיורינג – מעקב ותיאור (25 נקודות)

לפניכם מכונת טיורינג המקבלת מילים מעל הא"ב {a, b} (מקרא: R – ימין, L – שמאל).

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

א.(1)

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

(2)

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

(3)

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

ב.(1)

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

(2)

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

◂ פתרון מלא לשאלה 9

סעיף א(1)

תשובה

5 שלבים (0–4), מסתיים ב־q₄ ; המילה מתקבלת והסרט בסוף: ⊣ a b Δ …

מה נדרש: מעקב מלא על המילה b.

רעיון: מריצים את המעברים אחד אחד. מעברי המכונה: q₀: a/a,R → q₀ ; b/a,R → q₁ ; Δ/Δ,L → q₃ . q₁: a/b,R → q₂ ; b/b,R → q₁ ; Δ/b,L → q₃ . q₂: a/a,R → q₂ ; b/a,R → q₁ ; Δ/a,L → q₃ . q₃: a/a,L → q₃ ; b/b,L → q₃ ; ⊣/⊣,R → q₄ . q₄ – מצב מקבל.
בטבלה: תא 0 הוא הסמן ⊣ , הראש הקורא מסומן ב־[ ].

פתרון:

שלבמצבתא 0תא 1תא 2תא 3תא 4
0q₀⊣[b]ΔΔΔ...
1q₁⊣a[Δ]ΔΔ...
2q₃⊣[a]bΔΔ...
3q₃[⊣]abΔΔ...
4q₄⊣[a]bΔΔ...

שלב 0→1: ב־q₀ קוראים b ⇐ כותבים a , זזים ימינה, q₁ (המכונה "זוכרת" שעליה לכתוב b). שלב 1→2: ב־q₁ קוראים Δ ⇐ כותבים את ה־b , זזים שמאלה, q₃. שלבים 2→4: q₃ חוזר שמאלה עד הסמן ⊣ , ואז זז ימינה ל־q₄. המילה מתקבלת, ועל הסרט ab.

בדיקה: המעקב עובר בדיוק בשלבים האלה.

סעיף א(2)

תשובה

9 שלבים (0–8), מסתיים ב־q₄ ; המילה מתקבלת והסרט בסוף: ⊣ a a b b Δ …

מה נדרש: מעקב מלא על abb.

פתרון:

שלבמצבתא 0תא 1תא 2תא 3תא 4תא 5תא 6
0q₀⊣[a]bbΔΔΔ...
1q₀⊣a[b]bΔΔΔ...
2q₁⊣aa[b]ΔΔΔ...
3q₁⊣aab[Δ]ΔΔ...
4q₃⊣aa[b]bΔΔ...
5q₃⊣a[a]bbΔΔ...
6q₃⊣[a]abbΔΔ...
7q₃[⊣]aabbΔΔ...
8q₄⊣[a]abbΔΔ...

הסבר: ב־q₀ המכונה עוברת על ה־a (שלב 0→1). ב־b הראשון היא כותבת a ועוברת ל־q₁ – "מחזיקה" את ה־b שנמחק (1→2). ב־q₁ , כשקוראים b , כותבים את ה־b המוחזק ו"מחזיקים" את ה־b שנקרא – הוא שוב b , ולכן הסרט לא משתנה בתא הזה (2→3). ב־Δ נכתב ה־b המוחזק (3→4) – כל האותיות מה־b הראשון והלאה זזו תא אחד ימינה. q₃ חוזר שמאלה עד ⊣ (4→7), ו־q₄ (7→8). המילה מתקבלת, ועל הסרט aabb.

בדיקה: המעקב עובר באותם 9 שלבים, ובסוף: מתקבלת, תוכן הסרט aabb.

סעיף א(3)

תשובה

⊣ a b b a b Δ … (על הסרט: abbab)

מה נדרש: תוכן הסרט בסוף הריצה על bbab.

רעיון: כפי שראינו בסעיפים (1) ו־(2), המכונה מדלגת על ה־a שבהתחלה, במקום ה־b הראשון כותבת a , ומזיזה את כל האותיות מה־b הראשון והלאה תא אחד ימינה (q₁ "מחזיק" b , q₂ "מחזיק" a).

פתרון: ב־bbab ה־b הראשון בתא 1. נכתב שם a , ו־"bbab" כולה זזה תא אחד ימינה. הסרט בסוף:

תא 0תא 1תא 2תא 3תא 4תא 5תא 6
⊣abbabΔ...

כלומר על הסרט abbab, והמכונה עוצרת ב־q₄ (מקבלת).

בדיקה: עבור bbab: מתקבלת, תוכן הסרט abbab.

סעיף ב(1)

תשובה

לא. מילה בלי b נשארת כמו שהיא (ובתוצאה אין b), ובמילה עם b התוצאה היא aⁱ⁺¹ b … – ממנה אפשר לשחזר את המילה המקורית (למחוק a אחד לפני ה־b הראשון).

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

רעיון: לבדוק אם מהתוצאה אפשר לשחזר את המילה המקורית.

פתרון: לא קיימות.

•אם במילה אין b (כלומר היא aⁱ), המכונה לא משנה אותה, והתוצאה היא aⁱ – בלי b.
•אם במילה יש b , היא מהצורה aⁱ b u (ה־b הראשון אחרי i אותיות a), והתוצאה היא aⁱ⁺¹ b u – יש בה b.

לכן מילה בלי b ומילה עם b לא יכולות לתת אותה תוצאה. ושתי מילים עם b שנותנות אותה תוצאה aʲ b u חייבות להיות שתיהן aʲ⁻¹ b u – כלומר אותה מילה. (באופן דומה, aⁱ שונות נותנות תוצאות שונות.) מהתוצאה תמיד אפשר לשחזר את המקור: אם יש בה b – מוחקים a אחד לפני ה־b הראשון.

בדיקה: כל המילים מעל {a, b} נותנות תוצאות שונות זו מזו.

סעיף ב(2)

תשובה

המכונה מוסיפה a לפני ה־b הראשון במילה (מזיזה את שאר המילה תא ימינה) וחוזרת לתחילת הסרט; אם אין b – המילה לא משתנה. היא מקבלת כל מילה.

מה נדרש: תיאור קצר של פעולת המכונה.

פתרון: המכונה מכניסה a לפני ה־b הראשון במילה: היא מדלגת על ה־a שבהתחלה, כותבת a במקום ה־b הראשון, ומזיזה את כל האותיות מה־b הזה והלאה תא אחד ימינה (q₁ "מחזיק" b ו־q₂ "מחזיק" a שצריך לכתוב בתא הבא). בסוף היא חוזרת שמאלה עד הסמן ⊣ (q₃) ועוצרת ב־q₄ מעל התא הראשון. אם אין במילה b – המילה נשארת כמו שהיא. המכונה מקבלת כל מילה מעל {a, b} (גם את המילה הריקה).

בדיקה: b → ab , abb → aabb , bbab → abbab , a → a. פלט המכונה הוא בדיוק "הכנסת a לפני ה־b הראשון" לכל מילה.

תכנות מונחה עצמים

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

שאלה 10 · ירושה והכלה – מוזיאון (Exhibition) (25 נקודות)

נוסח השאלה:

במוזיאון "בית האוצרות" פותחה מערכת ממוחשבת לניהול תערוכות, ובה המחלקות האלה:

Exhibit – מוצג, Painting – ציור, Sculpture – פסל, Photo – צילום, Loan – השאלה של מוצג לתערוכה,
Exhibition – תערוכה.

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

למחלקה Exhibit (מוצג) שתי תכונות: title – שם המוצג (מטיפוס מחרוזת), year – שנת היצירה של המוצג (מטיפוס שלם).

למחלקה Painting (ציור) שלוש תכונות: title – שם המוצג (מטיפוס מחרוזת), year – שנת היצירה של המוצג (מטיפוס שלם), technique – טכניקת הציור, למשל "שמן" או "צבעי מים" (מטיפוס מחרוזת).

למחלקה Sculpture (פסל) שלוש תכונות: title – שם המוצג (מטיפוס מחרוזת), year – שנת היצירה של המוצג (מטיפוס שלם), weight – משקל הפסל בק"ג (מטיפוס שלם).

למחלקה Photo (צילום) שלוש תכונות: title – שם המוצג (מטיפוס מחרוזת), year – שנת היצירה של המוצג (מטיפוס שלם), isColor – האם הצילום צבעוני (מטיפוס בוליאני. אם הצילום צבעוני – true , ואם הוא בשחור־לבן – false).

למחלקה Loan (השאלה) שתי תכונות: lender – שם המשאיל (מטיפוס מחרוזת), exhibit – המוצג המושאל (מטיפוס Exhibit). בכל השאלה מושאל מוצג אחד בלבד.

למחלקה Exhibition (תערוכה) שתי תכונות: arrL – מערך של השאלות (מטיפוס Loan), count – כמות ההשאלות השמורות במערך (מטיפוס שלם).

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

א.(1)

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

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

(2)

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

ב.

כתבו במחלקה Exhibition פעולה ששמה totalWeight , המחזירה את סכום המשקלים של כל הפסלים בתערוכה. אם אין בתערוכה פסלים, הפעולה תחזיר 0.

ג.

מערך arrL "מסודר" הוא מערך שבו מופיעות קודם ההשאלות של הציורים, אחר כך ההשאלות של הפסלים, ואחר כך ההשאלות של הצילומים.

כתבו במחלקה Exhibition פעולה ששמה remove , המקבלת שם של מוצג – title .

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

אם אין בתערוכה מוצג ששמו title , או שהמוצג ששמו title הוא המוצג היחיד מסוגו בתערוכה (למשל, הפסל היחיד בתערוכה), הפעולה תחזיר false ולא תשנה את המערך. אחרת, הפעולה תוציא מהמערך את ההשאלה של המוצג באופן שבו המערך ימשיך להיות "מסודר" (וההשאלות יופיעו ברצף בתחילת המערך), ותחזיר true .

◂ פתרון מלא לשאלה 10

סעיף א(1)

תשובה

Painting, Sculpture, Photo יורשות מ־Exhibit; Loan מכילה Exhibit; Exhibition מכילה (מערך של) Loan.

מה נדרש – תרשים הייררכייה של שש המחלקות: מי יורשת ממי ומי מכילה את מי.

רעיון – שואלים על כל זוג מחלקות שתי שאלות:

•"הוא סוג של"? ציור, פסל וצילום הם סוגים של מוצג. לכולם יש title ו־year כמו ל־Exhibit, ולכל אחד תכונה נוספת משלו ⇐ Painting, Sculpture ו־Photo יורשות מ־Exhibit (חץ ירושה מכל אחת מהן אל Exhibit).
•"יש לו"? להשאלה יש מוצג (התכונה exhibit מטיפוס Exhibit) ⇐ Loan מכילה את Exhibit. לתערוכה יש השאלות (המערך arrL מטיפוס Loan) ⇐ Exhibition מכילה את Loan. המעוין המלא מצויר בצד של המחלקה המכילה.

פתרון

•ירושה: Painting, Sculpture ו־Photo יורשות מ־Exhibit – מכל אחת מהן יוצא חץ שראשו משולש חלול אל Exhibit (המחלקה Exhibit היא מחלקת הבסיס).
•הכלה: Loan מכילה את Exhibit – קו בין שתי המחלקות, והמעוין המלא בצד של Loan (השאלה מכילה מוצג אחד).
•הכלה: Exhibition מכילה את Loan – המעוין המלא בצד של Exhibition (תערוכה מכילה מערך של השאלות).

בדיקה – כל שש המחלקות מופיעות בתרשים. התכונות המשותפות title ו־year יוגדרו רק במחלקת הבסיס Exhibit, וכל מחלקה יורשת מוסיפה רק את התכונה המיוחדת לה.

טעות נפוצה: לסמן ירושה בין Loan ל־Exhibit. השאלה אינה "סוג של" מוצג; יש לה מוצג, ולכן זו הכלה.

סעיף א(2)

תשובה

Exhibit(title, year); Painting extends Exhibit(technique); Sculpture extends Exhibit(weight); Photo extends Exhibit(isColor); Loan(lender, exhibit: Exhibit); Exhibition(arrL: Loan[], count)

מה נדרש – כותרות שש המחלקות והתכונות שלהן (בלי פעולות; פעולות get/set ופעולות בונות מניחים שקיימות).

רעיון – בכותרת של מחלקה יורשת מציינים את מחלקת הבסיס (extends ב־Java, : ב־C#). התכונות title ו־year מוגדרות רק ב־Exhibit, והמחלקות היורשות מקבלות אותן בירושה. כל תכונה מוגדרת private (כימוס), ולגישה אליהן משתמשים בפעולות get/set.

פתרון (הקוד המלא – בלשונית Java/C#)

מחלקהכותרת (Java)תכונות
Exhibitpublic class ExhibitString title, int year
Paintingpublic class Painting extends ExhibitString technique
Sculpturepublic class Sculpture extends Exhibitint weight
Photopublic class Photo extends Exhibitboolean isColor
Loanpublic class LoanString lender, Exhibit exhibit
Exhibitionpublic class ExhibitionLoan[] arrL, int count

ב־C#: public class Painting : Exhibit וכן הלאה, והטיפוסים string ו־bool.

בדיקה – הכותרות והתכונות (Java ו־C#) תקינות.

טעות נפוצה: להגדיר שוב את title ו־year במחלקות Painting, Sculpture, Photo. הן כבר מתקבלות בירושה מ־Exhibit.

הקוד המלא
public class Exhibit {
    private String title;     // שם המוצג
    private int year;         // שנת היצירה
}

public class Painting extends Exhibit {
    private String technique; // טכניקת הציור
}

public class Sculpture extends Exhibit {
    private int weight;       // משקל הפסל
}

public class Photo extends Exhibit {
    private boolean isColor;  // האם הצילום בצבע
}

public class Loan {
    private String lender;    // שם המשאיל
    private Exhibit exhibit;  // המוצג המושאל
}

public class Exhibition {
    private Loan[] arrL;      // מערך ההשאלות
    private int count;        // כמות ההשאלות השמורות במערך
}

סעיף ב

תשובה

סוכמים את ((Sculpture)e).getWeight() לכל השאלה i<count שהמוצג שלה e הוא instanceof Sculpture.

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

רעיון – עוברים על ההשאלות שבמערך, רק עד count (אחרי count יש null). לכל השאלה לוקחים את המוצג שלה ובודקים:

1.האם המוצג הוא פסל: instanceof Sculpture ב־Java, is Sculpture ב־C#.
2.אם כן, ממירים (casting) ל־Sculpture ומוסיפים לסכום את המשקל בעזרת פעולת ה־get שלו (getWeight() / GetWeight()).

אם אין פסלים, הסכום נשאר 0.

פתרון

public int totalWeight() {
    int sum = 0;
    for (int i = 0; i < this.count; i++) {
        Exhibit e = this.arrL[i].getExhibit();
        if (e instanceof Sculpture)
            sum += ((Sculpture) e).getWeight();
    }
    return sum;
}

בדיקה – תערוכה עם 5 השאלות: Sunrise ו־Harbor (ציורים), Dancer (פסל, 120 ק"ג), Lion (פסל, 340 ק"ג), Market (צילום). הפעולה החזירה 460 (ב־Java וב־C#). בתערוכה בלי פסלים היא החזירה 0.

טעות נפוצה: לקרוא ל־getWeight() על הפניה מטיפוס Exhibit בלי המרה ל־Sculpture. זו שגיאת הידור, כי ב־Exhibit אין פעולה כזו. המרה בלי בדיקת instanceof מראש גורמת לשגיאת ריצה כשהמוצג הוא ציור או צילום.

הקוד המלא
public int totalWeight() {
    int sum = 0;
    for (int i = 0; i < this.count; i++) {
        Exhibit e = this.arrL[i].getExhibit();
        if (e instanceof Sculpture)
            sum += ((Sculpture) e).getWeight();
    }
    return sum;
}

סעיף ג

תשובה

מוצאים pos; אם לא נמצא או שהוא היחיד מסוגו (ספירה בעזרת kind) – false. אחרת הזזה שמאלה מ־pos, count--, null בתא האחרון ו־true.

מה נדרש – להוציא מהמערך "המסודר" (ציורים ← פסלים ← צילומים) את ההשאלה של המוצג ששמו title, כך שהמערך יישאר "מסודר" ורציף, אלא אם המוצג אינו קיים או שהוא היחיד מסוגו.

רעיון

1.מחפשים את המקום pos של המוצג ששמו title (עד count). אם לא נמצא – מחזירים false.
2.בודקים כמה מוצגים בתערוכה הם מאותו סוג כמו המוצג שנמצא. כדי להשוות סוגים כותבים פעולת עזר kind שמחזירה מספר לכל סוג (1 – ציור, 2 – פסל, 3 – צילום) בעזרת instanceof / is. אם יש רק מוצג אחד מסוגו – מחזירים false.
3.אחרת מזיזים את כל ההשאלות שאחרי pos מקום אחד שמאלה (מההתחלה לסוף, כדי לא לדרוס), מקטינים את count, שמים null בתא שהתפנה ומחזירים true.

הוצאה של איבר והזזת כל מה שאחריו שמאלה שומרת על הסדר היחסי של כל שאר ההשאלות, ולכן המערך נשאר "מסודר".

פתרון

// מחזירה 1 לציור, 2 לפסל, 3 לצילום
private static int kind(Exhibit e) {
    if (e instanceof Painting)
        return 1;
    if (e instanceof Sculpture)
        return 2;
    return 3;
}

public boolean remove(String title) {
    int pos = -1;                              // מיקום המוצג במערך
    for (int i = 0; i < this.count && pos == -1; i++)
        if (this.arrL[i].getExhibit().getTitle().equals(title))
            pos = i;
    if (pos == -1)                             // אין מוצג בשם הזה
        return false;
    int k = kind(this.arrL[pos].getExhibit());
    int same = 0;                              // כמה מוצגים מאותו סוג
    for (int i = 0; i < this.count; i++)
        if (kind(this.arrL[i].getExhibit()) == k)
            same++;
    if (same == 1)                             // המוצג היחיד מסוגו
        return false;
    for (int i = pos; i < this.count - 1; i++) // הזזה שמאלה במקום אחד
        this.arrL[i] = this.arrL[i + 1];
    this.count--;
    this.arrL[this.count] = null;
    return true;
}

בדיקה – מערך בגודל 10: Sunrise Harbor (ציורים) Dancer Lion (פסלים) Market (צילום), count=5. הפלט ב־Java וב־C#:

פעולהמוחזרהמערך אחרי הפעולה
הוצאת HarbortrueSunrise Dancer Lion Market (count=4)
הוצאת Sunrisefalseהציור היחיד – המערך לא השתנה
הוצאת Marketfalseהצילום היחיד – המערך לא השתנה
הוצאת Moonfalseאין מוצג כזה – המערך לא השתנה
הוצאת DancertrueSunrise Lion Market (count=3)

גם במערך מלא (3 מתוך 3) הוצאת צילום כשיש שני צילומים החזירה true, והתא האחרון התאפס ל־null.

הערה: מכיוון שהמערך "מסודר", מוצגים מאותו סוג נמצאים זה ליד זה. לכן אפשר גם לבדוק רק את השכנים: המוצג אינו היחיד מסוגו אם לשכן משמאל (pos−1) או לשכן מימין (pos+1) יש אותו סוג.

טעות נפוצה: להזיז את ההשאלות מהסוף להתחלה (for i = count-1 ...). כך כל התאים נדרסים באותו ערך. טעות נוספת היא לשכוח את count--, ואז ההשאלה האחרונה מופיעה פעמיים.

הקוד המלא
// מחזירה 1 לציור, 2 לפסל, 3 לצילום
private static int kind(Exhibit e) {
    if (e instanceof Painting)
        return 1;
    if (e instanceof Sculpture)
        return 2;
    return 3;
}

public boolean remove(String title) {
    int pos = -1;                              // מיקום המוצג במערך
    for (int i = 0; i < this.count && pos == -1; i++)
        if (this.arrL[i].getExhibit().getTitle().equals(title))
            pos = i;
    if (pos == -1)                             // אין מוצג בשם הזה
        return false;
    int k = kind(this.arrL[pos].getExhibit());
    int same = 0;                              // כמה מוצגים מאותו סוג
    for (int i = 0; i < this.count; i++)
        if (kind(this.arrL[i].getExhibit()) == k)
            same++;
    if (same == 1)                             // המוצג היחיד מסוגו
        return false;
    for (int i = pos; i < this.count - 1; i++) // הזזה שמאלה במקום אחד
        this.arrL[i] = this.arrL[i + 1];
    this.count--;
    this.arrL[this.count] = null;
    return true;
}

שאלה 11 · כרטיס מועדון וכרטיס זהב – עצמים, פלט ותקינות פקודות (25 נקודות)

נוסח השאלה:

לפניכם המחלקות Card (כרטיס מועדון) ו־ GoldCard (כרטיס זהב):

Java
public class Card
{
    private static int counter = 0;
    protected String owner;
    protected int points;

    public Card (String owner) {
        this.owner = owner;
        this.points = 10;
        counter++;
        System.out.println ("Card " + owner);
    }
    public Card (String owner, int points) {
        this.owner = owner;
        this.points = points;
        counter++;
        System.out.println ("Card " + owner + " " + points);
    }
    public static int getCounter() {
        return counter;
    }
    public int getPoints() {
        return points;
    }
    public int value() {
        return this.points * 2;
    }
    public void merge (Card other) {
        this.points += other.points;
        other.points = 0;
        System.out.println (this.owner + ": " + this.points);
    }
}

public class GoldCard extends Card
{
    private int bonus;

    public GoldCard (String owner, int bonus) {
        super (owner);
        this.bonus = bonus;
        System.out.println ("Gold " + bonus);
    }
    public int getBonus() {
        return bonus;
    }
    public int value() {
        return super.value() + this.bonus;
    }
    public void merge (Card other) {
        super.merge (other);
        if (other instanceof GoldCard) {
            this.bonus += ((GoldCard)other).bonus;
            ((GoldCard)other).bonus = 0;
        }
        System.out.println ("bonus: " + this.bonus);
    }
}

נתונה המחלקה Tester :

Java
public class Tester
{
    public static void main(String[] args)
    {
        Card c1 = new Card ("Dan");
        Card c2 = new Card ("Noa", 30);
        Card c3 = new GoldCard ("Tal", 5);
        GoldCard g1 = new GoldCard ("Ori", 8);
        GoldCard g2 = new GoldCard ("Lia", 3);
        // ***
    }
}
א.

ציירו את העצמים שנוצרו בפעולה main , וכתבו את הפלט של הפעולה.

ב.

הציבו כל אחת מן הפקודות 1–10 שלהלן בפעולה main במקום המצוין לעיל ב־ * .

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

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

1. System.out.println ("Cards: " + GoldCard.getCounter());
2. System.out.println (c3.value());
3. System.out.println (c3.getBonus());
4. System.out.println (((GoldCard)c3).getBonus());
5. g2 = c2;
6. GoldCard g3 = (GoldCard)c2;
7. c2.merge (g1);
8. g1.merge (g2);
9. ((Card)g1).merge (c3);
10. Card c4 = new GoldCard ("Ron");
◂ פתרון מלא לשאלה 11

סעיף א

תשובה

Card Dan / Card Noa 30 / Card Tal / Gold 5 / Card Ori / Gold 8 / Card Lia / Gold 3. עצמים: c1 Card(Dan,10), c2 Card(Noa,30), c3 GoldCard(Tal,10,5), g1 GoldCard(Ori,10,8), g2 GoldCard(Lia,10,3); counter = 5.

מה נדרש – לצייר את חמשת העצמים (עם ערכי התכונות שלהם) ולכתוב את הפלט של הפעולות הבונות.

רעיון

•Card(String owner) מציבה points = 10 ומדפיסה Card <owner>; Card(String, int) מציבה את הנקודות שהתקבלו ומדפיסה גם אותן.
•GoldCard(owner, bonus) קוראת קודם ל־super(owner) (ב־C#: : base(owner)) – כלומר לפעולה הבונה הראשונה של Card – ורק אחר כך מציבה את bonus ומדפיסה Gold <bonus>. לכן לכל כרטיס זהב מודפסות שתי שורות, ו־points שלו הוא 10.
•counter היא תכונה סטטית – משותפת לכל העצמים ואינה נמצאת בתוך אף עצם. כל אחת מחמש היצירות מגדילה אותה ב־1.
•המשתנה c3 הוא מטיפוס Card, אבל העצם שהוא מפנה אליו הוא GoldCard.

פתרון

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

הפלט:

Card Dan
Card Noa 30
Card Tal
Gold 5
Card Ori
Gold 8
Card Lia
Gold 3

בדיקה – הפלט ב־Java וב־C# הוא בדיוק הפלט שלמעלה.

טעות נפוצה: לחשוב שבעצם GoldCard מודפסת רק השורה Gold – הפעולה הבונה של Card רצה קודם ומדפיסה גם היא.

סעיף ב

תשובה

1 תקין – Cards: 5 | 2 תקין – 25 | 3 לא תקין (הידור: אין getBonus ב־Card) | 4 תקין – 5 | 5 לא תקין (הידור: Card ל־GoldCard בלי המרה) | 6 לא תקין (שגיאת ריצה – המרה לא חוקית) | 7 תקין – Noa: 40 | 8 תקין – Ori: 20, bonus: 11 | 9 תקין – Ori: 20, bonus: 13 | 10 לא תקין (הידור: אין פעולה בונה GoldCard(String))

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

רעיון – שלושה כללים:

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

פתרון

#תקין?פלט / הסבר
1תקיןCards: 5 – getCounter סטטית ונגישה גם דרך שם המחלקה היורשת; נוצרו 5 כרטיסים (כל פעולה בונה של Card מגדילה את counter, וגם GoldCard קוראת לפעולה הבונה של Card).
2תקין25 – c3 מפנה לעצם GoldCard, ולכן מופעלת value של GoldCard: 10·2 + 5.
3לא תקיןשגיאת הידור: c3 היא הפניה מטיפוס Card , ובמחלקה Card אין פעולה getBonus (ב־C#: GetBonus).
4תקין5 – אחרי המרה מפורשת ל־GoldCard מותר לזמן getBonus; העצם הוא באמת GoldCard.
5לא תקיןשגיאת הידור: אי אפשר להציב הפניה מטיפוס Card במשתנה מטיפוס GoldCard בלי המרה מפורשת.
6לא תקיןשגיאת ריצה: הפקודה עוברת הידור (יש המרה מפורשת), אבל c2 מפנה לעצם Card ולא GoldCard – ההמרה נכשלת (Java: ClassCastException , C#: InvalidCastException).
7תקיןNoa: 40 – c2 הוא Card, ולכן מופעלת merge של Card: 30 + 10 = 40 (הנקודות של g1 מתאפסות). אין הדפסת bonus.
8תקיןOri: 20 ובשורה הבאה bonus: 11 – מופעלת merge של GoldCard: super.merge מדפיסה Ori: 20; g2 הוא GoldCard ולכן הבונוס 8 + 3 = 11.
9תקיןOri: 20 ובשורה הבאה bonus: 13 – ההמרה ל־Card אינה משנה את העצם: מופעלת merge של GoldCard. גם c3 הוא GoldCard (בפועל), ולכן instanceof/is מחזיר true והבונוס 8 + 5 = 13.
10לא תקיןשגיאת הידור: ל־GoldCard אין פעולה בונה שמקבלת מחרוזת בלבד (יש רק (String, int)).

בדיקה – כל פקודה לבדה בפעולה הראשית תקינה ב־Java וב־C#. הפקודות הלא תקינות נכשלו בהידור בשתי השפות, פרט לפקודה שנכשלה בזמן ריצה (ב־Java: ClassCastException, ב־C#: InvalidCastException). הפלט של הפקודות התקינות זהה בשתי השפות.

שאלה 12 · K, L, M, N – העמסה, דריסה ופולימורפיזם (25 נקודות)

נוסח השאלה:

לשאלה זו שני נוסחים: בשפת Java ובשפת C# .

לפותרים בשפת Java

נתונות המחלקות K , L , M , N :

Java
public class K {
    private int v;
    public K (int v) { this.v = v; }
    public int getV() { return v; }
    public String act (K other) {
        System.out.println ("K1");
        return "" + (v + other.getV());
    }
}
---------------------------------------------
public class L extends K {
    public L (int v) { super (v + 2); }
    public String act (L other) {
        System.out.println ("L1");
        return "" + (getV() * other.getV());
    }
    public String act (K other) {
        System.out.println ("L2");
        return "L" + super.act (other);
    }
}
---------------------------------------------
public class M extends L {
    public M (int v) { super (v * 2); }
    public String act (int n) {
        System.out.println ("M1");
        return "" + (getV() - n);
    }
}
---------------------------------------------
public class N extends M {
    public N (int v) { super (v); }
    public String act (L other) {
        System.out.println ("N1");
        return act (other.getV());
    }
}
א.

סרטטו את תרשים ההייררכייה של המחלקות K , L , M , N . יש לסמן ירושה באמצעות החץ ◁——— .

ב.(1)

נתונה המחלקה Tester :

Java
public class Tester {
    public static void main (String[] args) {
        K k = new K (3);
        L l = new L (1);
        M m = new M (2);
        N n = new N (1);
        K kl = new L (5);
        L ln = new N (3);
        //***
    }
}

סרטטו את העצמים שנוצרו בפעולה main .

(2)

במקום המסומן ב־ * נוסף קטע הקוד התקין שלפניכם:

Java
System.out.println (kl.act(l));            //(1)
System.out.println (l.act(ln));            //(2)
System.out.println (ln.act(m));            //(3)
System.out.println (m.act(k));             //(4)
System.out.println (((M)ln).act(5));       //(5)

כתבו את הפלט של הפעולה (כתבו את מספר השורה המשויך לכל פלט).

◂ פתרון מלא לשאלה 12

סעיף א

תשובה

שרשרת: L יורשת מ־K, M יורשת מ־L, N יורשת מ־M.

מה נדרש – תרשים ירושה של K, L, M, N.

רעיון – לפי הכותרות: L extends K (ב־C#: L : K), M extends L, N extends M. זו שרשרת אחת: כל מחלקה יורשת מהקודמת, ולכן N היא בעקיפין גם M, גם L וגם K.

פתרון

בדיקה – התרשים מתאים לכותרות המחלקות; הקוד תקין ב־Java וב־C# עם ההייררכייה הזאת.

סעיף ב(1)

תשובה

k→K(v=3), l→L(v=3), m→M(v=6), n→N(v=4), kl (מוצהר K)→L(v=7), ln (מוצהר L)→N(v=8)

מה נדרש – לסרטט את ששת העצמים: לכל משתנה – הטיפוס המוצהר שלו, חץ אל העצם, הטיפוס בפועל של העצם וערך התכונה v.

רעיון – לכל העצמים יש תכונה אחת, v, שמוגדרת ב־K. נחשב אותה לפי שרשור הפעולות הבונות (מלמטה למעלה):

•K(v) – מציבה v.
•L(v) – קוראת ל־K(v + 2).
•M(v) – קוראת ל־L(v * 2), כלומר 2v + 2.
•N(v) – קוראת ל־M(v), כלומר גם 2v + 2.
משתנהטיפוס מוצהרטיפוס העצם בפועלחישובv
kKK33
lLL1 + 23
mMM2·2 + 26
nNN1·2 + 24
klKL5 + 27
lnLN3·2 + 28

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

בדיקה – הדפסה של getV() לכל עצם נותנת את הערכים 3, 3, 6, 4, 7, 8, ב־Java וב־C#.

טעות נפוצה: ב־M לחשב (2 + 2)·2 = 8 – M מכפילה לפני שהיא מעבירה ל־L, ו־L היא שמוסיפה 2.

סעיף ב(2)

תשובה

(1) L2 K1 L10 (2) L1 24 (3) N1 M1 2 (4) L2 K1 L9 (5) M1 3 (הפלט זהה ב־Java וב־C#)

מה נדרש – הפלט של חמש השורות, עם מספר השורה.

רעיון – שני שלבים בכל קריאה:

1.בזמן הידור (העמסה): המהדר בוחר את החתימה לפי הטיפוס המוצהר של המשתנה ושל הארגומנטים.
2.בזמן ריצה (דריסה): אם לחתימה שנבחרה יש גרסה דורסת בטיפוס בפועל של העצם – היא זו שמתבצעת.

הפעולות: ב־K – act(K); ב־L – act(L) חדשה ו־act(K) שדורסת את של K; ב־M – act(int) חדשה; ב־N – act(L) שדורסת את של L. ערכי v (מסעיף ב1): k=3, l=3, m=6, n=4, kl=7, ln=8.

שורהבחירת הפעולהביצועפלט
(1) kl.act(l)kl מוצהר K ← רק act(K); בפועל L ← הגרסה הדורסת של LL2, ואז super.act(l): K1, 7 + 3 = 10; מחזירה "L" + "10"L2, K1, L10
(2) l.act(ln)l מוצהר L; ln מוצהר L ← הספציפית ביותר act(L); l בפועל L3 · 8 = 24L1, 24
(3) ln.act(m)ln מוצהר L; m הוא L ← act(L); בפועל N ← הגרסה הדורסת של NN1, ואז act(other.getV()) = act(6) ← act(int) של M: 8 − 6N1, M1, 2
(4) m.act(k)k מוצהר K: act(int) ו־act(L) לא מתאימות ← act(K); העצם M יורש את הגרסה של LL2, ואז K1: 6 + 3 = 9; מחזירה "L9"L2, K1, L9
(5) ((M)ln).act(5)ההמרה תקינה (העצם N הוא גם M) ← act(int) של M8 − 5M1, 3

פתרון – הפלט:

(1) L2
    K1
    L10
(2) L1
    24
(3) N1
    M1
    2
(4) L2
    K1
    L9
(5) M1
    3

ב־C# הפלט זהה (הפעולות מחזירות מחרוזות, ולכן אין הבדל True/true). ב־C# הדריסה מתרחשת כי Act(K) מוגדרת ב־K כ־virtual וב־L כ־override, ו־Act(L) מוגדרת ב־L כ־virtual וב־N כ־override.

בדיקה – הקוד כפי שהוא (Java ו־C#) מדפיס בדיוק את הפלט שלמעלה.

טעות נפוצה: בשורה (1) לבחור act(L) כי הארגומנט l הוא L – הטיפוס המוצהר של kl הוא K, ו־K מכירה רק את act(K). בשורה (3) – לשכוח שהקריאה act(other.getV()) בתוך N מקבלת int ולכן מגיעה ל־act(int) של M. בשורה (1) ו־(4) – לשכוח את ה־"L" שנוסף לפני המספר.

טבלת התשובות
שאלהסעיףתשובה
1-

כל עוד התור אינו ריק: מוציאים v, מעבירים את שאר האיברים דרך תור עזר – מופעים נוספים של v נספרים ואינם מוחזרים; אם לא נמצא אף מופע נוסף – מגדילים את המונה. עבור הדוגמה מתקבל 3. סיבוכיות O(n²).

2א(1)

הפעולה מחזירה 5 (הערך 6 מופיע לראשונה בחוליה החמישית).

א(2)

הפעולה מחזירה את המקום (מספור מ־1) של החוליה הראשונה בשרשרת lst שערכה x. אם x אינו מופיע בשרשרת – מחזירה 0.

א(3)

O(n) במקרה הגרוע, כאשר n הוא מספר החוליות: כש־x אינו מופיע (או מופיע רק בסוף) יש קריאה אחת לכל חוליה ועוד אחת ל־null, ובכל קריאה עבודה קבועה.

ב(1)

הפעולה מחזירה 2.

ב(2)

הפעולה מחזירה את המרחק הקטן ביותר (בהפרש מקומות) בין שתי חוליות בשרשרת שיש להן אותו ערך. אם כל הערכים בשרשרת שונים – מחזירה 0.

ב(3)

O(n²), כאשר n הוא מספר החוליות. במקרה הגרוע (כל הערכים שונים) what עוברת לכל חוליה על כל ההמשך: (n−1) + (n−2) + … + 0 = n(n−1)/2.

3א

לדוגמה: שורש 1 עם הבנים 2 (שמאלי), 3 (ימני); ל־2 הבנים 4, 5; ל־3 הבנים 6, 7; ל־4 רק בן שמאלי 8; ל־6 רק בן שמאלי 9. (9 צמתים, 4 רמות, בכל מסלול ≤ פנייה אחת.)

ב

פעולת עזר zigzag(node, k, fromLeft): לבן השמאלי התקציב הוא k אם fromLeft, אחרת k − 1; לבן הימני – k אם !fromLeft, אחרת k − 1; תקציב שלילי ⇒ false. isZigzagK קוראת לה עם הבן השמאלי (fromLeft = true) והימני (false) של השורש עם K. יעילות O(n).

4א

סריקת BFS מ־ vⱼ שמחשבת לכל קודקוד את מרחקו dist (מספר הקשתות): קודקוד שמתגלה נכנס לתור ולרשימת התוצאה; קודקוד שמרחקו כבר k אינו מרחיב את שכניו. מוחזרים כל הקודקודים שהתגלו (חוץ מ־ vⱼ). זמן ריצה O(|V| + |E|). בדוגמה (4, k = 2): 2, 9, 0.

ב

סופרים את רכיבי הקשירות בעזרת BFS מכל קודקוד שעוד לא סומן, ושומרים את הקודקוד שפתח כל רכיב (נציג). אם יש c רכיבים – המספר המינימלי הוא c − 1, והקשתות: מהנציג של הרכיב הראשון לנציג של כל רכיב אחר. זמן ריצה O(|V| + |E|). בדוגמה: c = 3, מוחזר 2 עם הקשתות (0, 1), (0, 8).

5א

A: C ; C: F ; D: C, F ; F: – ; G: A, F

ב

הוצאות מן התור לפי הסדר: B, E, D, G, A, C, F. בסוף: כל ערכי indeg הם 0 ; level = (A 2, B 0, C 3, D 1, E 0, F 4, G 1).

ג

B , E , D , G , A , C , F

6א(1)

שלב 1 – דייקסטרה מ־vⱼ (d, π; בכל איטרציה u בעל d מינימלי ב־T עובר ל־P, ומרפים את קשתותיו). שלב 2 – אם d[vₜ] = ∞ מדפיסים "אין מסלול"; אחרת PrintPath(vₜ): אם v = vⱼ מדפיסים אותו, אחרת קוראים ל־PrintPath(π[v]) ואז מדפיסים את v.

א(2)

O(n²): דייקסטרה במערכים O(n²) + הדפסת המסלול O(n).

ב(1)

d/π: K 0/– , M 3/K , L 7/K , P 9/M , Q 13/P , N 14/P , T 15/Q , R 20/N , W 22/T , Z 24/T. סדר הקביעה: K, M, L, P, Q, N, T, R, W, Z. עבור W יודפס: K M P Q T W (22 דקות).

ב(2)

קשתות העץ (π[v] – v): K–L, K–M, M–P, P–N, P–Q, Q–T, N–R, T–W, T–Z.

7א(1)

לדוגמה: aabccca (יש עוד אפשרויות, למשל cabcc).

א(2)

לדוגמה: aba – מקיימת את שלוש הדרישות האחרות, אבל מכילה את הרצף ba.

ב

L = L₁ ∩ L₂ ∩ L₃ ∩ L₄, כאשר Lᵢ היא שפת המילים המקיימות את הדרישה ה־i. לכל Lᵢ יש אוטומט סופי דטרמיניסטי (בפתרון), ולכן כולן רגולריות. השפות הרגולריות סגורות לחיתוך ⇒ L רגולרית.

8א(1)

לא נכונה. דוגמה: L₁ = {a}, L₂ = {b}: abab ∈ (L₁·L₂)* אבל abab ∉ L₁*·L₂* = { aⁱbʲ | i, j ≥ 0 }.

א(2)

נכונה – היפוך פועל על כל מילה בנפרד, ולכן מתפלג מעל איחוד.

א(3)

לא נכונה. דוגמה: L₁ = {a}, L₂ = {ab}, L₃ = {b, ε}: אגף שמאל ∅·L₃ = ∅, אגף ימין {ab, a} ∩ {abb, ab} = {ab}.

ב

ה־a הראשון דוחף X, כל a נוסף דוחף A, כל b דוחף שני B; כל c שולף סימן אחד; ה־c ששולף את X עובר למצב המקבל f. b ו־c מותרים רק אחרי שתי a לפחות. טבלת המעברים המלאה בפתרון.

9א(1)

5 שלבים (0–4), מסתיים ב־q₄ ; המילה מתקבלת והסרט בסוף: ⊣ a b Δ …

א(2)

9 שלבים (0–8), מסתיים ב־q₄ ; המילה מתקבלת והסרט בסוף: ⊣ a a b b Δ …

א(3)

⊣ a b b a b Δ … (על הסרט: abbab)

ב(1)

לא. מילה בלי b נשארת כמו שהיא (ובתוצאה אין b), ובמילה עם b התוצאה היא aⁱ⁺¹ b … – ממנה אפשר לשחזר את המילה המקורית (למחוק a אחד לפני ה־b הראשון).

ב(2)

המכונה מוסיפה a לפני ה־b הראשון במילה (מזיזה את שאר המילה תא ימינה) וחוזרת לתחילת הסרט; אם אין b – המילה לא משתנה. היא מקבלת כל מילה.

10א(1)

Painting, Sculpture, Photo יורשות מ־Exhibit; Loan מכילה Exhibit; Exhibition מכילה (מערך של) Loan.

א(2)

Exhibit(title, year); Painting extends Exhibit(technique); Sculpture extends Exhibit(weight); Photo extends Exhibit(isColor); Loan(lender, exhibit: Exhibit); Exhibition(arrL: Loan[], count)

ב

סוכמים את ((Sculpture)e).getWeight() לכל השאלה i<count שהמוצג שלה e הוא instanceof Sculpture.

ג

מוצאים pos; אם לא נמצא או שהוא היחיד מסוגו (ספירה בעזרת kind) – false. אחרת הזזה שמאלה מ־pos, count--, null בתא האחרון ו־true.

11א

Card Dan / Card Noa 30 / Card Tal / Gold 5 / Card Ori / Gold 8 / Card Lia / Gold 3. עצמים: c1 Card(Dan,10), c2 Card(Noa,30), c3 GoldCard(Tal,10,5), g1 GoldCard(Ori,10,8), g2 GoldCard(Lia,10,3); counter = 5.

ב

1 תקין – Cards: 5 | 2 תקין – 25 | 3 לא תקין (הידור: אין getBonus ב־Card) | 4 תקין – 5 | 5 לא תקין (הידור: Card ל־GoldCard בלי המרה) | 6 לא תקין (שגיאת ריצה – המרה לא חוקית) | 7 תקין – Noa: 40 | 8 תקין – Ori: 20, bonus: 11 | 9 תקין – Ori: 20, bonus: 13 | 10 לא תקין (הידור: אין פעולה בונה GoldCard(String))

12א

שרשרת: L יורשת מ־K, M יורשת מ־L, N יורשת מ־M.

ב(1)

k→K(v=3), l→L(v=3), m→M(v=6), n→N(v=4), kl (מוצהר K)→L(v=7), ln (מוצהר L)→N(v=8)

ב(2)

(1) L2 K1 L10 (2) L1 24 (3) N1 M1 2 (4) L2 K1 L9 (5) M1 3 (הפלט זהה ב־Java וב־C#)

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

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

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