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

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

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

א.

כתבו פעולה ששמה isDecisive בשפת Java או IsDecisive בשפת C#, המקבלת תור – q מטיפוס שלם, ומספר שלם – m הגדול מ־ 0 וקטן או שווה לגודל התור.
הפעולה תחזיר true אם האיבר במקום ה־ m בתור הוא "סיבוב מכריע", אחרת היא תחזיר false.

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

לדוגמה: עבור התור שלפניכם:

עבור m = 1 הפעולה תחזיר false (האיבר הראשון בתור אינו "סיבוב מכריע")
עבור m = 3 הפעולה תחזיר true (3 + 3 = 6)
עבור m = 4 הפעולה תחזיר false (3 + 3 + 6 = 12 ≠ 5)
עבור m = 5 הפעולה תחזיר true (3 + 3 + 6 + 5 = 17)

ב.

כתבו פעולה ששמה atLeast בשפת Java או AtLeast בשפת C# המקבלת תור מטיפוס שלם – q , ומספר שלם k הגדול מ־ 0.
הפעולה תחזיר true אם יש בתור לפחות k "סיבובים מכריעים". אחרת הפעולה תחזיר false.
אפשר להשתמש בפעולה שכתבתם בסעיף א.

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

דוגמאות: עבור התור שבדוגמה שלעיל:
עבור k = 4 הפעולה תחזיר true מכיוון שיש בתור 4 "סיבובים מכריעים" (במקומות 2, 3, 5, 7).
עבור k = 2 הפעולה תחזיר true.
עבור k = 5 הפעולה תחזיר false.

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

סעיף א

תשובה

מעבירים את כל איברי התור לתור עזר תוך ספירת המקום; במקומות 1 עד m−1 צוברים את הסכום sum, ובמקום m בודקים x == sum. בסוף מחזירים את האיברים לתור. ל־m = 1 הסכום הוא 0 והערכים חיוביים, ולכן התשובה false.

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

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

•במקומות 1 עד m−1 מוסיפים את הערך לסכום sum.
•כשמגיעים למקום m (הערך x) בודקים אם x == sum.
•את שאר האיברים (אחרי המקום m) רק מעבירים לתור העזר.

המקרה m = 1 מטופל מעצמו: אין איברים לפניו, ולכן sum = 0. כל הערכים בתור חיוביים, ולכן x == 0 אינו מתקיים והתשובה false, כמו שנדרש בהערה.

בסוף מעבירים את כל האיברים מ־temp בחזרה ל־q, וכך התור חוזר למצבו המקורי ובאותו סדר.

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

יעילות: כל איבר יוצא מהתור ונכנס אליו פעמיים, ולכן הפעולה היא O(n), כאשר n הוא מספר האיברים בתור.

בדיקה (התור 3, 3, 6, 5, 17, 2, 36):

msum (לפני המקום m)הערך במקום mתוצאה
103false
233true
366true
4125false
51717true
6342false
73636true

מתקבלים בדיוק הערכים שבטבלה, ואחרי כל קריאה התור נשאר 3, 3, 6, 5, 17, 2, 36.

טעות נפוצה: להוסיף ל־sum גם את האיבר שבמקום m לפני ההשוואה. אז משווים את x לסכום שכולל אותו, והתשובה תמיד false.

הקוד המלא
// סעיף א: האם הסיבוב במקום ה-m בתור הוא "סיבוב מכריע". התור נשמר.
public static boolean isDecisive(Queue<Integer> q, int m)
{
    Queue<Integer> temp = new Queue<Integer>();
    int pos = 0;            // מקום האיבר הנוכחי בתור
    int sum = 0;            // סכום הנקודות במקומות 1 עד m-1
    boolean decisive = false;
    while (!q.isEmpty())
    {
        int x = q.remove();
        pos++;
        if (pos < m)
            sum = sum + x;
        else if (pos == m)
            decisive = (x == sum);
        temp.insert(x);
    }
    while (!temp.isEmpty())          // החזרת התור למצבו המקורי
        q.insert(temp.remove());
    return decisive;
}

סעיף ב

תשובה

סופרים את גודל התור size (ומחזירים את האיברים לתור), ואז עוברים על m = 1, …, size וסופרים כמה פעמים isDecisive(q, m) מחזירה true. כשהמונה מגיע ל־k מחזירים true; אם הלולאה הסתיימה – false.

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

רעיון:

1.כדי לעבור על כל המקומות צריך לדעת את גודל התור. סופרים את האיברים (size): מעבירים אותם לתור עזר תוך כדי ספירה, ואחר כך מחזירים אותם.
2.בלולאה על m = 1, …, size מפעילים את isDecisive מסעיף א וסופרים במונה count את המקומות שבהם התשובה true. isDecisive שומרת על התור, ולכן אפשר להפעיל אותה שוב ושוב על אותו התור.
3.ברגע ש־count מגיע ל־k אפשר להחזיר true (אין צורך להמשיך). אם הלולאה הסתיימה בלי להגיע ל־k – מחזירים false.

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

יעילות: יש עד n קריאות ל־isDecisive, וכל קריאה עולה O(n). לכן הפעולה היא O(n²), כאשר n הוא מספר האיברים בתור. (אפשר גם לספור במעבר אחד על התור, עם סכום מצטבר, ב־O(n), אבל השאלה מאפשרת להשתמש בסעיף א.)

בדיקה (התור 3, 3, 6, 5, 17, 2, 36): המקומות המכריעים הם 2, 3, 5, 7, כלומר 4 סיבובים.

•k = 4 ⇒ המונה מגיע ל־4 במקום 7 ⇒ true.
•k = 2 ⇒ המונה מגיע ל־2 כבר במקום 3 ⇒ true.
•k = 5 ⇒ הלולאה מסתיימת עם count = 4 ⇒ false.

מתקבל true, true, false, ובתור 2, 5, 1 עם k = 1 מתקבל false (אין בו סיבוב מכריע).

הקוד המלא
// סעיף ב: האם יש בתור לפחות k "סיבובים מכריעים"
public static boolean atLeast(Queue<Integer> q, int k)
{
    // ספירת איברי התור (והחזרתם לתור)
    Queue<Integer> temp = new Queue<Integer>();
    int size = 0;
    while (!q.isEmpty())
    {
        temp.insert(q.remove());
        size++;
    }
    while (!temp.isEmpty())
        q.insert(temp.remove());

    int count = 0;
    for (int m = 1; m <= size; m++)
    {
        if (isDecisive(q, m))
        {
            count++;
            if (count == k)
                return true;
        }
    }
    return false;
}

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

נתונה המחלקה Runner – רץ במועדון ריצה, ולה שלוש תכונות:

•name – שם הרץ, מטיפוס מחרוזת
•times – מערך מטיפוס שלם בגודל 30 , המכיל את הזמנים (בדקות) של ריצות ה־10 ק"מ שהרץ השלים בחודש האחרון. בחודש אחד רץ משלים עד 30 ריצות, והזמנים נשמרים ברצף מתחילת המערך.
•amount – מספר הריצות שהרץ השלים בפועל בחודש האחרון, מטיפוס שלם. כל רץ השלים ריצה אחת לפחות.

הניחו שלתכונות המחלקה יש פעולות get/Get ו־ set/Set .

א.

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

Java – public int countBelow (int limit)
C# – public int CountBelow (int limit)

הפעולה מקבלת מספר שלם limit , ומחזירה את מספר הריצות של הרץ שהזמן שלהן קטן ממש מ־ limit .

ב.

נתון מערך arr מטיפוס Runner , ובו כל הרצים במועדון, ונתון מספר שלם limit . ריצה שהזמן שלה קטן מ־ limit נקראת "ריצה מהירה".
כתבו פעולה חיצונית ששמה bestRunners בשפת Java או BestRunners בשפת C# , המקבלת את המערך arr ואת limit .
הפעולה מחזירה שרשרת חוליות מטיפוס מחרוזת, ובה השמות של הרצים שמספר הריצות המהירות שלהם הוא הגדול ביותר במועדון. אם יש כמה רצים כאלה, כולם יופיעו בשרשרת לפי סדר הופעתם במערך arr .
אם לאף רץ במועדון אין ריצה מהירה, הפעולה מחזירה null .

לדוגמה: במועדון ארבעה רצים, לפי סדרם במערך:

nameהזמנים במערך times
Dana52, 49, 47, 55, 48
Omer50, 46, 51
Yael45, 58, 49, 47, 60, 53
Noam61, 57

עבור limit = 50 הפעולה תחזיר שרשרת ובה Dana ואחריה Yael (לכל אחת מהן 3 ריצות מהירות, ולשאר פחות).
עבור limit = 45 הפעולה תחזיר null .

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

סעיף א

תשובה

ספירה במעבר על התאים 0 עד amount−1 של times: אם times[i] < limit מגדילים מונה ב־1. בסוף מחזירים את המונה.

מה נדרש: לספור כמה מזמני הריצה של הרץ קטנים מ־limit.

רעיון: הזמנים שמורים ברצף מתחילת המערך, ויש amount זמנים. לכן עוברים רק על התאים 0 עד amount−1 וסופרים את התאים שבהם times[i] < limit. אסור לעבור על כל 30 התאים: בתאים שאחרי amount יש 0, ו־0 קטן מכל limit חיובי, כך שהתאים הריקים ייספרו בטעות כ"ריצות".

פתרון: הקוד המלא – בלשונית Java/C#. זו פעולה פנימית של המחלקה, ולכן היא ניגשת ישירות לתכונות times ו־amount.

יעילות: מעבר אחד על amount תאים – O(amount), ולכל היותר 30 בדיקות.

בדיקה: לרצה Dana עם הזמנים 52, 49, 47, 55, 48: countBelow(50) ⇒ 3 (49, 47, 48), countBelow(47) ⇒ 0 (47 אינו קטן ממש מ־47). לרץ Noam עם 61, 57: countBelow(50) ⇒ 0. לרץ עם 30 ריצות (מערך מלא) נספרו כל 30 התאים.

הקוד המלא
public int countBelow(int limit)
{
    int count = 0;
    for (int i = 0; i < amount; i++)     // הזמנים שמורים ברצף בתאים 0 עד amount-1
    {
        if (times[i] < limit)
            count++;
    }
    return count;
}

סעיף ב

תשובה

שני מעברים על המערך. (1) מוצאים את max – המספר הגדול ביותר של ריצות מהירות, בעזרת countBelow(limit). אם max == 0 מחזירים null. (2) עוברים על המערך מהסוף להתחלה, ולכל רץ שעבורו countBelow(limit) == max מוסיפים חוליה עם שמו לראש השרשרת. מחזירים את השרשרת.

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

רעיון: אי אפשר לדעת מי "הכי טוב" לפני שרואים את כל הרצים, ולכן עובדים בשני מעברים:

1.מעבר ראשון: מחשבים לכל רץ את arr[i].countBelow(limit) (הפעולה מסעיף א) ושומרים את המקסימום max. מאתחלים את max ל־0.
2.אם max נשאר 0 – לאף רץ אין ריצה מהירה, ומחזירים null. (בלי הבדיקה הזאת כל הרצים היו "שווים" עם 0 ונכנסים לשרשרת.)
3.מעבר שני: אוספים את הרצים שעבורם countBelow(limit) == max. כדי שהשמות יופיעו לפי סדר המערך, עוברים על המערך מהסוף להתחלה ומוסיפים כל שם לראש השרשרת: new Node(name, result). השם שנוסף אחרון (מתחילת המערך) יהיה ראשון בשרשרת.

פתרון: הקוד המלא – בלשונית Java/C#. זו פעולה חיצונית (static) שמשתמשת רק בפעולות הממשק countBelow ו־getName.

יעילות: אם במועדון k רצים, כל מעבר קורא k פעמים ל־countBelow שעולה לכל היותר 30 צעדים. סה"כ O(k).

בדיקה (הרצים שבדוגמה):

limitריצות מהירות (Dana, Omer, Yael, Noam)maxהשרשרת
503, 1, 3, 03Dana → Yael
470, 1, 1, 01Omer → Yael
565, 3, 4, 05Dana
450, 0, 0, 00null

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

הקוד המלא
public static Node<String> bestRunners(Runner[] arr, int limit)
{
    // שלב 1: המספר הגדול ביותר של ריצות מהירות אצל רץ כלשהו
    int max = 0;
    for (int i = 0; i < arr.length; i++)
    {
        int c = arr[i].countBelow(limit);
        if (c > max)
            max = c;
    }
    if (max == 0)                        // לאף רץ אין ריצה בזמן הקטן מ-limit
        return null;
    // שלב 2: בניית השרשרת מסוף המערך להתחלה, בהוספה לראש השרשרת,
    // כך שבסוף השמות מופיעים לפי סדר הופעתם במערך
    Node<String> result = null;
    for (int i = arr.length - 1; i >= 0; i--)
    {
        if (arr[i].countBelow(limit) == max)
            result = new Node<String>(arr[i].getName(), result);
    }
    return result;
}

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

נוסח השאלה:

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

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

א.(1)

נתונה הפעולה zig שלפניכם:

Java
public static int zig (BinNode<Integer> tree) {
    if (tree.getLeft() == null && tree.getRight() == null)
        return 1;
    int sum = 0;
    if (tree.getLeft() != null)
        sum = sum + zig (tree.getLeft());
    if (tree.getRight() != null)
        sum = sum - zig (tree.getRight());
    return sum;
}

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

(2)

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

(3)

בעבור עץ שיש לו 6 צמתים, מהו המספר הכי גבוה שהפעולה zig יכולה להחזיר? הציגו את העץ.

(4)

בעבור עץ שיש לו 6 צמתים, מהו המספר הכי נמוך שהפעולה zig יכולה להחזיר? הציגו את העץ.

(5)

כתבו בקצרה מה הפעולה zig מחזירה בעבור עץ כלשהו שאינו ריק.

ב.(1)

נתונה הפעולה zag שלפניכם:

Java
public static boolean zag (BinNode<Integer> tree) {
    if (tree == null)
        return false;
    if (zig (tree) < 0)
        return true;
    return zag (tree.getLeft()) || zag (tree.getRight());
}

בצעו מעקב אחרי הפעולה zag והעץ שלפניכם, וכתבו מה הפעולה מחזירה. יש להציג את המעקב.
בסעיף זה אין צורך להציג מעקב אחרי הפעולה zig .

(2)

הציגו עץ שיש לו מעל 3 צמתים שבעבורו הפעולה zag תחזיר תוצאה הפוכה מן התוצאה שהתקבלה בתת־סעיף ב(1).

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

סעיף א(1)

תשובה

1

מה נדרש: מעקב אחרי zig על העץ: 8 ← בן שמאלי 3 (ל־3 רק בן שמאלי 1) ובן ימני 6 (ל־6 שני בנים: 4 משמאל ו־2 מימין).

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

פתרון (מעקב):

