Skip to content

Commit 0cacc6e

Browse files
committed
Implemented LockedCandidatesClaiming solving strategy + tests.
1 parent 27b47c7 commit 0cacc6e

File tree

4 files changed

+214
-0
lines changed

4 files changed

+214
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Out of the well known [Sudoku Solving Techniques](https://sudoku9x9.com/sudoku_s
2020
* Hidden Single
2121
* Naked Single
2222
* Locked Candidates (Pointing)
23+
* Locked Candidates (Claiming)
2324
* Naked Pair
2425
* Naked Triple
2526
* Naked Quad
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
using NUnit.Framework;
2+
using SimpleSudokuSolver.Model;
3+
using SimpleSudokuSolver.Strategy;
4+
5+
namespace SimpleSudokuSolver.Tests.Strategy
6+
{
7+
public class LockedCandidatesClaimingTests : BaseStrategyTest
8+
{
9+
private readonly ISudokuSolverStrategy _strategy = new LockedCandidatesClaiming();
10+
11+
[Test]
12+
public void LockedCandidatesClaimingTest1()
13+
{
14+
var sudoku = new[,]
15+
{
16+
// From: http://www.sudokuwiki.org/Intersection_Removal
17+
{ 0,1,6,0,0,7,8,0,3 },
18+
{ 0,9,0,8,0,0,0,0,0 },
19+
{ 8,7,0,0,0,1,2,6,0 },
20+
{ 0,4,8,0,0,0,3,0,0 },
21+
{ 6,5,0,0,0,9,0,8,2 },
22+
{ 0,3,9,0,0,0,6,5,0 },
23+
{ 0,6,0,9,0,0,0,2,0 },
24+
{ 0,8,0,0,0,2,9,3,6 },
25+
{ 9,2,4,6,0,0,5,1,0 }
26+
};
27+
28+
var sudokuPuzzle = new SudokuPuzzle(sudoku);
29+
SolveUsingStrategy(sudokuPuzzle, _strategy);
30+
31+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[1, 4].CanBe, 2);
32+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[2, 3].CanBe, 2);
33+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[2, 4].CanBe, 2);
34+
35+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[1, 6].CanBe, 4);
36+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[1, 8].CanBe, 4);
37+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[2, 6].CanBe, 4);
38+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[2, 8].CanBe, 4);
39+
}
40+
41+
[Test]
42+
public void LockedCandidatesClaimingTest2()
43+
{
44+
var sudoku = new[,]
45+
{
46+
// From: http://www.sudokuwiki.org/Intersection_Removal
47+
{ 0,2,0,9,4,3,7,1,5 },
48+
{ 9,0,4,0,0,0,6,0,0 },
49+
{ 7,5,0,0,0,0,0,4,0 },
50+
{ 5,0,0,4,8,0,0,0,0 },
51+
{ 2,0,0,0,0,0,4,5,3 },
52+
{ 4,0,0,3,5,2,0,0,0 },
53+
{ 0,4,2,0,0,0,0,8,1 },
54+
{ 0,0,5,0,0,4,2,6,0 },
55+
{ 0,9,0,2,0,8,5,0,4 }
56+
};
57+
58+
var sudokuPuzzle = new SudokuPuzzle(sudoku);
59+
SolveUsingStrategy(sudokuPuzzle, _strategy);
60+
61+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[3, 2].CanBe, 6);
62+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[4, 2].CanBe, 6);
63+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[5, 2].CanBe, 6);
64+
65+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[7, 1].CanBe, 3);
66+
CollectionAssert.DoesNotContain(sudokuPuzzle.Cells[8, 2].CanBe, 3);
67+
}
68+
}
69+
}

