The Challenge
Since I came across the first sudoku I was wondering if it could by solved by SQL using queries that select just the right value for a cell.
Well, some day I tried, here is what I have got now.
Domain Tables
Values
CREATETABLE Token(Character CHAR(1) NOT NULL,Value SMALLINTNULL,Mask INT NULL,CONSTRAINTPK_Token PRIMARY KEYCLUSTERED (Character));INSERTINTO Token(Character, Value, Mask)VALUES('.', NULL,NULL),('1', 1, 1),('2', 2, 2),('3', 3, 4),('4', 4, 8),('5', 5, 16),('6', 6, 32),('7', 7, 64),('8', 8, 128),('9', 9, 256)GOCREATEVIEW Value ASSELECT Value, MaskFROM TokenWHERE Value IS NOT NULL;
The period I intended to use for representing an empty cell. The column Mask was introduced later, it turned out to be helpful.
Location
CREATETABLE Location(CellID SMALLINTNOT NULL,GroupType CHAR(1) NOT NULL, -- 'C', 'R', 'S' meaning column, row and squareGroupValue SMALLINTNOT NULL,CONSTRAINTPK_Location PRIMARY KEYCLUSTERED (CellID,GroupType));INSERTINTO Location (CellID, GroupType, GroupValue)SELECTr.Value*10 + c.Value AS CellID, gt.GroupType, CASE WHEN gt.GroupType='C' THEN c.Value WHEN gt.GroupType='R' THEN r.Value ELSE (c.Value-1)/3 + ((r.Value-1)/3)*3 + 1 END AS GroupValueFROM (VALUES ('C'), ('R'), ('S') )gt(GroupType)crossjoin Value rcrossjoin Value cCREATETABLE LocationWide(CellID SMALLINTNOT NULL,[Column] SMALLINTNOT NULL,[Row] SMALLINTNOT NULL,[Square] SMALLINTNOT NULL,CONSTRAINTPK_LocationWide PRIMARY KEY CLUSTERED (CellID));INSERTINTO LocationWide (CellID, [Column], [Row], [Square])SELECTp.CellID, p.C AS [Column], p.R AS [Row], p.S AS [Square]FROM (SELECT CellID, GroupType,GroupValue FROM Location) lPIVOT (MAX(GroupValue) FOR GroupType IN ([C], [R], )) p;
I was going to need information about the location of cells, for grouping to rows, colums and squares. Because I was not sure if it is better to model the location "deep" or "wide", I made both. This data is static, so there is no problem with duplication.
First 20 rows of LocationWide:
CellID | Column | Row | Square |
11 | 1 | 1 | 1 |
12 | 2 | 1 | 1 |
13 | 3 | 1 | 1 |
14 | 4 | 1 | 2 |
15 | 5 | 1 | 2 |
16 | 6 | 1 | 2 |
17 | 7 | 1 | 3 |
18 | 8 | 1 | 3 |
19 | 9 | 1 | 3 |
21 | 1 | 2 | 1 |
22 | 2 | 2 | 1 |
23 | 3 | 2 | 1 |
24 | 4 | 2 | 2 |
25 | 5 | 2 | 2 |
26 | 6 | 2 | 2 |
27 | 7 | 2 | 3 |
28 | 8 | 2 | 3 |
29 | 9 | 2 | 3 |
31 | 1 | 3 | 1 |
32 | 2 | 3 | 1 |
Cell
CREATETABLE Cell(CellID SMALLINTNOT NULL,Value SMALLINTNOT NULL,Step INT IDENTITY(1,1) NOT NULL,[Rule] nvarchar(100) NULL,CONSTRAINTPK_Cell PRIMARY KEYCLUSTERED (CellID));
The columns Step and Rule are for traceability, I want to know why and when a cell was fillled.
Candidate
CREATETABLE Candidate(CellID SMALLINTNOT NULL,Value SMALLINTNOT NULL,Mask INT NOT NULL,CONSTRAINTPK_Candidate PRIMARY KEYCLUSTERED (CellID,Value));CREATETABLE CandidateMask (CellID SMALLINTNOT NULL,Mask INT NOT NULL,CONSTRAINTPK_CandidateMask PRIMARY KEY (CellID));
The table Candidate holds the values for a cell that are considered possible at the moment. The idea is to remove rows from the table candidate by rules. When there is only one candidate left its value is assigned to the cell, which removes further candidates for other cells.
The table CandidateMask was introduced later as ist turned out to be convenient to operate not only on certain values, but on sets of values. That leads us to sets.
Sets
CREATETABLE Set1(SetValue SMALLINTNOT NULL,Value1 SMALLINTNOT NULL,DisplayValue VARCHAR(10) NOT NULL,CONSTRAINTPK_Set1 PRIMARY KEYCLUSTERED (SetValue));INSERTINTO Set1 (SetValue, Value1, DisplayValue)SELECTv1.Mask, v1.Value, STUFF('.........', v1.Value, 1, CAST(v1.Value AS CHAR(1)))FROMValue v1;GOCREATETABLE Set2(SetValue SMALLINTNOT NULL,Value1 SMALLINTNOT NULL,Value2 SMALLINTNOT NULL,DisplayValue VARCHAR(10) NOT NULL,CONSTRAINTPK_Set2 PRIMARY KEYCLUSTERED (SetValue));INSERTINTO Set2 (SetValue, Value1, Value2,DisplayValue)SELECTs.SetValue+v.Mask, s.Value1, v.Value, STUFF(s.DisplayValue, v.Value, 1, CAST(v.Value AS CHAR(1)))FROMSet1 sINNERJOIN Value v ONv.Value>s.Value1;
The SetN tables hold the combinations of N values. Here are listed only Set1 and Set2, the download links in part 3 give you the complete source. What we have now is, showing the first 20 rows,:
SELECT* FROM SetAll ORDER BY Size, SetValue
SetValue | Size | DisplayValue |
1 | 1 | 1........ |
2 | 1 | .2....... |
4 | 1 | ..3...... |
8 | 1 | ...4..... |
16 | 1 | ....5.... |
32 | 1 | .....6... |
64 | 1 | ......7.. |
128 | 1 | .......8. |
256 | 1 | ........9 |
3 | 2 | 12....... |
5 | 2 | 1.3...... |
6 | 2 | .23...... |
9 | 2 | 1..4..... |
10 | 2 | .2.4..... |
12 | 2 | ..34..... |
17 | 2 | 1...5.... |
18 | 2 | .2..5.... |
20 | 2 | ..3.5.... |
Domain Logic
Validity
CREATEVIEW Violation ASSELECT l.GroupType, l.GroupValue, c.Value, COUNT(*) AS MultiplicityFROM Cell cINNER JOIN Location l ON c.CellID = l.CellIDGROUP BY l.GroupType, l.GroupValue, c.ValueHAVING COUNT(*) > 1;
If we get 0 rows from this view, the cell values are valid.
Consistency
CREATETRIGGER TrimCandidates ONCell AFTER INSERTASBEGINDELETE FROM Candidate WHERECellID IN (SELECT CellID FROM inserted)DELETE FROM cFROM inserted iINNER JOIN Location l1 ONl1.CellID=i.CellIDINNER JOIN Location l2 ONl1.GroupType=l2.GroupType AND l1.GroupValue=l2.GroupValueINNER JOIN Candidate c ONl2.CellID=c.CellID AND c.Value=i.ValueEND;GOCREATETRIGGER TrimCandidateMask ON Candidate AFTER DELETE ASBEGINUPDATE cmSET cm.Mask=cm.Mask & (~d.Mask)FROMCandidateMask cmINNER JOIN (SELECT d.CellID, SUM(d.Mask) AS MaskFROMdeleted dGROUP BY d.CellID) d ON d.CellID=cm.CellIDDELETE FROM CandidateMaskWHERE Mask=0END;
If rows are added to table Cell, candidates have to be removed. When removing candidates, table CandidateMask has to be updated.
Initialisation
CREATEPROCEDURE FillCandidates ASBEGININSERT INTO Candidate (CellID, Value, Mask)SELECT l.CellID, v.Value, v.MaskFROM Location lCROSS JOIN Value vWHERE l.GroupType='C'AND NOT EXISTS (SELECT 2 FROM Cell c WHERE c.CellID=l.CellID) -- any CellID that does not have a value yetAND NOT EXISTS ( -- any value that is not present in same groupSELECT 2 FROM Location lcINNER JOIN Cell cINNER JOIN Location l2 ON c.CellID=l2.CellIDON lc.GroupType=l2.GroupType AND lc.GroupValue=l2.GroupValueWHERE lc.CellID=l.CellID AND c.Value=v.Value)INSERT INTO CandidateMask (CellID, Mask)SELECT c.CellID, SUM(c.Mask) AS MaskFROMCandidate cGROUP BY c.CellIDEND;
As a first step, all possible candidates are inserted. It is assumed that there are some rows in table Cell that represent the "start position".
What we have right now
Domain Tables |