سودوکو جدولی است 9x9 با 81 خانه که تعدادی از این خانه ها با اعدادی پر شده اند و تعدادی دیگر نیز خالی می باشند . بازیکن می بایست کلیه خانه های خالی را با اعداد 1 تا 9 طبق قوانین زیر پر کند :
- هر عدد در یک سطر ، فقط می بایست یکبار تکرار شده باشد.
- هر عدد در یک ستون ، می بایست فقط یکبار تکرار شده باشد.
- هر عدد در مربعات 3x3 کنار هم می بایست فقط یکبار تکرار شده باشد.
به مثال زیر توجه کنید . جدول سمت چپ سودکوی حل نشده با تعدادی جای خالی و جدول سمت راست حل شده آن است . سعی کنید سه قانون فوق را در این مثال بررسی کنید :
+---+---+---+ +---+---+---+
|4..|...|8.5| |417|369|825|
|.3.|...|...| |632|158|947|
|...|7..|...| |958|724|316|
+---+---+---+ +---+---+---+
|.2.|...|.6.| |825|437|169|
|...|.8.|4..| |791|586|432|
|...|.1.|...| |346|912|758|
+---+---+---+ +---+---+---+
|...|6.3|.7.| |289|643|571|
|5..|2..|...| |573|291|684|
|1.4|...|...| |164|875|293|
+---+---+---+ +---+---+---+
در مثال بالا ، چهار گوشه ی هر مربع 3x3 یک علامت + قرار گرفته است . سطر ها و ستون ها نیز کاملا مشخص می باشند.
این برنامه به مانند اکثر برنامه های دیگر ما می بایست مطابق با استاندارد یونیکس باشد ؛ یعنی در صورت فراهم شدن نام فایل در خط فرمان هنگام فراخوانی ، برنامه باید محتوای مورد نیاز خود را از این فایل ها بخواند و در غیر این صورت می بایست منتظر ورود محتوا از « ورودی استاندارد » بماند .
توجه داشته باشید : برنامه ای که بتواند از ورودی استاندارد بخواند می تواند از pipe هم بخواند و برنامه ای که در خروجی استاندارد بنویسد می تواند در pipe هم بنویسد . این قانون باعث توانایی برنامه جهت ارتباط با سایر برنامه ها می شود.
شکل ورودی مورد نیاز حل کننده ی سودوکوی ما باید به صورت رشته ای 81 کاراکتری از اعداد و . (دات) به عنوان جای خالی باشد. برنامه می بایست توانایی حل بی نهایت سودوکو در هر فراخوانی را داشته باشد ، بنابر این هر خط حاوی یک سودوکو می باشد و خط بعدی سودوکوی دیگری است . یک نمونه از ورودی برنامه می تواند حاوی سه خط زیر باشد که نشان دهنده سه سودوکو متفاوت می باشد :
.6.5.4.3.1...9...8.........9...5...6.4.6.2.7.7...4...5.........4...8...1.5.2.3.4.
7.....4...2..7..8...3..8.799..5..3...6..2..9...1.97..6...3..9...3..4..6...9..1.35
....7..2.8.......6.1.2.5...9.54....8.........3....85.1...3.2.8.4.......9.7..6....
این سه خط نشان دهنده سه سودوکو به شکل زیر می باشند :
+---+---+---+ +---+---+---+ +---+---+---+
|.6.|5.4|.3.| |7..|...|4..| |...|.7.|.2.|
|1..|.9.|..8| |.2.|.7.|.8.| |8..|...|..6|
|...|...|...| |..3|..8|.79| |.1.|2.5|...|
+---+---+---+ +---+---+---+ +---+---+---+
|9..|.5.|..6| |9..|5..|3..| |9.5|4..|..8|
|.4.|6.2|.7.| |.6.|.2.|.9.| |...|...|...|
|7..|.4.|..5| |..1|.97|..6| |3..|..8|5.1|
+---+---+---+ +---+---+---+ +---+---+---+
|...|...|...| |...|3..|9..| |...|3.2|.8.|
|4..|.8.|..1| |.3.|.4.|.6.| |4..|...|..9|
|.5.|2.3|.4.| |..9|..1|.35| |.7.|.6.|...|
+---+---+---+ +---+---+---+ +---+---+---+
برنامه تا جای ممکن پاکسازی و بهینه سازی شده است . اساس کار آن به این شکل است که ابتدا به ازای هر خانه خالی لیست اعداد ممکن برای آن خانه را به دست می آورد. سودوکوی زیر و لیست اعداد ممکن برای هر خانه آن را ببینید :
+---+---+---+
|...|.7.|.2.|
|8..|...|..6|
|.1.|2.5|...|
+---+---+---+
|9.5|4..|..8|
|...|...|...|
|3..|..8|5.1|
+---+---+---+
|...|3.2|.8.|
|4..|...|..9|
|.7.|.6.|...|
+---+---+---+
+-----------------------------+-----------------------------+-----------------------------+
| 56 | 34569 | 3469 | 1689 | 7 | 13469 | 13489 | 2 | 345 |
| 8 | 23459 | 23479 | 19 | 1349 | 1349 | 13479 | 134579 | 6 |
| 67 | 1 | 34679 | 2 | 3489 | 5 | 34789 | 3479 | 347 |
+-----------------------------+-----------------------------+-----------------------------+
| 9 | 26 | 5 | 4 | 123 | 1367 | 2367 | 367 | 8 |
| 1267 | 2468 | 124678 | 15679 | 12359 | 13679 | 234679 | 34679 | 2347 |
| 3 | 246 | 2467 | 679 | 29 | 8 | 5 | 4679 | 1 |
+-----------------------------+-----------------------------+-----------------------------+
| 156 | 569 | 169 | 3 | 1459 | 2 | 1467 | 8 | 457 |
| 4 | 23568 | 12368 | 1578 | 158 | 17 | 12367 | 13567 | 9 |
| 125 | 7 | 12389 | 1589 | 6 | 149 | 1234 | 1345 | 2345 |
+-----------------------------+-----------------------------+-----------------------------+
به عنوان مثال برای اولین خانه در بالا سمت چپ فقط اعداد 5 و 6 می توانند جز جواب باشند و دیگر اعداد به هیچ وجه نمی توانند در این خانه قرار بگیرند. مثال بالا خروجی دو تابع print_sudoku و display را نشان می دهد.
وقتی لیست اعداد ممکن برای تمام خانه ها را پیدا کردیم ، این لیست را به ترتیب خانه هایی که کمترین امکان را دارند مرتب می کنیم و سپس خانه ابتدای این لیست را با اولین عدد ممکن برای آن خانه پر می کنیم و دوباره لیست اعداد ممکن برای هر خانه را پیدا می کنیم و سپس دوباره اقدام به پر کردن خانه خالی دیگری می کنیم . اگر به جایی برسیم که برای یک خانه هیچ عدد ممکنی وجود نداشته باشد به این معنی است که مسیر را اشتباه رفته ایم . پس از آخرین حرکت rollback می کنیم و برای آن خانه عدد ممکن دیگری را انتخاب می کنیم. این کار را تا زمانی انجام می دهیم که لیست اعداد ممکن ما خالی شود ، یعنی تمام خانه ها با یک عدد پر شده اند و خانه خالی وجود ندارد.
بعد از پر کردن هر خانه باید لیست اعداد ممکن هر خانه را دوباره محاسبه کنیم ولی با کمی دقت متوجه می شویم که نیازی به این کار نیست و فقط باید لیست اعداد ممکن خانه های مجاور آخرین خانه پر شده را دوباره محاسبه کنیم ، چون آخرین حرکت فقط ممکنات سطر و ستون و مربع خودش را تغییر می دهد و در انتخابهای بقیه خانه ها تاثیری ندارند. این کار باعث می شود برنامه ی ما تا حدود بسیار زیادی سریعتر عمل کند.
نکاتی در مورد کد نوشته شده ی برنامه :
- لیست اعداد سودوکو در آرایه ای به نام parray نگهداری می شود. اندیس این آرایه از 0 تا 80 می باشد و مقدار هر خانه نشان دهنده عدد محاسبه شده برای آن خانه تا آن لحظه است . خانه های خالی سودوکو با صفر پر می شوند و یک سودوکو زمانی حل شده است که هیچ خانه ای با مقدار صفر در آرایه parray وجود نداشته باشد .
- possibility_hash یک hash می باشد ( دیکشنری در زبان پایتون یا associative array در زبان هایی مثل PHP ) . اندیس های این hash شماره خانه خالی و مقدار آن یک reference به لیستی حاوی کلیه اعداد ممکن برای آن خانه می باشد. خانه هایی از سودوکو که مقدار آن معلوم شده است از possibility_hash حذف می شوند ، لذا وقتی possibility_hash خالی شود به این معناست که سودوکوی ما حل شده است.
- adjacent یک hash می باشد با تعداد 81 عضو که اندیس هر عضو شماره خانه سودوکو است و مقدار آن یک reference به لیستی حاوی شماره کلیه خانه های مجاور آن خانه ( خانه های قرار گرفته در همان سطر و ستون و مربع ) می باشد . زمانی که یک خانه خالی را با یک عدد مقدار دهی کردیم طبیعتا این عدد می بایست از لیست اعداد ممکن برای خانه های آن سطر و ستون و مربع حذف شود . لیست خانه های موجود در آن سطر و ستون و مربع از adjacent استخراج می شود.
- تابع print_sudoku جدول 9x9 سودوکو را نمایش می دهد. اگر این تابع قبل از حل کامل برنامه فراخوانی شود خانه های خالی با . (دات) پر می شوند.(مثال در اینجا )
- تابع display همانند تابع print_sudoku جدول 9x9 سودوکو را نمایش می دهد با این تفاوت که به جای نمایش خانه های خالی با . (دات) لیست اعداد ممکن برای آن خانه را نمایش می دهد. (مثال در اینجا)
- تابع find_possibilities اگر با مقدار 1- فراخوانی شود کلیه مقادیر ممکن برای هر خانه خالی را محاسبه می کند و در possibility_hash% قرار می دهد . در غیر این صورت باید با شماره یک خانه فراخوانی شود. تابع find_possibilities خانه های مجاور این خانه را پیدا می کند و لیست اعداد ممکن برای هر خانه را آپدیت می کند . در صورتی که یک خانه با یک مقدار به خصوص پر شده باشد از این hash حذف می شود.
- rollback آخرین خانه ای که پر کرده ایم را خالی می کند ( مقدار آن خانه را صفر می کند ) و شماره آن خانه و مقدارش را بر می گرداند.
- تابع find_best_move بر اساس متغیر possibility_hash% بهترین حرکت ممکن در آن لحظه را انجام می دهد و یک خانه را پر می کند. بهترین حرکت در الگوریتم مورد استفاده ما عبارتست از پر کردن خانه ای که کمترین لیست اعداد ممکن را دارد. اگر دو متغیر spot_be$ و must_not_be$ با مقادیری بزرگتر از 1- ست شده باشند می بایست مقداری برای خانه spot_be$ انتخاب کنیم که مقدارش بزرگتر از must_not_be$ باشد. این حالت بعد از یک rollback اتفاق می افتد.
- لیست حرکت های انجام شده در آرایه ای به نام moves ذخیره می شود. هر عنصر این آرایه یک reference به لیستی حاوی دو عنصر می باشد: شماره خانه پر شده و مقدار استفاده شده برای پر کردن آن خانه. از این آرایه در تابع rollback استفاده می شود.
سورس کامل حل کننده سودوکو نوشته شده به زبان Perl به صورت زیر است . این برنامه بدون احتساب خط های خالی و کامنتها حدود 170 خط کد است .
#!/usr/local/bin/perl
#
# Another sudoku solver with Perl!
#
# Programmer : Mohsen Safari
# Email : safari.tafreshi@gmail.com
# Weblog : sutal.blog.ir
#ا
# read sudoku tables from named file provided at command line
# or read it from standard input.
# each line has one sudoku challenge!
# blank entries must be specified with .(dot) or 0(zero).
# Examples:
#
# 4.....8.5.3..........7......2.....6.....8.4......1.......6.3.7.5..2.....1.4......
# 52...6.........7.13...........4..8..6......5...........418.........3..2...87.....
# 6.....8.3.4.7.................5.4.7.3..2.....1.6.......2.....5.....8.6......1....
# 48.3............71.2.......7.5....6....2..8.............1.76...3.....4......5....
# ....14....3....2...7..........9...3.6.1.............8.2.....1.4....5.6.....7.8...
#
# Usage:
# $ echo 4.....8.5.3..........7......2.....6.....8.4......1.......6.3.7.5..2.....1.4...... | perl sudoku-solver.pl
# $ perl sudoku-solver.pl file
#
use warnings;
use strict;
my (@parray, @moves, @adjacent, %possibility_hash);
#
# A little house keeping! calculate each row, column, and sqaure indexs
# and finally calculate all adjacent of each entry.
#
{
my (@rindex, @cindex, @sqindex);
my (@r, @c, @sq);
my ($r, $c, $sq);
my (%hash, $j);
# first index of each sqaure.
my @sq_start = (0, 3, 6, 27, 30, 33, 54, 57, 60);
foreach(0 .. 8) {
my @arr;
for(my $iter = $_; $iter <= 80; $iter += 9){
push @arr, $iter;
}
# fill indexes of each column.
$cindex[$_] = [@arr];
# Calculate indexes of each row.
$rindex[$_] = [$_ * 9 .. $_ * 9 + 8];
$j = $sq_start[$_];
# fill indexes of each square
$sqindex[$_] = [$j, $j+1,$j+2,$j+9,$j+10,$j+11,$j+18,$j+19,$j+20];
}
foreach(0 .. 80) {
$r = int($_ / 9);
$c = $_ % 9;
if ($r <= 2) {
if($c <= 2) { $sq = 0 } elsif ($c <= 5) { $sq = 1 } else { $sq = 2}
}
elsif ($r <= 5) {
if($c <= 2) { $sq = 3 } elsif ($c <= 5) { $sq = 4 } else { $sq = 5}
}
else {
if($c <= 2) { $sq = 6 } elsif ($c <= 5) { $sq = 7 } else { $sq = 8}
}
@r = @{$rindex[$r]};
@c = @{$cindex[$c]};
@sq = @{$sqindex[$sq]};
%hash = ();
foreach my $j ((@r, @c, @sq)) {
$hash{$j}++;
}
# fill adjacent indexes of each entry.
$adjacent[$_] = [sort {$a <=> $b} keys %hash];
}
}
#
# print sudoku table is a readable and familar format
#
sub print_sudoku {
print "+---+---+---+", "\n";
for (my $i = 0; $i <= $#parray; $i++) {
if ( $i % 3 == 0) {
print "|";
}
print $parray[$i] != 0 ? $parray[$i] : ".";
print "|\n" if ( ($i + 1) % 9 == 0);
if ( ($i + 1) % 27 == 0) {
print "+---+---+---+", "\n";
}
}
}
#
# Print sudoku table. each entry that has not a specified value
# will be filled with its possible values.
#
sub display {
find_possibilities(-1);
print "+-----------------------------+-----------------------------+-----------------------------+" , "\n";
for (my $i = 0; $i <= $#parray; $i++) {
if ( $i % 9 == 0) {
print "|";
}
if ($parray[$i] != 0) {
print " $parray[$i] |";
}
else {
my $b = $possibility_hash{$i};
my $s = "";
$s .= $_ foreach (@{$b});
$s = " " . $s . " " while length $s < 9;
$s = substr($s, 0, length($s) - 1) if length $s > 9;
print $s, "|";
}
print "\n" if ( ($i + 1) % 9 == 0);
if ( ($i + 1) % 27 == 0) {
print "+-----------------------------+-----------------------------+-----------------------------+" , "\n";
}
}
}
#
# when no progress is available we would rollback to previous move
#
sub rollback {
if (!@moves) {
print STDERR "Moves array is empty!\n";
exit 1;
}
my $r = pop @moves;
$parray[$r->[0]] = 0;
return ($r->[0], $r->[1]);
}
#
# find possibilities of each no valued entries.
#
sub find_possibilities {
my (%hash, @arr, @tmp);
my @iterate = $_[0] == -1 ? (0 .. 80) : @{$adjacent[$_[0]]};
delete @possibility_hash{@iterate};
foreach my $i (@iterate) {
next if $parray[$i] != 0;
%hash = ();
@arr = ();
@tmp = @parray[@{$adjacent[$i]}];
$hash{$_}++ foreach((@tmp));
foreach (1..9) {
push @arr, $_ if not exists $hash{$_};
}
return -1 if !@arr;
$possibility_hash{$i} = [@arr];
}
return 1;
}
#
# find best move at current position
#
sub find_best_move {
my $spot_be = shift;
my $must_not_be = shift;
my (@su, @tmp);
if ($spot_be >= 0) {
foreach(@{$possibility_hash{$spot_be}}) {
return ($spot_be, $_) if $_ > $must_not_be;
}
return (-1, -1);
}
for(my $i = 0; $i <= $#parray; $i++) {
next if $parray[$i] != 0;
push @su, "$i @{$possibility_hash{$i}}";
}
@su = sort { length $a <=> length $b } @su;
foreach my $i (@su){
@tmp = split " ", $i;
for (my $j = 1; $j <= $#tmp; $j++) {
return ($tmp[0], $tmp[$j]) if $tmp[$j] != $must_not_be;
}
}
# if no move is available return (-1, -1) to rollback
return (-1, -1);
}
my ($spot, $value, $try, $last);
my ($spot_be, $must_not_be) = (-1, -1);
#
# while there is a line at standard input or named file...
# each line is a sudoku.
#
while (<>) {
chomp;
if (length != 81) {
print STDERR "Invalid sudoku entry!\n";
next;
}
s/\./0/g;
@parray = split "";
print_sudoku;
display;
$try = 0;
$last = -1;
while (1) {
if (find_possibilities($last) == -1) {
($spot_be, $must_not_be) = rollback;
$last = $spot_be;
next;
}
last if !keys %possibility_hash;
($spot, $value) = find_best_move $spot_be, $must_not_be;
if ($spot == -1 and $value == -1) {
($spot_be, $must_not_be) = rollback;
$last = $spot_be;
next;
}
$parray[$spot] = $value;
push @moves, [$spot, $value];
$spot_be = $must_not_be = -1;
$try = $try + 1;
$last = $spot;
}
print "Moves: $try\n";
print_sudoku;
print "@" x 13, "\n" if !eof();
}
نمونه کاربرد برنامه را ببینید :
# head -n 1 top95.txt
4.....8.5.3..........7......2.....6.....8.4......1.......6.3.7.5..2.....1.4......
# head -n 1 top95.txt | perl sudoku-solver
+---+---+---+
|4..|...|8.5|
|.3.|...|...|
|...|7..|...|
+---+---+---+
|.2.|...|.6.|
|...|.8.|4..|
|...|.1.|...|
+---+---+---+
|...|6.3|.7.|
|5..|2..|...|
|1.4|...|...|
+---+---+---+
+-----------------------------+-----------------------------+-----------------------------+
| 4 | 1679 | 12679 | 139 | 2369 | 1269 | 8 | 1239 | 5 |
| 26789 | 3 | 1256789 | 14589 | 24569 | 1245689 | 12679 | 1249 | 124679 |
| 2689 | 15689 | 125689 | 7 | 234569 | 1245689 | 12369 | 12349 | 123469 |
+-----------------------------+-----------------------------+-----------------------------+
| 3789 | 2 | 135789 | 3459 | 34579 | 4579 | 13579 | 6 | 13789 |
| 3679 | 15679 | 135679 | 359 | 8 | 25679 | 4 | 12359 | 12379 |
| 36789 | 456789 | 356789 | 3459 | 1 | 245679 | 23579 | 23589 | 23789 |
+-----------------------------+-----------------------------+-----------------------------+
| 289 | 89 | 289 | 6 | 459 | 3 | 1259 | 7 | 12489 |
| 5 | 6789 | 36789 | 2 | 479 | 14789 | 1369 | 13489 | 134689 |
| 1 | 6789 | 4 | 589 | 579 | 5789 | 23569 | 23589 | 23689 |
+-----------------------------+-----------------------------+-----------------------------+
Moves: 481
+---+---+---+
|417|369|825|
|632|158|947|
|958|724|316|
+---+---+---+
|825|437|169|
|791|586|432|
|346|912|758|
+---+---+---+
|289|643|571|
|573|291|684|
|164|875|293|
+---+---+---+
سه سودوکوی زیر جز سخت ترین سودوکوهای طراحی شده می باشند که برنامه نوشته شده به خوبی یک پاسخ صحیح برای آنها می یابد. می توانید عملکرد برنامه را بر روی این سه سودوکو بررسی کنید :
# cat hardest
8..........36......7..9.2...5...7.......457.....1...3...1....68.85...1...9....4..
..53.....8......2..7..1.5..4....53...1..7...6..32...8..6.5....9..4....3......97..
85...24..72......9..4.........1.7..23.5...9...4...........8..7..17..........36.4.