מבוא למדעי המחשב, בן גוריון תרגול 6 - רקורסיות מבוא למדעי המחשב, בן גוריון
מבוא למדעי המחשב, בן גוריון רקורסיה פונקציה רקורסיבית היא פונקציה שקוראת לעצמה. פונקציה רקורסיבית מחושבת כמו כל פונקציה אחרת (העברת פרמטרים, משתנים לוקאליים, תחום הכרה של המשתנים וכו'). מוטיבציה: ישנן בעיות רבות שעבורן פתרון רקורסיבי פשוט יותר מפתרון איטרטיבי. מבוא למדעי המחשב, בן גוריון
מבוא למדעי המחשב, בן גוריון רקורסיה דוגמה 1: סכום מספרים טבעיים עד n אפשר בלולאה (פתרון איטרטיבי): public static int sum(int n) { int ans = 0; for (int i = 1; i <= n; i = i + 1) { ans = ans + i; } return ans; מבוא למדעי המחשב, בן גוריון
מבוא למדעי המחשב, בן גוריון רקורסיה אפשר גם בדרך אחרת: נניח שיש לנו פונקציה אחרת בשם magic שמחזירה את הסכום 1 + 2+ ... + (n-1). אז sum יכולה להראות כך: public static int sum(int n) { int ans = magic(n)+ n; return ans; } אבל magic(n) מחזירה בדיוק מה ש-sum(n-1) הייתה מחזירה. מבוא למדעי המחשב, בן גוריון
מבוא למדעי המחשב, בן גוריון רקורסיה ניתן להגדיר נוסחת נסיגה עבור החישוב: sum(n) = sum(n-1)+n … int ans = sum(n-1)+ n; מבוא למדעי המחשב, בן גוריון
מבוא למדעי המחשב, בן גוריון רקורסיה התוצאה היא פונקציה אחת שקוראת לעצמה: public static int sum(int n) { int ans; if (n == 1) // stop condition ans = 1; else ans = sum(n - 1) + n; return ans; } *** מעקב על דוגמת הרצה וציור טבלאות מעקב משתנים. מבוא למדעי המחשב, בן גוריון
מבוא למדעי המחשב, בן גוריון רקורסיה שלושת הכללים לבניית פונקציה רקורסיבית תנאי עצירה שניתן לענות עליו ללא קריאה רקורסיבית. אם לא נשים תנאי עצירה התוכנית עלולה להיכנס ללולאה אינסופית. קריאה רקורסיבית עם קלט הקרוב יותר לתנאי העצירה ("הקטנת הבעיה"). אם לא מקטינים את הבעיה לא נגיע לתנאי העצירה, כלומר שוב תהיה לולאה אינסופית. שימוש בתוצאת הקריאה הרקורסיבית לחישוב התוצאה המוחזרת (הנחת האינדוקציה). מבוא למדעי המחשב, בן גוריון
מבוא למדעי המחשב, בן גוריון דוגמה 2 – משולש פסקל תזכורת: משולש פסקל הוא סידור של מספרים בצורת משולש, הנבנה באופן הבא: הקודקוד העליון של משולש זה מכיל את המספר 1, וכל מספר במשולש מהווה את סכום שני המספרים שנמצאים מעליו (המספרים שנמצאים על שוקי המשולש הם כולם 1). n 0 1 1 1 1 2 1 2 1 3 1 3 3 1 4 1 4 6 4 1 5 1 5 10 10 5 1 m 0 1 2 3 4 5 המספר ה-m בשורה ה- n, נותן את התשובה לשאלה "בכמה דרכים שונות אפשר לבחור m עצמים מתוך n עצמים?" (מקדם בינומי). מבוא למדעי המחשב, בן גוריון
מבוא למדעי המחשב, בן גוריון דוגמה 2 – משולש פסקל נכתוב פונקציה רקורסיבית pascal(int n, int m) שתחשב את המספר המופיע בשורה n ובעמודה m במשולש פסקל. תאור האלגוריתם תנאי עצירה: אם m הוא 0 נחזיר ערך 1. אם n=m נחזיר ערך 1. חוקיות הקלט: n ו- m הם שלמים אי-שליליים. אם m>n נציין שיש שגיאה בקלט. קריאות רקורסיביות על קלט קטן יותר: קריאה אחת עם n-1 ו- m (המספר מעליו) קריאה שנייה עם n-1 ו m-1 (המספר מעל ומשמאל) שילוב התוצאות לקבלת תשובה: החזרת סכום של הערכים שהתקבלו משתי הקריאות הרקורסיביות. מבוא למדעי המחשב, בן גוריון
מבוא למדעי המחשב, בן גוריון דוגמה 2 – משולש פסקל // Pascal number in row n and column m. public static int pascal(int n, int m){ int ans; if (m<0 || n<0 || m>n) { ans = -1; } else if (m==0 || n == m) { ans = 1; } else { ans = pascal(n-1,m) + pascal(n-1,m-1); } return ans; } מבוא למדעי המחשב, בן גוריון
דוגמה 3- זוגיים ואי זוגיים המטרה: רוצים לבדוק האם מספר טבעי n הוא זוגי או אי-זוגי באמצעות הפונקציות even ו- odd (ללא פעולות חלוקה ושארית). public static boolean even(int n) { boolean ans; if (n == 0) ans = true; else ans = odd(n - 1); return ans; } public static boolean odd(int n) { if (n == 1) ans = even(n - 1); מה קורה כאשר מפעילים את even על מספר אי-זוגי גדול מ 0? מבוא למדעי המחשב, בן גוריון
דוגמה 3- זוגיים ואי זוגיים נסיון שני: public static boolean even(int n) { boolean ans; if (n == 0) ans = true; else ans = odd(n - 1); return ans; } public static boolean odd(int n) { ans = false; ans = even(n - 1); רקורסיה הדדית: even קוראת לעצמה דרך odd, ו- odd קוראת לעצמה דרך even. מבוא למדעי המחשב, בן גוריון
דוגמא 1 נכתוב פונקציה רקורסיבית שמציירת משולש הפוך בגובה של n שורות. ******* ****** ***** **** *** ** * דרך פעולה: נדפיס שורת כוכביות באורך n, ואז נדפיס משולש בגובה n-1. תנאי העצירה יהיה כאשר נגיע להדפיס משולש בגובה 0.
והפתרון: public static void drawTriangle(int n) { int i; if (n > 0) { for (i=0; i<n; i=i+1) System.out.print('*'); System.out.println(); drawTriangle(n-1); } *** ** * עבור המקרה בו קוראים לפונקציה עם n=3: רקורסית זנב? כן!
public static void drawTriangle(int n) { int i; if (n > 0) { * ** *** **** ***** ****** ******* מה היה קורה אם היינו רוצים להדפיס משולש ישר?? היינו צריכים קודם לקרוא ל- drawTriangle עם n-1, ואז להדפיס שורה של כוכביות באורך n: public static void drawTriangle(int n) { int i; if (n > 0) { drawTriangle(n-1); for (i=0; i<n; i=i+1) System.out.print('*'); System.out.println(); }
public static void drawHourglass (int n) { int i; if (n > 0) { אם נשלב את שני החלקים: public static void drawHourglass (int n) { int i; if (n > 0) { for (i=0; i<n; i=i+1) System.out.print('*'); System.out.println(); drawHourGlass(n-1); } נקבל: ***** **** *** ** *
דוגמא 2 כתבו פונקציה רקורסיבית שמקבלת מחרוזת ומחזירה true אם היא פָּלִינְדְרוֹם ו- false אחרת. תזכורת: פָלִינְדְרוֹם היא מחרוזת שניתן לקרוא משני הכיוונים, משמאל לימין ומימין לשמאל, ולקבל אותה תוצאה. לדוגמא: המחרוזות "ארון קיר היה ריק נורא", "ילד כותב בתוך דלי" ו- madam הן פָלִינְדְרוֹם, לעומת זאת המחרוזת hello אינה פָלִינְדְרוֹם. הנחת יסוד: מחרוזת ריקה (ללא תווים) ומחרוזת בעלת תו אחד הן פָלִינְדְרוֹם.
דוגמא 2 – דרך פעולה דרך פעולה: מקרה הבסיס: אם מדובר במחרוזת ריקה (ללא תווים) או במחרוזת בעלת תו אחד ניתן להחזיר true . אחרת, נבדוק עבור מחרוזת קטנה יותר (הקטנת הבעיה) ע"י צמצום המחרוזת בתו אחד מכל צד. אם הפעלת הפונקציה הרקורסיבית על המחרוזת המוקטנת תחזיר true וגם שני התווים הקיצוניים שהורדנו מהמחרוזת המקורית שווים נחזיר true, אחרת נחזיר false.
והפתרון: public static boolean isPalindrome(String pal) { boolean isPal = false; int length = pal.length(); if (length == 0 || length == 1) // can be “if (length <= 1)” instead isPal = true; else { isPal = (pal.charAt(0)==pal.charAt(length-1) && isPalindrome(pal.substring(1,length-1))); } return isPal;
בעיית Subset Sum (SUSU) בהנתן מערך של משקולות ומשקל נוסף, נרצה לבדוק האם ניתן להרכיב מהמשקולות משקל השווה למשקל הנתון. דוגמא לקלט: weights={1,7,9,3} Sum = 12 במקרה זה הפונקציה תחזיר true כי ניתן לחבר את המשקולות 9 ו 3 ולקבל את הסכום 12. מה יוחזר עבור sum=15? תשובה: false
אסטרטגיית הפתרון נעבור על כל האיברים במערך ונבחן אפשרויות בהן איבר נבחר או לא נבחר. נתבונן באיבר הראשון במערך. ייתכן שהוא ייבחר לקבוצת המשקולות שתרכיב את sum ויתכן שלא. אם הוא לא ייבחר – אזי נותר לפתור בעיה קטנה יותר והיא: האם ניתן להרכיב את הסכום sum מבין המשקולות שבתאים weights[1,…,n-1] אם הוא ייבחר – אזי נותר לפתור בעיה קטנה יותר והיא: האם ניתן להרכיב את הסכום sum-weight[0] מבין המשקולות שבתאים weights[1,…, n-1]. כנ"ל לגבי יתר האיברים בצורה רקורסיבית.
אסטרטגיית הפתרון – המשך נעבור על כל האיברים במערך ונבחן אפשרויות בהן איבר נבחר או לא נבחר. קיימים שני מקרי בסיס: הגענו לסכום הדרוש או במילים אחרות הסכום הנותר הינו אפס. הגענו לסוף המערך – עברנו על כל האיברים ולא מצאנו צירוף של איברים שסכומם שווה לסכום הנדרש.
פתרון פתרון זה קל להציג כפונקציה רקורסיבית: calcWeights(int[] weights, int i, int sum) אשר מקבלת בנוסף על sum ו weights פרמטר נוסף i שמייצג את המשקולת שבודקים כעת ומחזירה תשובה לשאלה: האם ניתן להרכיב את הסכום מבין קבוצת המשקולות שבתת המערך weights[i…weights.length]. public static boolean calcWeights(int[] weights, int sum) { return calcWeights(weights, 0, sum); }
פתרון (המשך) public static boolean calcWeights(int[] weights, int i, int sum) { boolean res = false; if (sum == 0) res = true; else if (i >= weights.length) res = false; else res =( calcWeights(weights,i+1,sum-weights[i]) || calcWeights(weights, i + 1, sum) ); return res; }
דוגמא 4 מטריצה משופעת: מטריצה לא משופעת: 2 4 1 7 5 -2 3 -1 -8 -6 -9 -3 מטריצה תקרא משופעת אם: כל איברי האלכסון הראשי שווים לאפס. כל האיברים שנמצאים מתחת לאלכסון הראשי הם שליליים. כל האיברים שנמצאים מעל לאלכסון הראשי הם חיוביים. (מניחים כי המימד הראשי מגדיר את השורות והמשני מגדיר את העמודות). 2 4 1 7 5 -2 3 -1 -8 -6 -9 -3 מטריצה משופעת: מטריצה לא משופעת: 2 4 1 7 5 -2 3 6 -1 -8 -6 9 -3
דוגמא 4 – הרעיון נשים לב שאם נחסיר ממטריצה משופעת את השורה הראשונה ואת העמודה הראשונה, נקבל מטריצה משופעת. 4 1 4 1 5 -2 -1 -8 -2 -8 5 -1 נשאר לבדוק האם השורה והעמודה שהחסרנו מקיימות את התנאים.
דוגמא 4 –המשך נניח שקיימת פונקציה check המקבלת מטריצה ריבועית ואינדקס של שורה/עמודה ומחזירה האם השורה והעמודה מקיימות את התנאים. כתבו פונקציה רקורסיבית המקבלת מטריצה מלאה במספרים, ומחזירה true אם היא משופעת ו-false אחרת: public static boolean slope(int[][] data);
דוגמא 4 –המשך 4 1 5 -2 -1 -8 5 -1 דרך פעולה: בכל שלב של הרקורסיה נתייחס למטריצה הקטנה יותר (נתקדם לאורך האלכסון) ונבדוק אם תת המטריצה משופעת וגם שאר התנאים עבור המטריצה הנוכחית מתקיימים . 4 1 5 -2 -1 -8 5 -1 מהו מקרה הבסיס? ומה נבצע עבורו? תת המערך בגודל 1x1 ולכן נבדוק אם מכיל אפס.
והפתרון: public static boolean slope(int[][] data) { return slope(data, 0); } public static boolean slope(int[][] data, int index) { boolean isSlope = false; //end of array – last cell, if it’s 0 then it’s OK if (index == data.length - 1) isSlope = (data[index][index] == 0); else isSlope = (check(data,index) && slope(data, index+1)); return isSlope;
ניתן לבצע זאת גם בצורה רקורסיבית. דוגמא 4 –המשך איך נראת הפונקציה check? /* Check if row index, in data array, contains positive numbers and column index contains negative numbers (starting from index to the right & down) */ public static boolean check(int[][] data, int index) { boolean flag = (data[index][index] == 0); for (int i=1;(i<data.length-index) && flag; i=i+1) { if (data[index][index+i]<=0 || data[index+i][index]>=0) flag = false; } return flag; ניתן לבצע זאת גם בצורה רקורסיבית. כיצד?
דוגמה 5 - המבוך
המבוך נתון מערך דו-מימדי n x m של שלמים, בשם grid, המתאר מבוך: נקודת ההתחלה היא התא במיקום (0,0) במערך, נקודת הסיום היא התא במיקום (n-1,m-1), במצב ההתחלתי לכל תא ערך 0 או 1, כאשר 1 מסמל "דרך פנויה" ו-0 מסמל "דרך חסומה" (קיר). הפיתרון הרצוי הוא מסלול רציף מנקודת ההתחלה לנקודת הסיום (מותר ללכת למטה, ימינה, למעלה ושמאלה). את המסלול נסמן בעזרת החלפת ה- 1-ים שעל המסלול, ב- 7.
המבוך - דוגמא מבוכים: 1110 1110 1011 1010 0001 1110 1101 0001 בפיתרון: נסמן ב- ערך 3 את התאים שבהם כבר ביקרנו כדי שלא נחזור אליהם שנית. נסמן ב-7 את הדרך מההתחלה אל הסיום. מבוך ללא פתרון מה יכול להתרחש אם נטייל על המסלול, ולא נבדוק האם ביקרנו כבר בתא אליו אנו הולכים?
המבוך בפיתרון: נסמן ב-3 את התאים שבהם כבר ביקרנו כדי שלא נחזור אליהם שנית. נסמן ב-7 את הדרך מההתחלה אל הסיום. מבוך: 1110110001111 1011101111001 0000101010100 1110111010111 1010000111001 1011111101111 1000000000000 1111111111111
המבוך פתרון רצוי: 7770110001111 3077707771001 0000707070300 7770777070333 7070000773003 7077777703333 7000000000000 7777777777777 הסבר: איך חושבים על זה בצורה רקורסיבית. ננסה מהקובייה הנוכחית להסתכל על הקוביות השכנות ולראות אם יש דרך מאחת מהן. אם כן – הדרך מהקובייה הנוכחית היא הדרך המתקבלת ע"י הוספת הקובייה הנוכחית לתחילת הדרך שבה ניתן להגיע מהקובייה השכנה.
איך חושבים על זה בצורה רקורסיבית? ננסה להסתכל מהתא הנוכחי על התאים השכנים ולראות אם יש דרך מאחד מהן. אם כן – הדרך מהתא הנוכחי היא הדרך המתקבלת ע"י הוספת התא הנוכחי לתחילת הדרך שבה ניתן להגיע מהתא השכן. המשימה שלנו היא למצוא מעבר מנקודת ההתחלה לנקודת הסיום, אך האלגוריתם שנתאר אינו מתחיל דווקא בנקודת ההתחלה, אלא ימצא את הדרך ליציאה מכל נקודה במבוך. solve(int[][] grid, int row, int col)
תיאור האלגוריתם הקריאה הראשונה לאלגוריתם תעביר כקלט את הכניסה בתור שורה 0 וטור 0. solve(grid, 0, 0) האלגוריתם נעזר בפונקציה valid(grid, row, col) המקבלת כקלט מבוך, שורה וטור, ומחזירה ערך בוליאני – true אםם התא במבוך המוגדר ע"י השורה והטור הוא תא חוקי. public static void main(String[] args) { int[][] grid = {…}; boolean ans = solve(grid, 0, 0); if (ans) System.out.println("Maze solved!:\n"); else System.out.println("There is no solution"); } תא חוקי = לא חורג מגבולות המבוך, פנוי ולא ביקרנו בו כבר (כלומר, מסומן ב-1).
תיאור solve(grid, row, col) הגדר משתנה בוליאני. אם ( valid(grid, row, col) ) 2.1. סימון שביקרנו בתא ואין צורך לבקר בו שוב. 2.2. בדיקת תנאי עצירה. 2.3. אחרת, 2.3.1. נסה למטה. 2.3.2. נסה ימינה. 2.3.3. נסה למעלה. 2.3.4. נסה שמאלה. 2.4. אם done הינו true סמן שתא זה הוא חלק מהפתרון. 3. החזר את done. Done מסמן שהגענו לנקודת היציאה. ומצאנו דרך במבוך.
תיאור solve(grid, row, col) הגדר משתנה בוליאני. /* done=false */ אם ( valid(grid, row, col) ) 2.1. סימון שביקרנו בתא ואין צורך לבקר בו שוב. /* grid[row][col]=3 */ 2.2. בדיקת תנאי עצירה. /* אם row היא השורה התחתונה וגם col הוא הטור הימיני ב- grid, אז done=true*/ 2.3. אחרת, 2.3.1. נסה למטה. /* done=solve(grid, row+1, col)*/ 2.3.2. נסה ימינה. /* done=solve(grid, row, col+1)*/ 2.3.3. נסה למעלה. /* done=solve(grid, row-1, col)*/ 2.3.4. נסה שמאלה. /* done=solve(grid, row, col-1)*/ 2.4. אם done הינו true סמן שתא זה הוא חלק מהפתרון. /* grid[row][col]=7*/ 3. החזר את done. Done מסמן שהגענו לנקודת היציאה. ומצאנו דרך במבוך.
public static boolean solve(int[][] grid, int row, int col) { boolean done = false; if (valid(grid, row, col)) { grid[row][col] = 3; // mark visted if ((row == grid.length-1) && (col == grid[0].length-1)) done = true; // maze is solved else { done=( solve(grid, row + 1, col) || // try down solve(grid, row, col+1) || // try right solve(grid, row-1, col) || // try up solve(grid, row, col-1) ); // try left } if (done) grid[row][col] = 7; // mark as part of the path return done;
דוגמת הרצה 1110110001111 1011101111001 0000101010100 1110111010111 1010000111001 1011111101111 1000000000000 1111111111111 3 3 נק' התחלה 3 נסה למטה נסה למטה נסה ימינה נסה למעלה נסה שמאלה חזור צעד אחד אחורה נסה ימינה ...
בשלב כלשהו בריצה, אנו נמצאים בקריאה אשר סימנה ב-3 את התא המודגש: (row=3, col=6) 3330110001111 3033301111001 0000301010100 1110333010111 1010000111001 1011111101111 1000000000000 1111111111111
בשלב כלשהו בריצה, אנו נמצאים בקריאה אשר סימנה ב-3 את התא המודגש: (row=3, col=6) 3330110001111 3033301111001 0000301010100 1110333010111 1010000111001 1011111101111 1000000000000 1111111111111 בשלב זה תהיה קריאה רקורסיבית על המשבצת מתחתיה, משבצת המסומנת ב-0, כלומר קיר. בדיקת ה-valid תחזיר false ואותה קריאה תסתיים.
תתבצע קריאה נוספת מהמשבצת המסומנת, הפעם ימינה, ובדיקת ה-valid תיכשל שוב. 3330110001111 3033301111001 0000301010100 1110333010111 1010000111001 1011111101111 1000000000000 1111111111111
בקריאה הבאה מהמשבצת המסומנת, הפעם למעלה, בדיקת ה-valid תחזיר true 3330110001111 3033301111001 0000301010100 1110333010111 1010000111001 1011111101111 1000000000000 1111111111111
הקריאה הרקורסיבית במשבצת העליונה תסמנה ב-3 והריצה תמשיך. 3330110001111 3033301111001 0000303010100 1110333010111 1010000111001 1011111101111 1000000000000 1111111111111
זו התמונה אחרי כמה קריאות זו התמונה אחרי כמה קריאות. בשלב זה תתבצע קריאה מהמשבצת המסומנת והיא נכשלת. הקריאות חוזרות. 3330110001111 3033303331001 0000303030300 1110333030333 1010000133003 1011111103333 1000000000000 1111111111111 הקריאה הבאה מתבצעת למשבצת משמאל עם הערך 1. נכשלנו: נחזור חזרה בכל קריאה ונחזיר false 3
בתום הקריאה הרקורסיבית במשבצת המסומנת במרובע ה – grid יראה כך בתום הקריאה הרקורסיבית במשבצת המסומנת במרובע ה – grid יראה כך. הקריאה הרקורסיבית במשבצת זו תחזיר ערך true. 3330110001111 3033303331001 0000303070300 7770333070333 7070000773003 7077777703333 7000000000000 7777777777777 במסגרת הקוראת (הקריאה במשבצת המסומנת באדום) תסמן גם 7 ותחזיר true. המשבצת הימנית לא תיבדק. 7
בסיום הריצה, כשהגענו אל היציאה, הדרך תסומן ב-7 והמבוך יראה כך: 7770110001111 3077707771001 0000707070300 7770777070333 7070000773003 7077777703333 7000000000000 7777777777777 ונחזיר true דיון: האם תמיד יש פתרון אחד? אם לא, איזה פתרון נמצא? מה היה יכול לקרות אם לא היינו מסמנים כל תא שבדקנו (כאן סימון זה היה המספר 3)?
דוגמא 6 כתבו פונקציה רקורסיבית שמקבלת מחרוזת ומחזירה את המחרוזת הפוכה. לדוגמא: עבור הקלט “hello” הפונקציה תחזיר את הפלט “olleh”. דרך פעולה: מקרה הבסיס: אם מדובר במחרוזת ריקה (ללא תווים) או במחרוזת בעלת תו אחד ניתן להחזיר את התו עצמו (ובמקרה של ריקה להחזיר ""( אחרת, נמצא את המחרוזת ההפוכה של מחרוזת קטנה יותר (הקטנת הבעיה) ע"י צמצום המחרוזת בתו אחד השמאלי, ונוסיף את התו השמאלי מימין למחרוזת ההפוכה שנקבל מהקריאה הרקורסיבית.
והפתרון: public static String reverse(String s){ String res = ""; if (s.length()==0) res = s; else res = reverse(s.substring(1)) + s.charAt(0); return res; } שימו לב: הפונקציה s.substring(i) מחזירה את המחרוזת s ממקום i. לא! (בכיתה ראינו דוגמא למימוש עם רקורסיית זנב) רקורסית זנב?