Sudoku Solver
لوحة سودوكو عبارة عن شبكة 9 × 9 مقسمة إلى تسعة مربعات 3 × 3. في اللوحة المحلولة، يحتوي كل صف وكل عمود وكل مربع على كل رقم من 1 إلى 9 مرة واحدة بالضبط.
يأتي اللغز على هيئة 9 سلاسل نصية، واحدة لكل صف بدءًا من الأعلى؛ وتمثل . خلية فارغة. املأ كل الخلايا الفارغة وأعِد اللوحة المحلولة بالشكل نفسه. للوحة حل واحد بالضبط.
الدالة
- boardstring-array
- الصفوف التسعة من اللغز، ونقطة في كل خلية فارغة
- تُرجعstring-array
- الصفوف التسعة للوحة المحلولة
القيود
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 في ثلاثة أقنعة؛ وتزيله عملية أو الحصرية.
الخوارزمية
- أنشئ الشبكة والأقنعة
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"]