SimpleSudokuSolver/DefaultSolver.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public DefaultSolver(params ISudokuSolverStrategy[] strategies)
3434
new HiddenSingle(),
3535
new NakedSingle(),
3636
new LockedCandidatesPointing(),
37+
new LockedCandidatesClaiming(),
3738
new NakedPair(),
3839
new NakedTriple(),
3940
new NakedQuad(),
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
using SimpleSudokuSolver.Model;
2+
using System;
3+
using System.Collections.Generic;
4+
using System.Linq;
5+
6+
namespace SimpleSudokuSolver.Strategy
7+
{
8+
/// <summary>
9+
/// Strategy is examining rows and columns, and is looking for candidates which are grouped together in just one block.
10+
/// If such a candidate is found, we can exclude it from the rest of the block.
11+
/// </summary>
12+
/// <remarks>
13+
/// See also:
14+
/// - http://www.sudokuwiki.org/Intersection_Removal (Box Line Reduction)
15+
/// </remarks>
16+
public class LockedCandidatesClaiming : ISudokuSolverStrategy
17+
{
18+
public string StrategyName => "Locked Candidates (Claiming)";
19+
20+
public SingleStepSolution SolveSingleStep(SudokuPuzzle sudokuPuzzle)
21+
{
22+
var cellCandidatePairsPerRow = new List<Tuple<Cell, int>>();
23+
var cellCandidatePairsPerColumn = new List<Tuple<Cell, int>>();
24+
25+
foreach (var row in sudokuPuzzle.Rows)
26+
{
27+
cellCandidatePairsPerRow.AddRange(GetCellCandidatePairsWhichAppearOnlyInSingleBlock(
28+
row.Cells, x => sudokuPuzzle.GetCellIndex(x).ColumnIndex));
29+
}
30+
31+
foreach (var column in sudokuPuzzle.Columns)
32+
{
33+
cellCandidatePairsPerColumn.AddRange(GetCellCandidatePairsWhichAppearOnlyInSingleBlock(
34+
column.Cells, x => sudokuPuzzle.GetCellIndex(x).RowIndex));
35+
}
36+
37+
var eliminations = new List<SingleStepSolution.Candidate>();
38+
eliminations.AddRange(GetEliminations(cellCandidatePairsPerRow, true, sudokuPuzzle));
39+
eliminations.AddRange(GetEliminations(cellCandidatePairsPerColumn, false, sudokuPuzzle));
40+
41+
return eliminations.Count > 0 ?
42+
new SingleStepSolution(eliminations.Distinct().ToArray(), StrategyName) :
43+
null;
44+
}
45+
46+
/// <summary>
47+
/// Method examines all the cells from <paramref name="cellsOfSingleRowOrColumn"/> and
48+
/// returns all the cells that have a candidate value that is present only in a single block.
49+
/// </summary>
50+
/// <param name="cellsOfSingleRowOrColumn">Cells of a row or cells of a column.</param>
51+
/// <param name="getIndexOfColumnOrRow">
52+
/// Returns column index of a cell if <paramref name="cellsOfSingleRowOrColumn"/>
53+
/// are part of a row and row index if they are part of a column.
54+
/// That index is later used to determine which block the cells belongs to.
55+
/// </param>
56+
/// <returns>Item1 of tuple is cell, and Item2 is cell's candidate value.</returns>
57+
private IEnumerable<Tuple<Cell, int>> GetCellCandidatePairsWhichAppearOnlyInSingleBlock(
58+
Cell[] cellsOfSingleRowOrColumn, Func<Cell, int> getIndexOfColumnOrRow)
59+
{
60+
var cellsWithNoValue = cellsOfSingleRowOrColumn.Where(x => !x.HasValue).ToArray();
61+
var indexesPerCandidate = new Dictionary<int, HashSet<int>>();
62+
63+
foreach (var cellWithNoValue in cellsWithNoValue)
64+
{
65+
var index = getIndexOfColumnOrRow(cellWithNoValue);
66+
67+
foreach (var candidate in cellWithNoValue.CanBe)
68+
{
69+
if (!indexesPerCandidate.ContainsKey(candidate))
70+
{
71+
indexesPerCandidate[candidate] = new HashSet<int>();
72+
}
73+
indexesPerCandidate[candidate].Add(index);
74+
}
75+
}
76+
77+
var candidateValuesWhichAppearOnlyInSingleBlock = new List<int>();
78+
79+
foreach (var item in indexesPerCandidate)
80+
{
81+
// We assume block contains 3 cells
82+
var allInSingleBlock = item.Value.Select(x => x / 3).Distinct().Count() == 1;
83+
if (allInSingleBlock)
84+
{
85+
candidateValuesWhichAppearOnlyInSingleBlock.Add(item.Key);
86+
}
87+
}
88+
89+
var result = new List<Tuple<Cell, int>>();
90+
91+
foreach (var item in candidateValuesWhichAppearOnlyInSingleBlock)
92+
{
93+
// We could return all the cells, but just one is enough to eliminate other candidates
94+
var cellOrNull = cellsWithNoValue.Where(x => x.CanBe.Contains(item)).FirstOrDefault();
95+
if (cellOrNull != null)
96+
{
97+
result.Add(new Tuple<Cell, int>(cellOrNull, item));
98+
}
99+
}
100+
101+
return result;
102+
}
103+
104+
private IEnumerable<SingleStepSolution.Candidate> GetEliminations(
105+
IEnumerable<Tuple<Cell, int>> cellCandidatePairs, bool perRow, SudokuPuzzle sudokuPuzzle)
106+
{
107+
var eliminations = new List<SingleStepSolution.Candidate>();
108+
109+
foreach (var cellCandidatePair in cellCandidatePairs)
110+
{
111+
var cell = cellCandidatePair.Item1;
112+
var cellIndex = sudokuPuzzle.GetCellIndex(cell);
113+
var candidate = cellCandidatePair.Item2;
114+
var blockIndex = sudokuPuzzle.GetBlockIndex(cell);
115+
var block = sudokuPuzzle.Blocks[blockIndex.RowIndex, blockIndex.ColumnIndex];
116+
117+
foreach (var blockCell in block.Cells)
118+
{
119+
// Ignore the same cell, or block cells that do not contain candidate
120+
if (blockCell == cell || !blockCell.CanBe.Contains(candidate))
121+
{
122+
continue;
123+
}
124+
125+
// If outside cells are part of the same row,
126+
// ignore block cells that are part of that same row (similar for columns)
127+
var blockCellIndex = sudokuPuzzle.GetCellIndex(blockCell);
128+
if ((perRow && cellIndex.RowIndex == blockCellIndex.RowIndex) ||
129+
(!perRow && cellIndex.ColumnIndex == blockCellIndex.ColumnIndex))
130+
{
131+
continue;
132+
}
133+
134+
var cellIndexOfBlockCell = sudokuPuzzle.GetCellIndex(blockCell);
135+
eliminations.Add(new SingleStepSolution.Candidate(
136+
cellIndexOfBlockCell.RowIndex, cellIndexOfBlockCell.ColumnIndex, candidate));
137+
}
138+
}
139+
140+
return eliminations;
141+
}
142+
}
143+
}

0 commit comments

Comments
 (0)