מתכונת 1 במדעי המחשב - מבני נתונים ומסלול 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 נקודות)
שימו לב: בשאלה זו אפשר להשתמש בפעולות של המחלקות תור, עץ בינרי וחוליה, בלי לממש אותן. אם משתמשים בפעולות נוספות, יש לממש אותן.
שני איברים בתור של מספרים, הנמצאים במקומות שונים בתור, הם "זוג קרוב" לפי d אם ההפרש בין ערכיהם (בערך מוחלט) קטן מ־ d או שווה לו.
ממשו את הפעולה החיצונית שלפניכם:
Java – public static int closePairs (Queue<Integer> q, int d)
C# – public static int ClosePairs (Queue<int> q, int d)
הפעולה מקבלת תור q של מספרים שלמים ומספר שלם d הגדול מ־ 0 או שווה לו, ומחזירה את מספר הזוגות הקרובים לפי d שיש בתור q. כל זוג נספר פעם אחת בלבד.
דוגמה: עבור התור q שלפניכם ו־ d = 2 הפעולה תחזיר 4, כי בתור יש ארבעה זוגות קרובים: (8 , 9), (3 , 5), (3 , 4), (5 , 4).

עבור אותו התור ו־ d = 0 הפעולה תחזיר 0 (אין בתור שני מספרים שווים).
הערות:
– הניחו שבתור q יש שני איברים לפחות.
– אין צורך לשמור על התור q.
– אין להשתמש בשאלה זו במערך וברשימה מקושרת.
ממשו את הפעולה closePairs/ClosePairs המתוארת לעיל.
◂ פתרון מלא לשאלה 1
פתרון
מוציאים בכל פעם את האיבר הראשון v (בלי להחזירו), מעבירים את שאר האיברים לתור עזר וסופרים את האיברים x שעבורם |v − x| ≤ d, ומחזירים אותם מתור העזר ל־ q; חוזרים עד שהתור ריק. עבור הדוגמה (d = 2) מתקבל 4. סיבוכיות O(n²).
מה נדרש: לספור כמה זוגות של איברים בתור (במקומות שונים) נבדלים זה מזה ב־ d לכל היותר, כאשר כל זוג נספר פעם אחת.
רעיון: מוציאים מהתור את האיבר הראשון v ולא מחזירים אותו. כעת בתור נמצאים בדיוק האיברים שבאו אחריו. מעבירים את כל האיברים האלה, אחד אחד, לתור עזר temp: כל איבר x מוצא, נבדק אם |v − x| ≤ d (אם כן – מגדילים את המונה), ומוכנס ל־ temp. אחר כך מחזירים את כל האיברים מ־ temp אל q (באותו הסדר). חוזרים על כך עד שהתור q מתרוקן.
כך כל זוג נבדק בדיוק פעם אחת – כשהאיבר המוקדם מבין השניים הוא v – ולכן אין ספירה כפולה.
פתרון: הקוד המלא – בלשונית Java/C#. בפעולה משתמשים רק בפעולות התור (insert, remove, isEmpty), בתור עזר ובמשתנים פשוטים, בלי מערך ובלי רשימה.
יעילות: הלולאה החיצונית מתבצעת n פעמים (בכל פעם יוצא מהתור איבר אחד לתמיד). כשנשארים בתור k איברים, הלולאות הפנימיות מעבירות אותם ל־ temp וחזרה – 2k פעולות. k = n − 1, n − 2, ..., 1, 0, ולכן סך הפעולות הוא 2·((n − 1) + (n − 2) + ... + 1) = n·(n − 1) ⇒ O(n²), כאשר n הוא מספר האיברים בתור.
בדיקה (התור 8, 3, 12, 5, 9, 20, 4 ו־ d = 2):
| v שהוצא | האיברים שנשארו בתור | זוגות קרובים שנמצאו | מונה |
|---|---|---|---|
| 8 | 3, 12, 5, 9, 20, 4 | (8 , 9) | 1 |
| 3 | 12, 5, 9, 20, 4 | (3 , 5), (3 , 4) | 3 |
| 12 | 5, 9, 20, 4 | – | 3 |
| 5 | 9, 20, 4 | (5 , 4) | 4 |
| 9 | 20, 4 | – | 4 |
| 20 | 4 | – | 4 |
| 4 | (ריק) | – | 4 |
הפעולה מחזירה 4. מקרים נוספים: d = 0 ← 0, d = 1 ← 3, התור 5, 5, 5, 5 עם d = 0 ← 6.
טעות נפוצה: להחזיר גם את v לתור אחרי הסבב. אז התור לעולם לא מתרוקן והלולאה אינה מסתיימת; וגם אם סופרים סבבים, כל זוג נבדק פעמיים והתשובה כפולה.
public static int closePairs(Queue<Integer> q, int d) {
int count = 0;
while (!q.isEmpty()) {
int v = q.remove(); // האיבר הנוכחי יוצא מהתור לתמיד
Queue<Integer> temp = new Queue<Integer>();
while (!q.isEmpty()) { // מעבר על כל האיברים שאחריו
int x = q.remove();
if (Math.abs(v - x) <= d) // זוג קרוב
count++;
temp.insert(x);
}
while (!temp.isEmpty()) // החזרת האיברים לתור
q.insert(temp.remove());
}
return count;
}שאלה 2 · ספירה רקורסיבית בשרשרת חוליות (25 נקודות)
שימו לב: לשאלה זו שני נוסחים: בשפת Java ובשפת C#.
לפותרים בשפת Java
נתונה הפעולה what :
public static int what (Node<Integer> lst, int x)
{
if (lst == null)
return 0;
int r = what (lst.getNext(), x);
if (lst.getValue() < x)
return r + 1;
return r;
}נתונה שרשרת חוליות – lst מטיפוס שלם:

(1) עקבו אחר הפעולה what (lst, 5) , ורשמו את הערך שהפעולה מחזירה.
מה עושה הפעולה what ? הסבירו את תשובתכם.
מהי סיבוכיות הפעולה what ? נמקו את תשובתכם.
נתונה הפעולה guess :
public static int guess (Node<Integer> lst)
{
if (lst == null)
return 0;
return what (lst.getNext(), lst.getValue()) + guess (lst.getNext());
}נתונה שרשרת חוליות – lst מטיפוס שלם:

(1) עקבו אחר הפעולה guess (lst) , ורשמו את הערך שהפעולה מחזירה.
בסעיף זה אין צורך לעקוב אחר הפעולה what .
מה עושה הפעולה guess ? הסבירו את תשובתכם.
מהי סיבוכיות הפעולה guess ? נמקו את תשובתכם.
◂ פתרון מלא לשאלה 2
סעיף א(1)
הפעולה מחזירה 3 (החוליות 4, 2, 1 קטנות מ־5).
מה נדרש: לעקוב אחר הקריאה what(lst, 5) ולרשום את הערך המוחזר.
רעיון: הפעולה קוראת לעצמה לפני שהיא בודקת את החוליה הנוכחית. לכן הקריאות יורדות עד סוף השרשרת (lst == null מחזירה 0), והבדיקה של החוליות מתבצעת בדרך חזרה, מהחוליה האחרונה לראשונה. כל קריאה מקבלת ב־r את התוצאה של המשך השרשרת, ומוסיפה 1 אם ערך החוליה שלה קטן מ־x.
פתרון – מעקב (השרשרת 4, 7, 2, 9, 5, 1; x = 5; השורות לפי סדר החזרה מהרקורסיה):
| קריאה על חוליה | r שהתקבל מהקריאה הרקורסיבית | ערך < 5 ? | מוחזר |
|---|---|---|---|
| null | – | – | 0 |
| 1 | 0 | כן | 1 |
| 5 | 1 | לא (5 אינו קטן מ־5) | 1 |
| 9 | 1 | לא | 1 |
| 2 | 1 | כן | 2 |
| 7 | 2 | לא | 2 |
| 4 | 2 | כן | 3 |
הפעולה מחזירה 3.
בדיקה: הקוד מדפיס what(lst, 5) = 3.
טעות נפוצה: לספור גם את החוליה 5. התנאי הוא קטן ממש (<), ולכן ערך השווה ל־x אינו נספר.
סעיף א(2)
הפעולה מחזירה את מספר החוליות בשרשרת lst שהערך שלהן קטן (ממש) מ־x. השרשרת עצמה לא משתנה.
מה נדרש: לתאר במילים מה הפעולה מחשבת.
רעיון: נסתכל על הפעולה כעל הגדרה רקורסיבית:
פתרון: הפעולה סופרת כמה חוליות בשרשרת lst ערכן קטן מ־x ומחזירה את המספר. הפעולה רק קוראת ערכים (getValue, getNext) ואינה משנה את השרשרת. אם אף ערך אינו קטן מ־x מוחזר 0, וגם עבור שרשרת ריקה מוחזר 0.
בדיקה: על השרשרת 4, 7, 2, 9, 5, 1:
סעיף א(3)
O(n), כאשר n הוא מספר החוליות בשרשרת: קריאה רקורסיבית אחת לכל חוליה (ועוד אחת ל־null), ובכל קריאה עבודה קבועה.
מה נדרש: סיבוכיות זמן, כאשר n הוא מספר החוליות ב־lst.
רעיון: סופרים את הקריאות הרקורסיביות ואת העבודה בכל קריאה.
פתרון: כל קריאה מבצעת קריאה רקורסיבית אחת בלבד, על החוליה הבאה. לכן יש n + 1 קריאות: אחת לכל חוליה ואחת ל־null בסוף. בכל קריאה, מלבד הקריאה הרקורסיבית, יש מספר קבוע של פעולות: השוואה, getValue, חיבור ו־return, כלומר O(1).
סה"כ: (n + 1) · O(1) = O(n). הפעולה תמיד עוברת על כל השרשרת, ולכן אין הבדל בין מקרה טוב למקרה גרוע.
בדיקה: סופרים את הקריאות במעקב. על שרשרות באורך 100, 200, 400, 800, 1600 היו 101, 201, 401, 801, 1601 קריאות, כלומר n + 1, גידול לינארי.
סעיף ב(1)
הפעולה מחזירה 9.
מה נדרש: לעקוב אחר guess(lst) ולרשום את הערך המוחזר.
רעיון: לפי סעיף א, what(lst.getNext(), lst.getValue()) מחזירה כמה חוליות אחרי החוליה הנוכחית קטנות מהערך שלה. guess מחברת את המספר הזה לתוצאה של guess על המשך השרשרת. כלומר התוצאה היא סכום של "כמה ערכים קטנים ממני באים אחריי" על כל החוליות.
פתרון – מעקב:
| חוליה נוכחית | הערכים שאחריה | הקטנים ממנה | what(…) |
|---|---|---|---|
| 4 | 7, 2, 9, 5, 1 | 2, 1 | 2 |
| 7 | 2, 9, 5, 1 | 2, 5, 1 | 3 |
| 2 | 9, 5, 1 | 1 | 1 |
| 9 | 5, 1 | 5, 1 | 2 |
| 5 | 1 | 1 | 1 |
| 1 | – | – | 0 |
| null | guess מחזירה 0 |
guess(lst) = 2 + 3 + 1 + 2 + 1 + 0 + 0 = 9.
בדיקה: הקוד מדפיס guess(lst) = 9.
סעיף ב(2)
הפעולה מחזירה את מספר הזוגות של חוליות בשרשרת (חוליה מוקדמת וחוליה מאוחרת יותר) שבהם הערך של החוליה המוקדמת גדול מהערך של החוליה המאוחרת.
מה נדרש: לתאר במילים מה guess מחשבת.
רעיון: לכל חוליה, מהראשונה ועד האחרונה, guess סופרת (בעזרת what) כמה חוליות שבאות אחריה קטנות ממנה, ומחברת את כל המספרים.
פתרון: הפעולה סופרת את הזוגות "ההפוכים" בשרשרת: זוגות של שתי חוליות שבהם החוליה שמופיעה קודם בשרשרת גדולה מהחוליה שמופיעה אחריה. כל זוג כזה נספר בדיוק פעם אחת – כשמגיעים לחוליה המוקדמת בזוג. הפעולה אינה משנה את השרשרת.
משמעות: התוצאה היא 0 בדיוק כשהשרשרת מסודרת בסדר עולה, כאשר ערכים שווים זה ליד זה מותרים (אין ערך שבא אחרי ערך גדול ממנו; למשל בשרשרת 2, 2, 5 התוצאה 0), והיא הכי גדולה, n(n−1)/2, כשהערכים שונים זה מזה ומסודרים בסדר יורד.
בדיקה: השרשרת 3, 1, 2 ⇒ 2 (הזוגות 3>1, 3>2); השרשרת 1, 2, 3 ⇒ 0; השרשרת 3, 2, 1 ⇒ 3.
סעיף ב(3)
O(n²), כאשר n הוא מספר החוליות: לכל חוליה מופעלת what על ההמשך שלה, שאורכו n−1, n−2, …, 0. הסכום הוא n(n−1)/2.
מה נדרש: סיבוכיות זמן של guess, כאשר n הוא מספר החוליות ב־lst.
רעיון: guess מופעלת פעם אחת על כל חוליה (ועוד פעם על null). בכל הפעלה יש קריאה ל־what על המשך השרשרת, ולפי סעיף א(3) היא עולה O(אורך ההמשך).
פתרון: לחוליה הראשונה what עוברת על n−1 חוליות, לשנייה על n−2, וכן הלאה, עד 0 לחוליה האחרונה:
(n−1) + (n−2) + … + 1 + 0 = n(n−1)/2 ⇒ O(n²).
what תמיד עוברת על כל ההמשך (אין בה עצירה מוקדמת), ולכן זו הסיבוכיות בכל מקרה, לא רק במקרה הגרוע.
בדיקה: סופרים את הקריאות ל־what כש־guess פועלת על שרשרות באורך n = 100, 200, 400, 800, 1600. היו 5050, 20100, 80200, 320400, 1280800 קריאות, כלומר בדיוק n(n+1)/2 (כולל הקריאות על null). כשמכפילים את n, מספר הקריאות גדל פי 4, כמו שמתאים ל־O(n²).
שאלה 3 · חיתוך מספר לספרותיו בעץ בינרי (25 נקודות)
שימו לב: בשאלה זו אפשר להשתמש בפעולות של המחלקות תור, עץ בינרי וחוליה, בלי לממש אותן. אם משתמשים בפעולות נוספות, יש לממש אותן.
לפניכם הפעולה החיצונית numDigits/NumDigits. אפשר להשתמש בפעולה בלי לממש אותה.
| כותרת הפעולה | תיאור הפעולה |
|---|---|
Java – public static int numDigits (int num) | הפעולה מקבלת מספר שלם num הגדול מ־ 0 או שווה לו, ומחזירה את מספר הספרות שלו. |
C# – public static int NumDigits (int num) | לדוגמה: עבור 4825 הפעולה תחזיר 4, ועבור 0 היא תחזיר 1. |
ממשו את הפעולה החיצונית שלפניכם:
Java – public static boolean cutDigits (BinNode<Integer> tr)
C# – public static bool CutDigits (BinNode<int> tr)
הפעולה מקבלת צומת ללא בנים (עלה) שערכו מספר שלם הגדול מ־ 0 או שווה לו. נסמן ב־ d את מספר הספרות של ערך הצומת.
אם d = 1 (מספר חד־ספרתי), הפעולה מחזירה false.
אחרת, הפעולה "חותכת" את המספר לשני חלקים ומוסיפה לצומת שני בנים: ערך הבן הימני הוא המספר שיוצרות d/2 הספרות הימניות של ערך הצומת (חלוקה בשלמים), וערך הבן השמאלי הוא המספר שיוצרות שאר הספרות (השמאליות). אם החלק הימני מתחיל בספרה 0, האפסים שבתחילתו אינם נשמרים (למשל, 03 נשמר כ־ 3). לאחר ההוספה הפעולה מחזירה true.
דוגמאות: בתום הפעולה ייראו הצמתים כך (עבור הערכים 4825 , 9603 , 6):