קריאהמצב הצומתחישובערך מוחזר
zig(8)שני בנים0 + zig(3) − zig(6)1
zig(3)רק בן שמאלי (1)0 + zig(1)1
zig(1)עלה–1
zig(6)שני בנים0 + zig(4) − zig(2)0
zig(4)עלה–1
zig(2)עלה–1

חזרה מהרקורסיה: zig(1) = 1 ← zig(3) = 1; zig(4) = 1, zig(2) = 1 ← zig(6) = 1 − 1 = 0; zig(8) = 1 − 0 = 1.

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

בדיקה: הקוד הנתון מדפיס על העץ 1, וערכי הביניים זהים לטבלה.

טעות נפוצה: לשכוח שאת הבן הימני מחסירים – אז מתקבל zig(6) = 2 ו־zig(8) = 3.

סעיף א(2)

תשובה

−2

מה נדרש: מעקב אחרי zig על העץ: 5 ← בן שמאלי 2 ובן ימני 9; ל־2 רק בן ימני 7; ל־7 שני בנים 3 ו־1; ל־9 בן שמאלי 8 ובן ימני 4; ל־4 רק בן ימני 0.

פתרון (מעקב):

קריאהמצב הצומתחישובערך מוחזר
zig(5)שני בנים0 + zig(2) − zig(9)−2
zig(2)רק בן ימני (7)0 − zig(7)0
zig(7)שני בנים0 + zig(3) − zig(1)0
zig(3)עלה–1
zig(1)עלה–1
zig(9)שני בנים0 + zig(8) − zig(4)2
zig(8)עלה–1
zig(4)רק בן ימני (0)0 − zig(0)−1
zig(0)עלה–1

חזרה מהרקורסיה: zig(7) = 1 − 1 = 0 ← zig(2) = −0 = 0; zig(0) = 1 ← zig(4) = −1 ← zig(9) = 1 − (−1) = 2; zig(5) = 0 − 2 = −2.

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

בדיקה: הקוד הנתון מדפיס על העץ −2, וערכי הביניים: zig(3)=1, zig(1)=1, zig(7)=0, zig(2)=0, zig(8)=1, zig(0)=1, zig(4)=−1, zig(9)=2, zig(5)=−2.

סעיף א(3)

תשובה

3 – למשל: לשורש בן שמאלי עלה ובן ימני R; ל־R בן ימני עלה ובן שמאלי X, ול־X רק בן ימני (עלה).

מה נדרש: הערך המקסימלי של zig בעץ בן 6 צמתים, ועץ שמשיג אותו.

רעיון: מהמעקב רואים שרק העלים "מייצרים" ערכים (1), וכל מעבר לבן ימני הופך את הסימן (ראו סעיף 5): עלה שבמסלול אליו מהשורש מספר זוגי של מעברים ימינה תורם +1, ועלה עם מספר אי־זוגי של מעברים ימינה תורם −1. לכן zig ≤ מספר העלים. בעץ בינרי בן 6 צמתים יש לכל היותר 3 עלים (מספר העלים גדול ב־1 ממספר הצמתים שיש להם שני בנים, ו־6 צמתים מאפשרים לכל היותר 2 כאלה). צריך אפוא עץ עם 3 עלים שבכולם מספר זוגי של מעברים ימינה.

פתרון: המספר הגבוה ביותר הוא 3. העץ (הערכים בצמתים לא משנים):

    a
   / \
  b   R
     / \
    X   d
     \
      e

העלים: b (0 מעברים ימינה) ← +1; d (ימינה, ימינה – 2) ← +1; e (ימינה, שמאלה, ימינה – 2) ← +1.

חישוב: zig(e) = 1 ← zig(X) = −1; zig(d) = 1 ← zig(R) = −1 − 1 = −2; zig(b) = 1 ← zig(a) = 1 − (−2) = 3.

בדיקה: בין כל צורות העצים בני 6 צמתים הערך הגבוה ביותר של zig הוא 3, והוא מתקבל רק בעץ הזה.

סעיף א(4)

תשובה

−3 – למשל: לשורש בן ימני עלה ובן שמאלי A; ל־A בן ימני עלה ובן שמאלי Y, ול־Y רק בן ימני (עלה).

מה נדרש: הערך המינימלי של zig בעץ בן 6 צמתים, ועץ שמשיג אותו.

רעיון: כמו בסעיף הקודם: zig ≥ −(מספר העלים) ≥ −3. צריך 3 עלים שבכולם מספר אי־זוגי של מעברים ימינה.

פתרון: המספר הנמוך ביותר הוא −3. העץ:

        a
       / \
      A   c
     / \
    Y   d
     \
      e

העלים: c (ימינה – 1) ← −1; d (שמאלה, ימינה – 1) ← −1; e (שמאלה, שמאלה, ימינה – 1) ← −1.

חישוב: zig(e) = 1 ← zig(Y) = −1; zig(d) = 1 ← zig(A) = −1 − 1 = −2; zig(c) = 1 ← zig(a) = −2 − 1 = −3.

בדיקה: בין כל העצים בני 6 צמתים הערך הנמוך ביותר הוא −3, והוא מתקבל רק בעץ הזה.

סעיף א(5)

תשובה

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

מה נדרש: לתאר במשפט מה zig מחזירה.

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

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

בדיקה: בעץ של סעיף (1): העלים 1 (0 מעברים ימינה), 4 (1), 2 (2) ← 2 − 1 = 1 ✓. בעץ של סעיף (2): 3 (1 – דרך 2←7), 1 (2), 8 (1), 0 (3) ← זוגי: 1 בלבד, אי־זוגי: 3, 8, 0 ← 1 − 3 = −2 ✓. zig שווה תמיד להפרש הזה.

טעות נפוצה: לכתוב ש־zig "סופרת עלים" – העלים נספרים עם סימן, והתוצאה יכולה להיות 0 או שלילית. שימו לב גם שעל עץ ריק (null) הפעולה נכשלת, ולכן השאלה מדברת על עץ שאינו ריק.

סעיף ב(1)

תשובה

true (zig של הצומת 0 היא −1)

מה נדרש: מעקב אחרי zag על העץ: 4 ← בן שמאלי 1 (ל־1 רק בן שמאלי 6) ובן ימני 8; ל־8 בן שמאלי 5 (בנים 9 ו־7) ובן ימני 0 (ל־0 רק בן ימני 3).

רעיון: zag מחזירה false לעץ ריק. בצומת לא ריק: אם zig של תת־העץ שלו שלילית – true מיד. אחרת ממשיכים לבן השמאלי, ורק אם הוא החזיר false – לבן הימני (|| מקוצר). כלומר zag מחפשת צומת כלשהו שבו zig < 0.

ערכי zig (לפי הכלל מסעיף 5 או בחישוב): zig(6) = 1, zig(1) = 1, zig(9) = 1, zig(7) = 1, zig(5) = 0, zig(3) = 1, zig(0) = −1, zig(8) = 0 − (−1) = 1, zig(4) = 1 − 1 = 0.

פתרון (מעקב):

קריאהzigמה קורהתוצאה
zag(4)0לא שלילי ← zag(1)–
zag(1)1← zag(6)–
zag(6)1zag(null) ‖ zag(null)false
zag(1)false ‖ zag(null)false
zag(4)השמאלי false ← zag(8)–
zag(8)1← zag(5)–
zag(5)0zag(9) ‖ zag(7) – שני עלים עם zig = 1, ובניהם nullfalse
zag(8)השמאלי false ← zag(0)–
zag(0)−1שלילי!true
zag(8)false ‖ truetrue
zag(4)false ‖ truetrue

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

בדיקה: הקוד הנתון מחזיר על העץ true, וסדר הקריאות זהה לטבלה.

סעיף ב(2)

תשובה

עץ שבו zig ≥ 0 בכל תת־עץ, למשל עץ מלא בן 7 צמתים: 4 ← (2, 6), 2 ← (1, 3), 6 ← (5, 7); zag מחזירה false.

מה נדרש: בסעיף ב(1) התקבל true, ולכן צריך עץ עם יותר מ־3 צמתים שבעבורו zag מחזירה false.

רעיון: zag מחזירה true בדיוק כשיש צומת שבו zig של תת־העץ שלו שלילית. לכן צריך עץ שבו zig ≥ 0 בכל צומת. למשל: בעץ מלא כל צומת פנימי מחשב 1 − 1 = 0, ובעלים zig = 1. (גם "שרשרת שמאלית" של 4 צמתים מתאימה: zig = 1 בכל צומת.)

פתרון: עץ מלא בן 7 צמתים:

        4
      /   \
     2     6
    / \   / \
   1   3 5   7

zig של העלים 1, 3, 5, 7 הוא 1; zig(2) = 1 − 1 = 0; zig(6) = 0; zig(4) = 0 − 0 = 0. אין צומת עם zig שלילית, ולכן zag(1), zag(3)… כולן false, ו־zag מחזירה false – ההפך מסעיף ב(1).

בדיקה: zag מחזירה על העץ הזה (ועל השרשרת השמאלית בת 4 צמתים) false. בנוסף נבדק על כל צורות העצים עד 8 צמתים ש־zag מחזירה true בדיוק כשיש צומת עם zig < 0.

טעות נפוצה: לבחור עץ שבו לשורש בן ימני עלה ובן שמאלי עם שני בנים – אז zig(שורש) = 0 − 1 = −1 ו־zag מחזירה true.

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

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

שאלה 4 · טענות על גרפים, BFS ועצי DFS (25 נקודות)

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

א.(1)

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

(1) נתון גרף לא מכוון ובו n קודקודים ו־n קשתות. בהכרח יש בגרף מעגל.

(2)

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

(3)

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

(4)

נתון גרף לא מכוון. הרצנו BFS מקודקוד s וקיבלנו לכל קודקוד v את d(v) – המרחק (מספר הקשתות במסלול הקצר ביותר) מ־s ל־v. לכל קשת u–v בגרף שאחד מקצותיה ישיג מ־s , מתקיים |d(u) − d(v)| ≤ 1 .

(5)

נתון גרף מכוון. הרצנו BFS מקודקוד s וקיבלנו לכל קודקוד v את d(v) – המרחק מ־s ל־v. לכל קשת u→v בגרף ששני קצותיה ישיגים מ־s , מתקיים |d(u) − d(v)| ≤ 1 .

(6)

נתון גרף לא מכוון. אם קיימת סריקת DFS מקודקוד s שבה התקבל עץ שהוא "שרשרת" (לכל קודקוד בעץ יש בן אחד לכל היותר), אז בכל סריקת DFS מקודקוד s יתקבל עץ שהוא שרשרת.

ב.

נתון גרף לא מכוון G ובו 5 קודקודים: s , e , f , g , h .

לאחר הרצת DFS מקודקוד התחלה s התקבל העץ שבסרטוט הנתון:

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

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

סעיף א(1)

תשובה

נכונה. גרף ללא מעגלים הוא יער: אם יש בו k רכיבי קשירות, יש בו n−k ≤ n−1 קשתות. לכן גרף עם n קשתות חייב להכיל מעגל.

מה נדרש: לקבוע אם כל גרף לא מכוון עם n קודקודים ו־n קשתות מכיל מעגל.

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

פתרון: הטענה נכונה. נניח בשלילה שאין בגרף מעגל. אז כל רכיב קשירות שלו הוא גרף קשיר ללא מעגלים – כלומר עץ. אם יש בגרף k רכיבים, ולרכיב ה־i יש nᵢ קודקודים, אז יש בו nᵢ−1 קשתות. סך הקשתות:
(n₁−1) + (n₂−1) + … + (n_k−1) = n − k ≤ n − 1

(כי k ≥ 1). אבל בגרף יש n קשתות – סתירה. לכן בגרף יש מעגל.

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

סעיף א(2)

תשובה

לא נכונה. דוגמה נגדית: a–b (1), b–c (2), a–c (3), c–d (4). העפ"מ היחיד הוא a–b , b–c , c–d (משקל 7), והקשת השלישית הקלה a–c אינה בו.

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

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

פתרון: הטענה לא נכונה. דוגמה נגדית עם 4 קודקודים:

קשתa–bb–ca–cc–d
משקל1234

הגרף קשיר וכל המשקלים שונים. קרוסקל: מוסיף a–b (1), מוסיף b–c (2), דוחה את a–c (3) כי היא סוגרת את המעגל a–b–c, מוסיף c–d (4). העפ"מ: a–b , b–c , c–d במשקל 7. כשכל המשקלים שונים העפ"מ יחיד – והקשת השלישית הקלה, a–c, אינה בו.

בדיקה: מבין העצים הפורשים של הגרף יש עפ"מ יחיד במשקל 7, והוא אינו מכיל את a–c.

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

סעיף א(3)

תשובה

נכונה. אם נמחקה הקשת u–v, כל קודקוד x עדיין מחובר ל־u או ל־v: אם המסלול הפשוט מ־x ל־u בגרף המקורי לא עובר בקשת – הוא נשאר; ואם הוא עובר בה – היא הקשת האחרונה בו, והחלק שלפניה מחבר את x ל־v. לכן כל קודקוד ברכיב של u או ברכיב של v – לכל היותר 2 רכיבים.

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

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

פתרון: הטענה נכונה. נניח שנמחקה הקשת u–v, ויהי x קודקוד כלשהו. בגרף המקורי (הקשיר) יש מסלול פשוט מ־x ל־u.

•אם המסלול אינו משתמש בקשת u–v – הוא קיים גם אחרי המחיקה, ו־x מחובר ל־u.
•אם המסלול משתמש בקשת u–v – במסלול פשוט שמסתיים ב־u, הקודקוד u מופיע רק בסוף, ולכן u–v היא הקשת האחרונה במסלול (מ־v ל־u). החלק שלפניה הוא מסלול מ־x ל־v שאינו משתמש בקשת u–v, ולכן הוא קיים גם אחרי המחיקה, ו־x מחובר ל־v.

כלומר כל קודקוד נמצא ברכיב של u או ברכיב של v – יש לכל היותר 2 רכיבי קשירות. (יהיו בדיוק 2 אם u–v היא "גשר", ו־1 אם היא על מעגל.)

סעיף א(4)

תשובה

נכונה. אם u ישיג מ־s אז גם v ישיג, ו־d(v) ≤ d(u)+1 (מסלול קצר ל־u ועוד הקשת). הגרף לא מכוון, ולכן גם d(u) ≤ d(v)+1. מכאן |d(u) − d(v)| ≤ 1.

מה נדרש: להראות שבגרף לא מכוון שני קצות של קשת נמצאים באותה "שכבה" של BFS או בשכבות סמוכות.

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

