מתכונת 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.
המקרה m = 1 מטופל מעצמו: אין איברים לפניו, ולכן sum = 0. כל הערכים בתור חיוביים, ולכן x == 0 אינו מתקיים והתשובה false, כמו שנדרש בהערה.
בסוף מעבירים את כל האיברים מ־temp בחזרה ל־q, וכך התור חוזר למצבו המקורי ובאותו סדר.
פתרון: הקוד המלא – בלשונית Java/C#. בפעולה משתמשים רק בפעולות התור (insert, remove, isEmpty) ובמשתנים פשוטים, בלי מערך ובלי רשימה.
יעילות: כל איבר יוצא מהתור ונכנס אליו פעמיים, ולכן הפעולה היא O(n), כאשר n הוא מספר האיברים בתור.
בדיקה (התור 3, 3, 6, 5, 17, 2, 36):
| m | sum (לפני המקום m) | הערך במקום m | תוצאה |
|---|---|---|---|
| 1 | 0 | 3 | false |
| 2 | 3 | 3 | true |
| 3 | 6 | 6 | true |
| 4 | 12 | 5 | false |
| 5 | 17 | 17 | true |
| 6 | 34 | 2 | false |
| 7 | 36 | 36 | true |
מתקבלים בדיוק הערכים שבטבלה, ואחרי כל קריאה התור נשאר 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.
רעיון:
פתרון: הקוד המלא – בלשונית Java/C#.
יעילות: יש עד n קריאות ל־isDecisive, וכל קריאה עולה O(n). לכן הפעולה היא O(n²), כאשר n הוא מספר האיברים בתור. (אפשר גם לספור במעבר אחד על התור, עם סכום מצטבר, ב־O(n), אבל השאלה מאפשרת להשתמש בסעיף א.)
בדיקה (התור 3, 3, 6, 5, 17, 2, 36): המקומות המכריעים הם 2, 3, 5, 7, כלומר 4 סיבובים.
מתקבל 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 – רץ במועדון ריצה, ולה שלוש תכונות:
הניחו שלתכונות המחלקה יש פעולות 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 |
|---|---|
| Dana | 52, 49, 47, 55, 48 |
| Omer | 50, 46, 51 |
| Yael | 45, 58, 49, 47, 60, 53 |
| Noam | 61, 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 מוסיפים חוליה עם שמו לראש השרשרת. מחזירים את השרשרת.
מה נדרש: לבנות שרשרת חוליות של שמות כל הרצים שיש להם הכי הרבה ריצות מהירות, לפי סדר המערך.
רעיון: אי אפשר לדעת מי "הכי טוב" לפני שרואים את כל הרצים, ולכן עובדים בשני מעברים:
פתרון: הקוד המלא – בלשונית Java/C#. זו פעולה חיצונית (static) שמשתמשת רק בפעולות הממשק countBelow ו־getName.
יעילות: אם במועדון k רצים, כל מעבר קורא k פעמים ל־countBelow שעולה לכל היותר 30 צעדים. סה"כ O(k).
בדיקה (הרצים שבדוגמה):
| limit | ריצות מהירות (Dana, Omer, Yael, Noam) | max | השרשרת |
|---|---|---|---|
| 50 | 3, 1, 3, 0 | 3 | Dana → Yael |
| 47 | 0, 1, 1, 0 | 1 | Omer → Yael |
| 56 | 5, 3, 4, 0 | 5 | Dana |
| 45 | 0, 0, 0, 0 | 0 | null |
טעות נפוצה: לעבור על המערך מההתחלה ולהוסיף כל שם לראש השרשרת – כך השמות יוצאים בסדר הפוך. אפשר לעבור מההתחלה רק אם מוסיפים לסוף השרשרת בעזרת הפניה לחוליה האחרונה.
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
נתונה הפעולה zig שלפניכם:
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 והעץ שלפניכם, וכתבו מה הפעולה מחזירה. יש להציג את המעקב.

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

