|
View previous topic :: View next topic |
Author |
Message |
| Ruud Site Admin
| Joined: 17 Sep 2005 | Posts: 708 | : | Location: Netherlands | Items |
|
Posted: Mon Jul 24, 2006 1:51 am Post subject: Sudoku implementation of DLX in PHP 5 objects |
|
|
The many requests for PHP solver/generator code has prompted me to rewrite some of my core code in PHP and make it available through this forum to the rest of the world.
I have tested this code on PHP 5.1.1 and it works like a charm.
On PHP 4.3.10-16 it does not work. Anyone who can retrofit the code to work with PHP 4 is encouraged to do so and post the modified code below.
There are several trace calls in the code that I have commented out. They have been very useful for debugging purposes, but maybe you want to change something in the code and use them again, so I left them in the code. A few to show the solution were left to show where the good things happen.
The solver is scalable, but I have not tested all settings.
Because this subject is so well documented elsewhere, I have not commented each line, but I did use self documenting names thoughout the code.
The optimizations in the search for the best column have resulted in extended column size administration. The overhead is justified by the speed increase of the main code.
Comments and improvements are appreciated.
Code: | <html>
<head>
</head>
<body style="font-family:Verdana;font-size:12px;">
<h1>Sudoku DLX objects for PHP 5</h1>
<?php
///////////////////////////////////////////////////////////////////////////////////
// Sudoku DLX objects for PHP 5 //
// This is free software //
// Author: Ruud van der Werf. Contact: ruud at sudocue dot net July 23, 2006 //
// ----------------------------------------------------------------------------- //
// You can use it as you please, modify it until it breaks, or whatever you like //
// The software is written in the expectation that it may be useful, but there //
// will be no guarantee that this will actually be the case. It may do nothing. //
// The author is not responsible for any damage directly or indirectly caused //
// by the use of this software. //
// Keep this notice attached to the software when you modify and copy it. //
///////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////
// Utility function to output a trace message //
////////////////////////////////////////////////
function trace($message) { echo '<br>'.$message; }
////////////////////////////////////////////////////////////
// Base class for all header and detail nodes in the grid //
// all methods are implemented in the extended classes //
////////////////////////////////////////////////////////////
class Node {
var $NextInCol;
var $PrevInCol;
var $ColHeader;
var $IsHeader;
}
////////////////////////////////////
// Candidate - acts as Row header //
////////////////////////////////////
class Candidate {
var $FirstNode;
var $Index;
var $Digit;
var $Enabled;
var $Name;
function Candidate(&$colHeaderCell,&$colHeaderRow,&$colHeaderCol,&$colHeaderBox,$digit) {
$this->FirstNode = new GridNode($this,$colHeaderCell);
$this->FirstNode->Append(new GridNode($this,$colHeaderRow));
$this->FirstNode->Append(new GridNode($this,$colHeaderCol));
$this->FirstNode->Append(new GridNode($this,$colHeaderBox));
$this->Name = $colHeaderCell->Name.'Val'.$digit;
$this->Index = $colHeaderCell->Index;
$this->Digit = $digit;
$this->Enabled = true;
//trace($this->Name.' initialized.');
}
function TraceStructure() {
$nd = &$this->FirstNode;
trace($nd->PrevInRow->ColHeader->Name. ' <- '.$nd->ColHeader->Name.' -> '.$nd->NextInRow->ColHeader->Name);
$nd = &$nd->NextInRow;
trace($nd->PrevInRow->ColHeader->Name. ' <- '.$nd->ColHeader->Name.' -> '.$nd->NextInRow->ColHeader->Name);
$nd = &$nd->NextInRow;
trace($nd->PrevInRow->ColHeader->Name. ' <- '.$nd->ColHeader->Name.' -> '.$nd->NextInRow->ColHeader->Name);
$nd = &$nd->NextInRow;
trace($nd->PrevInRow->ColHeader->Name. ' <- '.$nd->ColHeader->Name.' -> '.$nd->NextInRow->ColHeader->Name);
}
function Disable() {
if (!$this->Enabled) return;
$this->Enabled = false;
//trace('>'.$this->Name.' disable started.');
$nd = &$this->FirstNode;
do
{
$nd->Disable();
$nd= &$nd->NextInRow;
}
while ($nd!==$this->FirstNode);
//trace('>'.$this->Name.' disable finished.');
}
function Enable() {
if ($this->Enabled) return;
$this->Enabled = true;
//trace('>'.$this->Name.' enable started.');
$nd = &$this->FirstNode;
do
{
$nd->ColHeader->Add($nd);
$nd= &$nd->NextInRow;
}
while ($nd!==$this->FirstNode);
//trace('>'.$this->Name.' enable finished.');
}
}
///////////////////////////////////////////////////////////
// Grid Node - Each row has 4 and each column has base^2 //
///////////////////////////////////////////////////////////
class GridNode extends Node {
var $Candidate;
var $NextInRow;
var $PrevInRow;
var $Enabled;
var $Covered;
function GridNode(&$candidate,&$colHeader) {
$this->Candidate = &$candidate;
$this->ColHeader = &$colHeader;
$this->IsHeader = false;
$colHeader->Add($this);
$this->NextInRow = &$this;
$this->PrevInRow = &$this;
$this->Enabled = false;
$this->Covered = false;
}
function Append(&$newNode) {
$newNode->PrevInRow = &$this->PrevInRow;
$newNode->NextInRow = &$this;
$this->PrevInRow->NextInRow = &$newNode;
$this->PrevInRow = &$newNode;
}
function RemoveFromColumn() {
if ($this->Covered)
{
trace('Node in '.$this->ColHeader->Name.' already covered.');
return;
}
$this->NextInCol->PrevInCol = &$this->PrevInCol;
$this->PrevInCol->NextInCol = &$this->NextInCol;
$this->ColHeader->Decrement();
$this->Covered = true;
}
function Disable() {
//trace('Node in '.$this->ColHeader->Name.' disabled.');
$this->NextInCol->PrevInCol = &$this->PrevInCol;
$this->PrevInCol->NextInCol = &$this->NextInCol;
$this->NextInCol = &$this;
$this->PrevInCol = &$this;
$this->ColHeader->Decrement();
$this->Enabled = false;
}
function RestoreInColumn() {
if (!$this->Covered)
{
trace('Node in '.$this->ColHeader->Name.' already uncovered.');
return;
}
$this->NextInCol->PrevInCol = &$this;
$this->PrevInCol->NextInCol = &$this;
$this->ColHeader->Increment();
$this->Covered = false;
}
}
///////////////////////////////////////////////////
// Column Header class - Represents a constraint //
///////////////////////////////////////////////////
class ColHeader extends Node {
var $Size;
var $NextColumn;
var $PrevColumn;
var $Dancer;
var $IsRoot;
var $Type;
var $Index1;
var $Index2;
var $Index;
var $Name;
var $Covered;
function ColHeader(&$dancer,$type,$index1,$index2) {
$this->Dancer = &$dancer;
$this->Type = $type; // 0=root, 1=Cell, 2=Row, 3=Column, 4=Box
if ($type==0)
{
$this->IsRoot = true;
$this->NextColumn = &$this;
$this->PrevColumn = &$this;
$this->Name = 'root node';
}
else
{
$this->IsRoot = false;
$this->Index1 = $index1; // Cell: Row index 1-9, House: Row/Col/Box index 1-9
$this->Index2 = $index2; // Cell: Col index 1-9, House: Digit 1-9
$this->Index = (($index1-1)*$dancer->Size) + ($index2-1);
if ($type==1) $this->Name = 'Row'.$index1.'Col'.$index2;
if ($type==2) $this->Name = 'Row'.$index1.'Val'.$index2;
if ($type==3) $this->Name = 'Col'.$index1.'Val'.$index2;
if ($type==4) $this->Name = 'Box'.$index1.'Val'.$index2;
$this->IsHeader = true;
$this->ColHeader = &$this;
$this->NextInCol = &$this;
$this->PrevInCol = &$this;
$this->NextColumn = &$this->Dancer->Root;
$this->PrevColumn = &$this->Dancer->Root->PrevColumn;
$this->Dancer->Root->PrevColumn->NextColumn = &$this;
$this->Dancer->Root->PrevColumn = &$this;
}
$this->Size = 0;
$this->Covered = false;
}
function Add(&$node) {
$node->NextInCol = &$this;
$node->PrevInCol = &$this->PrevInCol;
$this->PrevInCol->NextInCol = &$node;
$this->PrevInCol = &$node;
$node->Enabled = true;
$this->Increment();
}
function Cover() {
if ($this->Covered)
{
trace('Column '.$this->Name.' already covered.');
return;
}
//trace('Covering column: '.$this->Name.' with size '.$this->Size);
$this->NextColumn->PrevColumn = &$this->PrevColumn;
$this->PrevColumn->NextColumn = &$this->NextColumn;
$this->Dancer->CoverFromSize($this->Size);
if ($this->Size>0)
{
for ($LoopNode=&$this->NextInCol;!$LoopNode->IsHeader;$LoopNode=&$LoopNode->NextInCol)
for ($PeerNode=&$LoopNode->NextInRow; $PeerNode!==$LoopNode;$PeerNode=&$PeerNode->NextInRow)
$PeerNode->RemoveFromColumn();
}
$this->Covered = true;
//trace('Covered column: '.$this->Name);
}
function UnCover() {
if (!$this->Covered)
{
trace('Column '.$this->Name.' is not covered.');
return;
}
//trace('Uncovering column: '.$this->Name);
if ($this->Size>0)
{
for ($LoopNode=&$this->PrevInCol;!$LoopNode->IsHeader;$LoopNode=&$LoopNode->PrevInCol)
for ($PeerNode=&$LoopNode->PrevInRow; $PeerNode!==$LoopNode;$PeerNode=&$PeerNode->PrevInRow)
$PeerNode->RestoreInColumn();
}
$this->NextColumn->PrevColumn = &$this;
$this->PrevColumn->NextColumn = &$this;
$this->Dancer->UncoverToSize($this->Size);
$this->Covered = false;
}
function Increment() {
$this->Size++;
//trace('Column size incremented to '.$this->Size);
$this->Dancer->ColSizeIncremented($this->Size);
}
function Decrement() {
$this->Size--;
//if ($this->Size==0) trace($this->Name.' size reduced to zero');
//trace('Column size decremented to '.$this->Size);
$this->Dancer->ColSizeDecremented($this->Size);
}
}
//////////////////////////////
// DLX main object - Dancer //
//////////////////////////////
class Dancer {
var $Base;
var $Size;
var $Area;
var $Root;
var $Cols;
var $Rows;
var $Sizes;
var $BestSize;
var $SolutionCount;
var $SolutionLimit;
var $Abort;
var $Depth;
var $Breath;
var $BackTracks;
var $SelectedRows;
var $Givens;
var $Solution;
function Dancer($base) {
$size = $base * $base;
$area = $size * $size;
$this->Base = $base;
$this->Size = $size;
$this->Area = $area;
$this->Givens='';
for ($n=0;$n<=$size;$n++) $this->Sizes[$n] = 0;
for ($n=0;$n<=$area;$n++) { $this->Solution[$n] = 0; $this->Givens .= '0'; }
$this->BestSize = 0;
$this->Root = new ColHeader($this,0,0,0);
for ($type=1;$type<=4;$type++)
{
for ($index1=1;$index1<=$size;$index1++)
{
for ($index2=1;$index2<=$size;$index2++)
{
$index = (($type-1)*$area)+(($index1-1)*$size)+($index2-1);
$this->Cols[$index] = new ColHeader($this,$type,$index1,$index2);
$this->Sizes[0]++;
}
}
}
//trace('columns initialized');
for ($R=0;$R<$size;$R++)
{
$floor = intval($R / $base);
for ($C=0;$C<$size;$C++)
{
$tower = intval($C / $base);
$B = ($floor * $base) + $tower;
$colHeaderCell = &$this->Cols[$R*$size+$C];
for ($D=0;$D<$size;$D++)
{
$colHeaderRow = &$this->Cols[(1*$area)+$R*$size+$D];
$colHeaderCol = &$this->Cols[(2*$area)+$C*$size+$D];
$colHeaderBox = &$this->Cols[(3*$area)+$B*$size+$D];
$rowIndex = ($R*$area)+($C*$size)+$D;
$this->Rows[$rowIndex] = new Candidate($colHeaderCell,$colHeaderRow,$colHeaderCol,$colHeaderBox,$D+1);
}
}
}
}
function CoverFromSize($size) {
$this->Sizes[$size]--;
if ($this->Sizes[$size]==0) if ($this->BestSize==$size) while ($this->BestSize<$this->Size && $this->Sizes[$this->BestSize]==0) $this->BestSize++;
}
function UncoverToSize($size) {
$this->Sizes[$size]++;
if ($this->BestSize>$size) $this->BestSize=$size;
}
function ColSizeIncremented($new) {
$old = $new-1;
$this->Sizes[$old]--;
if($this->Sizes[$old]==0) if ($this->BestSize==$old) $this->BestSize=$new;
$this->Sizes[$new]++;
}
function ColSizeDecremented($new) {
$old = $new+1;
$this->Sizes[$old]--;
$this->Sizes[$new]++;
if ($this->BestSize>$new) $this->BestSize=$new;
}
function SetGivens($givens) {
$this->Givens = $givens;
for ($n=0;$n<strlen($givens);$n++) if (intval(substr($givens,$n,1))>0) $this->Place($n,intval(substr($givens,$n,1)));
}
function RemoveGivens() {
for ($n=strlen($this->Givens)-1;$n>=0;$n--) if (intval(substr($this->Givens,$n,1))>0) $this->Erase($n);
}
function Place($index,$placedDigit) {
$R = intval($index / $this->Size) + 1;
$C = $index - (($R-1)*$this->Size) + 1;
//trace('Given digit '.$placedDigit.' in R'.$R.'C'.$C);
for ($otherDigit=1;$otherDigit<=$this->Size;$otherDigit++)
{
if ($otherDigit != $placedDigit)
{
$rowIndex = ($index*$this->Size)+$otherDigit-1;
$rowHeader = &$this->Rows[$rowIndex];
$rowHeader->Disable();
}
}
}
function Erase($index) {
$R = intval($index / $this->Size) + 1;
$C = $index - (($R-1)*$this->Size) + 1;
//trace('Clearing R'.$R.'C'.$C);
for ($digit=0;$digit<$this->Size;$digit++)
{
$rowIndex = ($index*$this->Size)+$digit;
$rowHeader = &$this->Rows[$rowIndex];
$rowHeader->Enable();
}
}
function Solve($limit) {
$this->SolutionLimit = $limit;
$this->SolutionCount = 0;
$this->Depth = 0;
$this->Breath = 0;
$this->BackTracks = 0;
$this->Abort = false;
$this->Recurse();
}
function AllColumnsCovered() {
$this->SolutionCount++;
if ($this->SolutionCount==1)
{
for ($n=0;$n<$this->Depth;$n++) $this->Solution[$this->SelectedRows[$n]->Index] = $this->SelectedRows[$n]->Digit;
$sol = "";
for ($n=0;$n<81;$n++) $sol .= $this->Solution[$n];
trace('<br>Solution: '.$sol);
trace('Breath: '.$this->Breath);
trace('Backtracked: '.$this->BackTracks);
}
else
{
trace('<br>Solution #'.$this->SolutionCount.' found.<br>');
if ($this->SolutionLimit>0) if ($this->SolutionCount>$this->SolutionLimit) $this->Abort = true;
}
}
function Recurse() {
//trace(' ');
//for ($n=0;$n<=$this->Size;$n++) trace('Size '.$n.': '.$this->Sizes[$n]);
// End condition: All columns covered
if ($this->Root->NextColumn->IsRoot)
{
$this->AllColumnsCovered();
return;
}
// Find the best column
//trace('Best size is now: '.$this->BestSize);
$SelectedCol = &$this->Root->NextColumn;
while ($SelectedCol->Size>$this->BestSize) $SelectedCol = &$SelectedCol->NextColumn;
// Main DLX function
$this->Breath += $SelectedCol->Size;
$SelectedCol->Cover();
for ($LoopNode=&$SelectedCol->NextInCol;!$LoopNode->IsHeader;$LoopNode=&$LoopNode->NextInCol)
{
//trace('<br>Selected '.$LoopNode->Candidate->Name.'<br>');
for ($PeerNode=&$LoopNode->NextInRow; $PeerNode!==$LoopNode;$PeerNode=&$PeerNode->NextInRow)
$PeerNode->ColHeader->Cover();
$this->SelectedRows[$this->Depth]=&$LoopNode->Candidate;
$this->Depth++;
//trace('<br>Depth increased to '.$this->Depth);
$this->Recurse();
$this->Depth--;
$this->SelectedRows[$this->Depth]=null;
//trace('Depth decreased to '.$this->Depth.'<br>');
for ($PeerNode=&$LoopNode->PrevInRow; $PeerNode!==$LoopNode;$PeerNode=&$PeerNode->PrevInRow)
$PeerNode->ColHeader->Uncover();
$this->BackTracks++;
//trace('<br>Deselected '.$LoopNode->Candidate->Name.'<br>');
if ($this->Abort) break;
}
$SelectedCol->Uncover();
$this->Breath -= $SelectedCol->Size;
//trace(' ');
}
}
//main code line
$dancer = new Dancer(3);
$dancer->SetGivens('901000802002000700400000005640209071080703020000405000000907000710000083004000200');
echo '<p>Puzzle: '.$dancer->Givens.'</p>';
$dancer->Solve(1);
if ($dancer->SolutionCount!=1) trace('Puzzle has '.$dancer->SolutionCount.' solutions.');
$dancer->RemoveGivens();
?>
</body>
</html>
|
The sample puzzle is todays nightmare. Finally a piece of code that helps you solve it
Ruud. _________________ Meet me at sudocue.net |
|
Back to top |
|
|
| lath
| Joined: 20 Jul 2006 | Posts: 3 | : | | Items |
|
Posted: Tue Jul 25, 2006 10:48 pm Post subject: |
|
|
Wow, thanks a ton! This will prove very useful.
So, correct me if I'm wrong, here's what I need to do:
I already have code that will generate completed puzzles.
1) Randomly remove a cell from the puzzle.
2) Run through $dancer->Solve
2a) If more than one solution, put number back and goto step 1 again.
3) Continue until x desired givens are left.
And I have a sudoku puzzle, right?
If I want to add different levels of difficulty, how can I tell which techniques are required to solve the puzzle?
Thanks. |
|
Back to top |
|
|
| Ruud Site Admin
| Joined: 17 Sep 2005 | Posts: 708 | : | Location: Netherlands | Items |
|
Posted: Tue Jul 25, 2006 11:46 pm Post subject: |
|
|
Your process sounds about right.
To add 180 degree symmetry, generate a random number N between 0 and 40, and clear cells N and 80-N,unless N=40.
You need a stop condition. Minimum number of clues is an option. Also create a mask for the clues that you tried to remove. Stop when all clues have been removed or tried.
Normally, difficulty rating is tested afterwards with a solver that uses human-style techniques.
This solver has a property that will show immediately whether the puzzle requires techniques beyond singles.
if ($dancer->Breath == 81) // only singles are required
if ($dancer->Breath > 81) // other techniques needed
There are a few rules of thumb, but you need to confirm this with a human-style solver. Use it as a kind of triage.
81 - Singles only
82-86 - Locked candidates
87-95 - Subsets and fish
96-120 - Multiple techniques
120-250 - + coloring, XY-chains
>250 - all kinds of weird crap
That's about it.
good luck! |
|
Back to top |
|
|
| humble_programmer
| Joined: 27 Jun 2006 | Posts: 69 | : | Location: Colorado Springs, USA | Items |
|
Posted: Thu Feb 01, 2007 3:19 pm Post subject: |
|
|
Can you elaborate on the purpose and logic of the CoverFromSize() and UncoverFromSize() functions in your PHP example? I'm trying to optimize my C# Dancing Links implementation, and suspect that I can gain some performance by looking harder at which column to choose. (Another "board member" rightly pointed out that the C# code I posted before Christmas is actually using Algorithm X instead of Dancing Links...oh, the shame!) _________________ Cheers!
Humble Programmer
,,,^..^,,,
www.humble-programmer.com |
|
Back to top |
|
|
| Ruud Site Admin
| Joined: 17 Sep 2005 | Posts: 708 | : | Location: Netherlands | Items |
|
Posted: Thu Feb 01, 2007 8:11 pm Post subject: |
|
|
The Sizes array keeps track of the number of remaining columns for each size. (ranging from 0 to 9)
ColSizeIncremented() and ColSizeDecremented() update this array when the column size is changed.
CoverFromSize() is called when a column is covered. Its size does not need to be zero at that point, so the Sizes element for the size of that column is decremented.
UncoverToSize() reverses this operation when the column is uncovered when the program backtracks (or tracks back?)
All 4 methods also process the effects for the BestSize variable. This variable is used in the recursive process to determine when to stop searching the columns. Once a column with size==BestSize is encountered, it is selected, because there will not be any columns with a smaller size.
This optimization works best if you mainly solve difficult puzzles. For easier Sudokus, it is quicker to stop the search after you found a column with size<=1.
Ruud |
|
Back to top |
|
|
| yAnTar
| Joined: 16 Apr 2007 | Posts: 2 | : | | Items |
|
Posted: Mon Apr 16, 2007 2:20 pm Post subject: Work not up to the end |
|
|
Work not up to the end
When sudoku is -
901000802002000700400000005640209071080703020000405000000907000710000083004000200
Solution -
901304862832601700406800305640289071080703020000405038308907100710020083064108297 |
|
Back to top |
|
|
| yAnTar
| Joined: 16 Apr 2007 | Posts: 2 | : | | Items |
|
Posted: Wed Apr 18, 2007 2:49 pm Post subject: |
|
|
Why ?
Is normal ?
I must solve by other methods to end or ... ?
Sorry of my english. |
|
Back to top |
|
|
| Pat
| Joined: 06 Sep 2006 | Posts: 128 | : | | Items |
|
Posted: Sun Apr 29, 2007 8:02 am Post subject: re: Work not up to the end |
|
|
yAnTar wrote: | Work not up to the end
When sudoku is -
Code: |
9 . 1 | . . . | 8 . 2
. . 2 | . . . | 7 . .
4 . . | . . . | . . 5
-------+-------+------
6 4 . | 2 . 9 | . 7 1
. 8 . | 7 . 3 | . 2 .
. . . | 4 . 5 | . . .
-------+-------+------
. . . | 9 . 7 | . . .
7 1 . | . . . | . 8 3
. . 4 | . . . | 2 . .
|
Solution -
Code: |
9 . 1 | 3 . 4 | 8 6 2
8 3 2 | 6 . 1 | 7 . .
4 . 6 | 8 . . | 3 . 5
-------+-------+------
6 4 . | 2 8 9 | . 7 1
. 8 . | 7 . 3 | . 2 .
. . . | 4 . 5 | . 3 8
-------+-------+------
3 . 8 | 9 . 7 | 1 . .
7 1 . | . 2 . | . 8 3
. 6 4 | 1 . 8 | 2 9 7
|
Why ?
Is normal ?
I must solve by other methods to end or ... ? |
sorry but i don't understand your question,
at the point you posted there's nothing left to do but "hidden singles" -- |
|
Back to top |
|
|
| Kaimi
| Joined: 23 May 2007 | Posts: 5 | : | | Items |
|
Posted: Fri May 25, 2007 8:21 am Post subject: |
|
|
there is a problem. dlx solver must find full solution if it exists, and in this case it exists. my version found it.
and there is another problem. when solving empty puzzle, solution 3 and 4 are equal. I found it out when tested my own implementation. |
|
Back to top |
|
|
|
|
You cannot post new topics in this forum You cannot reply to topics in this forum You cannot edit your posts in this forum You cannot delete your posts in this forum You cannot vote in polls in this forum
|
Powered by phpBB © 2001, 2005 phpBB Group
Igloo Theme Version 1.0 :: Created By: Andrew Charron
|