פתרון: הטענה נכונה. נניח ש־u ישיג מ־s (המקרה ש־v ישיג סימטרי). ניקח מסלול קצר ביותר מ־s ל־u (באורך d(u)) ונוסיף לו את הקשת u–v: קיבלנו מסלול מ־s ל־v באורך d(u)+1. לכן v ישיג ו־d(v) ≤ d(u) + 1.
הגרף לא מכוון, ולכן אפשר לעבור בקשת גם מ־v ל־u. באותו אופן: d(u) ≤ d(v) + 1.
משני אי־השוויונות: −1 ≤ d(u) − d(v) ≤ 1 , כלומר |d(u) − d(v)| ≤ 1.

סעיף א(5)

תשובה

לא נכונה. דוגמה נגדית: s→a , a→b , b→c , c→s. כל הקודקודים ישיגים מ־s, d(c) = 3 ו־d(s) = 0, ובקשת c→s ההפרש הוא 3.

מה נדרש: לבדוק אם התכונה מהטענה הקודמת נשמרת גם בגרף מכוון.

רעיון: בגרף מכוון הקשת u→v נותנת קיצור רק בכיוון אחד: d(v) ≤ d(u)+1. בכיוון ההפוך אין שום הגבלה – קשת יכולה "לחזור אחורה" לקודקוד קרוב מאוד ל־s.

פתרון: הטענה לא נכונה. דוגמה נגדית עם 4 קודקודים:
s→a , a→b , b→c , c→s

BFS מ־s: d(s) = 0 , d(a) = 1 , d(b) = 2 , d(c) = 3. שני הקצוות של הקשת c→s ישיגים, אבל |d(c) − d(s)| = |3 − 0| = 3 > 1.

בדיקה: BFS על הגרף נותן בדיוק את המרחקים 0, 1, 2, 3.

טעות נפוצה: להעתיק את הנימוק של הגרף הלא מכוון. בגרף מכוון נכון רק החלק d(v) ≤ d(u) + 1.

סעיף א(6)

תשובה

לא נכונה. דוגמה נגדית: s–a , a–b , b–s , a–c. הסריקה s→b→a→c נותנת שרשרת, אבל בסריקה s→a→b (חזרה ל־a) →c לקודקוד a יש שני בנים.

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

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

פתרון: הטענה לא נכונה. דוגמה נגדית עם 4 קודקודים s, a, b, c וקשתות:
s–a , a–b , b–s , a–c

סריקה 1 (מ־s בוחרים קודם את b): s → b → a → c. קשתות העץ: s–b , b–a , a–c – זו שרשרת.

סריקה 2 (מ־s בוחרים קודם את a, ומ־a בוחרים קודם את b): s → a → b (השכנים של b: s, a – כבר התגלו) → חוזרים ל־a → c. קשתות העץ: s–a , a–b , a–c. לקודקוד a יש שני בנים – זו אינה שרשרת.

בדיקה: בין עצי ה־DFS מ־s בגרף מתקבלים גם השרשרת s–b–a–c וגם העץ שבו ל־a שני בנים.

סעיף ב

תשובה

(i) ייתכן – אם ב־G יש קשת s–g (ואין קשת s–h): f→e→s→g, חזרה ל־e→h. (ii) לא ייתכן – הקשת e–h קיימת ב־G, ובעץ (ii) e ו־h הם אחים (בנים של s); ב־DFS בגרף לא מכוון אין קשת בין שני ענפים שונים. (iii) לא ייתכן – העץ משתמש בקשת h–g, ואין קשת כזו ב־G.

מה נדרש: לקבוע לכל אחד מהעצים (i), (ii), (iii) אם הוא יכול להיות עץ DFS מ־f של אותו גרף G.

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

פתרון:
קשתות העץ הנתון (ולכן בוודאי ב־G): s–e , e–f , f–g , e–h.
קשתות נוספות אפשריות – רק בין קודקוד לאב קדמון שלו: s–f , s–g , e–g , s–h (אולי קיימות ואולי לא).
קשתות שאינן יכולות להיות ב־G: h–f ו־h–g (h נמצא בענף אחר של e מאשר f ו־g).

עץקשתות העץקביעהנימוק
(i)f–e , e–s , s–g , e–hייתכןהקשתות f–e , e–s , e–h קיימות ב־G, והקשת s–g יכולה להיות ב־G (קשת אחורה בעץ הנתון). ראו את הסריקה למטה.
(ii)f–g , g–s , s–e , s–hלא ייתכןהקשת e–h קיימת ב־G (היא קשת בעץ הנתון). בעץ (ii) e ו־h הם שני בנים של s – ענפים שונים. כש־DFS היה בתוך e, הוא היה רואה את h שעוד לא התגלה ועובר אליו, ואז h היה צאצא של e ולא אח שלו.
(iii)f–e , e–s , e–h , h–gלא ייתכןנדרשת קשת h–g, אבל h ו־g נמצאים בענפים שונים של e בעץ הנתון, ולכן אין ביניהם קשת.

סריקה שנותנת את (i), בגרף G שבו הקשתות הן העץ הנתון ועוד s–g: מתחילים ב־f ← בוחרים את השכן e ← מ־e בוחרים את s ← מ־s עוברים ל־g (השכנים של g: f, s – כבר התגלו) ← חוזרים ל־s (אין לו עוד שכנים לא מגולים) ← חוזרים ל־e ועוברים ל־h ← חוזרים עד f; גם g כבר התגלה. התקבלו בדיוק קשתות העץ f–e , e–s , s–g , e–h.
(אילו הייתה ב־G גם הקשת s–h, ה־DFS היה מגלה את h מתוך s, ו־h היה בן של s. לכן (i) מתקבל רק כשאין s–h – אבל מספיק שקיים גרף G מתאים, ולכן התשובה "ייתכן".)

בדיקה: בדיוק 16 גרפים על 5 הקודקודים נותנים את העץ הנתון מ־s (העץ ועוד תת־קבוצה של {s–f , s–g , e–g , s–h}). בכל אחד מהם נמצאו כל עצי ה־DFS מ־f: עץ (i) מתקבל ב־4 גרפים (אלה שיש בהם s–g ואין בהם s–h), ועצים (ii) ו־(iii) אף פעם לא.

שאלה 5 · טענות על גרפים – נכון / לא נכון (25 נקודות)

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

א.

בגרף G לא מכוון, אם סריקה לעומק (DFS) שמתחילה בקודקוד s מבקרת בכל הקודקודים של הגרף, אז גם סריקה לרוחב (BFS) שמתחילה בקודקוד s מבקרת בכל הקודקודים של הגרף.

ב.

גרף לא מכוון שיש בו n קודקודים ו־ n קשתות (n ≥ 3) הוא בהכרח גרף קשיר.

ג.

בגרף מכוון ממושקל שבו כל המשקלים חיוביים, המסלול הקל ביותר מקודקוד s אל קודקוד t הוא תמיד גם מסלול שמספר הקשתות בו הוא הקטן ביותר מבין כל המסלולים מ־ s אל t .

ד.

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

ה.

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

ו.

נתון גרף מכוון G שיש בו לפחות שני קודקודים ויש לו מיון טופולוגי, ו־ x ו־ y הם הקודקוד הראשון והקודקוד האחרון במיון זה (בהתאמה).
אם נוסיף ל־ G את הקשת x → y (אם היא אינה קיימת בו), גם לגרף החדש יש מיון טופולוגי.

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

סעיף א

תשובה

נכונה. DFS ו־BFS מ־s מבקרים שניהם בדיוק בקודקודים שיש אליהם מסלול מ־s. אם DFS ביקר בכולם – יש מסלול מ־s לכל קודקוד, ולכן גם BFS יבקר בכולם.

מה נדרש: להראות ש־BFS מגיע לכל מקום ש־DFS מגיע אליו (מאותו קודקוד).

רעיון: שתי הסריקות מבקרות בדיוק בקבוצת הקודקודים הישיגים מ־ s ; הן נבדלות רק בסדר הביקור.

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

1.DFS עובר רק לאורך קשתות, ולכן כל קודקוד שהוא ביקר בו ישיג מ־ s . מכיוון שה־DFS ביקר בכל הקודקודים – כל קודקוד ישיג מ־ s .
2.BFS מבקר בכל קודקוד ישיג: יהי v ישיג, ו־ s = u₀, u₁, …, uₖ = v מסלול אליו. באינדוקציה: u₀ = s מתגלה בהתחלה; אם uᵢ התגלה, הוא נכנס לתור ובשלב כלשהו יוצא ממנו, ואז ה־BFS עובר על כל שכניו – ובהם uᵢ₊₁ – ומגלה אותו (אם לא התגלה קודם). לכן גם v = uₖ מתגלה.

מ־1 ו־2: ה־BFS מ־ s מבקר בכל הקודקודים.

סעיף ב

תשובה

לא נכונה. דוגמה נגדית: שני משולשים נפרדים a–b–c ו־d–e–f: יש 6 קודקודים ו־6 קשתות, אבל אין מסלול מ־a ל־d.

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

רעיון: קשירות תלויה באיך הקשתות מפוזרות, לא רק בכמה קשתות יש. אפשר "לבזבז" קשתות על מעגל בתוך חלק אחד של הגרף.

פתרון: הטענה לא נכונה. דוגמה נגדית: n = 6 , קודקודים a, b, c, d, e, f וקשתות a–b, b–c, c–a, d–e, e–f, f–d (שני משולשים). יש 6 קודקודים ו־6 קשתות, אבל אין אף קשת בין {a, b, c} ובין {d, e, f} , ולכן אין מסלול מ־ a אל d – הגרף אינו קשיר.
(הכיוון ההפוך כן נכון: גרף קשיר על n קודקודים חייב לפחות n − 1 קשתות; אבל מספר קשתות גדול אינו מבטיח קשירות.)

בדיקה: DFS מ־ a מגיע רק ל־ {a, b, c}.

טעות נפוצה: להסיק מ"עץ על n קודקודים יש n − 1 קשתות" שכל גרף עם n − 1 קשתות או יותר הוא קשיר.

סעיף ג

תשובה

לא נכונה. דוגמה נגדית: s→a (1), a→b (1), b→t (1), s→t (5). המסלול הקל ביותר s→a→b→t שוקל 3 ויש בו 3 קשתות, ואילו במסלול s→t יש קשת אחת בלבד (משקלו 5).

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

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

פתרון: הטענה לא נכונה. דוגמה נגדית: קשתות מכוונות s → a (1), a → b (1), b → t (1), s → t (5).

•המסלולים מ־ s אל t : s → t (קשת אחת, משקל 5) ו־ s → a → b → t (3 קשתות, משקל 3).
•המסלול הקל ביותר הוא s → a → b → t (משקל 3), אבל יש בו 3 קשתות, יותר מ־ s → t .

("מספר קשתות מינימלי" הוא מה ש־BFS מוצא; "משקל מינימלי" – מה שדייקסטרה מוצא. הם שווים רק כשכל המשקלים שווים.)

בדיקה: המסלולים מ־ s אל t עם משקליהם: הקל ביותר (3) עם 3 קשתות, והקצר ביותר במספר קשתות (1) במשקל 5.

סעיף ד

תשובה

נכונה. נניח שעץ פורש מינימלי T אינו מכיל את הקשת הקלה ביותר e. הוספת e ל־T יוצרת מעגל; על המעגל יש קשת אחרת f, ו־w(f) > w(e) (e היא הקלה היחידה). T + e − f הוא עץ פורש קל יותר מ־T – סתירה.

מה נדרש: להוכיח שהקשת הקלה היחידה נמצאת בכל עץ פורש מינימלי (או להפריך).

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

פתרון: הטענה נכונה. תהי e = (u, v) הקשת היחידה בעלת המשקל הקטן ביותר, ונניח בשלילה שיש עץ פורש מינימלי T שאינו מכיל את e .

•T הוא עץ פורש, ולכן יש בו מסלול בין u ל־ v . הוספת e ל־ T סוגרת את המסלול למעגל.
•על המעגל יש לפחות קשת נוספת f ≠ e מתוך T . e היא הקשת הקלה היחידה, ולכן w(f) > w(e) .
•נוציא את f : הגרף T + e − f עדיין קשיר (הסרת קשת ממעגל אינה מנתקת) ויש בו n − 1 קשתות – זה עץ פורש. משקלו w(T) + w(e) − w(f) < w(T) .

קיבלנו עץ פורש קל מ־ T – סתירה לכך ש־ T מינימלי. לכן e שייכת לכל עץ פורש מינימלי.
(גם האלגוריתם של קרוסקל בוחר את הקשת הזאת ראשונה. אם יש כמה קשתות קלות ביותר במשקל שווה, הטענה כבר לא נכונה – למשל במשולש שכל משקליו 1.)

סעיף ה

תשובה

לא נכונה. דוגמה נגדית: s→a (2), s→b (3), b→a (−2). דייקסטרה קובע את a ראשון עם d[a] = 2, אבל המסלול s→b→a שוקל 1. (בגרף כזה צריך את האלגוריתם של בלמן־פורד.)

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

רעיון: דייקסטרה "קובע" את הקודקוד בעל ה־ d הקטן ביותר ולא משנה אותו יותר. זה נכון רק כי בלי משקלים שליליים מסלול לא יכול להתקצר כשממשיכים בו. קשת שלילית שוברת את ההנחה.

פתרון: הטענה לא נכונה. דוגמה נגדית: קשתות מכוונות s → a (2), s → b (3), b → a (−2). אין מעגלים בכלל, ולכן אין מעגל שלילי.

•איטרציה 1: נקבע s ; d[a] = 2 , d[b] = 3 .
•איטרציה 2: נקבע a (d = 2, הקטן ביותר).
•איטרציה 3: נקבע b (d = 3); הקשת b → a מובילה לקודקוד שכבר קבוע, ו־ d[a] נשאר 2 .

אבל המסלול s → b → a שוקל 3 + (−2) = 1 < 2 . דייקסטרה טעה ב־ d[a] .
בלמן־פורד (הרפיה של כל הקשתות n − 1 פעמים) מוצא את המרחק הנכון d[a] = 1 .

בדיקה: דייקסטרה מחזיר d[a] = 2 , ובלמן־פורד מחזיר d[a] = 1 .

סעיף ו

תשובה

נכונה. אותו מיון עצמו עדיין תקין: לכל קשת ישנה u → v, u לפני v במיון, וגם בקשת החדשה x → y, x (הראשון) לפני y (האחרון). גרף שיש לו מיון טופולוגי – חסר מעגלים.

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

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

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

•כל הקשתות הישנות של G מקיימות את התנאי במיון הנתון.
•בקשת החדשה x → y : x הוא הראשון במיון ו־ y האחרון, ולכן x לפני y (בגרף יש לפחות שני קודקודים, ולכן x ≠ y ; בגרף עם קודקוד יחיד הקשת x → x הייתה לולאה – מעגל).

לכן אותו סידור הוא מיון טופולוגי גם של הגרף החדש (ובפרט הגרף החדש חסר מעגלים).
(לעומת זאת, הוספת הקשת ההפוכה y → x יכולה ליצור מעגל: למשל בגרף a → b → c עם המיון a, b, c, הקשת c → a סוגרת מעגל.)

