Sudoku Solver
לוח סודוקו הוא רשת בגודל 9 × 9 המחולקת לתשע תיבות בגודל 3 × 3. בלוח פתור, בכל שורה, בכל עמודה ובכל תיבה מופיעות כל הספרות מ־1 עד 9 פעם אחת בדיוק.
החידה ניתנת כמחרוזות 9, אחת לכל שורה מלמעלה למטה; . הוא תא ריק. מלאו את כל התאים הריקים והחזירו את הלוח הפתור באותה צורה. ללוח יש פתרון אחד בלבד.
פונקציה
- boardstring-array
- 9 השורות של הפאזל, נקודה בכל תא ריק
- מחזירהstring-array
- 9 השורות של הלוח הפתור
אילוצים
boardמכיל 9 מחרוזות באורך 9.- כל תו הוא
.או אחת הספרות1עד9. - הספרות הנתונות לעולם אינן חוזרות באותה שורה, באותו טור או באותו ריבוע.
- ללוח יש בדיוק פתרון אחד.
דוגמאות
- קלט
- board = ["9.7.....6", "824.3....", "..1..9..2", ".9...2.18", "...4....5", "1325.8...", "...687...", "......8.9", ".4.9...5."]
- פלט
- ["957214386", "824736591", "361859742", "495372618", "786491235", "132568974", "519687423", "273145869", "648923157"]
- הסבר
- השורה העליונה
9.7.....6כבר מכילה את 9, 7 ו־6, ולכן שש התאים הריקים שלה מקבלים את 1, 2, 3, 4, 5 ו־8. העמודות והתיבות קובעות היכן כל אחד מהם מופיע, והשורה הופכת ל־957214386.
שאלת המשך
בסודוקו קטלני, הסכומים של התאים בכלובים חייבים להסתכם גם הם בסכומים הנתונים. איך היית מוסיף את הכלל הזה לחיפוש ומשתמש בו כדי לגזום ענפים מוקדם?
רמזים
פתחו אותם אחד אחד. כל רמז חושף קצת יותר.
עקוב אחר הספרות שכבר מופיעות בכל שורה, בכל עמודה ובכל תיבה של 3 × 3. כך תוכל לדעת ב־O(1) אם ספרה מתאימה לתא ריק.
מלאו את התאים הריקים בזה אחר זה. נסו כל ספרה שמתאימה לתא, עברו לתא הבא, ומחקו את הספרה אם אי אפשר להשלים איתה את שאר הלוח.
סדר התאים חשוב. מלאו תחילה את התא הריק שנותרו בו הכי מעט ספרות: תא שיש בו אפשרות אחת אינו דורש פיצול, ותא שאין בו אף אפשרות אומר לכם מיד לחזור לאחור.
פתרון
פתור את זה באמצעות חיפוש עם חזרה לאחור: הצב ספרה מתאימה, המשך, ובטל את ההצבה כשהלוח אינו ניתן להשלמה. סדר התאים קובע את המהירות. בלוח קשה, מעבר לפי סדר הקריאה עשוי לבדוק מאות מיליוני לוחות חלקיים; מילוי התא עם מספר האפשרויות הקטן ביותר תחילה דורש בערך מאה.
חיפוש עם חזרה לאחור בסדר הקריאה
נכונה, אבל לא מסתיימת בבדיקות הגדולות ביותר
האינטואיציה
מלאו את התאים הריקים לפי סדר הקריאה, כל אחד בספרה הקטנה ביותר שמתאימה. כשלא נותרת ספרה לתא, חזרו לתא הקודם ונסו את הספרה הבאה שלו. כל צירוף שעומד בכללים נבדק בתורו, וכך החיפוש מוצא את הפתרון.
שלוש טבלאות הופכות כל בדיקת התאמה ל־O(1): rowUsed[r][d], colUsed[c][d] ו־boxUsed[b][d], כאשר b = (r / 3) * 3 + c / 3 הוא התיבה. הצבת ספרה מעדכנת שלושה ערכים, והסרתה מאפסת אותם.
הסדר הקבוע הוא נקודת התורפה: החיפוש יכול למלא שורה שלמה בספרות שמתאימות מקומית, ורק בהמשך לגלות שהן לא משאירות מקום לתאים רבים. לוח ששורותיו העליונות כמעט ריקות מרמזים, ושהפתרונות בהן הם ספרות גבוהות, גורם לחיפוש לבדוק יותר מ־300 מיליון לוחות חלקיים.
אלגוריתם
- קרא את הלוח לתוך רשת עם 0 עבור תא ריק, מלא את
rowUsed,colUsedו-boxUsed, ורשום את התאים הריקים לפי סדר הקריאה. backtrack(k): אםkשווה למספר התאים הריקים, החזר true.- עבור כל ספרה
dמ-1 עד 9 שאינה בשימוש בשורה, בעמודה ובתיבה של התאk, הצב אותה, סמן אותה, והחזר true אםbacktrack(k+1)מחזירה true; אחרת בטל את הסימון שלה. - אם אף ספרה אינה מתאימה, נקה את התא והחזר false. קרא ל-
backtrack(0)והמר את הרשת בחזרה למחרוזות.
def solveSudoku(board):
grid = [[0 if ch == "." else int(ch) for ch in row] for row in board]
# row_used[r][d] is True when digit d already sits in row r; same for
# columns and boxes. Box b holds the cells with (r // 3) * 3 + c // 3 == b.
row_used = [[False] * 10 for _ in range(9)]
col_used = [[False] * 10 for _ in range(9)]
box_used = [[False] * 10 for _ in range(9)]
empties = []
for r in range(9):
for c in range(9):
d = grid[r][c]
if d:
row_used[r][d] = col_used[c][d] = box_used[(r // 3) * 3 + c // 3][d] = True
else:
empties.append((r, c))
def backtrack(k):
if k == len(empties):
return True
r, c = empties[k]
b = (r // 3) * 3 + c // 3
for d in range(1, 10):
if not row_used[r][d] and not col_used[c][d] and not box_used[b][d]:
grid[r][c] = d
row_used[r][d] = col_used[c][d] = box_used[b][d] = True
if backtrack(k + 1):
return True
row_used[r][d] = col_used[c][d] = box_used[b][d] = False
grid[r][c] = 0
return False
backtrack(0)
return ["".join(str(d) for d in row) for row in grid]מלאו תחילה את התא הריק ביותר
האינטואיציה
הפתרון אינו תלוי בסדר המילוי, לכן בחרו בסדר שמשאיר את מרחב החיפוש קטן: בכל שלב, מלאו את התא הריק שמאפשר את מספר הספרות הקטן ביותר. תא שמותרת בו ספרה אחת בלבד הוא כפוי ואינו מוסיף ענף. תא שלא מותרת בו אף ספרה נבחר מיד, כך שלוח שגוי מתגלה מיד.
כדי לספור ביעילות את הספרות המותרות עבור 81 תאים בכל שלב, שמרו כל שורה, עמודה ותיבה כמסכת סיביות: הסיבית d של rows[r] מוגדרת כאשר הספרה d נמצאת בשורה r. תא מאפשר את ~(rows[r] | cols[c] | boxes[b]) & 0x3FE, כאשר 0x3FE שומר על הסיביות 1 עד 9, והספירה היא מספר הסיביות המוגדרות. הצבת d מגדירה את הסיבית d בשלוש מסכות; פעולת XOR מסירה אותה.
אלגוריתם
- בנה את הרשת ואת המסכות
rows,colsו-boxesמתוך הספרות הנתונות. backtrack(): סרוק את כל התאים, חשב את הספרות המותרות לכל תא ריק, ושמור את התא הראשון עם המספר הקטן ביותר. אם אין תאים ריקים, החזר true.- עבור כל ספרה מותרת באותו תא, הצב אותה, הגדר את הביט שלה בשלוש המסכות, והחזר true אם
backtrack()עושה זאת; אחרת, נקה את הביטים. - אם אף ספרה לא מתאימה, רוקן את התא והחזר false. קרא ל-
backtrack()והפוך את הרשת בחזרה למחרוזות.
def solveSudoku(board):
grid = [[0 if ch == "." else int(ch) for ch in row] for row in board]
# Bit d of rows[r] is set when digit d already sits in row r; same for
# columns and boxes. Box b holds the cells with (r // 3) * 3 + c // 3 == b.
rows, cols, boxes = [0] * 9, [0] * 9, [0] * 9
for r in range(9):
for c in range(9):
d = grid[r][c]
if d:
rows[r] |= 1 << d
cols[c] |= 1 << d
boxes[(r // 3) * 3 + c // 3] |= 1 << d
def backtrack():
# Pick the empty cell with the fewest digits left (the first one on a tie).
best_r, best_c, best_allowed, best_count = -1, -1, 0, 10
for r in range(9):
for c in range(9):
if grid[r][c] == 0:
# Bits 1 to 9 of 0x3FE are the nine digits.
allowed = ~(rows[r] | cols[c] | boxes[(r // 3) * 3 + c // 3]) & 0x3FE
count = bin(allowed).count("1")
if count < best_count:
best_r, best_c, best_allowed, best_count = r, c, allowed, count
if best_r == -1:
return True # no empty cell is left: the board is solved
r, c = best_r, best_c
b = (r // 3) * 3 + c // 3
for d in range(1, 10):
bit = 1 << d
if best_allowed & bit:
grid[r][c] = d
rows[r] |= bit
cols[c] |= bit
boxes[b] |= bit
if backtrack():
return True
rows[r] ^= bit
cols[c] ^= bit
boxes[b] ^= bit
grid[r][c] = 0
return False
backtrack()
return ["".join(str(d) for d in row) for row in grid]
מלכודות ומקרי קצה
- שוכחים לנקות ספרה מרשומות השורה, העמודה והתיבה כשמבטלים את ההצבה שלה. התאים הבאים רואים אז את הספרה כמשומשת, והחיפוש מחמיץ את הפתרון.
- משתמשים ב-
r / 3 + c / 3עבור התיבה. התאים(0, 4)ו-(4, 0)מגיעים שניהם ל-1, כך ששתי תיבות חולקות רשומה אחת ואי אפשר למלא את הלוח. השתמשו ב-(r / 3) * 3 + c / 3עם חלוקה שלמה. - לא עוצרים בפתרון הראשון. שלבי הביטול מרוקנים שוב את הלוח לפני שקוראים את תוכנו.
שאלות נפוצות3
מהי סיבוכיות הזמן של פותר סודוקו באמצעות חיפוש עם חזרה לאחור?
אקספוננציאלי במקרה הגרוע ביותר, O(9^m) עבור m תאים ריקים, מכיוון שכל תא יכול להסתעף לעד 9 ספרות. בחידות אמיתיות, כללי השורה, העמודה והאזור מצמצמים את רוב ההסתעפויות, ומילוי התא הריק ביותר תחילה מצמצם את רוב היתר.
איך בודקים שלסודוקו יש פתרון אחד בלבד?
הריצו את אותו חיפוש עם חזרה לאחור, אך ספרו את הפתרונות ועצרו כשהספירה מגיעה ל־2. ספירה של 1 פירושה שלחידה יש פתרון יחיד, ו־0 פירושה שאין לה פתרון. מחוללי חידות מריצים את הבדיקה הזו לאחר הסרת כל רמז.
האם יש דרך מהירה יותר לפתור סודוקו מאשר באמצעות חזרה לאחור?
רוב הפותרים המהירים עדיין חוזרים לאחור, אבל מסיקים יותר לפני כל ניחוש. התפשטות אילוצים ממלאת כל תא שיש לו אפשרות אחת בלבד, וכל ספרה שמתאימה לתא אחד בלבד בשורה, בעמודה או בתיבה שלה, ואז חוזרת על התהליך. אפשר גם לנסח סודוקו כבעיית כיסוי מדויק ולפתור אותה באמצעות אלגוריתם X של קנות'.
בעיות דומות
בעיות שמשתמשות באותם רעיונות. פתרון של שתיים או שלוש מהן מקבע את הדפוס.
def solveSudoku(board):
# כתבו כאן את הקודבדיקה #1
ארגומנט
board = ["9.7.....6", "824.3....", "..1..9..2", ".9...2.18", "...4....5", "1325.8...", "...687...", "......8.9", ".4.9...5."]
הפלט הצפוי
["957214386", "824736591", "361859742", "495372618", "786491235", "132568974", "519687423", "273145869", "648923157"]