(עבור 307: d = 3, ולכן הבן הימני הוא 7 – ספרה אחת – והבן השמאלי הוא 30.)
נתונה הפעולה what/What שלפניכם, המקבלת צומת ללא בנים שערכו מספר שלם חיובי שאין בו את הספרה 0.
בשפת :
public static int what(BinNode<Integer> tr) {
if (!cutDigits(tr))
return 1;
return what(tr.getLeft()) + what(tr.getRight());
}(1) סרטטו את העץ כפי שהוא ייראה בתום הפעולה what/What עבור צומת ללא בנים – tr שערכו 73846, וכתבו מה הפעולה מחזירה. יש להראות מעקב.
הסבירו מה מבצעת הפעולה what/What ומה היא מחזירה.
◂ פתרון מלא לשאלה 3
סעיף א
d = numDigits(val); אם d == 1 – false. אחרת p = 10 בחזקת d/2, בן שמאלי val / p, בן ימני val % p, ומחזירים true.
מה נדרש: לחתוך את ערך העלה לשני חלקים לפי הספרות: d/2 הספרות הימניות לבן הימני, ושאר הספרות לבן השמאלי.
רעיון:
פתרון: הקוד המלא – בלשונית Java/C#.
בדיקה: 4825: d = 4, p = 100 ← 48 ו־ 25. 9603: d = 4, p = 100 ← 96 ו־ 3. 307: d = 3, p = 10 ← 30 ו־ 7. 6: d = 1 ← false. מתקבלות בדיוק התוצאות האלה, וגם 10 ← בנים 1 ו־ 0.
טעות נפוצה: לחשב את p לפי d ולא לפי d/2 (או להפוך בין החלקים), ואז הבן השמאלי מקבל 0 והימני את כל המספר.
public static boolean cutDigits(BinNode<Integer> tr) {
int val = tr.getValue();
int d = numDigits(val);
if (d == 1)
return false;
int p = 1;
for (int i = 0; i < d / 2; i++) // p = 10 בחזקת (d/2)
p = p * 10;
tr.setLeft(new BinNode<Integer>(val / p)); // הספרות השמאליות
tr.setRight(new BinNode<Integer>(val % p)); // d/2 הספרות הימניות
return true;
}סעיף ב(1)
73846 ← 738 , 46; 738 ← 73 , 8; 73 ← 7 , 3; 46 ← 4 , 6. העלים משמאל לימין: 7, 3, 8, 4, 6. הפעולה מחזירה 5.
מה נדרש: לסרטט את העץ שנבנה בקריאה what(tr) כאשר tr הוא עלה שערכו 73846, להציג מעקב ולכתוב מה מוחזר.
רעיון: what מפעילה את cutDigits. אם הצומת לא נחתך (חד־ספרתי) – מחזירה 1. אחרת ממשיכה ברקורסיה על הבן השמאלי ועל הבן הימני ומחזירה את סכום התוצאות.
פתרון (מעקב):
| קריאה | d | cutDigits | בנים שנוספו | ערך מוחזר |
|---|---|---|---|---|
| what(73846) | 5 | true | 738 , 46 | 3 + 2 = 5 |
| what(738) | 3 | true | 73 , 8 | 2 + 1 = 3 |
| what(73) | 2 | true | 7 , 3 | 1 + 1 = 2 |
| what(7) | 1 | false | – | 1 |
| what(3) | 1 | false | – | 1 |
| what(8) | 1 | false | – | 1 |
| what(46) | 2 | true | 4 , 6 | 1 + 1 = 2 |
| what(4) | 1 | false | – | 1 |
| what(6) | 1 | false | – | 1 |
העץ בתום הפעולה:

הפעולה מחזירה 5.
בדיקה: מתקבל העץ 73846(738(73(7, 3), 8), 46(4, 6)) והערך המוחזר 5.
סעיף ב(2)
הפעולה מפרקת את המספר לספרותיו: היא בונה עץ שעליו, משמאל לימין, הם ספרות המספר לפי הסדר (כל צומת פנימי = צירוף הספרות של שני בניו), ומחזירה את מספר העלים – כלומר את מספר הספרות של המספר.
מה נדרש: להסביר מה מבצעת what ומה היא מחזירה.
רעיון: כל צומת שיש לו יותר מספרה אחת נחתך לשני חלקים שצירופם (זה אחר זה) נותן בחזרה את המספר. החיתוך ממשיך עד שכל העלים חד־ספרתיים.
פתרון (הסבר):
בדיקה: הערך המוחזר שווה ל־ numDigits, וצירוף העלים משמאל לימין שווה למספר.
פרק שני (50 נקודות) – אלגוריתמים
יש לענות על שתי שאלות במסלול שלמדתם (לכל שאלה – 25 נקודות).
שאלה 4 · טענות על גרפים וגרף "מיון יחיד" (25 נקודות)
לפניכם שני סעיפים, א–ב, שאין קשר ביניהם. ענו על שני הסעיפים.
לפניכם שבע טענות (1)–(7). בחרו בחמש מהן, וכתבו את מספריהן. ציינו בנוגע לכל טענה שבחרתם אם היא נכונה או לא נכונה. אם הטענה נכונה – נמקו מדוע, ואם היא לא נכונה – הביאו דוגמה נגדית מגרף שיש בו ארבעה קודקודים לפחות.
(1) בכל גרף לא מכוון, מספר הקודקודים שדרגתם אי־זוגית הוא מספר זוגי.
(2) נתון גרף קשיר לא מכוון שיש בו n קודקודים ו־ n − 1 קשתות. אין בגרף מעגל.
(3) נתון גרף ממושקל קשיר לא מכוון, ובו קשת e שמשקלה גדול ממשקל כל אחת משאר הקשתות. הקשת e אינה נמצאת באף עץ פורש מינימלי של הגרף.
(4) נתון גרף ממושקל קשיר לא מכוון, ובו קשת e שמשקלה קטן ממשקל כל אחת משאר הקשתות. הקשת e נמצאת בכל עץ פורש מינימלי של הגרף.
(5) נתון גרף מכוון G . אם סריקת DFS מקודקוד s מגלה את כל הקודקודים בגרף, אז הגרף קשיר היטב.
(6) בגרף מכוון חסר מעגלים (גמ"ל) שיש בו קשתות במשקל שלילי, האלגוריתם למציאת מסלולים קצרים בגמ"ל (מעבר על הקודקודים לפי מיון טופולוגי ושיפור הקשתות היוצאות מכל קודקוד) מחשב נכון את המרחקים הקצרים (הקלים) מקודקוד המקור.
(7) נתון גרף ממושקל קשיר לא מכוון שבו לכל קשת משקל שונה. עץ המסלולים הקצרים (הקלים) מקודקוד s , שמתקבל באלגוריתם של דייקסטרה, הוא תמיד עץ פורש מינימלי של הגרף.
מיון טופולוגי של גרף מכוון הוא סידור של כל קודקודי הגרף בשורה, כך שלכל קשת u → v הקודקוד u מופיע לפני הקודקוד v .
גרף מכוון מכונה "מיון יחיד" אם יש לו בדיוק מיון טופולוגי אחד.
דוגמה: הגרף שלפניכם הוא גרף "מיון יחיד" – המיון הטופולוגי היחיד שלו הוא P, Q, R .

לפניכם שבעה גרפים מכוונים D₁–D₇ . בחרו בארבעה מהם, וכתבו בנוגע לכל גרף שבחרתם אם הוא גרף "מיון יחיד" או לא. אם כתבתם שלא – הציגו שני מיונים טופולוגיים שונים של הגרף, או הסבירו מדוע אין לגרף מיון טופולוגי כלל.

◂ פתרון מלא לשאלה 4
סעיף א
(1) נכונה; (2) נכונה; (3) לא נכונה; (4) נכונה; (5) לא נכונה; (6) נכונה; (7) לא נכונה. (מספיק לענות על חמש מהן.)
מה נדרש: לבחור 5 מתוך 7 טענות, לקבוע אם כל אחת נכונה, ולנמק (טענה נכונה) או להביא דוגמה נגדית עם 4 קודקודים לפחות (טענה לא נכונה).
רעיון: כאן מוצגות כל שבע הטענות. טענה נכונה מוכיחים בנימוק כללי; טענה לא נכונה מפריכים בדוגמה אחת.
פתרון:
(1) נכונה. כל קשת תורמת 1 לדרגה של כל אחד משני קצותיה, ולכן סכום הדרגות של כל הקודקודים הוא 2·|E| – מספר זוגי. סכום הדרגות הזוגיות הוא זוגי, ולכן גם סכום הדרגות האי־זוגיות זוגי. סכום של מספרים אי־זוגיים הוא זוגי רק כשמספר המחוברים זוגי – לכן מספר הקודקודים שדרגתם אי־זוגית זוגי.
(2) נכונה. נניח בשלילה שיש מעגל. נסיר קשת אחת מן המעגל – הגרף נשאר קשיר (שני קצות הקשת עדיין מחוברים דרך שאר המעגל). נמשיך להסיר כך קשתות ממעגלים עד שלא יישאר מעגל: נקבל עץ פורש עם פחות מ־ n − 1 קשתות. אבל בכל עץ עם n קודקודים יש בדיוק n − 1 קשתות – סתירה.
(3) לא נכונה. דוגמה נגדית – גרף לא מכוון עם הקשתות A–B (1), B–C (2), A–C (3), C–D (9). הקשת C–D היא הכבדה ביותר, אבל היא הקשת היחידה שמגיעה ל־ D , ולכן היא חייבת להיות בכל עץ פורש. העפ"מ (היחיד) הוא {A–B, B–C, C–D} במשקל 12 , והוא מכיל את הקשת הכבדה ביותר.
(4) נכונה. נניח בשלילה שיש עפ"מ T שאינו מכיל את e = (u , v). נוסיף את e ל־ T – נוצר מעגל שעובר דרך e . במעגל יש קשת נוספת e′ (שונה מ־ e), ומשקלה גדול ממשקל e . נסיר את e′ : מתקבל שוב עץ פורש, ומשקלו w(T) − w(e′) + w(e) < w(T) – בסתירה לכך ש־ T מינימלי.
(5) לא נכונה. דוגמה נגדית – גרף מכוון s→a→b→c (שלוש קשתות). DFS מ־ s מגלה את s, a, b, c – את כל הקודקודים. אבל מ־ c אין מסלול לאף קודקוד אחר (בפרט לא ל־ s), ולכן הגרף אינו קשיר היטב.
(6) נכונה. כשהאלגוריתם מגיע לקודקוד v לפי המיון הטופולוגי, כל הקודקודים שיש מהם קשת אל v כבר טופלו (הם מופיעים לפני v במיון). באינדוקציה לפי סדר המיון: המסלול הקל ביותר ל־ v מסתיים בקשת u→v , כש־ u לפני v במיון; כש־ u טופל, d(u) כבר היה נכון, ושיפור הקשת u→v נתן ל־ v את הערך הנכון. הנימוק אינו משתמש בכך שהמשקלים חיוביים (בניגוד לדייקסטרה), ובגמ"ל אין מעגלים ולכן גם לא מעגלים שליליים.
(7) לא נכונה. דוגמה נגדית – גרף לא מכוון עם הקודקודים s, a, b, c והקשתות s–a (3), s–b (4), a–b (2), b–c (1) (כל המשקלים שונים). המרחקים מ־ s : a = 3 (s–a), b = 4 (s–b , ודרך a המשקל 5), c = 5 (s–b–c). עץ המסלולים הקצרים הוא {s–a, s–b, b–c} במשקל 8 . אבל העפ"מ (קרוסקל: b–c , a–b , s–a) הוא {b–c, a–b, s–a} במשקל 6 . העצים שונים, ועץ המסלולים הקצרים אינו מינימלי.
בדיקה: בדוגמאות הנגדיות: בטענה (3) העפ"מ היחיד מכיל את C–D ; בטענה (7) העפ"מ {s–a, a–b, b–c} והמרחקים 0, 3, 4, 5 .
טעות נפוצה: בטענה (3) – לחשוב שקרוסקל "אף פעם לא בוחר" בקשת הכבדה ביותר. הוא כן בוחר בה, כשאין דרך אחרת לחבר את אחד הקודקודים.
סעיף ב
D₁, D₃, D₆ הם גרפים "מיון יחיד". D₂ (A,B,C,D / A,C,B,D), D₄ (A,B,C,D / B,A,C,D), D₇ (A,B,C,D,E / D,A,B,C,E) אינם; ל־ D₅ אין מיון טופולוגי כלל (מעגל A→B→C→A).
מה נדרש: לקבוע לארבעה גרפים אם יש להם בדיוק מיון טופולוגי אחד; אם לא – להציג שני מיונים שונים, או להראות שאין מיון כלל.
רעיון: בונים מיון טופולוגי כך: בכל שלב בוחרים קודקוד שדרגת הכניסה שלו (בגרף שנותר) היא 0 , ומוחקים אותו. אם בכל שלב יש בדיוק מועמד אחד – המיון יחיד. אם בשלב כלשהו יש שני מועמדים – אפשר לבחור כל אחד מהם ולקבל שני מיונים שונים. אם בשלב כלשהו אין מועמד – יש בגרף מעגל ואין מיון כלל. (שקול לכך: המיון יחיד אם ורק אם כל שני קודקודים סמוכים בו מחוברים בקשת – כלומר יש בגרף מסלול מכוון שעובר בכל הקודקודים.)
פתרון: (הקשתות כפי שבאיור)
| גרף | קשתות | "מיון יחיד"? | נימוק / שני מיונים |
|---|---|---|---|
| D₁ | A→B, B→C, C→D, A→C | כן | המיון היחיד A, B, C, D (המסלול A→B→C→D עובר בכל הקודקודים) |
| D₂ | A→B, A→C, B→D, C→D | לא | A, B, C, D וגם A, C, B, D (אחרי A יש שני מועמדים: B ו־ C) |
| D₃ | A→B, B→C, C→D, D→E, A→E, B→D | כן | המיון היחיד A, B, C, D, E (המסלול A→B→C→D→E) |
| D₄ | A→C, B→C, C→D | לא | A, B, C, D וגם B, A, C, D (בהתחלה A ו־ B שניהם בדרגת כניסה 0) |
| D₅ | A→B, B→C, C→A, C→D | לא | אין מיון טופולוגי כלל: A→B→C→A מעגל – A חייב להופיע לפני B , B לפני C ו־ C לפני A – סתירה |
| D₆ | A→B, A→C, A→D, B→C, B→D, C→D | כן | המיון היחיד A, B, C, D (המסלול A→B→C→D) |
| D₇ | A→B, B→C, C→E, D→E | לא | A, B, C, D, E וגם D, A, B, C, E (בהתחלה A ו־ D שניהם בדרגת כניסה 0) |
בדיקה: מספר המיונים הטופולוגיים בכל גרף: D₁ – 1 , D₂ – 2 , D₃ – 1 , D₄ – 2 , D₅ – 0 , D₆ – 1 , D₇ – 4 .
טעות נפוצה: לחשוב ש־ D₅ "אינו מיון יחיד" בגלל שיש לו כמה מיונים – אין לו אף מיון, כי יש בו מעגל. וב־ D₇ , שנראה כמו "שרשרת", D יכול להופיע בכל מקום לפני E .
שאלה 5 · בלמן־פורד ומספר העלים בעצי DFS (25 נקודות)
בשאלה זו שני סעיפים, א–ב, שאין קשר ביניהם. ענו על שני הסעיפים.
לפניכם גרף מכוון ממושקל G₁ . בגרף יש קשתות שמשקלן שלילי, אך אין בו מעגל שמשקלו שלילי:

(1) איזה אלגוריתם מחזיר את המסלול הקצר (הקל) בין שני קודקודים בגרף מכוון ממושקל כלשהו, שבו ייתכנו משקלים שליליים (אך אין מעגל שמשקלו שלילי)? מהי סיבוכיות זמן הריצה של האלגוריתם? נמקו את תשובתכם.
נוסף על כך, הריצו את האלגוריתם שכתבתם, בעבור הגרף הנתון G₁ , מקודקוד S לקודקוד T . בכל סבב עברו על הקשתות בסדר הזה:
R→T , P→R , Q→P , Q→R , Q→T , S→P , S→Q
בצעו מעקב סבב אחר סבב, ורשמו את המסלול ואת אורכו (סכום משקליו).
עבור גרף מכוון G ממושקל כלשהו שאין בו מעגל שמשקלו שלילי, הרצנו את האלגוריתם המוצא את המסלול הקצר (הקל) מקודקוד s לקודקוד t , וקיבלנו מסלול P .
– האם P יישאר בהכרח מסלול קצר ביותר בין s ל־t אם משקל כל אחת מן הקשתות היוצאות מ־s יגדל ב־ x (0 < x) ? נמקו את תשובתכם.
– האם P יישאר בהכרח מסלול קצר ביותר בין s ל־t אם משקל כל אחת מן הקשתות בגרף יקטן ב־ x (0 < x) ? נמקו את תשובתכם.
לפניכם גרף מכוון G :

(1) הציגו שתי סריקות DFS מקודקוד התחלה a כך שיתקבלו שני עצים שמספר העלים בהם שונה.
הוסיפו מספר מינימלי של קשתות כך שבשלוש סריקות DFS מקודקוד a , יתקבלו שלושה עצים שמספרי העלים בהם שונים זה מזה. סרטטו את הגרף לאחר ההוספה, והציגו את שלוש הסריקות.
◂ פתרון מלא לשאלה 5
סעיף א(1)
אלגוריתם בלמן־פורד, O(V·E). אחרי סבב 1: P=4, Q=3; סבב 2: R=9, P=1, T=4; סבב 3: R=6; סבב 4: אין שינוי. המסלול: S→Q→T, אורכו 4.
מה נדרש: לבחור אלגוריתם למסלול קל ביותר בגרף עם משקלים שליליים (בלי מעגל שלילי), לתת את הסיבוכיות, ולהריץ אותו על G₁ בסדר הקשתות הנתון.
רעיון: אלגוריתם בלמן־פורד: מאתחלים d[S] = 0 ושאר הערכים ∞, ומבצעים V−1 סבבים. בכל סבב עוברים על כל הקשתות ומקלים כל קשת u→v: אם d[u] + w(u,v) < d[v] אז d[v] ← d[u] + w(u,v) ו־π[v] ← u. מסלול קל ביותר (בלי מעגל שלילי) הוא פשוט ויש בו לכל היותר V−1 קשתות, ואחרי הסבב ה־i כל מסלול קל ביותר עם i קשתות לכל היותר כבר נמצא. (דייקסטרה אינו מתאים לגרף "כלשהו" עם משקלים שליליים: הוא מקבע ערך של קודקוד כשהוא נשלף, וקשת שלילית שנבדקת אחר כך עלולה להקטין אותו.)
סיבוכיות: V−1 סבבים, ובכל סבב מקלים את כל E הקשתות, כל הקלה O(1): O(V·E). (סבב נוסף לבדיקת מעגל שלילי מוסיף O(E).)
פתרון – מעקב על G₁ (V = 5, ולכן 4 סבבים). בכל תא: d (π) בסוף הסבב.
| סבב | S | P | Q | R | T | הקלות שעדכנו |
|---|---|---|---|---|---|---|
| אתחול | 0 | ∞ | ∞ | ∞ | ∞ | – |
| 1 | 0 | 4 (S) | 3 (S) | ∞ | ∞ | S→P: 0+4 = 4 ; S→Q: 0+3 = 3 |
| 2 | 0 | 1 (Q) | 3 (S) | 9 (P) | 4 (Q) | P→R: 4+5 = 9 ; Q→P: 3−2 = 1 ; Q→T: 3+1 = 4 |
| 3 | 0 | 1 (Q) | 3 (S) | 6 (P) | 4 (Q) | P→R: 1+5 = 6 |
| 4 | 0 | 1 (Q) | 3 (S) | 6 (P) | 4 (Q) | אין |
הסברים:
שחזור המסלול לפי π: T ← Q ← S.
המסלול: S → Q → T, אורכו 3 + 1 = 4.
בדיקה: בלמן־פורד באותו סדר קשתות נותן בדיוק את הטבלה, ובסבב נוסף אין שיפור (אין מעגל שלילי). כל המסלולים מ־S ל־T: SQT = 4, SQPRT = 5, SPRT = 8, SQRT = 8 – והמינימום 4 יחיד.
סעיף א(2)
הגדלת הקשתות היוצאות מ־s: כן – כל מסלול פשוט מ־s ל־t משתמש בדיוק בקשת אחת שיוצאת מ־s, ולכן כל המסלולים גדלים באותו x. הקטנת כל הקשתות: לא בהכרח – מסלול עם k קשתות קטן ב־k·x. למשל ב־G₁ עם x = 1: S→Q→T שוקל 2, אבל S→Q→P→R→T שוקל 1.
מה נדרש: לבדוק אם P נשאר קצר ביותר אחרי (א) הוספת x לקשתות היוצאות מ־s, (ב) הפחתת x מכל הקשתות.
רעיון: נבדוק איך משתנה משקל של מסלול שלם בכל אחד מהשינויים. כשאין מעגל שלילי, מספיק להשוות מסלולים פשוטים (בלי חזרה על קודקוד).
פתרון:
הגדלת הקשתות היוצאות מ־s – כן, P נשאר קצר ביותר. מסלול פשוט מ־s ל־t יוצא מ־s בקשת הראשונה שלו ולא חוזר ל־s לעולם. לכן הוא משתמש בדיוק בקשת אחת שיוצאת מ־s, ומשקלו גדל בדיוק ב־x. כל המסלולים הפשוטים גדלים באותו x, והסדר ביניהם לא משתנה. (המשקלים רק גדלים, ולכן גם לא נוצר מעגל שלילי.) P נשאר מסלול קצר ביותר, ואורכו גדל ב־x.
הקטנת כל הקשתות ב־x – לא בהכרח. מסלול עם k קשתות קטן ב־k·x, ולכן מסלול עם הרבה קשתות "מרוויח" יותר ממסלול עם מעט קשתות. (בגרף עם מעגלים, הקטנה כזאת יכולה אפילו ליצור מעגל שלילי, ואז אין מסלול קצר ביותר בכלל.)
דוגמה נגדית – הגרף G₁ מסעיף (1), x = 1:
לפני השינוי S→Q→T היה הקצר ביותר (4 < 5), ואחרי השינוי S→Q→P→R→T קל יותר.
בדיקה: הגדלת הקשתות היוצאות מ־S ב־G₁ ב־x נותנת תמיד את S→Q→T באורך 4+x. אחרי הפחתת 1 מכל הקשתות בלמן־פורד החזיר S→Q→P→R→T באורך 1.
טעות נפוצה: לחשוב שאפשר "לתקן" משקלים שליליים על ידי הוספת אותו מספר לכל הקשתות ואז להריץ דייקסטרה. זה משנה את המסלולים, מאותה סיבה בדיוק.
סעיף ב(1)
סריקה 1: a→b→d, ואחר כך a→c, a→e – 3 עלים (d, c, e). סריקה 2: a→c→b→d, ואחר כך a→e – 2 עלים (d, e).
מה נדרש: שתי סריקות DFS מ־a שנותנות עצים עם מספר עלים שונה (עלה = קודקוד בלי בנים בעץ).
רעיון: הקשתות בגרף: a→b , a→c , a→e , c→b , b→d , e→d. אם b מתגלה דרך c, אז c אינו עלה; אם b מתגלה ישירות מ־a – c נשאר עלה.
פתרון:
סריקה 1 (סדר השכנים של a: b, c, e):
a → b → d (ל־d אין קשתות יוצאות, חוזרים עד a) → c (הקשת c→b מובילה לקודקוד שכבר התגלה) → e (הקשת e→d מובילה לקודקוד שכבר התגלה).
קשתות העץ: a–b , b–d , a–c , a–e. העלים: d, c, e – 3 עלים.
סריקה 2 (סדר השכנים של a: c, b, e):
a → c → b → d (חוזרים עד a) → הקשת a→b מובילה לקודקוד שכבר התגלה → e (e→d כבר התגלה).
קשתות העץ: a–c , c–b , b–d , a–e. העלים: d, e – 2 עלים.
בדיקה: יש 4 עצי DFS מ־a: מספרי העלים בהם הם בדיוק 2 ו־3, ושני העצים שלמעלה נמצאים ביניהם.
סעיף ב(2)
מספיק להוסיף קשת אחת, a→d. מספרי עלים: 4 (a→d, a→b, a→c, a→e), 3 (a→b→d, a→c, a→e), 2 (a→c→b→d, a→e).
מה נדרש: להוסיף כמה שפחות קשתות כך שיהיו שלוש סריקות DFS מ־a עם שלושה מספרי עלים שונים.
רעיון: קודם נראה שבגרף המקורי מתקבלים רק 2 או 3 עלים, ולכן צריך להוסיף לפחות קשת אחת. אחר כך נראה שקשת אחת מספיקה.
בעץ DFS מ־a יש 4 קודקודים מלבד השורש. נבדוק מי מהם יכול להיות אב (לא עלה):
כלומר מספר הקודקודים שאינם עלים (מלבד a) הוא 1 או 2, ומספר העלים הוא 4−1 = 3 או 4−2 = 2. צריך להוסיף לפחות קשת אחת.
פתרון: מוסיפים את הקשת a→d (מקווקוות באדום):

שלוש סריקות מ־a:
| סריקה | סדר השכנים של a | מהלך | קשתות העץ | עלים |
|---|---|---|---|---|
| 1 | d, b, c, e | a→d; a→b (b→d כבר התגלה); a→c (c→b כבר התגלה); a→e (e→d כבר התגלה) | a–d , a–b , a–c , a–e | 4 |
| 2 | b, c, e, d | a→b→d; a→c (c→b כבר התגלה); a→e (e→d כבר התגלה); d כבר התגלה | a–b , b–d , a–c , a–e | 3 |
| 3 | c, e, … | a→c→b→d; חוזרים; a→e (e→d כבר התגלה); שאר השכנים כבר התגלו | a–c , c–b , b–d , a–e | 2 |
מספר הקשתות שהוספנו: 1 – וזה המינימום. (מתאימות גם b→e , d→e או e→c – כל אחת מהן מאפשרת עץ שהוא שרשרת עם עלה אחד, למשל a→c→b→d→e אחרי הוספת d→e.)
בדיקה: בגרף המקורי יש רק 2 ו־3 עלים; מבין ההוספות של קשת אחת: a→d נותנת {2, 3, 4}, ו־b→e , d→e , e→c נותנות {1, 2, 3}. שלושת העצים שבטבלה נמצאו בין עצי ה־DFS של הגרף החדש.
שאלה 6 · קשתות קלות ועץ פורש מינימלי של רכיב הקשירות הגדול (25 נקודות)
נתון גרף ממושקל, לא קשיר ולא מכוון G(V, E) שבו n קודקודים, מ־ v₀ עד vₙ₋₁ . משקלי הקשתות הם מספרים חיוביים.
הגרף מיוצג ברשימות סמיכויות: לכל קודקוד יש רשימה של שכניו, ולכל שכן – משקל הקשת אליו.
נתונים קודקוד vⱼ ומספר k . כתבו אלגוריתם המוצא ומחזיר את כל הקודקודים שיש ביניהם ובין vⱼ מסלול שמשקל כל אחת מן הקשתות בו הוא k לכל היותר.
הערה: יש לכתוב אלגוריתם יעיל שאינו עובר על כל המסלולים האפשריים.
דוגמה: עבור הגרף שלפניכם, הקודקוד 0 ו־ k = 4 , האלגוריתם יחזיר את הקודקודים 3 , 7 .

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

הניחו שיש רק רכיב קשירות אחד שהוא הגדול ביותר.
הערה: יש לכתוב אלגוריתם יעיל שאינו עובר על כל המסלולים האפשריים.
◂ פתרון מלא לשאלה 6
סעיף א
סריקה לרוחב (BFS) מ־ vⱼ שמתעלמת מקשתות שמשקלן גדול מ־ k : עוברים בקשת u – v רק אם w ≤ k . כל קודקוד שמתגלה (חוץ מ־ vⱼ) מוחזר. זמן ריצה O(|V| + |E|). בדוגמה: 3 , 7 .
מה נדרש: הקודקודים שאפשר להגיע אליהם מ־ vⱼ בלי לעבור באף קשת "כבדה" (משקל > k).
רעיון: אם מוחקים מהגרף את כל הקשתות שמשקלן גדול מ־ k , מבוקשים בדיוק הקודקודים הישיגים מ־ vⱼ בגרף שנשאר. אין צורך למחוק בפועל – מספיק ש־BFS ידלג על קשת כבדה.
פתרון:
LightReach(G, vⱼ, k):
לכל קודקוד u: visited[u] ← false
visited[vⱼ] ← true ; Q ← תור ריק ; Q.insert(vⱼ)
R ← רשימה ריקה
כל עוד Q אינו ריק:
u ← Q.remove()
לכל שכן v של u , כשמשקל הקשת u – v הוא w:
אם w ≤ k וגם visited[v] = false:
visited[v] ← true ; הוסף את v ל־R ; Q.insert(v)
החזר את Rזמן ריצה: כל קודקוד נכנס לתור פעם אחת לכל היותר, וכל רשימת סמיכויות נסרקת פעם אחת (הבדיקה w ≤ k היא O(1)) ⇒ O(|V| + |E|).
בדיקה – על הגרף שבדוגמה עם vⱼ = 0 , k = 4 (שכנים בסדר עולה):
| צעד | הוצא מהתור | קשתות שנבדקו (משקל) | חדשים שסומנו | התור אחרי הצעד | R |
|---|---|---|---|---|---|
| 0 | – | – | 0 (המקור) | [0] | {} |
| 1 | 0 | 0–3 (4), 0–5 (8 > 4, מדלגים), 0–7 (6 > 4, מדלגים) | 3 | [3] | {3} |
| 2 | 3 | 3–0 (4), 3–7 (2), 3–9 (7 > 4, מדלגים) | 7 | [7] | {3, 7} |
| 3 | 7 | 7–0 (6 > 4, מדלגים), 7–3 (2), 7–9 (5 > 4, מדלגים) | – | [] | {3, 7} |
התוצאה {3, 7} – כמו בדוגמה. עם k = 5 היו מתקבלים 3 , 5 , 7 , 9 (דרך 7 – 9 במשקל 5 ו־ 9 – 5 במשקל 1).
טעות נפוצה: לבדוק רק את הקשתות היוצאות מ־ vⱼ – הקודקוד 7 מחובר ל־0 בקשת במשקל 6 , אבל הוא ישיג דרך 3 בקשתות 4 ו־ 2 .
סעיף ב
שלב 1: BFS מכל קודקוד שעוד לא סומן מוצא את רכיבי הקשירות, ושומרים את הגדול ביותר – O(|V| + |E|). שלב 2: האלגוריתם של פרים מקודקוד כלשהו ברכיב זה (פרים מגיע רק לקודקודי הרכיב) – O(|V|²) במימוש פשוט. מחזירים את |רכיב| − 1 הקשתות שנבחרו. בדוגמה: (0, 3) , (3, 7) , (7, 9) , (9, 5) , משקל 12.
מה נדרש: למצוא את רכיב הקשירות עם הכי הרבה קודקודים, ולבנות לו עץ פורש מינימלי.
רעיון:
פתרון:
MstOfLargest(G):
// שלב 1: מציאת רכיב הקשירות הגדול ביותר
לכל קודקוד u: visited[u] ← false
best ← קבוצה ריקה
לכל קודקוד s מ־v₀ עד vₙ₋₁:
אם visited[s] = false:
comp ← BFS(s) // מסמן ב־visited ומחזיר את כל הקודקודים שהתגלו
אם |comp| > |best|: best ← comp
// שלב 2: האלגוריתם של פרים בתוך best
r ← קודקוד כלשהו ב־best
לכל קודקוד v: key[v] ← ∞ ; parent[v] ← null ; inTree[v] ← false
key[r] ← 0 ; T ← רשימה ריקה
חזור |best| פעמים:
u ← הקודקוד שאינו בעץ וה־key שלו הקטן ביותר
inTree[u] ← true
אם parent[u] ≠ null: הוסף את הקשת (parent[u], u) ל־T
לכל שכן v של u , כשמשקל הקשת u – v הוא w:
אם inTree[v] = false וגם w < key[v]:
key[v] ← w ; parent[v] ← u
החזר את T(אפשר גם להריץ את קרוסקל רק על הקשתות שבתוך best: למיין אותן לפי משקל ולהוסיף כל קשת שאינה סוגרת מעגל.)
זמן ריצה: שלב 1 – כל הסריקות יחד O(|V| + |E|). שלב 2 – |best| ≤ |V| איטרציות, בכל אחת חיפוש המינימום O(|V|) ועדכון השכנים; סך עדכוני השכנים O(|E|) ⇒ O(|V|² + |E|) = O(|V|²) (או O(|E|·log|V|) עם ערימה).
בדיקה – על הגרף שבדוגמה. שלב 1: BFS מ־0 מוצא {0, 3, 5, 7, 9} (5 קודקודים), BFS מ־1 מוצא {1, 4, 8} (3), BFS מ־2 מוצא {2, 6} (2). הגדול: {0, 3, 5, 7, 9}. שלב 2: פרים מ־ r = 0 (בעמודה האחרונה – key החדש של שכן, ובסוגריים ה־parent שלו):
| צעד | קודקוד שנוסף לעץ | הקשת שנוספה | עדכוני key (הורה) |
|---|---|---|---|
| 1 | 0 | – | 3: 4 (0), 5: 8 (0), 7: 6 (0) |
| 2 | 3 | 0–3 (4) | 7: 2 (3), 9: 7 (3) |
| 3 | 7 | 3–7 (2) | 9: 5 (7) |
| 4 | 9 | 7–9 (5) | 5: 1 (9) |
| 5 | 5 | 9–5 (1) | – |
הקשתות שהוחזרו: (0, 3) , (3, 7) , (7, 9) , (9, 5) – משקל כולל 4 + 2 + 5 + 1 = 12 , כמו בדוגמה (אותן קשתות, בסדר אחר). זה העץ הפורש המינימלי היחיד של הרכיב.
טעות נפוצה: להריץ פרים על הגרף כולו ולצפות לעץ אחד – הגרף אינו קשיר, ופרים מקודקוד אחד לעולם לא יגיע לרכיבים האחרים (ואם בוחרים "קודקוד כלשהו" שאינו ברכיב הגדול – מקבלים עץ של רכיב אחר).
פרק שני (50 נקודות) – מודלים חישוביים
יש לענות על שתי שאלות במסלול שלמדתם (לכל שאלה – 25 נקודות).
שאלה 7 · טענות על שפות ואוטומט סופי דטרמיניסטי שאינו מלא (25 נקודות)
בשאלה זו שני סעיפים, א–ב, שאין ביניהם קשר. ענו על שני הסעיפים.
לפניכם שתי טענות (1)–(2) בנוגע לשפות שמעל הא"ב {a, b} .
בעבור כל טענה, ציינו אם היא נכונה או לא נכונה. אם הטענה נכונה – נמקו מדוע,
ואם היא לא נכונה – הביאו דוגמה נגדית. אין קשר בין הטענות.
(1) אם L₁ היא שפה רגולרית ו־ L₂ היא שפה לא רגולרית, בהכרח L₁ ∪ L₂ היא שפה לא רגולרית.
לכל שפה L מתקיים: L* • L* = L* .
נתונה השפה L מעל הא"ב {a, b, c} :
L = {w | הרצפים ba , cc אינם מופיעים ב־ w , #b(w) + #c(w) = מספר אי־זוגי }
#b(w) מציין את מספר המופעים של b במילה w .
#c(w) מציין את מספר המופעים של c במילה w .
דוגמה למילה בשפה L היא abcab , כי סכום המופעים של האות b (2) והאות c (1) הוא מספר אי־זוגי (3) , והרצפים ba ו־ cc אינם מופיעים במילה.
(1) לפניכם חמש מילים. בנוגע לכל אחת מהן ציינו אם המילה שייכת לשפה L או לא. נמקו את תשובתכם.
aabcb , bba , ε , bcb , cab .
בנו אוטומט סופי דטרמיניסטי שאינו מלא המקבל את השפה L .
◂ פתרון מלא לשאלה 7
סעיף א(1)
לא נכונה. L₁ = Σ* = {a, b}* (רגולרית), L₂ = {aⁿbⁿ | n ≥ 0} (לא רגולרית), ו־ L₁ ∪ L₂ = Σ* – שפה רגולרית.
מה נדרש: לבדוק אם איחוד של שפה רגולרית עם שפה לא רגולרית הוא בהכרח לא רגולרי.
רעיון: אם השפה הרגולרית "בולעת" את השפה הלא רגולרית (L₂ ⊆ L₁), האיחוד שווה ל־ L₁ ולכן רגולרי.
פתרון: הטענה לא נכונה. דוגמה נגדית:
בדיקה: מילים של L₂ עד אורך 8 – ε, ab, aabb, aaabbb, aaaabbbb – כולן ב־ Σ*, והאיחוד הוא Σ*.
טעות נפוצה: לחשוב ש"שפה לא רגולרית מקלקלת כל איחוד". האיחוד יכול להיות רגולרי כשהחלק הלא רגולרי מוכל בחלק הרגולרי.
סעיף א(2)
נכונה. ⊇: כל w ∈ L* שווה ל־ w • ε, ו־ ε ∈ L*. ⊆: אם x, y ∈ L* אז x הוא שרשור של k מילים מ־ L ו־ y של m מילים מ־ L, ולכן xy הוא שרשור של k + m מילים מ־ L, כלומר xy ∈ L*.
מה נדרש: להוכיח או להפריך ששרשור של L* עם עצמה שווה ל־ L*.
רעיון: מראים הכלה בשני הכיוונים. L* היא קבוצת כל השרשורים של 0 או יותר מילים מ־ L (כולל ε).
פתרון: הטענה נכונה.
משתי ההכלות: L* • L* = L*.
בדיקה: למשל עבור L = {a}: גם L* וגם L* • L* הן כל המילים מעל {a}. למשל L = {ab}: L* = {ε, ab, abab, …}, ו־ ab • abab = ababab ∈ L*.
סעיף ב(1)
aabcb ∈ L ; bcb ∈ L ; bba ∉ L (מכילה ba) ; ε ∉ L (0 – זוגי) ; cab ∉ L (1 + 1 = 2 – זוגי).
מה נדרש: לבדוק לכל מילה שני תנאים: (1) אין בה את הרצפים ba ו־ cc; (2) #b(w) + #c(w) אי־זוגי.
פתרון:
| מילה | רצף ba או cc? | #b + #c | שייכת ל־ L? |
|---|---|---|---|
| aabcb | לא (aa, ab, bc, cb) | 2 + 1 = 3 – אי־זוגי | כן |
| bba | כן – מופיע ba | 2 + 0 = 2 | לא (בגלל ba, וגם הסכום זוגי) |
| ε | לא (אין אותיות) | 0 + 0 = 0 – זוגי | לא |
| bcb | לא (bc, cb) | 2 + 1 = 3 – אי־זוגי | כן |
| cab | לא (ca, ab) | 1 + 1 = 2 – זוגי | לא |
בדיקה: הגדרת השפה והאוטומט מסעיף ב(2) נותנים בדיוק את התשובות האלה.
טעות נפוצה: לקבל את ε בגלל שאין בה רצפים אסורים – אבל 0 הוא מספר זוגי, ולכן ε ∉ L.
סעיף ב(2)
6 מצבים (זוגיות של #b+#c × סוג האות האחרונה): q0 (התחלה), q1, …, q5; מקבלים: q1, q3, q5. מעברים: q0: a→q0, b→q3, c→q5 ; q1: a→q1, b→q2, c→q4 ; q2: b→q3, c→q5 ; q3: b→q2, c→q4 ; q4: a→q0, b→q3 ; q5: a→q1, b→q2 (מעבר חסר = דחייה).
מה נדרש: אוטומט סופי דטרמיניסטי עבור L, שמותר שיחסרו בו מעברים (מעבר חסר = המילה נדחית).
רעיון: האוטומט צריך לזכור שני דברים:
לכן 3 × 2 = 6 מצבים. מעבר שיוצר רצף אסור אינו מוגדר (במקום מצב בור) – לכן האוטומט אינו מלא.
פתרון – Σ = {a, b, c}, מצב התחלתי q0, מצבים מקבלים {q1, q3, q5}:
| מצב | משמעות | a | b | c |
|---|---|---|---|---|
| → q0 | זוגי; התחלה או האות האחרונה a | q0 | q3 | q5 |
| q1 (מקבל) | אי־זוגי; האות האחרונה a | q1 | q2 | q4 |
| q2 | זוגי; האות האחרונה b | – | q3 | q5 |
| q3 (מקבל) | אי־זוגי; האות האחרונה b | – | q2 | q4 |
| q4 | זוגי; האות האחרונה c | q0 | q3 | – |
| q5 (מקבל) | אי־זוגי; האות האחרונה c | q1 | q2 | – |
("–" = אין מעבר, המילה נדחית.)
הסבר המעברים: a מעביר ל"אחרון a" בלי לשנות זוגיות (חסר אחרי b); b מעביר ל"אחרון b" ומחליף זוגיות; c מעביר ל"אחרון c" ומחליף זוגיות (חסר אחרי c).
בדיקה:
האוטומט מקבל בדיוק את המילים של השפה – כך עולה גם מבדיקה של כל המילים הקצרות מעל {a, b, c}.
טעות נפוצה: לסמן את q0 כמקבל – בהתחלה הסכום הוא 0 (זוגי), ולכן q0 אינו מקבל.
שאלה 8 · אוטומט מחסנית דטרמיניסטי – השלמת מעברים (25 נקודות)
נתונה השפה L מעל הא"ב {a, b, c} :
L = {aⁿ bᵐ cᵏ |
}
(בשני המקרים m ≥ 0 ו־ k ≥ 0 .)
דוגמה למילה בשפה L בעבור n זוגי: aaaa bbbb cc (n=4 , m=4 , k=2) .
הסבר: n%2 = 0 , ובהתאם לכך מתקיים m = n = 4 , k%2 = 0 וגם n > 0 .
דוגמה למילה בשפה L בעבור n אי־זוגי: aaa bb ccc (n=3 , m=2 , k=3) .
הסבר: n%2 = 1 , ובהתאם לכך מתקיים k = n = 3 וגם m%2 = 0 .
לפניכם חמש מילים. בעבור כל אחת מהן, ציינו אם המילה שייכת לשפה L . נמקו את תשובתכם.
aabbc, abbbbc, aaaabbcc, aabbcccc, aaabbbbccc .
כתבו את המילה הקצרה ביותר בשפה L בעבור n זוגי, ואת המילה הקצרה ביותר בשפה L בעבור n אי־זוגי שיש בה לפחות אות b אחת.
לפניכם אוטומט מחסנית דטרמיניסטי המקבל את השפה L . באוטומט קיימים כל המצבים, המעברים וסימני הקלט הנדרשים. אולם מלבד המעברים בין q₀ ל־ q₁ , חסרות במעברים האות שבראש המחסנית והפעולה על המחסנית (מעברים אלה ממוספרים מ־ 1 עד 14). נוסף על כך, המצבים המקבלים באוטומט לא סומנו.
בעבור כל מעבר ממוספר, ציינו את מספרו והשלימו את הסימון החסר: האות שבראש המחסנית והפעולה שעל המחסנית (דחוף/push או שלוף/pop או ללא שינוי). נוסף על כך, כתבו מה הם המצבים המקבלים באוטומט (אין צורך להעתיק את האוטומט למחברתכם).

◂ פתרון מלא לשאלה 8
סעיף א
aabbc – לא שייכת; abbbbc – שייכת; aaaabbcc – לא שייכת; aabbcccc – שייכת; aaabbbbccc – שייכת.
מה נדרש: לבדוק חמש מילים מול הגדרת L.
רעיון: מחשבים n, m, k, בודקים את זוגיות n ומפעילים את התנאי המתאים.
פתרון:
| מילה | n | m | k | n זוגי? | בדיקה | שייכת? |
|---|---|---|---|---|---|---|
| aabbc | 2 | 2 | 1 | כן | m = n ✓ , אבל k = 1 אי־זוגי ✗ | לא |
| abbbbc | 1 | 4 | 1 | לא | k = n = 1 ✓ , m = 4 זוגי ✓ | כן |
| aaaabbcc | 4 | 2 | 2 | כן | m = 2 ≠ n = 4 ✗ | לא |
| aabbcccc | 2 | 2 | 4 | כן | m = n ✓ , k = 4 זוגי ✓ , n > 0 ✓ | כן |
| aaabbbbccc | 3 | 4 | 3 | לא | k = n = 3 ✓ , m = 4 זוגי ✓ | כן |
בדיקה: לפי הגדרת השפה: aabbc ✗, abbbbc ✓, aaaabbcc ✗, aabbcccc ✓, aaabbbbccc ✓ – וכך גם האוטומט מסעיף ג.
סעיף ב
n זוגי: aabb . n אי־זוגי (עם b): abbc .
מה נדרש: המילה הקצרה ביותר בכל אחד מהמקרים.
פתרון:
בדיקה: בין המילים עד אורך 8: הקצרה ביותר עם n זוגי – aabb בלבד; הקצרה ביותר עם n אי־זוגי שיש בה b – abbc בלבד.
סעיף ג
(1) b,A/שלוף (2) b,A/שלוף (3) b,S/שלוף (4) c,ריקה/ללא שינוי (5) c,ריקה/ללא שינוי (6) b,A/ללא שינוי ; S/ללא שינוי (7) b,A/ללא שינוי ; S/ללא שינוי (8) b,A/ללא שינוי ; S/ללא שינוי (9) c,A/שלוף (10) c,A/שלוף (11) c,A/שלוף (12) c,S/שלוף (13) c,S/שלוף (14) c,S/שלוף. מצבים מקבלים: q₃ , q₈ .
מה נדרש: להשלים לכל מעבר ממוספר את ראש המחסנית והפעולה, ולציין את המצבים המקבלים.
רעיון: המעברים בין q₀ ל־q₁ נתונים: אחרי n אותיות a המחסנית היא S A…A (n סימנים, ראש המחסנית מימין), והאוטומט ב־q₀ אם n זוגי וב־q₁ אם n אי־זוגי. (כש־n = 1 ראש המחסנית הוא S.)
ענף n זוגי (q₀ → q₂ → q₃ ⇄ q₄): צריך m = n , ואחר כך מספר זוגי של c.
ענף n אי־זוגי (q₁ , q₅ , q₆ , q₇ , q₈): ה־b לא משנים את המחסנית, רק את הזוגיות: q₅ = מספר אי־זוגי של b , q₆ = מספר זוגי (לפחות 2). המעברים 6–8 צריכים לפעול גם כש־A בראש וגם כש־S בראש (n = 1), ולכן על כל אחד מהם שני סימונים.
פתרון: (ראש המחסנית / פעולה)
| מעבר | מ־ → אל | אות | ראש המחסנית / פעולה |
|---|---|---|---|
| (1) | q₀ → q₂ | b | A / שלוף |
| (2) | q₂ → q₂ | b | A / שלוף |
| (3) | q₂ → q₃ | b | S / שלוף |
| (4) | q₃ → q₄ | c | ריקה / ללא שינוי |
| (5) | q₄ → q₃ | c | ריקה / ללא שינוי |
| (6) | q₁ → q₅ | b | A / ללא שינוי ; S / ללא שינוי |
| (7) | q₅ → q₆ | b | A / ללא שינוי ; S / ללא שינוי |
| (8) | q₆ → q₅ | b | A / ללא שינוי ; S / ללא שינוי |
| (9) | q₁ → q₇ | c | A / שלוף |
| (10) | q₆ → q₇ | c | A / שלוף |
| (11) | q₇ → q₇ | c | A / שלוף |
| (12) | q₁ → q₈ | c | S / שלוף |
| (13) | q₆ → q₈ | c | S / שלוף |
| (14) | q₇ → q₈ | c | S / שלוף |
מצבים מקבלים: q₃ , q₈ .
האוטומט דטרמיניסטי: מכל מצב, לכל זוג (אות קלט, ראש מחסנית) יש לכל היותר מעבר אחד.
בדיקה:
השלמה חלופית: אפשר גם לרשום במעבר 3 "S / ללא שינוי" (ה־S נשאר בתחתית), ואז במעברים 4 ו־5 בראש המחסנית יופיע S (S / ללא שינוי). גם השלמה זו מקבלת בדיוק את L. באופן כללי, כל השלמה שמקבלת בדיוק את L נכונה.
טעות נפוצה: לערבב בין שתי ההשלמות – S / ללא שינוי במעבר 3 אבל "ריקה" במעברים 4–5: אז aabbcc נדחית. בענף האי־זוגי – לשכוח את הסימון S במעברים 6–8 (אז abbc נדחית).
שאלה 9 · אוטומט מחסנית דטרמיניסטי ושרשור עם שפה הפוכה (25 נקודות)
א. נתונה השפה L₁ מעל הא"ב {a, b, c} :
L₁ = {aⁿ bᵏ aᵏ c²ⁿ | n , k ≥ 1}
מהי המילה הקצרה ביותר בשפה L₁ ?
בנו אוטומט מחסנית דטרמיניסטי המקבל את השפה L₁ .
נגדיר את השפה L₂ מעל הא"ב {a, b, c} :
L₂ = R(L₁) • L₁
בעבור כל אחת מן המילים שלפניכם, ציינו אם המילה שייכת לשפה L₂ , ונמקו.
(1) ccabaabacc
(2) ccabaabaacc
(3) ccccabaaabbaacc
הערה: אין צורך לבנות אוטומט בעבור השפה L₂ .
◂ פתרון מלא לשאלה 9
סעיף א(1)
abacc
מה נדרש: המילה הקצרה ביותר ב־L₁.
רעיון:
אורך המילה הוא n + k + k + 2n = 3n + 2k. הוא הקטן ביותר כש־n ו־k קטנים ככל האפשר, כלומר n = k = 1.
פתרון:
n = 1, k = 1: a¹ b¹ a¹ c² = abacc (אורך 5). כל בחירה אחרת של n, k ≥ 1 נותנת אורך 7 לפחות, ולכן זו המילה הקצרה ביותר והיחידה באורך זה.
בדיקה: המילים של L₁ עד אורך 9 הן abacc, abbaacc, aabacccc, abbbaaacc – הקצרה ביותר abacc.
טעות נפוצה: לבחור n = 0 או k = 0 (למשל המילה הריקה או aacc) – בשפה n, k ≥ 1.
סעיף א(2)
דוחפים סימן לכל a בגוש הראשון (התחתון X) ו־B לכל b; כל a שבא אחרי ה־b שולף B; כל זוג c שולף סימן אחד; ה־c ששולף את X עובר למצב המקבל q6. הטבלה המלאה בפתרון.
מה נדרש: אוטומט מחסנית דטרמיניסטי ל־L₁ = {aⁿ bᵏ aᵏ c²ⁿ | n, k ≥ 1}.
רעיון:
פתרון:
| מצב | קלט | ראש המחסנית | מצב חדש | מחליפים את הראש ב־ | משמעות |
|---|---|---|---|---|---|
| q0 | a | ⊥ | q1 | X⊥ | ה־a הראשון: X מסמן את תחתית הספירה |
| q1 | a | X | q1 | AX | עוד a: דוחפים A |
| q1 | a | A | q1 | AA | עוד a: דוחפים A |
| q1 | b | X | q2 | BX | ה־b הראשון: דוחפים B |
| q1 | b | A | q2 | BA | ה־b הראשון: דוחפים B |
| q2 | b | B | q2 | BB | עוד b: דוחפים B |
| q2 | a | B | q3 | ε | a אחרי ה־b: שולפים B |
| q3 | a | B | q3 | ε | עוד a: שולפים B |
| q3 | c | A | q4 | A | c ראשון בזוג: בלי שינוי |
| q3 | c | X | q4 | X | c ראשון בזוג: בלי שינוי |
| q4 | c | A | q5 | ε | c שני בזוג: שולפים A |
| q4 | c | X | q6 | ε | c שני בזוג שולף את X – המחסנית ריקה |
| q5 | c | A | q4 | A | c ראשון בזוג הבא |
| q5 | c | X | q4 | X | c ראשון בזוג הבא |
בכל צירוף (מצב, אות, ראש מחסנית) יש לכל היותר מעבר אחד ואין מעברי ε – האוטומט דטרמיניסטי. צירוף שאינו בטבלה = דחייה (למשל b אחרי שהתחיל הגוש השני של a; a נוסף כשבראש המחסנית כבר A או X – יותר a מ־b; c כשבראש עדיין B – פחות a מ־b; כל אות אחרי q6).
מעקב על aabacccc (n = 2, k = 1):
| נקרא | מצב | מחסנית (ראש משמאל) |
|---|---|---|
| – | q0 | ⊥ |
| a | q1 | X⊥ |
| a | q1 | AX⊥ |
| b | q2 | BAX⊥ |
| a | q3 | AX⊥ |
| c | q4 | AX⊥ |
| c | q5 | X⊥ |
| c | q4 | X⊥ |
| c | q6 | ⊥ |
הקלט נגמר במצב q6 ⇒ מתקבלת.
בדיקה: האוטומט מקבל בדיוק את המילים של L₁ – גם מילים של L₁ וגם מילים שמתקבלות מהן בשינוי, הוספה או מחיקה של אות אחת מטופלות נכון. למשל abacc, abbaacc, aabacccc מתקבלות; abaccc (מספר אי־זוגי של c), abbacc (פחות a מ־b), aabaccc נדחות.
טעות נפוצה: לדחוף סימן גם על ה־a שבגוש השני – אז אין דרך להשוות אותם ל־b. ה־a השניים צריכים לשלוף.
סעיף ב
(1) שייכת: ccaba · abacc. (2) לא שייכת. (3) שייכת: ccccabaa · abbaacc.
מה נדרש: לקבוע לכל מילה אם אפשר לחלק אותה לשני חלקים: הראשון ב־R(L₁) והשני ב־L₁.
רעיון:
ההיפוך של aⁿ bᵏ aᵏ c²ⁿ הוא c²ⁿ aᵏ bᵏ aⁿ, ולכן R(L₁) = {c²ⁿ aᵏ bᵏ aⁿ | n, k ≥ 1}. לכן מילה ב־L₂ נראית כך: c²ⁿ aᵏ bᵏ aⁿ ואחריה aᵐ bʲ aʲ c²ᵐ (n, k, m, j ≥ 1). מספר ה־c בהתחלה קובע את n, מספר ה־a שאחריהם קובע את k – ולכן החלק הראשון נקבע באופן יחיד, ונשאר לבדוק את השארית.
פתרון:
(1) ccabaabacc: בהתחלה 2 c ⇒ n = 1; אחריהן a אחד ⇒ k = 1; אז b אחד ו־a אחד (aⁿ, n = 1). החלק הראשון: ccaba = R(abacc) ∈ R(L₁). השארית: abacc = a¹ b¹ a¹ c² ∈ L₁ (m = j = 1). ⇒ שייכת ל־L₂.
(2) ccabaabaacc: שוב 2 c ⇒ n = 1, ואחריהן a אחד ⇒ k = 1, b אחד, a אחד: החלק הראשון חייב להיות ccaba (אין חלוקה אחרת). השארית: abaacc – a¹ b¹ ואז aa: ב־L₁ אחרי b¹ באים בדיוק a¹ ואז c, אבל כאן באים שני a. השארית אינה ב־L₁. ⇒ לא שייכת ל־L₂.
(3) ccccabaaabbaacc: 4 c ⇒ n = 2; a אחד ⇒ k = 1; b אחד; ואז aⁿ = aa. החלק הראשון: ccccabaa = R(aabacccc) ∈ R(L₁). השארית: abbaacc = a¹ b² a² c² ∈ L₁ (m = 1, j = 2). ⇒ שייכת ל־L₂.
בדיקה: בכל נקודות החלוקה האפשריות לכל מילה: (1) – חלוקה אחת ccaba | abacc; (2) – אין אף חלוקה; (3) – חלוקה אחת ccccabaa | abbaacc.
טעות נפוצה: להפוך את L₁ "בגושים" (c²ⁿ bᵏ aᵏ aⁿ) במקום להפוך את סדר כל האותיות: ההיפוך של aⁿ bᵏ aᵏ c²ⁿ הוא c²ⁿ aᵏ bᵏ aⁿ.
תכנות מונחה עצמים
יש לענות על שתי שאלות במסלול שלמדתם (לכל שאלה – 25 נקודות).
שאלה 10 · מוזיאון – היררכיית מוצגים (25 נקודות)
לשאלה זו שני נוסחים: בשפת Java ובשפת C# .
לפותרים בשפת Java
במוזיאון "אמנות לכולם" פותחה מערכת ממוחשבת ובה שומרים נתונים על המוצגים. המערכת כוללת את המחלקות האלה: מוצג – Exhibit , ציור – Painting , דיוקן (ציור של אדם) – Portrait , פסל – Statue , פסל ראש – Bust .
נתון חלק ממבנה המחלקות במערכת הממוחשבת:
public class Exhibit {
private String name;
private int year;
public Exhibit (String name, int year)
{
this.name = name;
this.year = year;
}
public String toString() {
return name + ", " + year;
}
}
public class Painting {
private String name;
private int year;
private String artist;
public void light () {
System.out.println ("Light on " + artist);
}
}
public class Portrait {
private String name;
private int year;
private String artist;
private String person; // מי מצויר בדיוקן
public void light () {
System.out.println ("Light on " + artist);
}
}
public class Statue {
private String name;
private int year;
private String material; // חומר: אבן/ברונזה/שיש
}
public class Bust {
private String name;
private int year;
private String material; // חומר: אבן/ברונזה/שיש
private int height; // גובה בסנטימטרים
}הניחו שיש פעולות get ו־ set לתכונות המחלקות.
כתבו מחדש את הכותרות, התכונות והפעולות של המחלקות: Painting , Portrait , Statue ו־ Bust , לפי עקרונות תכנות מונחה עצמים (אין לשנות את המחלקה Exhibit). בסעיף זה אין צורך לכתוב פעולות בונות. הניחו שהפעולות get ו־ set קיימות.
נוסף על כך, סרטטו תרשים הייררכייה בין כל המחלקות. יש לסמן ירושה באמצעות החץ ◁——— .
נתונה המחלקה Tester :
public class Tester {
public static void main (String[] args) {
Exhibit[] hall = new Exhibit [5];
hall [0] = new Portrait ("Queen", 1720, "Ruth", "Esther");
hall [1] = new Statue ("Rider", 1904, "bronze");
hall [2] = new Bust ("Poet", 1860, "marble", 55);
hall [3] = new Painting ("Garden", 1889, "Vincent");
hall [4] = new Bust ("Sailor", 1911, "stone", 30);
//***
}
}הוסיפו פעולות בונות למחלקות Painting , Portrait , Statue ו־ Bust , המאתחלות את תכונות המחלקות (כך שהפעולה main תרוץ ללא שגיאות).
הערה: אין לשנות את הפעולה הבונה הנתונה של Exhibit .
בפעולה main נוסף קטע הקוד שלפניכם (במקום המסומן ב־ *):
for (int i = 0; i < hall.length; i++) {
System.out.println (hall[i]); }קטע הקוד תקין ומדפיס את הפלט שלפניכם:
Queen, 1720 by Ruth
Statue: Rider, 1904 [bronze]
Large bust of Poet
Garden, 1889 by Vincent
Small bust of Sailorפסל ראש שגובהו 50 ס"מ לפחות נחשב גדול (Large), ופסל ראש נמוך יותר נחשב קטן (Small).
הוסיפו את הפעולה toString במחלקות (רק היכן שיש צורך) לפי עקרונות תכנות מונחה עצמים, כך שהפלט יהיה מתאים.
הערה: אין לשנות את הפעולה toString במחלקה Exhibit . תשובה שבה יהיה שינוי לא תזוכה בנקודות.
במחלקה Tester נוספה הפעולה שלפניכם:
public static void lightAll (Exhibit [] arr) {
for (int i = 0; i < arr.length; i++) {
arr[i].light(); }
}אם הפעולה תקינה, כתבו את הפלט שלה עבור המערך hall (הנתון לעיל), ואם הפעולה לא תקינה – תקנו אותה (כך שהקריאה ל־ light בתוך הלולאה תהיה תקינה) וכתבו את הפלט עבור המערך hall .
הערות:
◂ פתרון מלא לשאלה 10
סעיף א
Painting יורשת מ־Exhibit (artist, light); Portrait יורשת מ־Painting (person); Statue יורשת מ־Exhibit (material); Bust יורשת מ־Statue (height). התכונות name ו־year נשארות רק ב־Exhibit.
מה נדרש – לארגן את המחלקות בהיררכיית ירושה כך שכל תכונה וכל פעולה יופיעו פעם אחת בלבד, ולסרטט את תרשים ההייררכייה.
רעיון
כך מתקבלים שני "ענפים" מתחת ל־Exhibit: ציורים ופסלים.
פתרון – :
public class Painting extends Exhibit {
private String artist;
public void light() {
System.out.println("Light on " + artist);
}
}
public class Portrait extends Painting {
private String person; // מי מצויר בדיוקן
}
public class Statue extends Exhibit {
private String material; // חומר: אבן/ברונזה/שיש
}
public class Bust extends Statue {
private int height; // גובה בסנטימטרים
}תרשים ההייררכייה (החץ עם המשולש הריק מצביע מהמחלקה היורשת אל מחלקת הבסיס):

בדיקה – המחלקות המלאות (אותן כותרות ותכונות, יחד עם הפעולות הבונות מסעיף ב) תקינות ב־Java וב־C#.
טעות נפוצה: להוריש את Bust ישירות מ־Exhibit ולהשאיר בה את material – כפילות קוד; פסל ראש הוא פסל.
סעיף ב
Painting(name, year, artist) → super(name, year); Portrait(name, year, artist, person) → super(name, year, artist); Statue(name, year, material) → super(name, year); Bust(name, year, material, height) → super(name, year, material). ב־C#: base(...) במקום super(...).
מה נדרש – פעולות בונות שמתאימות לקריאות ב־main / Main ומאתחלות את כל התכונות.
רעיון
super(name, year) ב־Java, : base(name, year) ב־C#. ל־Exhibit אין פעולה בונה ללא פרמטרים, ולכן הקריאה המפורשת חובה.new Portrait ("Queen", 1720, "Ruth", "Esther") – שם, שנה, צייר, מי מצויר; new Bust ("Poet", 1860, "marble", 55) – שם, שנה, חומר, גובה (הפלט בסעיף ג1 מאשר ש־Ruth היא הציירת).פתרון – הקוד המלא בלשונית Java/C#.
בדיקה – המחלקות עם הפעולות הבונות ו־Tester הנתונה תקינות ב־Java וב־C#, והפעולה הראשית מתבצעת ללא שגיאות.
טעות נפוצה: לכתוב ב־Portrait this.artist = artist; – artist פרטית ב־Painting, ולכן זו שגיאת הידור; מעבירים אותה ב־super.
public class Painting extends Exhibit {
private String artist;
public Painting(String name, int year, String artist) {
super(name, year);
this.artist = artist;
}
public void light() {
System.out.println("Light on " + artist);
}
}
public class Portrait extends Painting {
private String person; // מי מצויר בדיוקן
public Portrait(String name, int year, String artist, String person) {
super(name, year, artist);
this.person = person;
}
}
public class Statue extends Exhibit {
private String material; // חומר: אבן/ברונזה/שיש
public Statue(String name, int year, String material) {
super(name, year);
this.material = material;
}
}
public class Bust extends Statue {
private int height; // גובה בסנטימטרים
public Bust(String name, int year, String material, int height) {
super(name, year, material);
this.height = height;
}
}סעיף ג(1)
דורסים toString ב־Painting (super.toString() + " by " + artist), ב־Statue ("Statue: " + super.toString() + " [" + material + "]") וב־Bust ("Large bust of " / "Small bust of " לפי height >= 50, ואחר כך getName()). ב־Portrait לא צריך – היא יורשת את toString של Painting.
מה נדרש – לדרוס את toString (ב־C#: ToString) רק במחלקות שבהן הפלט שונה מזה שהמחלקה כבר יורשת.
רעיון – println(hall[i]) מפעיל את toString של הטיפוס בפועל של העצם. נבדוק כל שורה בפלט:
| עצם | שורת הפלט | מה צריך |
|---|---|---|
| Portrait | Queen, 1720 by Ruth – בדיוק כמו ציור; person לא מודפס | לא צריך toString ב־Portrait – היא יורשת את הגרסה של Painting |
| Statue | Statue: + הפלט של Exhibit + [bronze] | לדרוס ב־Statue: "Statue: " + super.toString() + " [" + material + "]" |
| Bust (55 ס"מ) | Large bust of Poet – בלי שנה ובלי Statue: | לדרוס ב־Bust: מחרוזת חדשה לפי height; השם פרטי ב־Exhibit, ולכן getName() |
| Painting | הפלט של Exhibit + by Vincent | לדרוס ב־Painting: super.toString() + " by " + artist |
| Bust (30 ס"מ) | Small bust of Sailor | אותה פעולה של Bust, הענף השני של התנאי |
לפי הנתון, פסל ראש שגובהו 50 ס"מ לפחות הוא Large, ולכן התנאי הוא height >= 50 (Poet בגובה 55 – Large; Sailor בגובה 30 – Small).
ב־C# כל ToString מוגדרת עם override (גם ב־Bust, שדורסת את הגרסה של Statue).
פתרון – הקוד המלא (המחלקות הסופיות) בלשונית Java/C#.
בדיקה – Tester עם הלולאה הנתונה (Java ו־C#) מדפיסה בדיוק:
Queen, 1720 by Ruth
Statue: Rider, 1904 [bronze]
Large bust of Poet
Garden, 1889 by Vincent
Small bust of Sailorטעות נפוצה: להוסיף toString גם ל־Portrait (מיותר), או לכתוב ב־Bust super.toString() – זה היה מדפיס Statue: Poet, 1860 [marble].
public class Painting extends Exhibit {
private String artist;
public Painting(String name, int year, String artist) {
super(name, year);
this.artist = artist;
}
public void light() {
System.out.println("Light on " + artist);
}
public String toString() {
return super.toString() + " by " + artist;
}
}
public class Portrait extends Painting {
private String person; // מי מצויר בדיוקן
public Portrait(String name, int year, String artist, String person) {
super(name, year, artist);
this.person = person;
}
}
public class Statue extends Exhibit {
private String material; // חומר: אבן/ברונזה/שיש
public Statue(String name, int year, String material) {
super(name, year);
this.material = material;
}
public String toString() {
return "Statue: " + super.toString() + " [" + material + "]";
}
}
public class Bust extends Statue {
private int height; // גובה בסנטימטרים
public Bust(String name, int year, String material, int height) {
super(name, year, material);
this.height = height;
}
public String toString() {
if (height >= 50)
return "Large bust of " + getName();
return "Small bust of " + getName();
}
}סעיף ג(2)
לא תקינה (שגיאת הידור: ב־Exhibit אין light). תיקון: if (arr[i] instanceof Painting) ((Painting)arr[i]).light(); (ב־C#: is Painting / Light). הפלט עבור hall: Light on Ruth / Light on Vincent.
מה נדרש – לקבוע אם הפעולה תקינה; אם לא – לתקן ולכתוב את הפלט עבור hall.
רעיון – הפעולה לא תקינה (שגיאת הידור): הטיפוס המוצהר של arr[i] הוא Exhibit, וב־Exhibit אין פעולה light() (ב־C#: Light()). המהדר בודק את הקריאה לפי הטיפוס המוצהר, ולא לפי העצם שיהיה שם בזמן ריצה.
התיקון: לבדוק אם העצם הוא ציור (instanceof Painting ב־Java, is Painting ב־C#), ורק אז להמיר ל־Painting ולקרוא ל־light(). Portrait יורשת מ־Painting, ולכן גם דיוקן עובר את הבדיקה; פסלים ופסלי ראש לא.
פתרון – הפעולה המתוקנת בלשונית Java/C#.
בדיקה – עבור hall: Portrait (תא 0, הציירת Ruth) ו־Painting (תא 3, הצייר Vincent) מדפיסים. הפלט (Java ו־C#):
Light on Ruth
Light on Vincentהפעולה המקורית אכן לא עוברת הידור (Java: cannot find symbol; C#: שגיאה CS1061). במערך עם Bust, תא null, Exhibit ו־Painting מודפסת שורה אחת (Light on Maya), בלי שגיאה.
public static void lightAll (Exhibit [] arr) {
for (int i = 0; i < arr.length; i++) {
if (arr[i] instanceof Painting) {
((Painting) arr[i]).light();
}
}
}שאלה 11 · Lamp, Led, Strip – תכונה סטטית ושרשור פעולות בונות (25 נקודות)
לשאלה זו שני נוסחים: בשפת Java ובשפת C# .
לפותרים בשפת Java
נתונות המחלקות Lamp , Led , Strip :
public class Lamp {
public static int total = 0;
private int watt;
public Lamp (int w) {
watt = w;
total = total + w;
}
public Lamp() {
this (40);
}
public String toString() {
return "[" + watt + "W/" + total + "W]";
}
}
---------------------------------------------
public class Led extends Lamp {
private String color;
public Led (String c) {
super (8);
color = c;
}
public Led() {
color = "white";
}
public String toString() {
return color + super.toString();
}
}
---------------------------------------------
public class Strip extends Led {
private int meters;
public Strip (int m) {
meters = m;
}
public String toString() {
return meters + "m " + super.toString();
}
}נתונה המחלקה Test :
public class Test
{
public static void main (String[] args)
{
Lamp[] arr = new Lamp[6];
arr[0] = … // (1)
arr[1] = … // (2)
arr[2] = … // (3)
arr[3] = … // (4)
arr[4] = … // (5)
arr[5] = … // (6)
for (int i = 0; i < arr.length; i++) {
System.out.println (arr[i]);
}
}
}השלימו את השורות (1)–(6) כך שהפלט יהיה:
red[8W/196W]
3m white[40W/196W]
[60W/196W]
white[40W/196W]
blue[8W/196W]
2m white[40W/196W]הכותרת של המחלקה Strip השתנתה לכותרת:
public class Strip extends Lampהאם שינוי זה יגרום לשגיאה? אם כן – הסבירו את השגיאה, אם לא – רשמו מה יהיה הפלט של הפעולה main אחרי השינוי (השורות החסרות הן כפי שכתבתם בסעיף א).
הערה: אין שינויים נוספים במחלקה.
◂ פתרון מלא לשאלה 11
סעיף א
(1) new Led("red") (2) new Strip(3) (3) new Lamp(60) (4) new Led() (5) new Led("blue") (6) new Strip(2)
מה נדרש – לבחור לכל תא במערך עצם (ופעולה בונה) כך שתודפס בדיוק השורה הנתונה.
רעיון
total היא תכונה סטטית – אחת לכל העצמים. כל יצירת עצם עוברת בסופו של דבר ב־Lamp(int w), שמוסיפה ל־total את ההספק של העצם. ההדפסה מתבצעת אחרי שכל העצמים נוצרו, ולכן בכל השורות מודפס הערך הסופי של total – 196.Lamp() קוראת ל־this(40) – כלומר ל־Lamp(40): watt = 40.Led(String c) קוראת ל־Lamp(8): watt = 8, color = c.Led() קוראת במובלע ל־Lamp() ← Lamp(40): watt = 40, color = "white".Strip(int m) קוראת במובלע ל־Led() ← Lamp() ← Lamp(40): watt = 40, color = "white", meters = m.[watt W/total W]; Led מוסיפה לפניה את הצבע; Strip מוסיפה לפני הכול m מטרים ורווח.| שורה | פלט נדרש | עצם | watt |
|---|---|---|---|
| (1) | red[8W/196W] | new Led("red") | 8 |
| (2) | 3m white[40W/196W] | new Strip(3) | 40 |
| (3) | [60W/196W] | new Lamp(60) | 60 |
| (4) | white[40W/196W] | new Led() | 40 |
| (5) | blue[8W/196W] | new Led("blue") | 8 |
| (6) | 2m white[40W/196W] | new Strip(2) | 40 |
בדיקת הסכום: 8 + 40 + 60 + 40 + 8 + 40 = 196 – בדיוק הערך שמודפס. השורה (4) חייבת להיות new Led() ולא new Led("white"): Led עם מחרוזת נותנת תמיד 8W. השורה (2) – Strip: רק ל־Strip יש מטרים, והצבע שלה תמיד white כי היא עוברת דרך Led().
פתרון – השורות החסרות (זהות ב־Java וב־C#):
arr[0] = new Led("red"); // (1)
arr[1] = new Strip(3); // (2)
arr[2] = new Lamp(60); // (3)
arr[3] = new Led(); // (4)
arr[4] = new Led("blue"); // (5)
arr[5] = new Strip(2); // (6)התוכנית המלאה בלשונית Java/C#.
בדיקה – הפלט ב־Java וב־C# הוא בדיוק הפלט הנדרש:
red[8W/196W]
3m white[40W/196W]
[60W/196W]
white[40W/196W]
blue[8W/196W]
2m white[40W/196W]מעבר על כל הבחירות האפשריות של פעולות בונות (עם הארגומנטים שמופיעים בפלט) מראה שזה הפתרון היחיד.
טעות נפוצה: לחשוב שבשורה (1) יודפס red[8W/8W] – total סטטית, וההדפסה מתבצעת רק אחרי שכל העצמים נוצרו. טעות נוספת: לשכוח ש־Lamp() לא מציבה בעצמה דבר – היא עוברת ל־Lamp(40), ושם total גדלה.
public class Test
{
public static void main (String[] args)
{
Lamp[] arr = new Lamp[6];
arr[0] = new Led("red"); // (1)
arr[1] = new Strip(3); // (2)
arr[2] = new Lamp(60); // (3)
arr[3] = new Led(); // (4)
arr[4] = new Led("blue"); // (5)
arr[5] = new Strip(2); // (6)
for (int i = 0; i < arr.length; i++) {
System.out.println (arr[i]);
}
}
}סעיף ב
אין שגיאה. הפלט: red[8W/196W] / 3m [40W/196W] / [60W/196W] / white[40W/196W] / blue[8W/196W] / 2m [40W/196W]
מה נדרש – לקבוע אם Strip שיורשת ישירות מ־Lamp גורמת לשגיאה, ואם לא – מה הפלט.
רעיון
Strip(int m) קוראת עכשיו במובלע ל־Lamp() – פעולה בונה ללא פרמטרים שקיימת ב־Lamp (והיא ממשיכה ל־Lamp(40)). גם super.toString() (ב־C#: base.ToString()) קיימת ב־Lamp, וב־C# ה־override של Strip עדיין תקין, כי ToString של Lamp היא override (וירטואלית).Lamp[] arr עדיין יכול להכיל עצמי Strip, כי Strip יורשת מ־Lamp.super.toString() היא של Lamp – לכן המילה white לא מודפסת בשורות של Strip. ההספק של Strip עדיין 40 (דרך Lamp()), ולכן total נשאר 196.| שורה | עצם | פלט |
|---|---|---|
| (1) | new Led("red") | red[8W/196W] |
| (2) | new Strip(3) | 3m [40W/196W] |
| (3) | new Lamp(60) | [60W/196W] |
| (4) | new Led() | white[40W/196W] |
| (5) | new Led("blue") | blue[8W/196W] |
| (6) | new Strip(2) | 2m [40W/196W] |
בדיקה – הגרסה ששונתה (Java ו־C#) מדפיסה:
red[8W/196W]
3m [40W/196W]
[60W/196W]
white[40W/196W]
blue[8W/196W]
2m [40W/196W]טעות נפוצה: לחשוב שאחרי השינוי total ישתנה – ההספק של Strip נקבע ב־Lamp() גם לפני השינוי (דרך Led()) וגם אחריו.
שאלה 12 · מעקב – ירושה, דריסה, תכונת מחלקה (static) והמרת טיפוסים (25 נקודות)
נתונות המחלקות Ring , Dot :
public class Dot {
private static int count = 0;
private int v;
public Dot() { this(5); }
public Dot(int v) {
this.v = v;
count++;
}
public int getV() { return this.v; }
public static int getCount() { return count; }
public void up() { this.v = this.v + 2; }
public void step() { up(); up(); }
public void step(int k) { this.v = this.v * k; }
public String toString() { return "v = " + this.v; }
}
public class Ring extends Dot {
private int r;
public Ring() {
super();
this.r = 1;
}
public Ring(int v) {
super(v + 1);
this.r = v;
}
public Ring(Ring other) {
super(other.getV());
this.r = other.r + 10;
}
public void up() { this.r = this.r + getV(); }
public void step(int k) {
super.step(k);
this.r = this.r - 1;
}
public String toString() {
return "(" + super.toString() + ", r = " + this.r + ")";
}
}לפניכם קטע קוד:
Dot[] arr = new Dot[5];
arr[0] = new Dot();
arr[1] = new Ring();
arr[2] = new Ring(3);
arr[3] = arr[1];
arr[4] = new Ring((Ring) arr[2]);
for (int i = 0; i < arr.length; i++)
System.out.println(arr[i]);
System.out.println("count = " + Dot.getCount());ציירו את העצמים שנוצרו, וכתבו מה הקוד מדפיס.
לפניכם המשך קטע הקוד:
arr[0].step();
arr[1].step();
arr[2].step(2);
((Dot) arr[4]).up();
Ring q = (Ring) arr[3];
q.step(3);
Dot e = new Ring(2);
e.step();
System.out.println("e: " + e);
arr[0] = e;
for (int i = 0; i < arr.length; i++)
System.out.println(arr[i]);
System.out.println("count = " + Dot.getCount());כתבו מה הקוד מדפיס.
◂ פתרון מלא לשאלה 12
סעיף א
v = 5
(v = 5, r = 1)
(v = 4, r = 3)
(v = 5, r = 1)
(v = 4, r = 13)
count = 4מה נדרש – לצייר את העצמים שנוצרים ולכתוב את הפלט.
רעיון – לכל new רצה הפעולה הבונה המתאימה לפרמטרים. Dot() קוראת ל־this(5) (ב־C# : this(5)), כלומר ל־Dot(int v). הפעולות הבונות של Ring קוראות קודם לפעולה הבונה של Dot (super/base). התכונה count היא תכונת מחלקה (static) – יש ממנה עותק אחד משותף לכל העצמים, והיא גדלה ב־1 בכל פעם ש־Dot(int v) רצה, כלומר פעם אחת לכל עצם שנוצר (Dot או Ring). arr[3] = arr[1] לא יוצרת עצם. ההדפסה מזמנת את toString של העצם בפועל.
פתרון
| שורה | פעולות בונות שרצות | v | r | count |
|---|---|---|---|---|
arr[0] = new Dot() | Dot() → Dot(5) | 5 | – | 1 |
arr[1] = new Ring() | Ring() → Dot() → Dot(5), ואז r = 1 | 5 | 1 | 2 |
arr[2] = new Ring(3) | Ring(int v) → Dot(3 + 1), ואז r = 3 | 4 | 3 | 3 |
arr[3] = arr[1] | אין עצם חדש – העתקת הפניה | (5) | (1) | 3 |
arr[4] = new Ring((Ring) arr[2]) | Ring(Ring other) → Dot(other.getV()) = Dot(4), ואז r = 3 + 10 | 4 | 13 | 4 |
ההמרה (Ring) arr[2] נחוצה כי arr[2] הוא מטיפוס Dot (ההפניה), והפעולה הבונה מצפה ל־Ring; העצם עצמו הוא אכן Ring, לכן ההמרה מצליחה.
העצמים (4 עצמים בלבד):
arr[0] ──► Dot v = 5
arr[1] ──► Ring v = 5, r = 1 ◄── arr[3] (אותו עצם!)
arr[2] ──► Ring v = 4, r = 3
arr[4] ──► Ring v = 4, r = 13הפלט:
v = 5
(v = 5, r = 1)
(v = 4, r = 3)
(v = 5, r = 1)
(v = 4, r = 13)
count = 4בדיקה – הפלט זהה ב־Java וב־C# ותואם לטבלה.
טעות נפוצה: לחשוב ש־Dot() ו־Dot(5) מגדילות את count פעמיים – Dot() רק מעבירה את הבנייה ל־Dot(int v), ולכן כל עצם מוסיף 1 בדיוק. וגם: לספור את arr[3] כעצם חמישי.
סעיף ב
e: (v = 3, r = 8)
(v = 3, r = 8)
(v = 15, r = 10)
(v = 8, r = 2)
(v = 15, r = 10)
(v = 4, r = 17)
count = 5מה נדרש – לעקוב אחר הזימונים ולכתוב את כל מה שמודפס.
רעיון – up ו־step(int) דרוסות ב־Ring; step() בלי פרמטרים קיימת רק ב־Dot, והיא קוראת פעמיים ל־up() – וזו נבחרת לפי סוג העצם בזמן ריצה. ב־Ring, step(int k) קוראת קודם ל־super.step(k) (הגרסה של Dot, שמכפילה את v) ואחר כך מקטינה את r. המרה (casting) משנה רק את טיפוס ההפניה, לא את העצם. ב־C# Up ו־Step(int) מוגדרות virtual ונדרסות ב־override, ו־Step() (שאינה virtual) קוראת ל־Up הווירטואלית – ההתנהגות זהה.
פתרון
| פקודה | טיפוס בזמן ריצה | מה רץ בפועל | שינוי |
|---|---|---|---|
arr[0].step() | Dot | Dot.step → Dot.up פעמיים | arr[0]: v = 5 → 7 → 9 |
arr[1].step() | Ring | Dot.step → Ring.up פעמיים | arr[1]: r = 1 + 5 = 6, ואז 6 + 5 = 11 |
arr[2].step(2) | Ring | Ring.step(int) → Dot.step(int) | arr[2]: v = 4·2 = 8, r = 3 − 1 = 2 |
((Dot) arr[4]).up() | Ring | ההמרה לא משנה את העצם ⇒ Ring.up | arr[4]: r = 13 + 4 = 17 |
Ring q = (Ring) arr[3]; q.step(3) | Ring (אותו עצם כמו arr[1]) | Ring.step(int) | arr[1]: v = 5·3 = 15, r = 11 − 1 = 10 |
Dot e = new Ring(2) | Ring | Ring(int v) → Dot(3) | e: v = 3, r = 2; count = 5 |
e.step() | Ring | Dot.step → Ring.up פעמיים | e: r = 2 + 3 = 5, ואז 5 + 3 = 8 |
| הדפסה | Ring.toString → super.toString() | e: (v = 3, r = 8) | |
arr[0] = e | arr[0] מפנה עכשיו ל־e |
מצב המערך בסוף: arr[0] → e (v=3, r=8); arr[1] ו־arr[3] → אותו Ring (v=15, r=10); arr[2] → Ring (v=8, r=2); arr[4] → Ring (v=4, r=17). count = 5 (חמישה עצמים נוצרו בסך הכול; העצם Dot המקורי של arr[0] כבר לא מודפס, אבל נספר).
הפלט:
e: (v = 3, r = 8)
(v = 3, r = 8)
(v = 15, r = 10)
(v = 8, r = 2)
(v = 15, r = 10)
(v = 4, r = 17)
count = 5בדיקה – הפלט של ההמשך (אחרי סעיף א) זהה ב־Java וב־C# ותואם לטבלה. שימו לב ש־v של עצמי Ring משתנה רק ב־step(int), כי Dot.up לא רצה על עצמי Ring.
טעות נפוצה: לחשוב ש־arr[1].step() מפעילה את Dot.up (כי step כתובה ב־Dot) – הקריאה up() שבתוך step נבחרת לפי סוג העצם, ולכן רצה Ring.up. וגם: לשכוח ש־q הוא אותו עצם כמו arr[1] ו־arr[3].
טבלת התשובות
| שאלה | סעיף | תשובה |
|---|---|---|
| 1 | - | מוציאים בכל פעם את האיבר הראשון v (בלי להחזירו), מעבירים את שאר האיברים לתור עזר וסופרים את האיברים x שעבורם |v − x| ≤ d, ומחזירים אותם מתור העזר ל־ q; חוזרים עד שהתור ריק. עבור הדוגמה (d = 2) מתקבל 4. סיבוכיות O(n²). |
| 2 | א(1) | הפעולה מחזירה 3 (החוליות 4, 2, 1 קטנות מ־5). |
| א(2) | הפעולה מחזירה את מספר החוליות בשרשרת lst שהערך שלהן קטן (ממש) מ־x. השרשרת עצמה לא משתנה. | |
| א(3) | O(n), כאשר n הוא מספר החוליות בשרשרת: קריאה רקורסיבית אחת לכל חוליה (ועוד אחת ל־null), ובכל קריאה עבודה קבועה. | |
| ב(1) | הפעולה מחזירה 9. | |
| ב(2) | הפעולה מחזירה את מספר הזוגות של חוליות בשרשרת (חוליה מוקדמת וחוליה מאוחרת יותר) שבהם הערך של החוליה המוקדמת גדול מהערך של החוליה המאוחרת. | |
| ב(3) | O(n²), כאשר n הוא מספר החוליות: לכל חוליה מופעלת what על ההמשך שלה, שאורכו n−1, n−2, …, 0. הסכום הוא n(n−1)/2. | |
| 3 | א | d = numDigits(val); אם d == 1 – false. אחרת p = 10 בחזקת d/2, בן שמאלי val / p, בן ימני val % p, ומחזירים true. |
| ב(1) | 73846 ← 738 , 46; 738 ← 73 , 8; 73 ← 7 , 3; 46 ← 4 , 6. העלים משמאל לימין: 7, 3, 8, 4, 6. הפעולה מחזירה 5. | |
| ב(2) | הפעולה מפרקת את המספר לספרותיו: היא בונה עץ שעליו, משמאל לימין, הם ספרות המספר לפי הסדר (כל צומת פנימי = צירוף הספרות של שני בניו), ומחזירה את מספר העלים – כלומר את מספר הספרות של המספר. | |
| 4 | א | (1) נכונה; (2) נכונה; (3) לא נכונה; (4) נכונה; (5) לא נכונה; (6) נכונה; (7) לא נכונה. (מספיק לענות על חמש מהן.) |
| ב | D₁, D₃, D₆ הם גרפים "מיון יחיד". D₂ (A,B,C,D / A,C,B,D), D₄ (A,B,C,D / B,A,C,D), D₇ (A,B,C,D,E / D,A,B,C,E) אינם; ל־ D₅ אין מיון טופולוגי כלל (מעגל A→B→C→A). | |
| 5 | א(1) | אלגוריתם בלמן־פורד, O(V·E). אחרי סבב 1: P=4, Q=3; סבב 2: R=9, P=1, T=4; סבב 3: R=6; סבב 4: אין שינוי. המסלול: S→Q→T, אורכו 4. |
| א(2) | הגדלת הקשתות היוצאות מ־s: כן – כל מסלול פשוט מ־s ל־t משתמש בדיוק בקשת אחת שיוצאת מ־s, ולכן כל המסלולים גדלים באותו x. הקטנת כל הקשתות: לא בהכרח – מסלול עם k קשתות קטן ב־k·x. למשל ב־G₁ עם x = 1: S→Q→T שוקל 2, אבל S→Q→P→R→T שוקל 1. | |
| ב(1) | סריקה 1: a→b→d, ואחר כך a→c, a→e – 3 עלים (d, c, e). סריקה 2: a→c→b→d, ואחר כך a→e – 2 עלים (d, e). | |
| ב(2) | מספיק להוסיף קשת אחת, a→d. מספרי עלים: 4 (a→d, a→b, a→c, a→e), 3 (a→b→d, a→c, a→e), 2 (a→c→b→d, a→e). | |
| 6 | א | סריקה לרוחב (BFS) מ־ vⱼ שמתעלמת מקשתות שמשקלן גדול מ־ k : עוברים בקשת u – v רק אם w ≤ k . כל קודקוד שמתגלה (חוץ מ־ vⱼ) מוחזר. זמן ריצה O(|V| + |E|). בדוגמה: 3 , 7 . |
| ב | שלב 1: BFS מכל קודקוד שעוד לא סומן מוצא את רכיבי הקשירות, ושומרים את הגדול ביותר – O(|V| + |E|). שלב 2: האלגוריתם של פרים מקודקוד כלשהו ברכיב זה (פרים מגיע רק לקודקודי הרכיב) – O(|V|²) במימוש פשוט. מחזירים את |רכיב| − 1 הקשתות שנבחרו. בדוגמה: (0, 3) , (3, 7) , (7, 9) , (9, 5) , משקל 12. | |
| 7 | א(1) | לא נכונה. L₁ = Σ* = {a, b}* (רגולרית), L₂ = {aⁿbⁿ | n ≥ 0} (לא רגולרית), ו־ L₁ ∪ L₂ = Σ* – שפה רגולרית. |
| א(2) | נכונה. ⊇: כל w ∈ L* שווה ל־ w • ε, ו־ ε ∈ L*. ⊆: אם x, y ∈ L* אז x הוא שרשור של k מילים מ־ L ו־ y של m מילים מ־ L, ולכן xy הוא שרשור של k + m מילים מ־ L, כלומר xy ∈ L*. | |
| ב(1) | aabcb ∈ L ; bcb ∈ L ; bba ∉ L (מכילה ba) ; ε ∉ L (0 – זוגי) ; cab ∉ L (1 + 1 = 2 – זוגי). | |
| ב(2) | 6 מצבים (זוגיות של #b+#c × סוג האות האחרונה): q0 (התחלה), q1, …, q5; מקבלים: q1, q3, q5. מעברים: q0: a→q0, b→q3, c→q5 ; q1: a→q1, b→q2, c→q4 ; q2: b→q3, c→q5 ; q3: b→q2, c→q4 ; q4: a→q0, b→q3 ; q5: a→q1, b→q2 (מעבר חסר = דחייה). | |
| 8 | א | aabbc – לא שייכת; abbbbc – שייכת; aaaabbcc – לא שייכת; aabbcccc – שייכת; aaabbbbccc – שייכת. |
| ב | n זוגי: aabb . n אי־זוגי (עם b): abbc . | |
| ג | (1) b,A/שלוף (2) b,A/שלוף (3) b,S/שלוף (4) c,ריקה/ללא שינוי (5) c,ריקה/ללא שינוי (6) b,A/ללא שינוי ; S/ללא שינוי (7) b,A/ללא שינוי ; S/ללא שינוי (8) b,A/ללא שינוי ; S/ללא שינוי (9) c,A/שלוף (10) c,A/שלוף (11) c,A/שלוף (12) c,S/שלוף (13) c,S/שלוף (14) c,S/שלוף. מצבים מקבלים: q₃ , q₈ . | |
| 9 | א(1) | abacc |
| א(2) | דוחפים סימן לכל a בגוש הראשון (התחתון X) ו־B לכל b; כל a שבא אחרי ה־b שולף B; כל זוג c שולף סימן אחד; ה־c ששולף את X עובר למצב המקבל q6. הטבלה המלאה בפתרון. | |
| ב | (1) שייכת: ccaba · abacc. (2) לא שייכת. (3) שייכת: ccccabaa · abbaacc. | |
| 10 | א | Painting יורשת מ־Exhibit (artist, light); Portrait יורשת מ־Painting (person); Statue יורשת מ־Exhibit (material); Bust יורשת מ־Statue (height). התכונות name ו־year נשארות רק ב־Exhibit. |
| ב | Painting(name, year, artist) → super(name, year); Portrait(name, year, artist, person) → super(name, year, artist); Statue(name, year, material) → super(name, year); Bust(name, year, material, height) → super(name, year, material). ב־C#: base(...) במקום super(...). | |
| ג(1) | דורסים toString ב־Painting (super.toString() + " by " + artist), ב־Statue ("Statue: " + super.toString() + " [" + material + "]") וב־Bust ("Large bust of " / "Small bust of " לפי height >= 50, ואחר כך getName()). ב־Portrait לא צריך – היא יורשת את toString של Painting. | |
| ג(2) | לא תקינה (שגיאת הידור: ב־Exhibit אין light). תיקון: if (arr[i] instanceof Painting) ((Painting)arr[i]).light(); (ב־C#: is Painting / Light). הפלט עבור hall: Light on Ruth / Light on Vincent. | |
| 11 | א | (1) new Led("red") (2) new Strip(3) (3) new Lamp(60) (4) new Led() (5) new Led("blue") (6) new Strip(2) |
| ב | אין שגיאה. הפלט: red[8W/196W] / 3m [40W/196W] / [60W/196W] / white[40W/196W] / blue[8W/196W] / 2m [40W/196W] | |
| 12 | א | |
| ב | |