שאלה 6 · גרף "מנסרה" (25 נקודות)

גרף "מנסרה" – Pₙ הוא גרף לא מכוון שמורכב משני מעגלים של n קודקודים כל אחד: "המעגל החיצוני", שקודקודיו הם 1, 2, …, n , ו"המעגל הפנימי", שקודקודיו הם 1', 2', …, n' . בנוסף, כל קודקוד i של המעגל החיצוני מחובר בקשת לקודקוד i' של המעגל הפנימי (כך שסך הכול יש 2n קודקודים בגרף). מספר הקודקודים בכל מעגל גדול מ־ 2 (כלומר n > 2).
דוגמה: הגרף שלפניכם הוא גרף "מנסרה" – P₆ (קשתות המעגל החיצוני מודגשות).

"קשתות חיצוניות" הן הקשתות של המעגל החיצוני (למשל בדוגמה לעיל, הקשת שמחברת את הקודקודים 1 ו־ 2 זה לזה, הקשת שמחברת את הקודקודים 6 ו־ 1 זה לזה וכן הלאה).
"קשתות פנימיות" הן הקשתות של המעגל הפנימי (למשל בדוגמה לעיל, הקשת שמחברת את הקודקודים 1' ו־ 2' זה לזה).
"חישורים" הם הקשתות שמחברות קודקוד i לקודקוד i' (למשל בדוגמה לעיל, הקשת שמחברת את הקודקוד 3 לקודקוד 3').

א.(1)

נתון גרף "מנסרה" – Pₙ .

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

(2)

כמה "קשתות חיצוניות", כמה "קשתות פנימיות" וכמה "חישורים" יש בגרף? נמקו את תשובתכם.

(3)

בעבור אילו ערכים של n הגרף הוא דו־צדדי? נמקו את תשובתכם.

ב.

נתון גרף ממושקל שהוא "מנסרה" – Pₙ , שבו לכל "הקשתות החיצוניות" ולכל "הקשתות הפנימיות" משקל 1 , ולכל "החישורים" משקל 2 .
כמה עפ"מים (עצים פורשים מינימליים) שונים יש בגרף? נמקו את תשובתכם.

ג.(1)

נתון גרף ממושקל שהוא "מנסרה" – Pₙ , שבו לכל "הקשתות החיצוניות" משקל a , לכל "הקשתות הפנימיות" משקל b , ולכל "החישורים" משקל s (כל המשקלים גדולים מ־ 0).

(1) הציבו ערכים ל־ a , b ו־ s , שבעבורם כל מסלול קצר (קל) בין שני קודקודים של המעגל החיצוני יעבור דרך קודקוד אחד לפחות של המעגל הפנימי. נמקו את תשובתכם.

(2)

נתון כי a = 10 ו־ b = 2 . מהו הערך הגדול ביותר של s שבעבורו כל מסלול קצר (קל) בין שני קודקודים של המעגל החיצוני עדיין יעבור דרך קודקוד של המעגל הפנימי? נמקו את תשובתכם.

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

סעיף א(1)

תשובה

הדרגה של כל קודקוד בגרף (חיצוני ופנימי) היא 3 .

מה נדרש: הדרגות של הקודקודים ב־ Pₙ .

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

