|
| 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 iterating through each row: |
| 10 | + /// - it is looking for a value which can be present only in two cells of the row |
| 11 | + /// If such a value is found, and there is another row where situation is the same (same value, same two columns), |
| 12 | + /// then all other candidates for this value in the two columns can be eliminated. |
| 13 | + /// Same is true when iterating through columns instead of rows (then we are eliminating candidates in rows). |
| 14 | + /// </summary> |
| 15 | + /// <remarks> |
| 16 | + /// See also: |
| 17 | + /// - https://sudoku9x9.com/x_wing.html |
| 18 | + /// - http://www.sudokuwiki.org/X_Wing_Strategy |
| 19 | + /// </remarks> |
| 20 | + public class XWing : ISudokuSolverStrategy |
| 21 | + { |
| 22 | + public string StrategyName => "X-Wing"; |
| 23 | + |
| 24 | + public SingleStepSolution SolveSingleStep(SudokuPuzzle sudokuPuzzle) |
| 25 | + { |
| 26 | + var eliminations = new List<SingleStepSolution.Candidate>(); |
| 27 | + |
| 28 | + var xWingMembersPerRow = GetXWingMembers(sudokuPuzzle, true); |
| 29 | + eliminations.AddRange(GetEliminations(sudokuPuzzle, xWingMembersPerRow, true)); |
| 30 | + |
| 31 | + var xWingMembersPerColumn = GetXWingMembers(sudokuPuzzle, false); |
| 32 | + eliminations.AddRange(GetEliminations(sudokuPuzzle, xWingMembersPerColumn, false)); |
| 33 | + |
| 34 | + return eliminations.Count > 0 ? |
| 35 | + new SingleStepSolution(eliminations.Distinct().ToArray(), StrategyName) : |
| 36 | + null; |
| 37 | + } |
| 38 | + |
| 39 | + /// <summary> |
| 40 | + /// Iterates the entire puzzle either per row or per columns and returns all found XWings. |
| 41 | + /// </summary> |
| 42 | + /// <param name="sudokuPuzzle">Sudoku puzzle.</param> |
| 43 | + /// <param name="perRow">Determines if the method is iterating per row or per column.</param> |
| 44 | + /// <returns> |
| 45 | + /// Tuple where: |
| 46 | + /// - Item1 is the value which is in the XWing |
| 47 | + /// - Item2, Item3, Item4, Item5 are four cells that are members of the XWing |
| 48 | + /// </returns> |
| 49 | + private Tuple<int, Cell, Cell, Cell, Cell>[] GetXWingMembers(SudokuPuzzle sudokuPuzzle, bool perRow) |
| 50 | + { |
| 51 | + var candidatesPerRow = new List<Tuple<int, int, int>[]>(); |
| 52 | + var candidatesPerColumn = new List<Tuple<int, int, int>[]>(); |
| 53 | + |
| 54 | + Tuple<int, int, int>[] ToCandidatesWithRowOrCellIndex(Tuple<int, Cell, Cell>[] candidates) |
| 55 | + { |
| 56 | + return candidates.Select( |
| 57 | + x => |
| 58 | + { |
| 59 | + var cell1Index = sudokuPuzzle.GetCellIndex(x.Item2); |
| 60 | + var cell2Index = sudokuPuzzle.GetCellIndex(x.Item3); |
| 61 | + return new Tuple<int, int, int>(x.Item1, |
| 62 | + perRow ? cell1Index.ColumnIndex : cell1Index.RowIndex, |
| 63 | + perRow ? cell2Index.ColumnIndex : cell2Index.RowIndex); |
| 64 | + } |
| 65 | + ).ToArray(); |
| 66 | + } |
| 67 | + |
| 68 | + var xWingMembers = new List<Tuple<int, Cell, Cell, Cell, Cell>>(); |
| 69 | + |
| 70 | + if (perRow) |
| 71 | + { |
| 72 | + for (int i = 0; i < sudokuPuzzle.Rows.Length; i++) |
| 73 | + { |
| 74 | + var candidates = GetCandidates(sudokuPuzzle.Rows[i].Cells, sudokuPuzzle.PossibleCellValues); |
| 75 | + var candidatesWithColumnIndex = ToCandidatesWithRowOrCellIndex(candidates); |
| 76 | + candidatesPerRow.Add(candidatesWithColumnIndex); |
| 77 | + } |
| 78 | + |
| 79 | + for (int i = 0; i < sudokuPuzzle.Rows.Length - 1; i++) |
| 80 | + { |
| 81 | + var row1 = candidatesPerRow[i]; |
| 82 | + |
| 83 | + for (int j = i + 1; j < sudokuPuzzle.Rows.Length; j++) |
| 84 | + { |
| 85 | + var row2 = candidatesPerRow[j]; |
| 86 | + |
| 87 | + var intersect = row1.Intersect(row2).ToArray(); |
| 88 | + if (intersect.Length > 0) |
| 89 | + { |
| 90 | + foreach (var intersectInstance in intersect) |
| 91 | + { |
| 92 | + var value = intersectInstance.Item1; |
| 93 | + var cell1 = sudokuPuzzle.Cells[i, intersectInstance.Item2]; |
| 94 | + var cell2 = sudokuPuzzle.Cells[i, intersectInstance.Item3]; |
| 95 | + var cell3 = sudokuPuzzle.Cells[j, intersectInstance.Item2]; |
| 96 | + var cell4 = sudokuPuzzle.Cells[j, intersectInstance.Item3]; |
| 97 | + xWingMembers.Add(new Tuple<int, Cell, Cell, Cell, Cell>(value, cell1, cell2, cell3, cell4)); |
| 98 | + } |
| 99 | + } |
| 100 | + } |
| 101 | + } |
| 102 | + } |
| 103 | + else |
| 104 | + { |
| 105 | + for (int i = 0; i < sudokuPuzzle.Columns.Length; i++) |
| 106 | + { |
| 107 | + var candidates = GetCandidates(sudokuPuzzle.Columns[i].Cells, sudokuPuzzle.PossibleCellValues); |
| 108 | + var candidatesWithRowIndex = ToCandidatesWithRowOrCellIndex(candidates); |
| 109 | + candidatesPerColumn.Add(candidatesWithRowIndex); |
| 110 | + } |
| 111 | + |
| 112 | + for (int i = 0; i < sudokuPuzzle.Columns.Length - 1; i++) |
| 113 | + { |
| 114 | + var column1 = candidatesPerColumn[i]; |
| 115 | + |
| 116 | + for (int j = i + 1; j < sudokuPuzzle.Columns.Length; j++) |
| 117 | + { |
| 118 | + var column2 = candidatesPerColumn[j]; |
| 119 | + |
| 120 | + var intersect = column1.Intersect(column2).ToArray(); |
| 121 | + if (intersect.Length > 0) |
| 122 | + { |
| 123 | + foreach (var intersectInstance in intersect) |
| 124 | + { |
| 125 | + var value = intersectInstance.Item1; |
| 126 | + var cell1 = sudokuPuzzle.Cells[intersectInstance.Item2, i]; |
| 127 | + var cell2 = sudokuPuzzle.Cells[intersectInstance.Item3, i]; |
| 128 | + var cell3 = sudokuPuzzle.Cells[intersectInstance.Item2, j]; |
| 129 | + var cell4 = sudokuPuzzle.Cells[intersectInstance.Item3, j]; |
| 130 | + xWingMembers.Add(new Tuple<int, Cell, Cell, Cell, Cell>(value, cell1, cell2, cell3, cell4)); |
| 131 | + } |
| 132 | + } |
| 133 | + } |
| 134 | + } |
| 135 | + } |
| 136 | + |
| 137 | + return xWingMembers.ToArray(); |
| 138 | + } |
| 139 | + |
| 140 | + /// <summary> |
| 141 | + /// Returns all the candidates that can be eliminated using the XWings. |
| 142 | + /// </summary> |
| 143 | + private SingleStepSolution.Candidate[] GetEliminations(SudokuPuzzle sudokuPuzzle, Tuple<int, Cell, Cell, Cell, Cell>[] xWingMembers, bool perRow) |
| 144 | + { |
| 145 | + var eliminations = new List<SingleStepSolution.Candidate>(); |
| 146 | + |
| 147 | + foreach (var xWingMember in xWingMembers) |
| 148 | + { |
| 149 | + var value = xWingMember.Item1; |
| 150 | + |
| 151 | + // diagonal cells |
| 152 | + var firstCellIndex = sudokuPuzzle.GetCellIndex(xWingMember.Item2); |
| 153 | + var secondCellIndex = sudokuPuzzle.GetCellIndex(xWingMember.Item5); |
| 154 | + |
| 155 | + if (perRow) |
| 156 | + { |
| 157 | + var column1 = sudokuPuzzle.Columns[firstCellIndex.ColumnIndex]; |
| 158 | + var column2 = sudokuPuzzle.Columns[secondCellIndex.ColumnIndex]; |
| 159 | + var row1Index = firstCellIndex.RowIndex; |
| 160 | + var row2Index = secondCellIndex.RowIndex; |
| 161 | + |
| 162 | + // eliminations are those cell that: |
| 163 | + // - are in 'column1' or 'column2' |
| 164 | + // - 'value' is a possible value for that cell |
| 165 | + // - are not in row1Index or row2Index |
| 166 | + foreach (var cell in column1.Cells.Union(column2.Cells)) |
| 167 | + { |
| 168 | + var cellIndex = sudokuPuzzle.GetCellIndex(cell); |
| 169 | + if (cell.CanBe.Contains(value) && cellIndex.RowIndex != row1Index && cellIndex.RowIndex != row2Index) |
| 170 | + eliminations.Add(new SingleStepSolution.Candidate(cellIndex.RowIndex, cellIndex.ColumnIndex, value)); |
| 171 | + } |
| 172 | + } |
| 173 | + else |
| 174 | + { |
| 175 | + var row1 = sudokuPuzzle.Rows[firstCellIndex.RowIndex]; |
| 176 | + var row2 = sudokuPuzzle.Rows[secondCellIndex.RowIndex]; |
| 177 | + var column1Index = firstCellIndex.ColumnIndex; |
| 178 | + var column2Index = secondCellIndex.ColumnIndex; |
| 179 | + |
| 180 | + // eliminations are those cell that: |
| 181 | + // - are in 'row1' or 'row2' |
| 182 | + // - 'value' is a possible value for that cell |
| 183 | + // - are not in column1Index or column2Index |
| 184 | + foreach (var cell in row1.Cells.Union(row2.Cells)) |
| 185 | + { |
| 186 | + var cellIndex = sudokuPuzzle.GetCellIndex(cell); |
| 187 | + if (cell.CanBe.Contains(value) && cellIndex.ColumnIndex != column1Index && cellIndex.ColumnIndex != column2Index) |
| 188 | + eliminations.Add(new SingleStepSolution.Candidate(cellIndex.RowIndex, cellIndex.ColumnIndex, value)); |
| 189 | + } |
| 190 | + } |
| 191 | + } |
| 192 | + |
| 193 | + |
| 194 | + return eliminations.ToArray(); |
| 195 | + } |
| 196 | + |
| 197 | + /// <summary> |
| 198 | + /// For each value in <paramref name="possibleCellValues"/> method iterates through all the <paramref name="cells"/> |
| 199 | + /// and is looking for a pair of cells that are the only cells that can contain the possible value. |
| 200 | + /// </summary> |
| 201 | + /// <param name="cells">Cells of a row or column.</param> |
| 202 | + /// <param name="possibleCellValues"><see cref="SudokuPuzzle.PossibleCellValues"/></param> |
| 203 | + /// <returns> |
| 204 | + /// Tuple where: |
| 205 | + /// - Item1 is possible value |
| 206 | + /// - Item2 is first cell containing possible value |
| 207 | + /// - Item3 is second cell containing possible value |
| 208 | + /// </returns> |
| 209 | + private Tuple<int, Cell, Cell>[] GetCandidates(Cell[] cells, int[] possibleCellValues) |
| 210 | + { |
| 211 | + var result = new List<Tuple<int, Cell, Cell>>(); |
| 212 | + |
| 213 | + foreach (var possibleCellValue in possibleCellValues) |
| 214 | + { |
| 215 | + var cellsContaingValue = cells.Where(x => x.CanBe.Contains(possibleCellValue)).ToArray(); |
| 216 | + if (cellsContaingValue.Length == 2) |
| 217 | + { |
| 218 | + result.Add(new Tuple<int, Cell, Cell>(possibleCellValue, cellsContaingValue[0], cellsContaingValue[1])); |
| 219 | + } |
| 220 | + } |
| 221 | + |
| 222 | + return result.ToArray(); |
| 223 | + } |
| 224 | + } |
| 225 | +} |
0 commit comments