/// <summary>Attempts to solve a Sudoku puzzle.</summary> /// <param name="state">The state of the puzzle to be solved.</param> /// <param name="options">Options to use for solving.</param> /// <returns>The result of the solve attempt.</returns> /// <remarks>No changes are made to the parameter state.</remarks> public static SolverResults Solve(PuzzleState state, SolverOptions options) { // Validate parameters if (state == null) { throw new ArgumentNullException("state"); } if (options == null) { throw new ArgumentNullException("options"); } options = options.Clone(); if (options.EliminationTechniques.Count == 0) { options.EliminationTechniques.Add(new NakedSingleTechnique()); } // Turn off the raising of changed events while solving, // though it probably doesn't matter as the first thing // SolveInternal does is make a clone, and RaiseStateChangedEvent // is not cloned (on purpose). bool raiseChangedEvent = state.RaiseStateChangedEvent; state.RaiseStateChangedEvent = false; // Attempt to solve the puzzle SolverResults results = SolveInternal(state, options); // Reset whether changed events should be raised state.RaiseStateChangedEvent = raiseChangedEvent; // Return the solver results return(results); }
/// <summary>Evaluates the difficulty level of a particular puzzle.</summary> /// <param name="state">The puzzle to evaluate.</param> /// <returns>The perceived difficulty level of the puzzle.</returns> public static PuzzleDifficulty EvaluateDifficulty(PuzzleState state) { if (state == null) { throw new ArgumentNullException("state"); } SolverOptions options = new SolverOptions(); foreach (PuzzleDifficulty diff in new PuzzleDifficulty[] { PuzzleDifficulty.Easy, PuzzleDifficulty.Medium, PuzzleDifficulty.Hard, PuzzleDifficulty.VeryHard }) { GeneratorOptions go = GeneratorOptions.Create(diff); if (state.NumberOfFilledCells < go.MinimumFilledCells) { continue; } options.AllowBruteForce = !go.MaximumNumberOfDecisionPoints.HasValue; options.EliminationTechniques = go.Techniques; options.MaximumSolutionsToFind = options.AllowBruteForce ? 2u : 1u; SolverResults results = Solver.Solve(state, options); if (results.Status == PuzzleStatus.Solved && results.Puzzles.Count == 1) { return(diff); } } return(PuzzleDifficulty.Invalid); }
/// <summary>Attempts to solve a Sudoku puzzle.</summary> /// <param name="state">The state of the puzzle to be solved.</param> /// <param name="options">Options to use for solving.</param> /// <returns>The result of the solve attempt.</returns> /// <remarks>No changes are made to the parameter state.</remarks> private static SolverResults SolveInternal(PuzzleState state, SolverOptions options) { // First, make a copy of the state and work on that copy. That way, the original // instance passed to us by the client remains unmodified. state = state.Clone(); // Fill cells using logic and analysis techniques until no more // can be filled. var totalUseOfTechniques = new Dictionary <EliminationTechnique, int>(); FastBitArray [][] possibleNumbers = FillCellsWithSolePossibleNumber(state, options.EliminationTechniques, ref totalUseOfTechniques); // Analyze the current state of the board switch (state.Status) { // If the puzzle is now solved, return it. If the puzzle is in an inconsistent state (such as // two of the same number in the same row), return that as well as there's nothing more to be done. case PuzzleStatus.Solved: case PuzzleStatus.CannotBeSolved: return(new SolverResults(state.Status, state, 0, totalUseOfTechniques)); // If the puzzle is still in progress, it means no more cells // can be filled by elimination alone, so do a brute-force step. // BruteForceSolve recursively calls back to this method. default: if (options.AllowBruteForce) { SolverResults results = BruteForceSolve(state, options, possibleNumbers); if (results.Status == PuzzleStatus.Solved) { AddTechniqueUsageTables(results.UseOfTechniques, totalUseOfTechniques); } return(results); } else { return(new SolverResults(PuzzleStatus.CannotBeSolved, state, 0, null)); } } }
/// <summary> /// Based on the specified GeneratorOptions, determines whether the SolverResults /// created by solving a puzzle with a particular cell value removed represents a valid /// new state. /// </summary> /// <param name="state">The puzzle state being validated.</param> /// <param name="results">The SolverResults to be verified.</param> /// <returns>true if the removal that led to this call is valid; false, otherwise.</returns> private static bool IsValidRemoval(PuzzleState state, SolverResults results, GeneratorOptions options) { // Make sure we have a puzzle with one and only one solution if (results.Status != PuzzleStatus.Solved || results.Puzzles.Count != 1) { return(false); } // Make sure we don't have too few cells if (state.NumberOfFilledCells < options.MinimumFilledCells) { return(false); } // Now check to see if too many decision points were involved if (options.MaximumNumberOfDecisionPoints.HasValue && results.NumberOfDecisionPoints > options.MaximumNumberOfDecisionPoints) { return(false); } // Otherwise, it's a valid removal. return(true); }
/// <summary>Generates a random Sudoku puzzle.</summary> /// <returns>The generated results.</returns> private static SolverResults GenerateOne(GeneratorOptions options) { // Generate a full solution randomly, using the solver to solve a completely empty grid. // For this, we'll use the elimination techniques that yield fast solving. PuzzleState solvedState = new PuzzleState(); SolverOptions solverOptions = new SolverOptions(); solverOptions.MaximumSolutionsToFind = 1; solverOptions.EliminationTechniques = new List <EliminationTechnique> { new NakedSingleTechnique() }; solverOptions.AllowBruteForce = true; SolverResults newSolution = Solver.Solve(solvedState, solverOptions); // Create options to use for removing filled cells from the complete solution. // MaximumSolutionsToFind is set to 2 so that we look for more than 1, but there's no // need in continuing once we know there's more than 1, so 2 is a find value to use. solverOptions.MaximumSolutionsToFind = 2; solverOptions.AllowBruteForce = !options.MaximumNumberOfDecisionPoints.HasValue || options.MaximumNumberOfDecisionPoints > 0; solverOptions.EliminationTechniques = solverOptions.AllowBruteForce ? new List <EliminationTechnique> { new NakedSingleTechnique() } : options.Techniques; // For perf: if brute force is allowed, techniques don't matter! // Now that we have a full solution, we want to randomly remove values from cells // until we get to a point where there is not a unique solution for the puzzle. The // last puzzle state that did have a unique solution can then be used. PuzzleState newPuzzle = newSolution.Puzzle; // Get a random ordering of the cells in which to test their removal Point [] filledCells = GetRandomCellOrdering(newPuzzle); // Do we want to ensure symmetry? int filledCellCount = options.EnsureSymmetry && (filledCells.Length % 2 != 0) ? filledCells.Length - 1 : filledCells.Length; // If ensuring symmetry... if (options.EnsureSymmetry) { // Find the middle cell and put it at the end of the ordering for (int i = 0; i < filledCells.Length - 1; i++) { Point p = filledCells[i]; if (p.X == newPuzzle.GridSize - p.X - 1 && p.Y == newPuzzle.GridSize - p.Y - 1) { Point temp = filledCells[i]; filledCells[i] = filledCells[filledCells.Length - 1]; filledCells[filledCells.Length - 1] = temp; } } // Modify the random ordering so that paired symmetric cells are next to each other // i.e. filledCells[i] and filledCells[i+1] are symmetric pairs for (int i = 0; i < filledCells.Length - 1; i += 2) { Point p = filledCells[i]; Point sp = new Point(newPuzzle.GridSize - p.X - 1, newPuzzle.GridSize - p.Y - 1); for (int j = i + 1; j < filledCells.Length; j++) { if (filledCells[j].Equals(sp)) { Point temp = filledCells[i + 1]; filledCells[i + 1] = filledCells[j]; filledCells[j] = temp; break; } } } // In the order of the array, try to remove each pair from the puzzle and see if it's // still solvable and in a valid way. If it is, greedily leave those cells out of the puzzle. // Otherwise, skip them. byte [] oldValues = new byte[2]; for (int filledCellNum = 0; filledCellNum < filledCellCount && newPuzzle.NumberOfFilledCells > options.MinimumFilledCells; filledCellNum += 2) { // Store the old value so we can put it back if necessary, // then wipe it out of the cell oldValues[0] = newPuzzle[filledCells[filledCellNum]].Value; oldValues[1] = newPuzzle[filledCells[filledCellNum + 1]].Value; newPuzzle[filledCells[filledCellNum]] = null; newPuzzle[filledCells[filledCellNum + 1]] = null; // Check to see whether removing it left us in a good position (i.e. a // single-solution puzzle that doesn't violate any of the generation options) SolverResults newResults = Solver.Solve(newPuzzle, solverOptions); if (!IsValidRemoval(newPuzzle, newResults, options)) { newPuzzle[filledCells[filledCellNum]] = oldValues[0]; newPuzzle[filledCells[filledCellNum + 1]] = oldValues[1]; } } // If there are an odd number of cells in the puzzle (which there will be // as everything we're doing is 9x9, 81 cells), try to remove the odd // cell that doesn't have a pairing. This will be the middle cell. if (filledCells.Length % 2 != 0) { // Store the old value so we can put it back if necessary, // then wipe it out of the cell int filledCellNum = filledCells.Length - 1; byte oldValue = newPuzzle[filledCells[filledCellNum]].Value; newPuzzle[filledCells[filledCellNum]] = null; // Check to see whether removing it left us in a good position (i.e. a // single-solution puzzle that doesn't violate any of the generation options) SolverResults newResults = Solver.Solve(newPuzzle, solverOptions); if (!IsValidRemoval(newPuzzle, newResults, options)) { newPuzzle[filledCells[filledCellNum]] = oldValue; } } } // otherwise, it's much easier. else { // Look at each cell in the random ordering. Try to remove it. // If it works to remove it, do so greedily. Otherwise, skip it. for (int filledCellNum = 0; filledCellNum < filledCellCount && newPuzzle.NumberOfFilledCells > options.MinimumFilledCells; filledCellNum++) { // Store the old value so we can put it back if necessary, // then wipe it out of the cell byte oldValue = newPuzzle[filledCells[filledCellNum]].Value; newPuzzle[filledCells[filledCellNum]] = null; // Check to see whether removing it left us in a good position (i.e. a // single-solution puzzle that doesn't violate any of the generation options) SolverResults newResults = Solver.Solve(newPuzzle, solverOptions); if (!IsValidRemoval(newPuzzle, newResults, options)) { newPuzzle[filledCells[filledCellNum]] = oldValue; } } } // Make sure to now use the techniques specified by the user to score this thing solverOptions.EliminationTechniques = options.Techniques; SolverResults finalResult = Solver.Solve(newPuzzle, solverOptions); // Return the best puzzle we could come up with newPuzzle.Difficulty = options.Difficulty; return(new SolverResults(PuzzleStatus.Solved, newPuzzle, finalResult.NumberOfDecisionPoints, finalResult.UseOfTechniques)); }
/// <summary>Uses brute-force search techniques to solve the puzzle.</summary> /// <param name="state">The state to be solved.</param> /// <param name="options">The options to use in solving.</param> /// <param name="possibleNumbers">The possible numbers off of which to base the search.</param> /// <returns>The result of the solve attempt.</returns> /// <remarks>Changes may be made to the parameter state.</remarks> private static SolverResults BruteForceSolve(PuzzleState state, SolverOptions options, FastBitArray [][] possibleNumbers) { // A standard brute-force search would take way, way, way too long to solve a Sudoku puzzle. // Luckily, there are ways to significantly trim the search tree to a point where // brute-force is not only possible, but also very fast. The idea is that not every number // can be put into every cell. In fact, using elimination techniques, we can narrow down the list // of numbers for each cell, such that only those need be are tried. Moreover, every time a new number // is entered into a cell, other cell's possible numbers decrease. It's in our best interest // to start the search with a cell that has the least possible number of options, thereby increasing // our chances of "guessing" the right number sooner. To this end, I find the cell in the grid // that is empty and that has the least number of possible numbers that can go in it. If there's more // than one cell with the same number of possibilities, I choose randomly among them. This random // choice allows the solver to be used for puzzle generation. List <Point> bestGuessCells = new List <Point>(); int bestNumberOfPossibilities = state.GridSize + 1; for (int i = 0; i < state.GridSize; i++) { for (int j = 0; j < state.GridSize; j++) { int count = possibleNumbers[i][j].CountSet; if (!state[i, j].HasValue) { if (count < bestNumberOfPossibilities) { bestNumberOfPossibilities = count; bestGuessCells.Clear(); bestGuessCells.Add(new Point(i, j)); } else if (count == bestNumberOfPossibilities) { bestGuessCells.Add(new Point(i, j)); } } } } // If there are no cells available to fill, there's nothing we can do // to make forward progress. If there are cells available, which should // always be the case when this method is called, go through each of the possible // numbers in the cell and try to solve the puzzle with that number in it. SolverResults results = null; if (bestGuessCells.Count > 0) { // Choose a random cell from amongst the possibilities found above Point bestGuessCell = bestGuessCells[RandomHelper.GetRandomNumber(bestGuessCells.Count)]; // Get the possible numbers for that cell. For each possible number, // fill that number into the cell and recursively call to Solve. FastBitArray possibleNumbersForBestCell = possibleNumbers[bestGuessCell.X][bestGuessCell.Y]; for (byte p = 0; p < possibleNumbersForBestCell.Length; p++) { if (possibleNumbersForBestCell[p]) { PuzzleState newState = state; // Fill in the cell and solve the puzzle newState[bestGuessCell] = p; SolverOptions tempOptions = options.Clone(); if (results != null) { tempOptions.MaximumSolutionsToFind = (uint)(tempOptions.MaximumSolutionsToFind.Value - results.Puzzles.Count); } SolverResults tempResults = SolveInternal(newState, tempOptions); // If it could be solved, update information about the solving process // and return the solution. Only if the user wants to find multiple // solutions will the search continue. if (tempResults.Status == PuzzleStatus.Solved) { if (results != null && results.Puzzles.Count > 0) { results.Puzzles.AddRange(tempResults.Puzzles); } else { results = tempResults; results.NumberOfDecisionPoints++; } if (options.MaximumSolutionsToFind.HasValue && results.Puzzles.Count >= options.MaximumSolutionsToFind.Value) { return(results); } } // If we're not cloning, we need to cancel out the change newState[bestGuessCell] = null; } } } // We'll get here if the requested number of solutions could not be found, or if no // solutions at all could be found. Either return a solution if we did get at least one, // or return that none could be found. return(results != null ? results : new SolverResults(PuzzleStatus.CannotBeSolved, state, 0, null)); }