פתרון: קודקוד i של המעגל החיצוני נוגע בשתי קשתות חיצוניות (אל הקודקוד שלפניו ואל הקודקוד שאחריו במעגל) ובחישור אחד (אל i') ⇐ דרגה 3 . באותו אופן קודקוד i' של המעגל הפנימי נוגע בשתי קשתות פנימיות ובחישור אחד (אל i) ⇐ דרגה 3 . כלומר כל הקודקודים בגרף בדרגה 3 .

בדיקה: ב־ P₆ שבאיור: לקודקוד 1 שכנים 6, 2, 1' , ולקודקוד 1' שכנים 6', 2', 1 . כך גם לכל 3 ≤ n ≤ 8 .

סעיף א(2)

תשובה

n קשתות חיצוניות, n קשתות פנימיות ו־ n חישורים (סך הכול 3n קשתות).

מה נדרש: מספר הקשתות מכל סוג.

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

בדיקה: לפי משפט סכום הדרגות: 2n קודקודים · 3 = 6n = 2·(מספר הקשתות) ⇐ 3n קשתות = n + n + n . ב־ P₆ : 6 + 6 + 6 = 18 קשתות.

סעיף א(3)

תשובה

הגרף דו־צדדי אם ורק אם n זוגי.

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

רעיון: גרף הוא דו־צדדי אם ורק אם אין בו מעגל באורך אי־זוגי. בגרף יש שני מעגלים באורך n , ולכן n קובע.

פתרון:

•n אי־זוגי – לא דו־צדדי: המעגל החיצוני הוא מעגל באורך n , אי־זוגי. אם נצבע את 1 בצבע X , אז 2 בצבע Y , 3 בצבע X , … , ו־ n (אי־זוגי) בצבע X – אבל n ו־ 1 שכנים. סתירה.
•n זוגי – דו־צדדי: נשים בצד אחד את הקודקודים החיצוניים האי־זוגיים (1, 3, …) ואת הקודקודים הפנימיים הזוגיים (2', 4', …), ובצד השני את כל השאר. קשת חיצונית מחברת i ל־ i+1 (זוגיות שונה), והקשת n–1 מחברת n זוגי ל־ 1 אי־זוגי ⇐ בין הצדדים. כך גם קשת פנימית. חישור מחבר את i ל־ i' , ולפי החלוקה הם תמיד בצדדים שונים.

בדיקה: צביעה בשני צבעים מצליחה ל־ n = 4, 6, 8 ונכשלת ל־ n = 3, 5, 7 .

סעיף ב

תשובה

n³ עפ"מים (כל אחד במשקל 2n): משמיטים קשת חיצונית אחת, קשת פנימית אחת, ובוחרים חישור אחד.

מה נדרש: מספר העצים הפורשים המינימליים.

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

פתרון:

•עץ אינו מכיל מעגל, ולכן הוא מכיל לכל היותר n − 1 מבין n הקשתות החיצוניות ולכל היותר n − 1 מבין n הקשתות הפנימיות – לכל היותר 2n − 2 קשתות במשקל 1 . לכן בכל עץ פורש יש לפחות קשת אחת במשקל 2 (חישור), ומשקלו לפחות (2n − 2)·1 + 2 = 2n .
•משקל 2n מתקבל בדיוק כשהעץ מכיל n − 1 קשתות חיצוניות, n − 1 קשתות פנימיות וחישור אחד. כל בחירה כזאת היא אכן עץ פורש: n − 1 הקשתות החיצוניות יוצרות מסלול שעובר בכל הקודקודים החיצוניים, n − 1 הקשתות הפנימיות – מסלול שעובר בכל הקודקודים הפנימיים, והחישור מחבר את שני המסלולים בלי ליצור מעגל.
•מספר האפשרויות: n (איזו קשת חיצונית משמיטים) · n (איזו קשת פנימית משמיטים) · n (איזה חישור בוחרים) = n³ .

למשל ב־ P₆ יש 6³ = 216 עפ"מים.

בדיקה: מספר העפ"מים: n = 3 – 27 , n = 4 – 64 , n = 5 – 125 , n = 6 – 216 , כולם במשקל 2n .

טעות נפוצה: לענות "n עפ"מים" (רק בחירת החישור) – ולשכוח שגם בכל אחד משני המעגלים אפשר להשמיט כל אחת מ־ n הקשתות.

סעיף ג(1)

תשובה

למשל a = 5 , b = 1 , s = 1 (כל בחירה שבה 2s + b < a מתאימה).

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

רעיון: נסמן ב־ k את מספר הקשתות בקשת הקצרה של המעגל החיצוני בין שני הקודקודים (1 ≤ k ≤ n/2). מסלול שנשאר על המעגל החיצוני שוקל לפחות k·a . המסלול "למטה–לאורך המעגל הפנימי–למעלה" (חישור, k קשתות פנימיות, חישור) שוקל 2s + k·b .

פתרון: אם 2s + b < a , אז לכל k ≥ 1 : 2s + k·b ≤ k·(2s + b) < k·a . כלומר המסלול דרך המעגל הפנימי קל ממש מכל מסלול שנשאר על המעגל החיצוני, ולכן כל מסלול קל ביותר עובר בקודקוד פנימי.
דוגמה: a = 5 , b = 1 , s = 1 : בין שני קודקודים שכנים – הקשת החיצונית 5 , ודרך המעגל הפנימי 1 + 1 + 1 = 3 ; בין קודקודים במרחק k – על המעגל החיצוני 5k , ודרך המעגל הפנימי 2 + k .

בדיקה: בעבור 3 ≤ n ≤ 8, עם a = 5 , b = 1 , s = 1 , אף מסלול קל ביותר בין שני קודקודים חיצוניים אינו נשאר על המעגל החיצוני. הוא גם אישר על טבלת ערכים שלמים שהתנאי מתקיים בדיוק כאשר 2s + b < a .

סעיף ג(2)

תשובה

צריך 2s + 2 < 10 , כלומר s < 4 . במשקלים שלמים – s = 3 (ב־ s = 4 יש גם מסלול קל שנשאר על המעגל החיצוני).

מה נדרש: הערך הגדול ביותר של s כאשר a = 10 ו־ b = 2 .

רעיון: המקרה הקשה הוא שני קודקודים שכנים במעגל החיצוני: הקשת הישירה שוקלת a = 10 , וכל מסלול שעובר בקודקוד פנימי צריך לרדת בחישור, לעבור לפחות קשת פנימית אחת (כי i' ≠ (i+1)') ולעלות בחישור – לפחות 2s + b .

פתרון: צריך 2s + 2 < 10 ⇐ s < 4 . אם המשקלים שלמים, הערך הגדול ביותר הוא s = 3 : אז בין שכנים 3 + 2 + 3 = 8 < 10 , ולפי סעיף (1) התנאי מתקיים לכל זוג.
ב־ s = 4 : בין שני שכנים יש שני מסלולים קלים במשקל 10 – הקשת הישירה (10) והמסלול 4 + 2 + 4 . הקשת הישירה אינה עוברת בקודקוד פנימי, ולכן לא כל מסלול קל עובר במעגל הפנימי.
(אם s יכול להיות לא שלם, כל s < 4 מתאים ואין "גדול ביותר"; התשובה המקובלת היא s = 3 למשקלים שלמים, או "s קטן מ־ 4".)

בדיקה: עם a = 10 , b = 2 ו־ s = 1, 2, 3, 3.9, 4, 5 : התנאי מתקיים רק ל־ s = 1, 2, 3, 3.9 .

טעות נפוצה: לענות s = 4 – במצב של שוויון יש מסלול קל נוסף שאינו עובר במעגל הפנימי.

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

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

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

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

L₁ = {bⁿ c²ᵏ aⁿ⁺ᵏ⁺² | n , k ≥ 1}

א.(1)

מהי המילה הקצרה ביותר בשפה L₁ ?

(2)

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

ב.

נגדיר את השפה L₂ מעל הא"ב {a, b, c} :

L₂ = L₁ • L₁

בעבור כל אחת מן המילים שלפניכם, ציינו אם המילה שייכת לשפה L₂ , ונמקו.

(1) bccaaaabccaaaa

(2) bccaaabccaaaaa

(3) bbccaaaaabccccaaaaa

הערה: אין צורך לבנות אוטומט בעבור השפה L₂ .

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

סעיף א(1)

תשובה

bccaaaa

מה נדרש: המילה הקצרה ביותר ב־L₁.

רעיון:
אורך המילה הוא n + 2k + (n + k + 2) = 2n + 3k + 2, והוא הקטן ביותר כש־n = k = 1.

פתרון:
n = 1, k = 1: b¹ c² a⁴ = bccaaaa (אורך 7; 4 = 1 + 1 + 2). כל בחירה אחרת נותנת אורך 9 לפחות.

בדיקה: המילים של L₁ עד אורך 10 הן bccaaaa, bbccaaaaa, bccccaaaaa – הקצרה ביותר bccaaaa.

טעות נפוצה: לכתוב bcaaaa – מספר ה־c הוא 2k, כלומר זוגי ולפחות 2.

סעיף א(2)

תשובה

דוחפים סימן לכל b (התחתון X) ולכל זוג c; כל a שולף סימן; ה־a ששולף את X מעביר לספירת שני a נוספים (q5 → q6 → q7), ו־q7 מקבל. הטבלה המלאה בפתרון.

מה נדרש: אוטומט מחסנית דטרמיניסטי ל־L₁ = {bⁿ c²ᵏ aⁿ⁺ᵏ⁺² | n, k ≥ 1}.

רעיון:

•מספר ה־a הוא n + k + 2: a אחד לכל b, a אחד לכל זוג c, ועוד 2 a.
•לכן דוחפים סימן על כל b, ועל כל זוג c דוחפים סימן אחד (ה־c הראשון בזוג רק מחליף מצב, השני דוחף). אחר כך כל a שולף סימן.
•הסימן התחתון (של ה־b הראשון) הוא X, וכל השאר A. ה־a ששולף את X הוא ה־a ה־(n+k). אחריו המחסנית ריקה (⊥), וצריך לקרוא בדיוק עוד 2 a – סופרים אותם במצבים q5, q6, q7.
•מספר ה־c חייב להיות זוגי: a מותר רק במצב q3 (אחרי c שני בזוג).

פתרון:

•מצבים: q0 (התחלתי), q1–q7. מצב מקבל: q7.
•א"ב הקלט: {a, b, c}. א"ב המחסנית: {⊥, X, A}; ⊥ – סימן תחתית המחסנית.
•סימון מעבר: (מצב, אות קלט, ראש המחסנית) → (מצב חדש, מחרוזת שמחליפה את ראש המחסנית). התו השמאלי הוא ראש המחסנית החדש; ε = שליפה; מחרוזת שווה לראש = בלי שינוי.
מצבקלטראש המחסניתמצב חדשמחליפים את הראש ב־משמעות
q0b⊥q1X⊥ה־b הראשון: X
q1bXq1AXעוד b: דוחפים A
q1bAq1AAעוד b: דוחפים A
q1cXq2Xc ראשון בזוג: בלי שינוי
q1cAq2Ac ראשון בזוג: בלי שינוי
q2cXq3AXc שני בזוג: דוחפים A
q2cAq3AAc שני בזוג: דוחפים A
q3cAq2Ac ראשון בזוג הבא
q3aAq4εה־a הראשון: שולפים
q4aAq4εעוד a: שולפים
q4aXq5εה־a ה־(n+k): שולף את X
q5a⊥q6⊥a נוסף ראשון (מתוך 2)
q6a⊥q7⊥a נוסף שני – מצב מקבל

בכל צירוף (מצב, אות, ראש מחסנית) יש לכל היותר מעבר אחד ואין מעברי ε – האוטומט דטרמיניסטי. כל צירוף שאינו בטבלה = דחייה (למשל a אחרי מספר אי־זוגי של c – במצב q2; b אחרי c; a שלישי אחרי ריקון המחסנית – ב־q7 אין מעברים).

מעקב על bbccaaaaa (n = 2, k = 1, ולכן 2 + 1 + 2 = 5 אותיות a):

נקראמצבמחסנית (ראש משמאל)
–q0⊥
bq1X⊥
bq1AX⊥
cq2AX⊥
cq3AAX⊥
aq4AX⊥
aq4X⊥
aq5⊥
aq6⊥
aq7⊥

הקלט נגמר במצב q7 ⇒ מתקבלת. לעומת זאת bbccaaaa נגמרת ב־q6 (לא מקבל) ונדחית.

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

טעות נפוצה: לשכוח את ה־"+2": לקבל כבר כשהמחסנית מתרוקנת. אחרי ש־X נשלף חייבים לקרוא בדיוק עוד שני a.

סעיף ב

תשובה

(1) שייכת: bccaaaa · bccaaaa. (2) לא שייכת. (3) שייכת: bbccaaaaa · bccccaaaaa.

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

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

פתרון:
(1) bccaaaabccaaaa: החלוקה: bccaaaa | bccaaaa. בכל חלק n = 1, k = 1 (2 c) ⇒ צריך 1 + 1 + 2 = 4 a, ויש 4. ⇒ שייכת ל־L₂.

(2) bccaaabccaaaaa: החלוקה היחידה האפשרית: bccaaa | bccaaaaa. בחלק הראשון n = 1, k = 1 ⇒ צריך 4 a, אבל יש רק 3. ⇒ לא שייכת ל־L₂ (גם החלק השני אינו ב־L₁: 5 a במקום 4).

(3) bbccaaaaabccccaaaaa: החלוקה: bbccaaaaa | bccccaaaaa. חלק ראשון: n = 2, k = 1 ⇒ 2 + 1 + 2 = 5 a ✔. חלק שני: n = 1, k = 2 (4 c) ⇒ 1 + 2 + 2 = 5 a ✔. ⇒ שייכת ל־L₂.

בדיקה: בכל נקודות החלוקה לכל מילה: (1) – חלוקה אחת, (2) – אין אף חלוקה, (3) – חלוקה אחת, כמו בפתרון.

טעות נפוצה: לספור את כל ה־a במילה יחד (למשל ב־(2) יש 3 + 5 = 8 = 4 + 4) ולהסיק שהיא שייכת. כל חלק צריך להתאים בנפרד.

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

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

א.(1)

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

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

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

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

(2)

R(L₁*) = (R(L₁))*

(3)

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

ב.

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

L = {aⁱbʲaᵏ | i ≥ 1, k ≥ 1, j = i + k}

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

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

סעיף א(1)

תשובה

לא נכונה. דוגמה: L₁ = {a}, L₂ = {b}: ab ∈ (L₁ ∪ L₂)* = {a,b}*, אבל ab ∉ L₁* ∪ L₂* (רק מילים של a בלבד או של b בלבד).

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

רעיון: ב־(L₁ ∪ L₂)* אפשר לערבב בין חלקים מ־L₁ לחלקים מ־L₂ באותה מילה. ב־L₁* ∪ L₂* כל מילה בנויה רק מחלקים של שפה אחת.

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

•(L₁ ∪ L₂)* = {a, b}* – כל המילים. בפרט ab ∈ (L₁ ∪ L₂)*.
•L₁* = {ε, a, aa, …}, L₂* = {ε, b, bb, …}. ב־ab יש גם a וגם b, ולכן ab ∉ L₁* ∪ L₂*.

השפות שונות – הטענה לא מתקיימת. (הכיוון ⊇ תמיד נכון: L₁* ∪ L₂* ⊆ (L₁ ∪ L₂)*.)

בדיקה: בדוגמה האגפים שונים; באופן כללי אגף ימין תמיד מוכל באגף שמאל, אבל לא תמיד שווה לו.

סעיף א(2)

תשובה

נכונה – ההיפוך של שרשור מילים מ־L₁ הוא שרשור (בסדר הפוך) של ההיפוכים שלהן, שכולן ב־R(L₁).

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

רעיון: R(xy) = R(y)·R(x). היפוך של שרשור של כמה מילים הופך כל מילה וגם את הסדר שלהן – אבל בסגור הסדר לא משנה, כי כל סדר של חלקים מותר.

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

•⊆: תהי z ∈ R(L₁*). אז z = R(w) עבור w ∈ L₁*, כלומר w = x₁x₂…xₖ עם k ≥ 0 וכל xᵢ ∈ L₁. לכן z = R(xₖ)·…·R(x₂)·R(x₁), וכל R(xᵢ) ∈ R(L₁). זהו שרשור של k מילים מ־R(L₁), ולכן z ∈ (R(L₁))*. (עבור k = 0: z = ε, שנמצאת בכל סגור.)
•⊇: תהי z ∈ (R(L₁))*. אז z = y₁y₂…yₖ עם yᵢ = R(xᵢ), xᵢ ∈ L₁. נגדיר w = xₖ…x₂x₁ ∈ L₁*. אז R(w) = R(x₁)R(x₂)…R(xₖ) = y₁…yₖ = z, ולכן z ∈ R(L₁*).

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

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

סעיף א(3)

תשובה

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

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

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

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

•אגף שמאל: L₁ − L₂ = {a}, ולכן (L₁ − L₂)·L₃ = {ab, aab}.
•אגף ימין: L₁·L₃ = {ab, aab}, L₂·L₃ = {b, ab}, וההפרש הוא {aab} – המילה ab נמחקה, כי ab = ε·ab ∈ L₂·L₃.

{ab, aab} ≠ {aab} – הטענה לא מתקיימת.

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

סעיף ב

תשובה

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

מה נדרש: אוטומט מחסנית דטרמיניסטי (אמ"ד) שמקבל בדיוק את המילים aⁱbʲaᵏ עם i, k ≥ 1 ו־j = i + k.

רעיון: נחלק את בלוק ה־b לשני חלקים: i ה־b הראשונים "מבטלים" את ה־a של הבלוק הראשון, ו־k ה־b הנותרים צריכים להתאים ל־a של הבלוק השני.

•שלב 1: דוחפים סימן לכל a בבלוק הראשון (הראשון X, השאר A). כל b שולף סימן. ה־b ששולף את X הוא ה־b ה־i־י: עכשיו המחסנית שוב ריקה (רק ⊥).
•שלב 2: כל b נוסף נדחף למחסנית (הראשון Y, השאר B) – אלה ה־k שצריך להתאים. כל a בבלוק השני שולף סימן, וה־a ששולף את Y הוא האחרון ⇒ מצב מקבל f, שאין ממנו מעברים.
•k ≥ 1: אחרי ששולפים את X חייב לבוא עוד b (במצב q₃ יש מעבר רק ב־b). i ≥ 1: המילה חייבת להתחיל ב־a.

פתרון:

•א"ב הקלט: {a, b}. א"ב המחסנית: {⊥, X, A, Y, 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 / εq₂b ראשון: שולף A
q₁bXb, X / εq₃b ראשון כש־i = 1: שולף את X
q₂bAb, A / εq₂b מבטל a מהבלוק הראשון
q₂bXb, X / εq₃ה־b שמבטל את ה־a הראשון
q₃b⊥b, ⊥ / Y⊥q₄ה־b הראשון "העודף": מסמן Y
q₄bYb, Y / BYq₄עוד b עודף
q₄bBb, B / BBq₄עוד b עודף
q₄aBa, B / εq₅a מהבלוק השני שולף B
q₄aYa, Y / εfa יחיד בבלוק השני (k = 1)
q₅aBa, B / εq₅עוד a
q₅aYa, Y / εfה־a האחרון שולף את Y ⇒ f

(q₁ – בבלוק ה־a הראשון; q₂ – בבלוק ה־b, שלב 1; q₃ – שלב 1 הסתיים בדיוק; q₄ – בבלוק ה־b, שלב 2; q₅ – בבלוק ה־a השני.)

מעקב על aabbbbbaaa (i = 2, j = 5, k = 3, ו־5 = 2 + 3 ⇒ בשפה):

נקראמצב אחרימחסנית (הראש מימין)
–q₀⊥
aq₁⊥X
aq₁⊥XA
bq₂⊥X
bq₃⊥
bq₄⊥Y
bq₄⊥YB
bq₄⊥YBB
aq₅⊥YB
aq₅⊥Y
af⊥

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

דוגמאות דחייה: aba (j = 1 = i: אחרי שליפת X, במצב q₃ נקרא a – אין מעבר); aabba (j = 2 = i: אותו דבר); abbaa (k = 2 אבל רק b אחד עודף: ה־a הראשון שולף את Y ⇒ f, ה־a השני – אין מעבר מ־f); abbab (b אחרי f – אין מעבר).

בדיקה: האוטומט מקבל בדיוק את המילים של L. בין השאר: abba, aabbba, abbbaa, aabbbbaa, aaabbbba מתקבלות; ε, a, aba, abb, abbaa, aabba, abbba, bba נדחות.

טעות נפוצה: להמשיך לשלוף בשלב 2 במקום לדחוף. אחרי ש־X נשלף אין במחסנית מה "לזכור" – צריך לספור מחדש את ה־b העודפים בדחיפה, כדי להשוות אותם ל־a של הבלוק השני.

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

שזירה של שתי מילים x ו־y שאורכן זהה היא המילה שמתקבלת כשלוקחים לסירוגין אות מ־x ואות מ־y, משמאל לימין: האות הראשונה של x , האות הראשונה של y , האות השנייה של x , האות השנייה של y , וכן הלאה.

דוגמה: השזירה של x = aab ושל y = bba היא ababba :

012
xaab
ybba

השזירה: x₀ y₀ x₁ y₁ x₂ y₂ = a b a b b a .

כתבו מכונת טיורינג המקבלת בתחילת הסרט שתי מילים שאורכן זהה מעל הא"ב {a, b} , ומחזירה את השזירה שלהן.

הנחיות:

– שתי המילים בקלט מופרדות ב־ # , ואורך כל אחת מהן 1 לפחות.

– אין צורך לשמור על הקלט (אפשר לשנות את הקלט לסימנים שונים).

– המילה המוחזרת תופיע במקום כלשהו בסרט בין שני סימני $ .

– אין צורך לבדוק את תקינות הקלט.

דוגמה:

הסרט לפני ההרצה:

⊢aab#bbaΔΔ...

הסרט לאחר ההרצה:

...$ababba$...

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

(25 נקודות)
◂ פתרון מלא לשאלה 9

פתרון

תשובה

כותבים $ אחרי המילה השנייה. בכל סבב: האות השמאלית שלא הועתקה במילה הראשונה ← X ומועתקת (לפי מצב) לתא הריק הראשון בסוף הסרט; אחר כך האות השמאלית שלא הועתקה במילה השנייה ← Y ומועתקת גם היא לסוף. כשהמילה הראשונה נגמרת – $ בתא הריק הראשון. התוצאה: ⊢X…X#Y…Y$x₀y₀x₁y₁…$.

מה נדרש: מכונת טיורינג שמקבלת ⊢x#y (x, y מעל {a, b}, באותו אורך) ומשאירה על הסרט $x₀y₀x₁y₁…$.

רעיון:

1.הכנה: הולכים ימינה עד התא הריק הראשון וכותבים $. מימינו נבנית השזירה.
2.אות מ־x: חוזרים ל־⊢, מדלגים על X, מחליפים את האות הראשונה שלא הועתקה ב־X וזוכרים אותה במצב (q4 = a, q5 = b). הולכים ימינה עד התא הריק הראשון וכותבים בו את האות.
3.אות מ־y: חוזרים שמאלה עד #, מדלגים על Y, מחליפים את האות הראשונה שלא הועתקה ב־Y וזוכרים אותה (q8 = a, q9 = b). הולכים ימינה עד התא הריק וכותבים אותה.
4.חוזרים לשלב 2. בכל סבב נוספות לסוף הסרט שתי אותיות – x_i ואחריה y_i – ולכן נבנית בדיוק השזירה.
5.סיום: כשבחיפוש במילה הראשונה פוגשים # – כל האותיות הועתקו (וגם של y, כי האורכים שווים). הולכים ימינה עד התא הריק וכותבים $ הסוגר.

למה החיפושים לא מתבלבלים עם האותיות שהועתקו לסוף? בחיפוש ב־x עוצרים באות הראשונה שאינה X – היא לפני ה־#. בחיפוש ב־y מתחילים מה־# – האות הראשונה שאינה Y היא לפני ה־$.

פתרון:

•א"ב הסרט: {⊢, a, b, #, X, Y, $, Δ}. Δ – תא ריק.
•הראש מתחיל על ⊢ במצב q0. מצב עצירה: h. תזוזות: R ימינה, L שמאלה.
•כל שורה היא מעבר בסימון "קורא/כותב, תזוזה"; שורה עם כמה סימנים ב"קורא" ו"אותו סימן" ב"כותב" – מעבר אחד לכל סימן, שאינו משנה אותו.
מצבקוראכותבתזוזהמצב חדשהסבר
q0⊢⊢Rq1התחלה
q1a, b, #אותו סימןRq1ימינה עד סוף הקלט
q1Δ$Lq2$ הפותח אחרי המילה השנייה
q2a, b, X, Y, #, $אותו סימןLq2שמאלה עד ⊢
q2⊢⊢Rq3סבב חדש
q3XXRq3דילוג על אותיות שהועתקו
q3aXRq4אות מהמילה הראשונה: a
q3bXRq5אות מהמילה הראשונה: b
q3##Rq10המילה הראשונה נגמרה – סיום
q4a, b, Y, #, $אותו סימןRq4ימינה עד התא הריק
q4ΔaLq6מעתיקים a
q5a, b, Y, #, $אותו סימןRq5ימינה עד התא הריק
q5ΔbLq6מעתיקים b
q6a, b, Y, $אותו סימןLq6שמאלה עד #
q6##Rq7למילה השנייה
q7YYRq7דילוג על אותיות שהועתקו
q7aYRq8אות מהמילה השנייה: a
q7bYRq9אות מהמילה השנייה: b
q8a, b, $אותו סימןRq8ימינה עד התא הריק
q8ΔaLq2מעתיקים a
q9a, b, $אותו סימןRq9ימינה עד התא הריק
q9ΔbLq2מעתיקים b
q10a, b, Y, $אותו סימןRq10ימינה עד התא הריק
q10Δ$Rh$ הסוגר – עצירה

מעקב – הקלט aab#bba (הסרט בכל פעם שהמכונה מתחילה סבב, כלומר נכנסת ל־q3, ובסוף):

סבבהסרט
0⊢aab#bba$
1⊢Xab#Yba$ab
2⊢XXb#YYa$abab
3⊢XXX#YYY$ababba
4⊢XXX#YYY$ababba$

בין שני ה־$: ababba ✔ (כמו בדוגמה).

בדיקה: לכל הזוגות x, y מעל {a, b} המכונה עוצרת, על הסרט נשארים בדיוק שני סימני $, וביניהם בדיוק השזירה. למשל aab#bba ⇒ $ababba$ (146 צעדים), abba#bbab ⇒ $abbbbaab$, a#b ⇒ $ab$.

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

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

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

שאלה 10 · חברת שליחויות – היררכיית חבילות (25 נקודות)

נוסח השאלה:

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

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

בחברת השליחויות "מהר ובטוח" פותחה מערכת ממוחשבת ובה שומרים נתונים על המשלוחים. המערכת כוללת את המחלקות האלה: חבילה – Parcel , חבילה שבירה – Fragile , חבילה ובה כלי זכוכית – Glass , חבילה ובה מכשיר אלקטרוני – Electronic , מכתב – Letter .

נתון חלק ממבנה המחלקות במערכת הממוחשבת:

Java
public class Parcel {
    private String sender;
    private int weight;
    public Parcel (String sender, int weight)
    {
        this.sender = sender;
        this.weight = weight;
    }
    public String toString() {
        return "From " + this.sender + ", " + weight + " kg.";
    }
}
public class Fragile {
    private String sender;
    private int weight;
    private boolean isInsured;     // האם החבילה מבוטחת?
    public void warn () {
        System.out.println ("Careful! " + sender);
    }
}
public class Glass {
    private String sender;
    private int weight;
    private boolean isInsured;     // האם החבילה מבוטחת?
    private int layers;            // מספר שכבות הריפוד
    public void warn () {
        System.out.println ("Careful! " + sender);
    }
}
public class Electronic {
    private String sender;
    private int weight;
    private boolean isInsured;     // האם החבילה מבוטחת?
    private boolean hasBattery;    // האם יש במכשיר סוללה?
    public void warn () {
        System.out.println ("Careful! " + sender);
    }
}
public class Letter {
    private String sender;
    private int weight;
    private boolean isRegistered;  // האם זה דואר רשום?
}

הניחו שיש פעולות get ו־ set לתכונות המחלקות.

א.

כתבו מחדש את הכותרות, התכונות והפעולות של המחלקות: Fragile , Glass , Electronic ו־ Letter , לפי עקרונות תכנות מונחה עצמים (אין לשנות את המחלקה Parcel). בסעיף זה אין צורך לכתוב פעולות בונות. הניחו שהפעולות get ו־ set קיימות.

נוסף על כך, סרטטו תרשים הייררכייה בין כל המחלקות. יש לסמן ירושה באמצעות החץ ◁——— .

ב.

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

Java
public class Tester {
    public static void main (String[] args) {
        Parcel[] parcels = new Parcel [5];
        parcels [0] = new Glass ("Dana", 3, true, 4);
        parcels [1] = new Letter ("Omer", 1, true);
        parcels [2] = new Fragile ("Noa", 6, false);
        parcels [3] = new Electronic ("Yossi", 2, true, false);
        parcels [4] = new Letter ("Lior", 1, false);
        //***
    }
}

הוסיפו פעולות בונות למחלקות Fragile , Glass , Electronic ו־ Letter , המאתחלות את תכונות המחלקות (כך שהפעולה main תרוץ ללא שגיאות). בכל פעולה בונה סדר הפרמטרים הוא: קודם התכונות של Parcel, ואחריהן שאר התכונות לפי סדר הופעתן במחלקה הנתונה.

הערה: אין לשנות את הפעולה הבונה הנתונה של Parcel .

ג.(1)

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

Java
for (int i = 0; i < parcels.length; i++) {
    System.out.println (parcels[i]); }

קטע הקוד תקין ומדפיס את הפלט שלפניכם:

Glass from Dana! 4 layers
Registered letter. From Omer, 1 kg.
From Noa, 6 kg. Fragile!
From Yossi, 2 kg. Fragile!
Letter. From Lior, 1 kg.

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

הערה: אין לשנות את הפעולה toString במחלקה Parcel . תשובה שבה יהיה שינוי לא תזוכה בנקודות.

(2)

במחלקה Tester נוספה הפעולה שלפניכם:

Java
public static void alert (Parcel [] arr) {
    for (int i = 0; i < arr.length; i++) {
        arr[i].warn(); }
}

אם הפעולה תקינה, כתבו את הפלט שלה עבור המערך parcels (הנתון לעיל), ואם הפעולה לא תקינה – תקנו אותה (כך שהקריאה ל־ warn בתוך הלולאה תהיה תקינה) וכתבו את הפלט עבור המערך parcels .

הערות:

•אין לשנות את המחלקות עצמן. תשובה שבה יהיה שינוי במחלקות לא תזוכה בנקודות.
•הפעולה צריכה לרוץ ללא שגיאות עבור כל מערך מטיפוס Parcel .
◂ פתרון מלא לשאלה 10

סעיף א

תשובה

Fragile יורשת מ־Parcel (isInsured, warn); Glass יורשת מ־Fragile (layers); Electronic יורשת מ־Fragile (hasBattery); Letter יורשת מ־Parcel (isRegistered). התכונות sender ו־weight נשארות רק ב־Parcel, וב־warn ניגשים לשולח דרך getSender().

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

רעיון

•sender ו־weight משותפות לכל המשלוחים, והן כבר נמצאות ב־Parcel. לכן כל המחלקות יורשות (ישירות או בעקיפין) מ־Parcel, ומוחקים מהן את sender ו־weight.
•Fragile, Glass ו־Electronic חולקות גם את isInsured ואת הפעולה warn(). חבילת זכוכית וחבילה עם מכשיר אלקטרוני הן חבילות שבירות, ולכן Glass ו־Electronic יורשות מ־Fragile ומוסיפות רק את התכונה הייחודית להן (layers, hasBattery).
•Letter יורשת ישירות מ־Parcel ומוסיפה רק את isRegistered.
•הפעולה warn() עוברת ל־Fragile. התכונה sender פרטית (private) ב־Parcel, ולכן ב־warn() אי אפשר לכתוב sender – משתמשים ב־getSender() (ב־C#: GetSender()).

פתרון – :

public class Fragile extends Parcel {
    private boolean isInsured;     // האם החבילה מבוטחת?
    public void warn() {
        System.out.println("Careful! " + getSender());
    }
}
public class Glass extends Fragile {
    private int layers;            // מספר שכבות הריפוד
}
public class Electronic extends Fragile {
    private boolean hasBattery;    // האם יש במכשיר סוללה?
}
public class Letter extends Parcel {
    private boolean isRegistered;  // האם זה דואר רשום?
}

תרשים ההייררכייה (החץ עם המשולש הריק מצביע מהמחלקה היורשת אל מחלקת הבסיס):

בדיקה – המחלקות המלאות (אותן כותרות ותכונות, יחד עם הפעולות הבונות מסעיף ב) תקינות ב־Java וב־C#.

טעות נפוצה: להשאיר ב־warn() את sender – אחרי שהתכונה נמחקה מ־Fragile זו שגיאת הידור (התכונה פרטית ב־Parcel). טעות נוספת: להוריש את Glass ואת Electronic ישירות מ־Parcel ולהשאיר בהן את isInsured ואת warn() – זו כפילות קוד.

סעיף ב

תשובה

Fragile(sender, weight, isInsured) → super(sender, weight); Glass(sender, weight, isInsured, layers) → super(sender, weight, isInsured); Electronic(sender, weight, isInsured, hasBattery) → super(sender, weight, isInsured); Letter(sender, weight, isRegistered) → super(sender, weight). ב־C#: base(...) במקום super(...).

מה נדרש – פעולות בונות שמתאימות לקריאות ב־main / Main ומאתחלות את כל התכונות.

רעיון

•sender ו־weight פרטיות ב־Parcel, ולכן מחלקה יורשת לא יכולה להציב בהן ישירות. מעבירים אותן לפעולה הבונה של מחלקת הבסיס: super(sender, weight) ב־Java, : base(sender, weight) ב־C#.
•ל־Parcel אין פעולה בונה ללא פרמטרים, ולכן חובה לקרוא במפורש לפעולה הבונה של מחלקת הבסיס בכל פעולה בונה של מחלקה יורשת.
•Glass ו־Electronic יורשות מ־Fragile, ולכן הן קוראות לפעולה הבונה של Fragile עם (sender, weight, isInsured) ומאתחלות בעצמן רק את התכונה שלהן.
•לפי הקריאות: new Glass ("Dana", 3, true, 4) – שולח, משקל, מבוטחת, שכבות; new Electronic ("Yossi", 2, true, false) – שולח, משקל, מבוטחת, יש סוללה.

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

בדיקה – המחלקות עם הפעולות הבונות ו־Tester הנתונה תקינות ב־Java וב־C#, והפעולה הראשית מתבצעת ללא שגיאות.

טעות נפוצה: לכתוב ב־Glass super(sender, weight) – ל־Fragile אין פעולה בונה עם שני פרמטרים, וגם isInsured לא תאותחל.

הקוד המלא
public class Fragile extends Parcel {
    private boolean isInsured;     // האם החבילה מבוטחת?

    public Fragile(String sender, int weight, boolean isInsured) {
        super(sender, weight);
        this.isInsured = isInsured;
    }

    public void warn() {
        System.out.println("Careful! " + getSender());
    }
}

public class Glass extends Fragile {
    private int layers;            // מספר שכבות הריפוד

    public Glass(String sender, int weight, boolean isInsured, int layers) {
        super(sender, weight, isInsured);
        this.layers = layers;
    }
}

public class Electronic extends Fragile {
    private boolean hasBattery;    // האם יש במכשיר סוללה?

    public Electronic(String sender, int weight, boolean isInsured, boolean hasBattery) {
        super(sender, weight, isInsured);
        this.hasBattery = hasBattery;
    }
}

public class Letter extends Parcel {
    private boolean isRegistered;  // האם זה דואר רשום?

    public Letter(String sender, int weight, boolean isRegistered) {
        super(sender, weight);
        this.isRegistered = isRegistered;
    }
}

סעיף ג(1)

תשובה

דורסים toString ב־Fragile (super.toString() + " Fragile!"), ב־Glass ("Glass from " + getSender() + "! " + layers + " layers") וב־Letter (קידומת לפי isRegistered + super.toString()). ב־Electronic לא צריך – היא יורשת את toString של Fragile.

מה נדרש – לדרוס את toString (ב־C#: ToString) רק במחלקות שבהן הפלט שונה מזה שהמחלקה כבר יורשת.

רעיון – println(parcels[i]) מפעיל את toString של הטיפוס בפועל של העצם (פולימורפיזם). נבדוק כל שורה בפלט:

עצםשורת הפלטמה צריך
GlassGlass from Dana! 4 layers – בלי משקל ובלי Fragile!לדרוס ב־Glass: מחרוזת חדשה; השולח פרטי ב־Parcel, ולכן getSender(); layers היא תכונה של Glass
Letter (רשום)Registered letter. + הפלט של Parcelלדרוס ב־Letter: קידומת לפי isRegistered + super.toString()
Fragileהפלט של Parcel + Fragile!לדרוס ב־Fragile: super.toString() + " Fragile!"
Electronicבדיוק הפלט של Fragileלא צריך toString ב־Electronic – היא יורשת את הגרסה של Fragile
Letter (לא רשום)Letter. + הפלט של Parcelאותה פעולה של Letter, הענף השני של התנאי

ב־C# כל ToString מוגדרת עם override (גם ב־Glass, שדורסת את הגרסה של Fragile).

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

בדיקה – Tester עם הלולאה הנתונה (Java ו־C#) מדפיסה בדיוק:

Glass from Dana! 4 layers
Registered letter. From Omer, 1 kg.
From Noa, 6 kg. Fragile!
From Yossi, 2 kg. Fragile!
Letter. From Lior, 1 kg.

טעות נפוצה: לכתוב ב־Glass super.toString() + ... – זה היה מדפיס גם From Dana, 3 kg. Fragile!. או לשכוח ש־Electronic כבר יורשת את toString של Fragile (ולא של Parcel), ולכן אין צורך לדרוס בה.

הקוד המלא
public class Fragile extends Parcel {
    private boolean isInsured;     // האם החבילה מבוטחת?

    public Fragile(String sender, int weight, boolean isInsured) {
        super(sender, weight);
        this.isInsured = isInsured;
    }

    public void warn() {
        System.out.println("Careful! " + getSender());
    }

    public String toString() {
        return super.toString() + " Fragile!";
    }
}

public class Glass extends Fragile {
    private int layers;            // מספר שכבות הריפוד

    public Glass(String sender, int weight, boolean isInsured, int layers) {
        super(sender, weight, isInsured);
        this.layers = layers;
    }

    public String toString() {
        return "Glass from " + getSender() + "! " + layers + " layers";
    }
}

public class Electronic extends Fragile {
    private boolean hasBattery;    // האם יש במכשיר סוללה?

    public Electronic(String sender, int weight, boolean isInsured, boolean hasBattery) {
        super(sender, weight, isInsured);
        this.hasBattery = hasBattery;
    }
}

public class Letter extends Parcel {
    private boolean isRegistered;  // האם זה דואר רשום?

    public Letter(String sender, int weight, boolean isRegistered) {
        super(sender, weight);
        this.isRegistered = isRegistered;
    }

    public String toString() {
        if (isRegistered)
            return "Registered letter. " + super.toString();
        return "Letter. " + super.toString();
    }
}

סעיף ג(2)

תשובה

לא תקינה (שגיאת הידור: ב־Parcel אין warn). תיקון: if (arr[i] instanceof Fragile) ((Fragile)arr[i]).warn(); (ב־C#: is Fragile / Warn). הפלט עבור parcels: Careful! Dana / Careful! Noa / Careful! Yossi.

מה נדרש – לקבוע אם הפעולה תקינה; אם לא – לתקן ולכתוב את הפלט עבור parcels.

רעיון – הפעולה לא תקינה (שגיאת הידור). הטיפוס המוצהר של arr[i] הוא Parcel, והמהדר בודק קריאה לפי הטיפוס המוצהר; ב־Parcel אין פעולה warn() (ב־C#: Warn()), ולכן הקריאה לא עוברת הידור, גם אם בזמן ריצה העצם הוא חבילה שבירה.
התיקון: לבדוק אם העצם הוא חבילה שבירה (instanceof Fragile ב־Java, is Fragile ב־C#) ורק אז להמיר (casting) ל־Fragile ולקרוא ל־warn(). Glass ו־Electronic יורשות מ־Fragile, ולכן גם הן עוברות את הבדיקה.

•בלי הבדיקה, המרה של Letter ל־Fragile הייתה זורקת חריגה בזמן ריצה (ClassCastException / InvalidCastException).
•תא ריק (null) לא עובר את הבדיקה instanceof / is, ולכן גם אז אין שגיאה.

פתרון – הפעולה המתוקנת בלשונית Java/C#.

בדיקה – עבור parcels: Glass (תא 0), Fragile (תא 2) ו־Electronic (תא 3) מדפיסים, שני המכתבים לא. הפלט (Java ו־C#):

Careful! Dana
Careful! Noa
Careful! Yossi

הפעולה המקורית אכן לא עוברת הידור (Java: cannot find symbol; C#: שגיאה CS1061 – ל־Parcel אין הגדרה ל־Warn). במערך עם Letter, תא null, Electronic ו־Parcel מודפסת שורה אחת (Careful! Tom), בלי שגיאה.

הקוד המלא
public static void alert (Parcel [] arr) {
    for (int i = 0; i < arr.length; i++) {
        if (arr[i] instanceof Fragile) {
            ((Fragile) arr[i]).warn();
        }
    }
}

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

נוסח השאלה:

מרכז הספורט "קפיצה" בנה את המחלקה Time — שעה, שלה שתי תכונות מטיפוס שלם: שעה — hour , דקה — minute .

נוסף על כך, לצורך ניהול הפעילויות במרכז נבנתה המחלקה Activity — פעילות.

במרכז יש שלושה סוגי פעילויות: שיעורים קבוצתיים, השכרת מגרשים וטורנירים. כדי לשמור את המידע שבכל סוג פעילות, נבנו המחלקות האלה: שיעור קבוצתי — Lesson , השכרת מגרש — Rental , טורניר — Tournament .

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

המידע ששומרים על שיעור קבוצתי — Lesson :

•day — היום בשבוע שבו מתקיים השיעור (1–7), מטיפוס שלם
•start — שעת ההתחלה, מטיפוס Time
•coach — שם המאמן, מטיפוס מחרוזת
•maxParticipants — מספר המשתתפים המרבי, מטיפוס שלם

המידע ששומרים על השכרת מגרש — Rental :

•day — היום בשבוע (1–7), מטיפוס שלם
•start — שעת ההתחלה, מטיפוס Time
•renter — שם השוכר, מטיפוס מחרוזת
•court — מספר המגרש המושכר, מטיפוס שלם

המידע ששומרים על טורניר — Tournament :

•day — היום בשבוע (1–7), מטיפוס שלם
•start — שעת ההתחלה, מטיפוס Time
•teams — מספר הקבוצות המשתתפות, מטיפוס שלם
•courts — מספרי המגרשים שבהם מתקיים הטורניר, במערך מטיפוס שלם (גודל המערך הוא לפי מספר המגרשים)

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

נוסף על המחלקות האמורות, נבנתה המחלקה Center — מרכז הספורט, ולה שתי תכונות: arr — מערך בגודל 500 מטיפוס Activity , ו־ count — מספר הפעילויות השמורות במערך. הפעילויות שמורות ברצף מתחילת המערך (בתאים 0 עד count–1), ואין ביניהן ערכי null .

לפניכם תרשים חלקי של המחלקות:

שימו לב: ירושה מסומנת באמצעות החץ ◁——— (המשולש הריק בצד מחלקת־העל), והכלה מסומנת באמצעות הסימן ◆——— (המעוין המלא בצד המחלקה המכילה).

על הסעיפים שלפניכם יש לענות לפי עקרונות של תכנות מונחה עצמים.

א.

העתיקו את התרשים למחברת והוסיפו את המחלקה Time לתרשים. כתבו את כותרת המחלקה פעילות — Activity ואת התכונות שלה.

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

ב.

כתבו במחלקה Center פעולה ששמה lessonsBefore המקבלת מספר יום — day ועצם — t מטיפוס Time , ומחזירה מערך מטיפוס Lesson המכיל את כל השיעורים הקבוצתיים המתקיימים ביום day ומתחילים לפני השעה t . גודל המערך המוחזר צריך להיות שווה למספר השיעורים האלה (אם אין שיעורים כאלה, יוחזר מערך בגודל 0).

שימו לב:

•הניחו שקיימת הפעולה שלפניכם במחלקה Time :
Java
public boolean before (Time other)

הפעולה מחזירה true אם השעה הנוכחית מוקדמת מהשעה other , אחרת (גם אם השעות שוות) היא מחזירה false . אפשר להשתמש בפעולה בלי לממש אותה.

ג.

לפניכם כותרת הפעולה הפנימית — usesCourt במחלקה Activity :

Java
public boolean usesCourt (int court)

זימון הפעולה יחזיר true אם הפעילות (Activity) היא השכרה (Rental) של המגרש שמספרו court , או אם הפעילות היא טורניר (Tournament) שאחד המגרשים שלו הוא המגרש שמספרו court , אחרת יוחזר false (שיעורים קבוצתיים מתקיימים באולם ולא במגרש).

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

הערה: בסעיף זה אין להשתמש בפעולה instanceof ובפעולות של המחלקה Object .

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

סעיף א

תשובה

Activity מכילה את Time (◆ בצד Activity). public class Activity { private int day; private Time start; } – Lesson, Rental, Tournament יורשות ממנה.

מה נדרש – להוסיף את Time לתרשים ולכתוב את כותרת המחלקה Activity עם התכונות שלה.

רעיון

•day ו־start מופיעות בכל שלושת סוגי הפעילויות, ולכן הן נשמרות פעם אחת במחלקת־העל Activity.
•התכונות הייחודיות (coach , maxParticipants , renter , court , teams , courts) נשארות במחלקות היורשות.
•start היא עצם מטיפוס Time , ולכן Activity מכילה Time (המעוין המלא בצד Activity).

פתרון

תרשים המחלקות המלא:

כותרת המחלקה ותכונותיה – :

public class Activity {
    private int day;        // היום בשבוע (1–7)
    private Time start;     // שעת ההתחלה
}

בדיקה – המחלקה (יחד עם Time) תקינה ב־Java וב־C#.

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

הקוד המלא
public class Activity {
    private int day;        // היום בשבוע (1–7)
    private Time start;     // שעת ההתחלה
}

סעיף ב

תשובה

שני מעברים על arr[0..count-1]: סופרים את העצמים שהם Lesson (instanceof / is) עם getDay()==day ו־getStart().before(t); יוצרים מערך Lesson בגודל המונה; במעבר שני מעתיקים אליו (עם המרה ל־Lesson) ומחזירים.

מה נדרש – להחזיר מערך Lesson שגודלו בדיוק מספר השיעורים ביום day שמתחילים לפני t.

רעיון

•גודל המערך אינו ידוע מראש, ולכן עוברים על המערך פעמיים: במעבר הראשון סופרים את השיעורים המתאימים, יוצרים מערך בגודל הזה, ובמעבר השני מעתיקים אליו את השיעורים.
•עוברים רק על התאים 0 עד count–1 (שאר התאים ריקים).
•arr[i] instanceof Lesson (ב־C#: arr[i] is Lesson) מזהה שיעור; את getDay ואת getStart אפשר לזמן ישירות על הפניה מטיפוס Activity , כי הן מוגדרות ב־Activity. רק בהכנסה למערך התוצאה צריך המרה (Lesson).
•"מתחיל לפני t" נבדק בעזרת הפעולה הנתונה before של Time.

פתרון – :

public class Center {
    private Activity[] arr;
    private int count;

    public Center() {
        this.arr = new Activity[500];
        this.count = 0;
    }
    public void add(Activity a) {
        this.arr[this.count] = a;
        this.count++;
    }

    public Lesson[] lessonsBefore(int day, Time t) {
        int num = 0;
        for (int i = 0; i < this.count; i++) {
            if (this.arr[i] instanceof Lesson && this.arr[i].getDay() == day
                    && this.arr[i].getStart().before(t)) {
                num++;
            }
        }
        Lesson[] result = new Lesson[num];
        int k = 0;
        for (int i = 0; i < this.count; i++) {
            if (this.arr[i] instanceof Lesson && this.arr[i].getDay() == day
                    && this.arr[i].getStart().before(t)) {
                result[k] = (Lesson) this.arr[i];
                k++;
            }
        }
        return result;
    }
}

(הפעולה הבונה ו־add נכתבו לצורך הבדיקה בלבד.)

בדיקה – מרכז עם שיעורים ביום 2 בשעות 8:30, 10:00, 9:59, 10:15, שיעור ביום 3, השכרה וטורניר ביום 2: lessonsBefore(2, 10:00) החזירה 2 שיעורים (המאמנים Gal ו־Dana; השיעור של 10:00 אינו "לפני" 10:00). ביום שאין בו שיעורים הוחזר מערך בגודל 0. גם במרכז מלא (500 פעילויות) התקבל המספר הנכון (Java ו־C#).

טעות נפוצה: לרוץ עד arr.length במקום עד count – התאים שאחרי count הם null; או ליצור מערך בגודל count ולהשאיר בו תאים ריקים.

הקוד המלא
public class Center {
    private Activity[] arr;
    private int count;

    public Center() {
        this.arr = new Activity[500];
        this.count = 0;
    }
    public void add(Activity a) {
        this.arr[this.count] = a;
        this.count++;
    }

    public Lesson[] lessonsBefore(int day, Time t) {
        int num = 0;
        for (int i = 0; i < this.count; i++) {
            if (this.arr[i] instanceof Lesson && this.arr[i].getDay() == day
                    && this.arr[i].getStart().before(t)) {
                num++;
            }
        }
        Lesson[] result = new Lesson[num];
        int k = 0;
        for (int i = 0; i < this.count; i++) {
            if (this.arr[i] instanceof Lesson && this.arr[i].getDay() == day
                    && this.arr[i].getStart().before(t)) {
                result[k] = (Lesson) this.arr[i];
                k++;
            }
        }
        return result;
    }
}

סעיף ג

תשובה

Activity: usesCourt מחזירה false. Rental דורסת: return this.court == court. Tournament דורסת: true אם court נמצא במערך courts, אחרת false. ב־Lesson אין צורך בפעולה.

מה נדרש – פעולה usesCourt שמחזירה תשובה לפי סוג הפעילות, בלי instanceof (ב־C#: בלי is/as).

רעיון – דריסה:

•ב־Activity: return false – ברירת המחדל, מתאימה לשיעור קבוצתי (לכן אין צורך לכתוב את הפעולה ב־Lesson).
•Rental דורסת: return this.court == court.
•Tournament דורסת: חיפוש court במערך courts.
•ב־C#: virtual ב־Activity ו־override ב־Rental וב־Tournament.

פתרון – :

public class Activity {
    private int day;        // היום בשבוע (1–7)
    private Time start;     // שעת ההתחלה

    public Activity(int day, Time start) {
        this.day = day;
        this.start = start;
    }
    public int getDay() { return day; }
    public Time getStart() { return start; }

    public boolean usesCourt(int court) {
        return false;
    }
}

public class Rental extends Activity {
    private String renter;
    private int court;

    public Rental(int day, Time start, String renter, int court) {
        super(day, start);
        this.renter = renter;
        this.court = court;
    }
    public String getRenter() { return renter; }
    public int getCourt() { return court; }

    public boolean usesCourt(int court) {
        return this.court == court;
    }
}

public class Tournament extends Activity {
    private int teams;
    private int[] courts;

    public Tournament(int day, Time start, int teams, int[] courts) {
        super(day, start);
        this.teams = teams;
        this.courts = courts;
    }
    public int getTeams() { return teams; }
    public int[] getCourts() { return courts; }

    public boolean usesCourt(int court) {
        for (int i = 0; i < this.courts.length; i++) {
            if (this.courts[i] == court) {
                return true;
            }
        }
        return false;
    }
}

הפעולות שנוספו: usesCourt במחלקה Activity (מחזירה false), דריסה במחלקה Rental ודריסה במחלקה Tournament.

בדיקה – שיעור, מגרש 3 → false; השכרה של מגרש 3, מגרש 3 → true; השכרה של מגרש 5, מגרש 3 → false; טורניר במגרשים {1, 2, 4}: מגרש 4 → true, מגרש 3 → false (Java ו־C#).

טעות נפוצה: בטורניר לכתוב if (courts[i] == court) return true; else return false; בתוך הלולאה – כך נבדק רק המגרש הראשון.

הקוד המלא
public class Activity {
    private int day;        // היום בשבוע (1–7)
    private Time start;     // שעת ההתחלה

    public Activity(int day, Time start) {
        this.day = day;
        this.start = start;
    }
    public int getDay() { return day; }
    public Time getStart() { return start; }

    public boolean usesCourt(int court) {
        return false;
    }
}

public class Rental extends Activity {
    private String renter;
    private int court;

    public Rental(int day, Time start, String renter, int court) {
        super(day, start);
        this.renter = renter;
        this.court = court;
    }
    public String getRenter() { return renter; }
    public int getCourt() { return court; }

    public boolean usesCourt(int court) {
        return this.court == court;
    }
}

public class Tournament extends Activity {
    private int teams;
    private int[] courts;

    public Tournament(int day, Time start, int teams, int[] courts) {
        super(day, start);
        this.teams = teams;
        this.courts = courts;
    }
    public int getTeams() { return teams; }
    public int[] getCourts() { return courts; }

    public boolean usesCourt(int court) {
        for (int i = 0; i < this.courts.length; i++) {
            if (this.courts[i] == court) {
                return true;
            }
        }
        return false;
    }
}

שאלה 12 · פולימורפיזם, העמסה ודריסה – מעקב והדפסה (P, Q, R) (25 נקודות)

נוסח השאלה:

לפניכם המחלקות P, Q, R :

Java
public class P {
    public int n;

    public P() {
        this(5);
    }
    public P(int n) {
        this.n = n;
    }
    public int f(int k) {
        return k - this.n;
    }
    public int f(P other) {
        return this.n + other.n;
    }
}
Java
public class Q extends P {
    public Q() {
        this.n = this.n * 2;
    }
    public Q(int k) {
        super(k + 1);
    }
    public int f(P other) {
        return this.n * other.n;
    }
    public int f(Q other) {
        return other.n - this.n;
    }
}
Java
public class R extends Q {
    public R(int k) {
        super(k);
        this.n = this.n * 3 + f(2);
    }
    public int f(Q other) {
        return this.n - 2 * other.n;
    }
}
א.

לפניכם קטע קוד שרץ ללא שגיאות:

Java
P p = new P();
P pq = new Q();
P pr = new R(4);
Q q = new Q(2);
Q qr = new R(0);
R r = new R(1);

הציגו את העצמים שנוצרו (סוג העצם, סוג ההפניה, וערך התכונה n).

ב.

לפניכם המשך קטע הקוד (ממוספר) שרץ ללא שגיאות.

ציינו את מספר שורת ההדפסה, וכתבו מה הקוד מדפיס.

Java
1.   System.out.println(p.f(q));
2.   System.out.println(pq.f(p));
3.   System.out.println(pq.f(qr));
4.   System.out.println(q.f(qr));
5.   System.out.println(qr.f(q));
6.   System.out.println(qr.f(pr));
7.   System.out.println(r.f(r));
8.   System.out.println(((Q)pr).f((Q)pq));
9.   System.out.println(pr.f(pr.f(20)));
10.  System.out.println(r.f(p));
◂ פתרון מלא לשאלה 12

סעיף א

תשובה

p: P/P n=5 · pq: הפניה P, עצם Q, n=10 · pr: הפניה P, עצם R, n=12 · q: Q/Q n=3 · qr: הפניה Q, עצם R, n=4 · r: R/R n=6

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

רעיון – עוקבים אחרי שרשרת הפעולות הבונות:

•P() מזמנת את P(5) (this(5) / : this(5)) ⇐ n = 5.
•Q() – אין קריאה מפורשת, ולכן קודם P() ⇐ n = 5, ואחר כך n = 5 · 2 = 10.
•Q(k) ⇐ P(k + 1) ⇐ n = k + 1.
•R(k) ⇐ Q(k) ⇐ n = k + 1. אחר כך n = n * 3 + f(2). הארגומנט 2 מטיפוס int, ולכן נבחרת f(int k) של P, שמחזירה 2 − n. שימו לב: הביטוי שמימין מחושב כולו לפני ההשמה, ולכן f(2) משתמשת בערך הישן של n.
יצירהחישוב
new R(4)n = 5 ⇐ 5·3 + (2 − 5) = 12
new Q(2)n = 2 + 1 = 3
new R(0)n = 1 ⇐ 1·3 + (2 − 1) = 4
new R(1)n = 2 ⇐ 2·3 + (2 − 2) = 6

פתרון

הפניהסוג ההפניהסוג העצםn
pPP5
pqPQ10
prPR12
qQQ3
qrQR4
rRR6

בדיקה – מדפיסים לכל עצם את המחלקה שלו ואת n. התקבלו בדיוק הערכים שבטבלה.

טעות נפוצה: לחשב את f(2) בבנייה של R עם הערך החדש של n (למשל ב־new R(4): n = 15 ואז 15 + (2 − 15) = 2). f(2) מחושבת לפני שההשמה מתבצעת, כש־n עדיין 5.

סעיף ב

תשובה

1: 8 · 2: 50 · 3: 40 · 4: 1 · 5: -2 · 6: 48 · 7: -6 · 8: -8 · 9: -4 · 10: 30

מה נדרש – מה מודפס בכל אחת מעשר השורות.

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

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

מצב העצמים (מסעיף א): p=P(5), pq=Q(10), pr=R(12), q=Q(3), qr=R(4), r=R(6).

פתרון

שורההפניה . פעולה (טיפוס הארגומנט)חתימה שנבחרהמתבצעת בפועלחישובפלט
1p:P . f(Q)f(P) (Q הוא P)P.f(P)5 + 38
2pq:P . f(P)f(P)Q.f(P) (דריסה)10 · 550
3pq:P . f(Q)f(P) (ל־P אין f(Q))Q.f(P) (דריסה)10 · 440
4q:Q . f(Q)f(Q)Q.f(Q)4 − 31
5qr:Q . f(Q)f(Q)R.f(Q) (דריסה)4 − 2·3-2
6qr:Q . f(P)f(P)Q.f(P) (R לא דורסת אותה)4 · 1248
7r:R . f(R)f(Q) (R הוא Q – המתאימה ביותר)R.f(Q)6 − 2·6-6
8(Q)pr:Q . f(Q)f(Q)R.f(Q) (דריסה)12 − 2·10-8
9pr.f(pr.f(20))f(int) פעמייםP.f(int)20−12=8, 8−12-4
10r:R . f(P)f(P)Q.f(P) (בירושה)6 · 530

הפלט:

8
50
40
1
-2
48
-6
-8
-4
30

בדיקה – הפלט ב־Java וב־C# זהה.

הבדל בין הנוסחים: ב־C#, F(P) מוגדרת virtual ב־P ו־override ב־Q, ו־F(Q) מוגדרת virtual ב־Q ו־override ב־R. כך שתי הפעולות נדרסות כמו ב־Java, והתוצאות זהות.

טעות נפוצה: בשורה 3 לבחור את f(Q), כי הארגומנט qr מטיפוס Q והעצם pq הוא Q. טיפוס ההפניה pq הוא P, ול־P אין f(Q). לכן נבחרת f(P), ובזמן ריצה מתבצעת הגרסה של Q (מכפלה).

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

מעבירים את כל איברי התור לתור עזר תוך ספירת המקום; במקומות 1 עד m−1 צוברים את הסכום sum, ובמקום m בודקים x == sum. בסוף מחזירים את האיברים לתור. ל־m = 1 הסכום הוא 0 והערכים חיוביים, ולכן התשובה false.

ב

סופרים את גודל התור size (ומחזירים את האיברים לתור), ואז עוברים על m = 1, …, size וסופרים כמה פעמים isDecisive(q, m) מחזירה true. כשהמונה מגיע ל־k מחזירים true; אם הלולאה הסתיימה – false.

2א

ספירה במעבר על התאים 0 עד amount−1 של times: אם times[i] < limit מגדילים מונה ב־1. בסוף מחזירים את המונה.

ב

שני מעברים על המערך. (1) מוצאים את max – המספר הגדול ביותר של ריצות מהירות, בעזרת countBelow(limit). אם max == 0 מחזירים null. (2) עוברים על המערך מהסוף להתחלה, ולכל רץ שעבורו countBelow(limit) == max מוסיפים חוליה עם שמו לראש השרשרת. מחזירים את השרשרת.

3א(1)

1

א(2)

−2

א(3)

3 – למשל: לשורש בן שמאלי עלה ובן ימני R; ל־R בן ימני עלה ובן שמאלי X, ול־X רק בן ימני (עלה).

א(4)

−3 – למשל: לשורש בן ימני עלה ובן שמאלי A; ל־A בן ימני עלה ובן שמאלי Y, ול־Y רק בן ימני (עלה).

א(5)

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

ב(1)

true (zig של הצומת 0 היא −1)

ב(2)

עץ שבו zig ≥ 0 בכל תת־עץ, למשל עץ מלא בן 7 צמתים: 4 ← (2, 6), 2 ← (1, 3), 6 ← (5, 7); zag מחזירה false.

4א(1)

נכונה. גרף ללא מעגלים הוא יער: אם יש בו k רכיבי קשירות, יש בו n−k ≤ n−1 קשתות. לכן גרף עם n קשתות חייב להכיל מעגל.

א(2)

לא נכונה. דוגמה נגדית: a–b (1), b–c (2), a–c (3), c–d (4). העפ"מ היחיד הוא a–b , b–c , c–d (משקל 7), והקשת השלישית הקלה a–c אינה בו.

א(3)

נכונה. אם נמחקה הקשת u–v, כל קודקוד x עדיין מחובר ל־u או ל־v: אם המסלול הפשוט מ־x ל־u בגרף המקורי לא עובר בקשת – הוא נשאר; ואם הוא עובר בה – היא הקשת האחרונה בו, והחלק שלפניה מחבר את x ל־v. לכן כל קודקוד ברכיב של u או ברכיב של v – לכל היותר 2 רכיבים.

א(4)

נכונה. אם u ישיג מ־s אז גם v ישיג, ו־d(v) ≤ d(u)+1 (מסלול קצר ל־u ועוד הקשת). הגרף לא מכוון, ולכן גם d(u) ≤ d(v)+1. מכאן |d(u) − d(v)| ≤ 1.

א(5)

לא נכונה. דוגמה נגדית: s→a , a→b , b→c , c→s. כל הקודקודים ישיגים מ־s, d(c) = 3 ו־d(s) = 0, ובקשת c→s ההפרש הוא 3.

א(6)

לא נכונה. דוגמה נגדית: s–a , a–b , b–s , a–c. הסריקה s→b→a→c נותנת שרשרת, אבל בסריקה s→a→b (חזרה ל־a) →c לקודקוד a יש שני בנים.

ב

(i) ייתכן – אם ב־G יש קשת s–g (ואין קשת s–h): f→e→s→g, חזרה ל־e→h. (ii) לא ייתכן – הקשת e–h קיימת ב־G, ובעץ (ii) e ו־h הם אחים (בנים של s); ב־DFS בגרף לא מכוון אין קשת בין שני ענפים שונים. (iii) לא ייתכן – העץ משתמש בקשת h–g, ואין קשת כזו ב־G.

5א

נכונה. DFS ו־BFS מ־s מבקרים שניהם בדיוק בקודקודים שיש אליהם מסלול מ־s. אם DFS ביקר בכולם – יש מסלול מ־s לכל קודקוד, ולכן גם BFS יבקר בכולם.

ב

לא נכונה. דוגמה נגדית: שני משולשים נפרדים a–b–c ו־d–e–f: יש 6 קודקודים ו־6 קשתות, אבל אין מסלול מ־a ל־d.

ג

לא נכונה. דוגמה נגדית: s→a (1), a→b (1), b→t (1), s→t (5). המסלול הקל ביותר s→a→b→t שוקל 3 ויש בו 3 קשתות, ואילו במסלול s→t יש קשת אחת בלבד (משקלו 5).

ד

נכונה. נניח שעץ פורש מינימלי T אינו מכיל את הקשת הקלה ביותר e. הוספת e ל־T יוצרת מעגל; על המעגל יש קשת אחרת f, ו־w(f) > w(e) (e היא הקלה היחידה). T + e − f הוא עץ פורש קל יותר מ־T – סתירה.

ה

לא נכונה. דוגמה נגדית: s→a (2), s→b (3), b→a (−2). דייקסטרה קובע את a ראשון עם d[a] = 2, אבל המסלול s→b→a שוקל 1. (בגרף כזה צריך את האלגוריתם של בלמן־פורד.)

ו

נכונה. אותו מיון עצמו עדיין תקין: לכל קשת ישנה u → v, u לפני v במיון, וגם בקשת החדשה x → y, x (הראשון) לפני y (האחרון). גרף שיש לו מיון טופולוגי – חסר מעגלים.

6א(1)

הדרגה של כל קודקוד בגרף (חיצוני ופנימי) היא 3 .

א(2)

n קשתות חיצוניות, n קשתות פנימיות ו־ n חישורים (סך הכול 3n קשתות).

א(3)

הגרף דו־צדדי אם ורק אם n זוגי.

ב

n³ עפ"מים (כל אחד במשקל 2n): משמיטים קשת חיצונית אחת, קשת פנימית אחת, ובוחרים חישור אחד.

ג(1)

למשל a = 5 , b = 1 , s = 1 (כל בחירה שבה 2s + b < a מתאימה).

ג(2)

צריך 2s + 2 < 10 , כלומר s < 4 . במשקלים שלמים – s = 3 (ב־ s = 4 יש גם מסלול קל שנשאר על המעגל החיצוני).

7א(1)

bccaaaa

א(2)

דוחפים סימן לכל b (התחתון X) ולכל זוג c; כל a שולף סימן; ה־a ששולף את X מעביר לספירת שני a נוספים (q5 → q6 → q7), ו־q7 מקבל. הטבלה המלאה בפתרון.

ב

(1) שייכת: bccaaaa · bccaaaa. (2) לא שייכת. (3) שייכת: bbccaaaaa · bccccaaaaa.

8א(1)

לא נכונה. דוגמה: L₁ = {a}, L₂ = {b}: ab ∈ (L₁ ∪ L₂)* = {a,b}*, אבל ab ∉ L₁* ∪ L₂* (רק מילים של a בלבד או של b בלבד).

א(2)

נכונה – ההיפוך של שרשור מילים מ־L₁ הוא שרשור (בסדר הפוך) של ההיפוכים שלהן, שכולן ב־R(L₁).

א(3)

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

ב

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

9-

כותבים $ אחרי המילה השנייה. בכל סבב: האות השמאלית שלא הועתקה במילה הראשונה ← X ומועתקת (לפי מצב) לתא הריק הראשון בסוף הסרט; אחר כך האות השמאלית שלא הועתקה במילה השנייה ← Y ומועתקת גם היא לסוף. כשהמילה הראשונה נגמרת – $ בתא הריק הראשון. התוצאה: ⊢X…X#Y…Y$x₀y₀x₁y₁…$.

10א

Fragile יורשת מ־Parcel (isInsured, warn); Glass יורשת מ־Fragile (layers); Electronic יורשת מ־Fragile (hasBattery); Letter יורשת מ־Parcel (isRegistered). התכונות sender ו־weight נשארות רק ב־Parcel, וב־warn ניגשים לשולח דרך getSender().

ב

Fragile(sender, weight, isInsured) → super(sender, weight); Glass(sender, weight, isInsured, layers) → super(sender, weight, isInsured); Electronic(sender, weight, isInsured, hasBattery) → super(sender, weight, isInsured); Letter(sender, weight, isRegistered) → super(sender, weight). ב־C#: base(...) במקום super(...).

ג(1)

דורסים toString ב־Fragile (super.toString() + " Fragile!"), ב־Glass ("Glass from " + getSender() + "! " + layers + " layers") וב־Letter (קידומת לפי isRegistered + super.toString()). ב־Electronic לא צריך – היא יורשת את toString של Fragile.

ג(2)

לא תקינה (שגיאת הידור: ב־Parcel אין warn). תיקון: if (arr[i] instanceof Fragile) ((Fragile)arr[i]).warn(); (ב־C#: is Fragile / Warn). הפלט עבור parcels: Careful! Dana / Careful! Noa / Careful! Yossi.

11א

Activity מכילה את Time (◆ בצד Activity). public class Activity { private int day; private Time start; } – Lesson, Rental, Tournament יורשות ממנה.

ב

שני מעברים על arr[0..count-1]: סופרים את העצמים שהם Lesson (instanceof / is) עם getDay()==day ו־getStart().before(t); יוצרים מערך Lesson בגודל המונה; במעבר שני מעתיקים אליו (עם המרה ל־Lesson) ומחזירים.

ג

Activity: usesCourt מחזירה false. Rental דורסת: return this.court == court. Tournament דורסת: true אם court נמצא במערך courts, אחרת false. ב־Lesson אין צורך בפעולה.

12א

p: P/P n=5 · pq: הפניה P, עצם Q, n=10 · pr: הפניה P, עצם R, n=12 · q: Q/Q n=3 · qr: הפניה Q, עצם R, n=4 · r: R/R n=6

ב

1: 8 · 2: 50 · 3: 40 · 4: 1 · 5: -2 · 6: 48 · 7: -6 · 8: -8 · 9: -4 · 10: 30

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

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

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