Generate Parentheses
מחרוזת של סוגריים נחשבת תקינה אם, כשקוראים אותה משמאל לימין, מספר התווים ) לעולם אינו עולה על מספר התווים (, ושני המספרים שווים בסוף. לכן (())() תקינה, ואילו ())( אינה תקינה: התו השלישי שלה סוגר זוג שלא נפתח מעולם.
בהינתן מספר שלם n, יש להחזיר את כל המחרוזות התקינות שמכילות n סוגריים פותחים ו-n סוגריים סוגרים, ממוינות בסדר לקסיקוגרפי, שבו ( מופיע לפני ).
פונקציה
- ninteger
- מספר זוגות הסוגריים
- מחזירהstring-array
- כל מחרוזת תקינה של n זוגות, בסדר לקסיקוגרפי
אילוצים
1 ≤ n ≤ 8- עבור
n = 8, התשובה מכילה 1,430 מחרוזות.
דוגמאות
- קלט
- n = 3
- פלט
- ["((()))", "(()())", "(())()", "()(())", "()()()"]
- הסבר
- שלושה זוגות יוצרים חמש מחרוזות תקינות.
((()))פותחת את שלושתם תחילה, ולכן היא ממוינת ראשונה;()()()סוגרת כל זוג מיד, ומגיעה אחרונה.
- קלט
- n = 1
- פלט
- ["()"]
- הסבר
- בזוג אחד יש מחרוזת אחת תקינה. הסידור האפשרי היחיד האחר,
)(, סוגר לפני שמשהו נפתח.
שאלת המשך
האם אפשר לספור את המחרוזות התקינות עבור n זוגות בלי ליצור אותן?
רמזים
פתחו אותם אחד אחד. כל רמז חושף קצת יותר.
קראו מחרוזת משמאל לימין וספרו את הזוגות הפתוחים. מה השתבש אם הספירה תרד מתחת לאפס?
בנה את המחרוזת תו אחד בכל פעם. אפשר להוסיף
(כל עוד הוספת פחות מ־nכאלה, ואפשר להוסיף)כל עוד הוספת פחות)מ־(. תמיד אפשר להשלים מחרוזת שנבנתה כך.בצעו קריאה רקורסיבית עם שני מונים,
openedו-closed. נסו את ענף(לפני ענף), הסירו כל תו לאחר שהקריאה שלו מסתיימת, ושמרו את המחרוזת כשהיא מגיעה לאורך2n. ניסיון של(תחילה שומר על פלט ממוין.
פתרון
מעט מחרוזות באורך 2n תקינות: 1,430 מתוך 65,536 עבור n = 8. בונים כל מחרוזת משמאל לימין ומוסיפים תו רק כאשר עדיין אפשר להשלים את הקידומת. שני מונים קובעים זאת, וניסיון של ( לפני ) גורם למחרוזות להתקבל ממוינות.
בנה כל מחרוזת, ואז בדוק אותה
האינטואיציה
מלא את 2n המקומות בכל שילוב אפשרי של ( ו־) — 4^n מחרוזות — והשאר רק את אלה שתקינות. כדי לבדוק מחרוזת, עבור עליה תוך מעקב אחר מאזן: הוסף 1 עבור (, והפחת 1 עבור ). המחרוזת תקינה אם המאזן לעולם אינו יורד מתחת ל־0 ומסתיים ב־0. הצבת ( לפני ) בכל מיקום מציגה את המחרוזות בסדר ממוין.
בדיקת כל מחרוזת עולה O(n). עבור n = 8, מדובר ב־65,536 מחרוזות עבור 1,430 תשובות, כך שכ־98% מהעבודה מתבזבזים, והכמות גדלה פי ארבעה עם כל זוג נוסף.
אלגוריתם
- החזיקו מאגר של
2nתווים ורשימה עבור התשובות. fill(pos): אםposשווה ל־2n, שמרו את המאגר אם הוא תקין.- אחרת, שימו
(במיקוםposוקראו ל־fill(pos + 1), ואז שימו שם)וקראו לה שוב. - כדי לבדוק, הוסיפו 1 עבור כל
(והחסירו 1 עבור כל). דחו אם המאזן יורד מתחת ל־0 או אינו מסתיים ב־0. - קראו ל־
fill(0)והחזירו את המחרוזות השמורות.
def generateParenthesis(n):
result = []
path = []
def is_balanced(text):
balance = 0
for ch in text:
balance += 1 if ch == "(" else -1
if balance < 0:
return False # a ")" with nothing open to close
return balance == 0
def fill():
if len(path) == 2 * n:
text = "".join(path)
if is_balanced(text):
result.append(text)
return
for ch in "()": # "(" first keeps the output sorted
path.append(ch)
fill()
path.pop()
fill()
return resultחזור לאחור בספירות הפתיחה והסגירה
האינטואיציה
העבירו את הבדיקה אל תוך הבנייה. קידומת יכולה להתרחב למחרוזת תקינה בדיוק כאשר היא משתמשת בעד n תווים מסוג ( ולעולם אין בה יותר תווים מסוג ) מאשר תווים מסוג (. לכן מותר להוסיף ( כאשר opened < n, ומותר להוסיף ) כאשר closed < opened. באורך 2n שני המונים הם n, והמחרוזת תקינה ואין עוד מה לבדוק.
לא מפספסים אף תשובה, כי כל קידומת של מחרוזת תקינה מצייתת לשני הכללים. אף ענף אינו מבוזבז: עבור n = 2, ענף (( יכול רק לסגור פעמיים, וכתוצאה מכך מתקבלת (()), וענף () יכול רק לפתוח ולסגור, וכתוצאה מכך מתקבלת ()(). כל מחרוזת היא מסלול אחד בעץ, ולכן היא מופיעה פעם אחת, וענף ( נבדק ראשון, כך שהפלט ממוין.
כל עלה הוא תשובה, ולכן העבודה פרופורציונית לפלט: O(4^n / √n).
אלגוריתם
- שמור את המחרוזת שנבנית ואת שני המונים,
openedו-closed, שניהם 0. - אם אורך המחרוזת הוא
2n, שמור עותק והחזר. - אם
opened < n, הוסף(, בצע קריאה רקורסיבית עםopened + 1, והסר אותו. - אם
closed < opened, הוסף), בצע קריאה רקורסיבית עםclosed + 1, והסר אותו. - התחל מהמחרוזת הריקה והחזר את המחרוזות שנשמרו.
def generateParenthesis(n):
result = []
path = []
def backtrack(opened, closed):
if len(path) == 2 * n:
result.append("".join(path))
return
# "(" sorts before ")", so trying it first keeps the output sorted
if opened < n:
path.append("(")
backtrack(opened + 1, closed)
path.pop()
if closed < opened: # only close a pair that is open
path.append(")")
backtrack(opened, closed + 1)
path.pop()
backtrack(0, 0)
return result
מלכודות ומקרי קצה
- לאפשר
)כאשרclosed < nבמקוםclosed < opened. כך נבנית())(, שסוגרת זוג שלא נפתח. - לבדוק רק שהספירות תואמות בסוף. כך מתקבלת
)(; האיזון חייב להישאר 0 או יותר בכל שלב. - לנסות
)לפני(. המחרוזות הנכונות מתקבלות בסדר הפוך. - לשמור את המאגר המשותף במקום עותק, כאשר הרשימות או בוני המחרוזות ניתנים לשינוי. כל תשובה שנשמרת מציגה אז את אותו מאגר, שריקון באמצעות חזרה לאחור מרוקן שוב.
- לקבוע את גודלו של מערך תוצאות קבוע לפי ניחוש קטן. עבור
n = 8יש 1,430 תשובות.
שאלות נפוצות3
מהי סיבוכיות הזמן של Generate Parentheses?
חיפוש עם חזרה לאחור מפיק מחרוזות C(n) = (2n)! / ((n+1)! n!), מספר קטלן, שגדל כמו 4^n / (n^1.5 √π). בניית כל אחת והעתקתה דורשת O(n), לכן זמן הריצה הוא O(4^n / √n). נדרשים O(n) מקום נוסף עבור המחרוזת הנוכחית ומחסנית הקריאות, נוסף על הפלט.
כמה מחרוזות סוגריים תקינות יש עבור n זוגות?
מספר קטלאן ה־n: 1, 2, 5, 14, 42, 132, 429 ו־1,430 עבור n מ־1 עד 8. כל מחרוזת תקינה מתפצלת ל־( A ) B, כאשר ) הזה תואם ל־( הראשון, ול־A ול־B יש יחד n - 1 זוגות. סכימה לפי הגודל של A נותנת את נוסחת הנסיגה של מספרי קטלאן.
האם אפשר לפתור את בעיית יצירת הסוגריים בלי רקורסיה?
כן. שמרו מחסנית של מצבים חלקיים, שכל אחד מהם הוא מחרוזת עם שני המונים שלה, והרחיבו כל מצב לפי אותם שני כללים. דחפו את ההרחבה ) לפני ההרחבה (, כך שההרחבה ( תישלף ראשונה והפלט יישאר ממוין.
בעיות דומות
בעיות שמשתמשות באותם רעיונות. פתרון של שתיים או שלוש מהן מקבע את הדפוס.
def generateParenthesis(n):
# כתבו כאן את הקודבדיקה #1
בדיקה #2
ארגומנט
n = 3
הפלט הצפוי
["((()))", "(()())", "(())()", "()(())", "()()()"]