בעבור עץ שיש לו 6 צמתים, מהו המספר הכי גבוה שהפעולה zig יכולה להחזיר? הציגו את העץ.
בעבור עץ שיש לו 6 צמתים, מהו המספר הכי נמוך שהפעולה zig יכולה להחזיר? הציגו את העץ.
כתבו בקצרה מה הפעולה zig מחזירה בעבור עץ כלשהו שאינו ריק.
נתונה הפעולה zag שלפניכם:
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 .

הציגו עץ שיש לו מעל 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) | 1 | zag(null) ‖ zag(null) | false |
| zag(1) | false ‖ zag(null) | false | |
| zag(4) | השמאלי false ← zag(8) | – | |
| zag(8) | 1 | ← zag(5) | – |
| zag(5) | 0 | zag(9) ‖ zag(7) – שני עלים עם zig = 1, ובניהם null | false |
| zag(8) | השמאלי false ← zag(0) | – | |
| zag(0) | −1 | שלילי! | true |
| zag(8) | false ‖ true | true | |
| zag(4) | false ‖ true | true |
הפעולה מחזירה 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 7zig של העלים 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–6. בחרו בארבע מהן, כתבו את מספר הטענה, וציינו בנוגע לכל טענה שבחרתם אם היא נכונה או לא נכונה. אם הטענה נכונה – נמקו מדוע, ואם היא אינה נכונה – הביאו דוגמה נגדית מגרף שיש בו 4 קודקודים לפחות.
(1) נתון גרף לא מכוון ובו n קודקודים ו־n קשתות. בהכרח יש בגרף מעגל.
נתון גרף ממושקל, קשיר ולא מכוון, שבו כל משקלי הקשתות שונים זה מזה. הקשת השלישית הקלה ביותר בגרף (כלומר: יש בגרף בדיוק שתי קשתות קלות ממנה) נמצאת בהכרח בעץ הפורש המינימלי של הגרף.
נתון גרף לא מכוון וקשיר ובו n קודקודים. אם מוחקים מהגרף קשת אחת (ומשאירים את כל הקודקודים), יהיו בגרף לכל היותר 2 רכיבי קשירות.
נתון גרף לא מכוון. הרצנו BFS מקודקוד s וקיבלנו לכל קודקוד v את d(v) – המרחק (מספר הקשתות במסלול הקצר ביותר) מ־s ל־v. לכל קשת u–v בגרף שאחד מקצותיה ישיג מ־s , מתקיים |d(u) − d(v)| ≤ 1 .
נתון גרף מכוון. הרצנו BFS מקודקוד s וקיבלנו לכל קודקוד v את d(v) – המרחק מ־s ל־v. לכל קשת u→v בגרף ששני קצותיה ישיגים מ־s , מתקיים |d(u) − d(v)| ≤ 1 .
נתון גרף לא מכוון. אם קיימת סריקת 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–b | b–c | a–c | c–d |
|---|---|---|---|---|
| משקל | 1 | 2 | 3 | 4 |
הגרף קשיר וכל המשקלים שונים. קרוסקל: מוסיף 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 – יש לכל היותר 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 ו־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).
("מספר קשתות מינימלי" הוא מה ש־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 – סתירה לכך ש־ 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). אין מעגלים בכלל, ולכן אין מעגל שלילי.
אבל המסלול 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 .
לכן אותו סידור הוא מיון טופולוגי גם של הגרף החדש (ובפרט הגרף החדש חסר מעגלים).
(לעומת זאת, הוספת הקשת ההפוכה 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').
נתון גרף "מנסרה" – Pₙ .
(1) מה הן הדרגות של קודקודי המעגל החיצוני, ומה הן הדרגות של קודקודי המעגל הפנימי? נמקו את תשובתכם.
כמה "קשתות חיצוניות", כמה "קשתות פנימיות" וכמה "חישורים" יש בגרף? נמקו את תשובתכם.
בעבור אילו ערכים של n הגרף הוא דו־צדדי? נמקו את תשובתכם.
נתון גרף ממושקל שהוא "מנסרה" – Pₙ , שבו לכל "הקשתות החיצוניות" ולכל "הקשתות הפנימיות" משקל 1 , ולכל "החישורים" משקל 2 .
כמה עפ"מים (עצים פורשים מינימליים) שונים יש בגרף? נמקו את תשובתכם.
נתון גרף ממושקל שהוא "מנסרה" – Pₙ , שבו לכל "הקשתות החיצוניות" משקל a , לכל "הקשתות הפנימיות" משקל b , ולכל "החישורים" משקל s (כל המשקלים גדולים מ־ 0).
(1) הציבו ערכים ל־ a , b ו־ s , שבעבורם כל מסלול קצר (קל) בין שני קודקודים של המעגל החיצוני יעבור דרך קודקוד אחד לפחות של המעגל הפנימי. נמקו את תשובתכם.
נתון כי 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 = 4, 6, 8 ונכשלת ל־ n = 3, 5, 7 .
סעיף ב
n³ עפ"מים (כל אחד במשקל 2n): משמיטים קשת חיצונית אחת, קשת פנימית אחת, ובוחרים חישור אחד.
מה נדרש: מספר העצים הפורשים המינימליים.
רעיון: לכל עץ פורש על 2n קודקודים יש בדיוק 2n − 1 קשתות. נמצא חסם תחתון למשקל, ונספור את העצים שמשיגים אותו.
פתרון:
למשל ב־ 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}
מהי המילה הקצרה ביותר בשפה L₁ ?
בנו אוטומט מחסנית דטרמיניסטי המקבל את השפה 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}.
רעיון:
פתרון:
| מצב | קלט | ראש המחסנית | מצב חדש | מחליפים את הראש ב־ | משמעות |
|---|---|---|---|---|---|
| q0 | b | ⊥ | q1 | X⊥ | ה־b הראשון: X |
| q1 | b | X | q1 | AX | עוד b: דוחפים A |
| q1 | b | A | q1 | AA | עוד b: דוחפים A |
| q1 | c | X | q2 | X | c ראשון בזוג: בלי שינוי |
| q1 | c | A | q2 | A | c ראשון בזוג: בלי שינוי |
| q2 | c | X | q3 | AX | c שני בזוג: דוחפים A |
| q2 | c | A | q3 | AA | c שני בזוג: דוחפים A |
| q3 | c | A | q2 | A | c ראשון בזוג הבא |
| q3 | a | A | q4 | ε | ה־a הראשון: שולפים |
| q4 | a | A | q4 | ε | עוד a: שולפים |
| q4 | a | X | q5 | ε | ה־a ה־(n+k): שולף את X |
| q5 | a | ⊥ | q6 | ⊥ | a נוסף ראשון (מתוך 2) |
| q6 | a | ⊥ | q7 | ⊥ | a נוסף שני – מצב מקבל |
בכל צירוף (מצב, אות, ראש מחסנית) יש לכל היותר מעבר אחד ואין מעברי ε – האוטומט דטרמיניסטי. כל צירוף שאינו בטבלה = דחייה (למשל a אחרי מספר אי־זוגי של c – במצב q2; b אחרי c; a שלישי אחרי ריקון המחסנית – ב־q7 אין מעברים).
מעקב על bbccaaaaa (n = 2, k = 1, ולכן 2 + 1 + 2 = 5 אותיות a):
| נקרא | מצב | מחסנית (ראש משמאל) |
|---|---|---|
| – | q0 | ⊥ |
| b | q1 | X⊥ |
| b | q1 | AX⊥ |
| c | q2 | AX⊥ |
| c | q3 | AAX⊥ |
| a | q4 | AX⊥ |
| a | q4 | X⊥ |
| a | q5 | ⊥ |
| a | q6 | ⊥ |
| a | q7 | ⊥ |
הקלט נגמר במצב 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 נקודות)
בשאלה זו שני סעיפים, א–ב, שאין קשר ביניהם. ענו על שני הסעיפים.
נתונות שלוש שפות L₁ , L₂ , L₃ מעל הא"ב {a, b} . הסימון R(L) מציין את השפה ההפוכה של L , הסימון L* מציין את סגור קלייני של L (פעולת הכוכב) , והסימון L₁ − L₂ מציין את ההפרש בין השפות (המילים של L₁ שאינן ב־L₂).
לפניכם שלוש טענות. בנוגע לכל אחת מן הטענות, קבעו אם היא נכונה או לא נכונה.
אם היא נכונה – הסבירו מדוע, ואם היא לא נכונה – הפריכו אותה באמצעות דוגמה נגדית: בחרו שפות ספציפיות והראו שעבורן הטענה לא מתקיימת.
(1) (L₁ ∪ L₂)* = L₁* ∪ L₂*
R(L₁*) = (R(L₁))*
(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₂* ⊆ (L₁ ∪ L₂)*.)
בדיקה: בדוגמה האגפים שונים; באופן כללי אגף ימין תמיד מוכל באגף שמאל, אבל לא תמיד שווה לו.
סעיף א(2)
נכונה – ההיפוך של שרשור מילים מ־L₁ הוא שרשור (בסדר הפוך) של ההיפוכים שלהן, שכולן ב־R(L₁).
מה נדרש: לקבוע אם היפוך הסגור שווה לסגור של ההיפוך.
רעיון: R(xy) = R(y)·R(x). היפוך של שרשור של כמה מילים הופך כל מילה וגם את הסדר שלהן – אבל בסגור הסדר לא משנה, כי כל סדר של חלקים מותר.
פתרון: נכונה.
שתי ההכלות מתקיימות, ולכן השפות שוות.
בדיקה: השוויון מתקיים תמיד – אפשר לבדוק אותו על כמה שפות סופיות קטנות.
סעיף א(3)
לא נכונה. דוגמה: L₁ = {a}, L₂ = {ε}, L₃ = {b, ab}: אגף שמאל {ab, aab}, אגף ימין {ab, aab} − {b, ab} = {aab}.
מה נדרש: לקבוע אם שרשור מימין "מתפלג" מעל הפרש.
רעיון: אותה מילה יכולה להתקבל בשרשור בשתי דרכים שונות: פעם כמילה מ־L₁ ועוד חלק מ־L₃, ופעם כמילה מ־L₂ ועוד חלק אחר מ־L₃. אז באגף ימין היא "נמחקת", ובאגף שמאל היא נשארת.
פתרון: לא נכונה. דוגמה נגדית: L₁ = {a} , L₂ = {ε} , L₃ = {b, ab}.
{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 של הבלוק השני.
פתרון:
| מצב | קלט | ראש המחסנית | מעבר (קלט, ראש / נדחף) | מצב חדש | הסבר |
|---|---|---|---|---|---|
| q₀ | a | ⊥ | a, ⊥ / X⊥ | q₁ | ה־a הראשון: מסמן X |
| q₁ | a | X | a, X / AX | q₁ | עוד a |
| q₁ | a | A | a, A / AA | q₁ | עוד a |
| q₁ | b | A | b, A / ε | q₂ | b ראשון: שולף A |
| q₁ | b | X | b, X / ε | q₃ | b ראשון כש־i = 1: שולף את X |
| q₂ | b | A | b, A / ε | q₂ | b מבטל a מהבלוק הראשון |
| q₂ | b | X | b, X / ε | q₃ | ה־b שמבטל את ה־a הראשון |
| q₃ | b | ⊥ | b, ⊥ / Y⊥ | q₄ | ה־b הראשון "העודף": מסמן Y |
| q₄ | b | Y | b, Y / BY | q₄ | עוד b עודף |
| q₄ | b | B | b, B / BB | q₄ | עוד b עודף |
| q₄ | a | B | a, B / ε | q₅ | a מהבלוק השני שולף B |
| q₄ | a | Y | a, Y / ε | f | a יחיד בבלוק השני (k = 1) |
| q₅ | a | B | a, B / ε | q₅ | עוד a |
| q₅ | a | Y | a, 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₀ | ⊥ |
| a | q₁ | ⊥X |
| a | q₁ | ⊥XA |
| b | q₂ | ⊥X |
| b | q₃ | ⊥ |
| b | q₄ | ⊥Y |
| b | q₄ | ⊥YB |
| b | q₄ | ⊥YBB |
| a | q₅ | ⊥YB |
| a | q₅ | ⊥Y |
| a | f | ⊥ |
סוף הקלט במצב 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 :
| 0 | 1 | 2 | |
|---|---|---|---|
| x | a | a | b |
| y | b | b | a |
השזירה: x₀ y₀ x₁ y₁ x₂ y₂ = a b a b b a .
כתבו מכונת טיורינג המקבלת בתחילת הסרט שתי מילים שאורכן זהה מעל הא"ב {a, b} , ומחזירה את השזירה שלהן.
הנחיות:
– שתי המילים בקלט מופרדות ב־ # , ואורך כל אחת מהן 1 לפחות.
– אין צורך לשמור על הקלט (אפשר לשנות את הקלט לסימנים שונים).
– המילה המוחזרת תופיע במקום כלשהו בסרט בין שני סימני $ .
– אין צורך לבדוק את תקינות הקלט.
דוגמה:
הסרט לפני ההרצה:
| ⊢ | a | a | b | # | b | b | a | Δ | Δ | ... |
|---|
הסרט לאחר ההרצה:
| ... | $ | a | b | a | b | b | a | $ | ... |
|---|
כתבו מכונת טיורינג המקבלת בתחילת הסרט שתי מילים כמתואר לעיל, ומחזירה את השזירה שלהן בין שני סימני $ .
◂ פתרון מלא לשאלה 9
פתרון
כותבים $ אחרי המילה השנייה. בכל סבב: האות השמאלית שלא הועתקה במילה הראשונה ← X ומועתקת (לפי מצב) לתא הריק הראשון בסוף הסרט; אחר כך האות השמאלית שלא הועתקה במילה השנייה ← Y ומועתקת גם היא לסוף. כשהמילה הראשונה נגמרת – $ בתא הריק הראשון. התוצאה: ⊢X…X#Y…Y$x₀y₀x₁y₁…$.
מה נדרש: מכונת טיורינג שמקבלת ⊢x#y (x, y מעל {a, b}, באותו אורך) ומשאירה על הסרט $x₀y₀x₁y₁…$.
רעיון:
למה החיפושים לא מתבלבלים עם האותיות שהועתקו לסוף? בחיפוש ב־x עוצרים באות הראשונה שאינה X – היא לפני ה־#. בחיפוש ב־y מתחילים מה־# – האות הראשונה שאינה Y היא לפני ה־$.
פתרון:
| מצב | קורא | כותב | תזוזה | מצב חדש | הסבר |
|---|---|---|---|---|---|
| q0 | ⊢ | ⊢ | R | q1 | התחלה |
| q1 | a, b, # | אותו סימן | R | q1 | ימינה עד סוף הקלט |
| q1 | Δ | $ | L | q2 | $ הפותח אחרי המילה השנייה |
| q2 | a, b, X, Y, #, $ | אותו סימן | L | q2 | שמאלה עד ⊢ |
| q2 | ⊢ | ⊢ | R | q3 | סבב חדש |
| q3 | X | X | R | q3 | דילוג על אותיות שהועתקו |
| q3 | a | X | R | q4 | אות מהמילה הראשונה: a |
| q3 | b | X | R | q5 | אות מהמילה הראשונה: b |
| q3 | # | # | R | q10 | המילה הראשונה נגמרה – סיום |
| q4 | a, b, Y, #, $ | אותו סימן | R | q4 | ימינה עד התא הריק |
| q4 | Δ | a | L | q6 | מעתיקים a |
| q5 | a, b, Y, #, $ | אותו סימן | R | q5 | ימינה עד התא הריק |
| q5 | Δ | b | L | q6 | מעתיקים b |
| q6 | a, b, Y, $ | אותו סימן | L | q6 | שמאלה עד # |
| q6 | # | # | R | q7 | למילה השנייה |
| q7 | Y | Y | R | q7 | דילוג על אותיות שהועתקו |
| q7 | a | Y | R | q8 | אות מהמילה השנייה: a |
| q7 | b | Y | R | q9 | אות מהמילה השנייה: b |
| q8 | a, b, $ | אותו סימן | R | q8 | ימינה עד התא הריק |
| q8 | Δ | a | L | q2 | מעתיקים a |
| q9 | a, b, $ | אותו סימן | R | q9 | ימינה עד התא הריק |
| q9 | Δ | b | L | q2 | מעתיקים b |
| q10 | a, b, Y, $ | אותו סימן | R | q10 | ימינה עד התא הריק |
| q10 | Δ | $ | R | h | $ הסוגר – עצירה |
מעקב – הקלט 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 .
נתון חלק ממבנה המחלקות במערכת הממוחשבת:
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 :
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 .
בפעולה main נוסף קטע הקוד שלפניכם (במקום המסומן ב־ *):
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 . תשובה שבה יהיה שינוי לא תזוכה בנקודות.
במחלקה Tester נוספה הפעולה שלפניכם:
public static void alert (Parcel [] arr) {
for (int i = 0; i < arr.length; i++) {
arr[i].warn(); }
}אם הפעולה תקינה, כתבו את הפלט שלה עבור המערך parcels (הנתון לעיל), ואם הפעולה לא תקינה – תקנו אותה (כך שהקריאה ל־ warn בתוך הלולאה תהיה תקינה) וכתבו את הפלט עבור המערך parcels .
הערות:
◂ פתרון מלא לשאלה 10
סעיף א
Fragile יורשת מ־Parcel (isInsured, warn); Glass יורשת מ־Fragile (layers); Electronic יורשת מ־Fragile (hasBattery); Letter יורשת מ־Parcel (isRegistered). התכונות sender ו־weight נשארות רק ב־Parcel, וב־warn ניגשים לשולח דרך getSender().
מה נדרש – לארגן את המחלקות בהיררכיית ירושה כך שכל תכונה וכל פעולה יופיעו פעם אחת בלבד, ולסרטט את תרשים ההייררכייה.
רעיון
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 ומאתחלות את כל התכונות.
רעיון
super(sender, weight) ב־Java, : base(sender, weight) ב־C#.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 של הטיפוס בפועל של העצם (פולימורפיזם). נבדוק כל שורה בפלט:
| עצם | שורת הפלט | מה צריך |
|---|---|---|
| Glass | Glass 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, ולכן גם הן עוברות את הבדיקה.
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 :
המידע ששומרים על השכרת מגרש — Rental :
המידע ששומרים על טורניר — Tournament :
המידע ששומרים על פעילות — Activity :
אותן דרישות המידע המפורטות בסוגי הפעילויות שלעיל ושצריכות להישמר במחלקה Activity בהתאם לעקרונות תכנות מונחה עצמים.
נוסף על המחלקות האמורות, נבנתה המחלקה Center — מרכז הספורט, ולה שתי תכונות: arr — מערך בגודל 500 מטיפוס Activity , ו־ count — מספר הפעילויות השמורות במערך. הפעילויות שמורות ברצף מתחילת המערך (בתאים 0 עד count–1), ואין ביניהן ערכי null .
לפניכם תרשים חלקי של המחלקות:

שימו לב: ירושה מסומנת באמצעות החץ ◁——— (המשולש הריק בצד מחלקת־העל), והכלה מסומנת באמצעות הסימן ◆——— (המעוין המלא בצד המחלקה המכילה).
על הסעיפים שלפניכם יש לענות לפי עקרונות של תכנות מונחה עצמים.
העתיקו את התרשים למחברת והוסיפו את המחלקה Time לתרשים. כתבו את כותרת המחלקה פעילות — Activity ואת התכונות שלה.
הניחו שבשאר המחלקות כתובות התכונות המתאימות, ושבכל המחלקות יש פעולות בונות ופעולות get ו־ set .
כתבו במחלקה Center פעולה ששמה lessonsBefore המקבלת מספר יום — day ועצם — t מטיפוס Time , ומחזירה מערך מטיפוס Lesson המכיל את כל השיעורים הקבוצתיים המתקיימים ביום day ומתחילים לפני השעה t . גודל המערך המוחזר צריך להיות שווה למספר השיעורים האלה (אם אין שיעורים כאלה, יוחזר מערך בגודל 0).
שימו לב:
public boolean before (Time other)הפעולה מחזירה true אם השעה הנוכחית מוקדמת מהשעה other , אחרת (גם אם השעות שוות) היא מחזירה false . אפשר להשתמש בפעולה בלי לממש אותה.
לפניכם כותרת הפעולה הפנימית — usesCourt במחלקה Activity :
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 עם התכונות שלה.
רעיון
פתרון
תרשים המחלקות המלא:

כותרת המחלקה ותכונותיה – :
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.
רעיון
arr[i] instanceof Lesson (ב־C#: arr[i] is Lesson) מזהה שיעור; את getDay ואת getStart אפשר לזמן ישירות על הפניה מטיפוס Activity , כי הן מוגדרות ב־Activity. רק בהכנסה למערך התוצאה צריך המרה (Lesson).פתרון – :
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).
רעיון – דריסה:
return false – ברירת המחדל, מתאימה לשיעור קבוצתי (לכן אין צורך לכתוב את הפעולה ב־Lesson).return this.court == court.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 :
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;
}
}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;
}
}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;
}
}לפניכם קטע קוד שרץ ללא שגיאות:
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).
לפניכם המשך קטע הקוד (ממוספר) שרץ ללא שגיאות.
ציינו את מספר שורת ההדפסה, וכתבו מה הקוד מדפיס.
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 |
|---|---|---|---|
| p | P | P | 5 |
| pq | P | Q | 10 |
| pr | P | R | 12 |
| q | Q | Q | 3 |
| qr | Q | R | 4 |
| r | R | R | 6 |
בדיקה – מדפיסים לכל עצם את המחלקה שלו ואת 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
מה נדרש – מה מודפס בכל אחת מעשר השורות.
רעיון – שני שלבים בכל קריאה:
מצב העצמים (מסעיף א): p=P(5), pq=Q(10), pr=R(12), q=Q(3), qr=R(4), r=R(6).
פתרון
| שורה | הפניה . פעולה (טיפוס הארגומנט) | חתימה שנבחרה | מתבצעת בפועל | חישוב | פלט |
|---|---|---|---|---|---|
| 1 | p:P . f(Q) | f(P) (Q הוא P) | P.f(P) | 5 + 3 | 8 |
| 2 | pq:P . f(P) | f(P) | Q.f(P) (דריסה) | 10 · 5 | 50 |
| 3 | pq:P . f(Q) | f(P) (ל־P אין f(Q)) | Q.f(P) (דריסה) | 10 · 4 | 40 |
| 4 | q:Q . f(Q) | f(Q) | Q.f(Q) | 4 − 3 | 1 |
| 5 | qr:Q . f(Q) | f(Q) | R.f(Q) (דריסה) | 4 − 2·3 | -2 |
| 6 | qr:Q . f(P) | f(P) | Q.f(P) (R לא דורסת אותה) | 4 · 12 | 48 |
| 7 | r: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 |
| 9 | pr.f(pr.f(20)) | f(int) פעמיים | P.f(int) | 20−12=8, 8−12 | -4 |
| 10 | r:R . f(P) | f(P) | Q.f(P) (בירושה) | 6 · 5 | 30 |
הפלט:
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 |