מבוא למדעי המחשב 2019 תרגול 12 מחסנית )Stack( memoization
ראינו בהרצאה מחסנית Infix/Postfix Notations
בתרגול היום מחסנית בדיקת איזון סוגריים בביטוי אריתמטי מימוש תור באמצעות מחסנית memoization דוגמאות שימוש: צפרדע, בעיית העודף.
תזכורת: מחסנית :)stack( מבנה נתונים שפועל בשיטת )LIFO( : האיבר שנכנס אחרון למחסנית יוצא ממנה ראשון Last In - First Out Data Data Data Data Data Data
תזכורת: ממשק Stack public interface Stack<T> { /** * push - adds an element to the top of the stack. */ public void push (T element); /** * pop - removes an element form the stack (LIFO order). * @return the element from the top of the stack. */ public T pop (); /** * peek - looks at the object at the top of this stack without removing it. * @return the element from the top of the stack. */ public T peek (); /** * isempty - checks if the stack is empty or not. * @return true if there is no more elements in the stack. */ public boolean isempty();
בדיקת איזון סוגריים בביטוי אריתמטי בעיה: קלט: ביטוי אריתמטי )מחרוזת( שעשוי להכיל סוגריים : (, ) פלט: true אם הסוגריים בביטוי מאוזנים כהלכה false אחרת )האלגוריתם לא מתעניין בשאר רכיבי הביטוי פרט לסוגריים( ((6) + (12 (9))) ((6 + (12 9)) true false
איזון סוגריים: אלגוריתם כיצד נפתור את הבעיה? נשים לב לעובדה הבאה: בקריאת ביטוי מאוזן משמאל לימין, בהגיענו למקום מסוים i, מספר הסוגרים הפותחים "(" גדול שווה למספר הסוגרים הסוגרים ")" מתחילת הביטוי ועד למקום ה- i.."(" עד לחץ )כולל( יש 5 סוגרים פותחים "(" ו- 2 סוגרים סוגרים לדוגמא, בביטוי:( ) ) ( ( ) ) ( ( ( נשתמש במחסנית על מנת לבדוק האם התנאי מתקיים
public class Balance { public static boolean balanced(string expr) { Stack<Character> st = new StackAsDynamicArray<>() ; for(int i=0; i < expr.length(); i=i+1) { char c = expr.charat(i); if(c == '(') st.push(c); else if(c == ')') { if(st.isempty()) return false; else st.pop(); return st.isempty(); איזון סוגריים: מימוש פשוט
איזון סוגריים: פתרון כללי בעיה: <>, (), {, [] קלט: ביטוי אריתמטי )מחרוזת( שעשוי להכיל סוגריים ממספר סוגים: public class Balance { public static void main(string[] args) { String open = "[<({"; String close = "]>)"; String expr1 = "(<6>+{12 [9])"; String expr2 = "(<6+{12 9])"; System.out.println(balanced(expr1, open, close)); // true System.out.println(balanced(expr2, open, close)); // false
איזון סוגריים: אלגוריתם נשנה במעט את הפתרון הקודם נשים לב לעובדה הבאה: אם נתקלנו בסוגר סוגר סוגר 'סוגר' מאותו סוג 'פותח' מסוג כלשהו אחרת הביטוי לא מאוזן 'פותח' מסוג כלשהו, אז הסוגר הבא שנתקל בו הינו
איזון סוגריים: מימוש public class Balance { public static boolean balanced(string expr, String open, String close) { Stack<Character> st = new StackAsDynamicArray<>() ; for(int i=0; i < expr.length(); i=i+1) { char c = expr.charat(i); if(open.indexof(c) >= 0) st.push(c); else { int index = close.indexof(c) ; if(index >= 0( if(st.isempty() (st.pop()!= open.charat(index))) return false ; { return st.isempty();
public interface Queue<T> { /** * Checks if the queue is empty or not. * @return true if the queue is empty */ public boolean isempty(); /** * Removes an element from the head of the queue. * (FIFO order) * @return the next element in the queue. */ public T dequeue(); /** * Inserts an element into the queue. * @param elemment the element to be enqueued. */ public void enqueue(t element); /** * Returns the top element without removing it. * @return the next element in the queue. */ public T peek(); תזכורת: ממשק Queue
מימוש תור באמצעות מחסנית לרשותנו מבנה נתונים יחיד מסוג מחסנית. כיצד ניתן לממש את הממשק "תור" באמצעות מבנה זה ללא שימוש במבני נתונים נוספים? תשובה: לא ניתן לעשות זאת באמצעות לולאה ושימוש במחסנית יחידה, אך ניתן לבצע זאת באמצעות שתי מחסניות.
מימוש תור באמצעות מחסנית הרעיון המרכזי: בכל רגע בו המחסנית אינה ריקה, האיבר הראשון בתור, יהיה האיבר האחרון במחסנית. כלומר: ביצוע "pop" למחסנית שולף את האיבר הראשון בתור. עלינו לדאוג, שהכנסת איבר חדש לתור תשמר תכונה זו, העבודה יהיה במימוש השיטה."enqueue" לכן עיקר
4? מימוש תור באמצעות מחסנית רעיון המימוש )באמצעות שתי מחסניות(: מחסנית א' תכיל את איברי התור כסדרם, ביצוע pop ישלוף את האיברים בסדר המצופה מהתור )לפי סדר כניסתם לתור.)FIFO מחסנית ב' תשמש כמחסנית עזר והיא תהיה ריקה. כאשר נתבקש להכניס איבר חדש לתור :)enqueue( נעביר את תוכן מחסנית א' לתוך מחסנית ב'. נדחף את האיבר החדש למחסנית א' )הוא יהיה בתחתית המחסנית(. נחזיר חזרה את האיברים ממחסנית ב', למחסנית א'. 1 2 3 מחסנית ב' מחסנית א'
מימוש תור באמצעות מחסנית אך לרשותנו אובייקט מה עושים? יחיד מסוג מחסנית. תשובה: מחסנית הקריאות של,Java תשמש אותנו כ"מחסנית ב' ". כזכור, רקורסיה, מעניקה לנו מחסנית נוספת מתנה.
public class QueueAsStack<T> implements Queue<T> { מימוש תור באמצעות מחסנית private Stack<T> stack; public QueueAsStack() { stack = new StackAsDynamicArray<>();//implemented // in class public void enqueue(t element) { if(element == null) throw new IllegalArgumentException(); if(stack.isempty()) stack.push(element); else { T elementinstack = stack.pop(); enqueue(element); stack.push(elementinstack); public boolean isempty() { return stack.isempty(); public T dequeue() { if(stack.isempty()) throw new NoSuchElementException(); return stack.pop(); public T peek() { if(stack.isempty()) throw new NoSuchElementException(); return stack.peek(); //end of class QueueAsStack
מימוש תור באמצעות מחסנית if(stack.isempty()) stack.push(element); בדוגמא הבאה נכניס לתור את המספרים: 2 1, ו- 3. element = 1 enqueue(1) 1 מחסנית הקריאות של Java המחסנית שלרשותנו
מימוש תור באמצעות מחסנית else { T elementinstack = stack.pop(); enqueue(element); stack.push(elementinstack); כבר בתור( element = 2 eis = 1 Enqueue(2) 1( הכנסת 2
מימוש תור באמצעות מחסנית else { T elementinstack = stack.pop(); enqueue(element); stack.push(elementinstack); element = 2 Enqueue(2) element = 2 eis = 1 Enqueue(2)
מימוש תור באמצעות מחסנית if(stack.isempty()) stack.push(element); 2 element = 2 element = 2 Enqueue(2) element = 2 eis = 1 Enqueue(2)
מימוש תור באמצעות מחסנית else { T elementinstack = stack.pop(); enqueue(element); stack.push(elementinstack); element = 2 eis = 1 Enqueue(2) 1 2
מימוש תור באמצעות מחסנית else { T elementinstack = stack.pop(); enqueue(element); stack.push(elementinstack); ו- 2 בתור( element = 3 eis = 1 Enqueue(3) 1( הכנסת 3 2
מימוש תור באמצעות מחסנית else { T elementinstack = stack.pop(); enqueue(element); stack.push(elementinstack); element = 3 eis = 1 Enqueue(3) 2
מימוש תור באמצעות מחסנית else { T elementinstack = stack.pop(); enqueue(element); stack.push(elementinstack); element = 3 eis = 2 Enqueue(3) element = 3 eis = 1 Enqueue(3)
מימוש תור באמצעות מחסנית if(stack.isempty()) stack.push(element); 3 element = 3 Enqueue(3) element = 3 eis = 2 Enqueue(3) element = 3 eis = 1 Enqueue(3)
מימוש תור באמצעות מחסנית else { T elementinstack = stack.pop(); enqueue(element); stack.push(elementinstack); 2 3 element = 3 eis = 2 Enqueue(3) element = 3 eis = 1 Enqueue(3)
מימוש תור באמצעות מחסנית else { T elementinstack = stack.pop(); enqueue(element); stack.push(elementinstack); element = 3 eis = 1 Enqueue(3) 1 2 3
הפסקה
Memoization
צפרדע בהינתן ערך טבעי n, המייצג לוח משובץ בגודל n x n וצפרדע המצויה בפינתו השמאלית התחתונה. בכמה דרכים היא יכולה להגיע לפינה הימנית העליונה אם מותר לה לקפץ באופן הבא: או קפיצה אחת ימינה או שתי קפיצות ימינה או קפיצה אחת למעלה או שתי קפיצות למעלה? דוגמאות: עבור לוח בגודל 1, x 1 יש פתרון יחיד )כי היא כבר נמצאת בפינה הימנית העליונה(. עבור לוח בגודל 3, x 3 ישנן 14 דרכים.
צפרדע פתרון: public static int frog(int n){ return frog(n, n); private static int frog(int n, int m) { if(n == 1 && m == 1) //Frog has reached the top-right corner return 1; if(n < 1 m < 1) //Frog got lost return 0; return frog(n-1, m) + frog(n-2, m) + frog(n, m-1) + frog(n, m-2);
צפרדע: דוגמת ריצה עבור לוח בגודל 3: x 3 3 x 3 2 x 3 1 x 3 3 x 2 3 x 1 1 x 3 2 x 2 2 x 1 1 x 2 1 x 2 2 x 1
צפרדע התייעלות נרצה להימנע מביצוע חישובים שכבר חישבנו בעבר. נשתמש בשיטת הפתקאות (memoization) : נשמור במבנה נתונים מתאים )שמימדו כמס' המשתנים בבעיה( את תוצאות החישובים שכבר ביצענו. בהינתן קלט, נבדוק האם כבר חושב עבורו הפלט המתאים. אם כבר חושב, נחזיר את התוצאה שחושבה בעבר. אם עדיין לא חושב, נבצע את החישוב. לבסוף, נשמור את התוצאה במקום המתאים במבנה הנתונים שלנו.
צפרדע memoization איך נראית טבלת ה?memoization מה הגודל שלה? אמנם לבעיה המקורית יש פרמטר אחד, n, אך בכל שלב אנחנו שואלים על שני דברים: > m <, n :0 < n, m n לכן גודל הטבלה יהיה: 1) + (n.(n + 1) n m 0 1 2 0 1 2 1 1 1 2? frog(1, 2) + frog(2, 1) לדוגמה עבור :frogmemo(2)
public static int frogmemo(int n) { int[][] memo = new int[n+1][n+1]; צפרדע memoization for(int[] row : memo) //initialize memoization table for(int i = 0; i < row.length; i = i + 1) row[i] = -1; return frogmemo(n, n, memo);
צפרדע memoization private static int frogmemo(int n, int m, int[][] memo) { if(n == 1 & m == 1) //Frog has reached the top-right corner return 1; if(n < 1 m < 1) //Frog got lost return 0; if (memo[n][m] == -1){ memo[n][m] = frogmemo(n-1, m, memo) + frogmemo(n-2, m, memo) + frogmemo(n, m-1, memo) + frogmemo(n, m-2, memo); memo[m][n] = memo[n][m]; return memo[n][m];
בעיית העודף בהינתן ערך N, נרצה להחזיר עודף של N שקלים. יש לנו אספקה אינסופית של מטבעות m.weights={w 1 w, 2 w,, בכמה דרכים שונות נוכל להחזיר עודף של N שקלים? )כאשר סדר המטבעות לא משנה( 4 לדוגמה: עבור 4=N,,weights={1,2,3 {1,1,1,1, {2,1,1, {2,2, {3,1 לכן הפלט הנכון הוא 4. יש פתרונות אפשריים: שימו לב ש = 1 1 w,אחרת לא נוכל לתת עודף בחלק מהמקרים.
בעיית העודף כיצד נוכל לפתור את הבעיה באופן רקורסיבי? מה יהיה תנאי העצירה? אילו בעיות קטנות יותר נרצה לפתור "במקום" הבעיה הגדולה? כיצד נשלב בין הפתרונות לתתי הבעיות הקטנות?
בעיית העודף public static int count(int[] weights, int n) { return count(weights, n, weights.length - 1); private static int count(int[] weights, int n, int i) { // If n is 0 then there is 1 solution (do not include any coin) if(n == 0) return 1; weights { 1 2 3 // If n is less than 0 or there are no coins left, then no solution exists if(n < 0 i < 0) return 0; i // count is sum of solutions (1) excluding weights[i] (2) including weights[i] return count(weights, n, i - 1) + count(weights, n - weights[i], i);
בעיית העודף:דוגמת ריצה
בעיית העודף memoization איך נראת טבלת ה?memoization מה הגודל שלה? n i 0 1 0 1 2 1 1 1 1 2? הטבלה מחזיקה מופעים של הבעיה <i,n> : 0 n N; 0 i weights.length-1 לכן גודל הטבלה (N+1)*weights.length לדוגמה עבור (2 :count({1,2, count(weights, i - 1, n) + count(weights, i, n - weights[i])
public static int countmemo(int[] weights, int n ) { int[][] memo = new int[n+1][weights.length]; for(int[] row : memo) //initialize memoization table for(int i = 0; i < row.length; i = i + 1) row[i] = -1; return countmemo(weights, weights.length - 1, n, memo); בעיית העודף memoization private static int countmemo(int[] weights, int i, int n,int[][] memo) { if(n == 0) // If n is 0 then there is 1 solution (do not include any coin) return 1; // If n is less than 0 or there are no coins left, then no solution exists if(n < 0 i < 0) return 0; if(memo[n][i] == -1) // count is sum of solutions (1) excluding weights[i], (2) including weights[i] memo[n][i] = countmemo(weights, i - 1, n, memo) + countmemo(weights, i, n - weights[i], memo); return memo[n][i];
סיכום ומשימות תרגלנו: מחסנית memoization