所以我在Java中使用这个数独求解器已经有好几天了,虽然它似乎有用,但是在我用更难的sudokus运行之后,它有时会崩溃。
解算器本身并不具备蛮力性质。它的灵感来自Knuth的算法X.所以我有一个巨大的0和1矩阵,算法试图完全减少,并通过这样做,解决数独。
程序有点像这样:
现在算法将始终在开始删除正确的列和行时找到解决方案,并始终设法解决容易出现的问题。我怀疑这是因为它必须减少的矩阵非常小(填充空白的可能性矩阵),因此它必须回溯的可能性较小(如果删除了错误的行,则重建矩阵)。
我已经尝试了几天,修复了矩阵修复的部分,因为这似乎是它在硬拼图上崩溃的问题。但是(似乎)之后我解决了这个问题,有些事情不可能发生,我不知道为什么 - 该程序似乎消除了解决这个难题的所有可能性。不要被代码量吓到,注释行“// algorithm X”之前的方法工作并且不是问题的一部分。只有下面的行。
这应该是不可能的,因为其中一个列最少的列将始终是解决方案的一部分但不知何故它将所有列标识为无效......
我对代码进行了评论,并将其全部放入一个.java文件中,因此您可以随意运行并尝试一下。有3个sudokus我正在测试它 - “数独”(简单,大多是正确的),“sudoku1”很难,大多数失败,“sudoku3”空,总是随机解决方案解决。
在程序开始时,我也使用“myFakeRandomGenerator”,因为我希望随机行选择始终采用相同的路径以便于调试。
为了更容易理解我如何在程序中使用HashMaps,即时链接python程序,这启发了我到目前为止写的内容:
http://www.cs.mcgill.ca/~aassaf9/python/algorithm_x.html
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Random;
import java.util.Set;
public class Sudoku {
public static ArrayList<int[]> coverageMatrix = new ArrayList();
public static HashMap<String, ArrayList<Integer>> rowsMatrix = new HashMap<>();
public static HashMap<Integer, ArrayList<String>> columnsMatrix = new HashMap<>();
public static HashMap<String, HashMap<Integer, ArrayList<String>>> backupMonster = new HashMap<String, HashMap<Integer, ArrayList<String>>>();
public static ArrayList<Integer> invalidColumns = new ArrayList<Integer>();
public static ArrayList<String> invalidRows = new ArrayList<String>();
public static ArrayList<String> solution = new ArrayList<String>();
public static Random myFakeRandomGenerator = new Random(152);
public static void main(String[] args) {
//just rename the one you want to run into "sudoku" and off you go
int[][] sudoku = {
{5, 3, 0, 0, 7, 0, 0, 0, 0},
{6, 0, 0, 1, 9, 5, 0, 0, 0},
{0, 9, 8, 0, 0, 0, 0, 6, 0},
{8, 0, 0, 0, 6, 0, 0, 0, 3},
{4, 0, 0, 0, 0, 0, 0, 0, 1},
{7, 0, 0, 0, 0, 0, 0, 0, 6},
{0, 6, 0, 0, 0, 0, 0, 8, 0},
{0, 0, 0, 4, 1, 0, 0, 0, 5},
{0, 0, 0, 0, 8, 0, 0, 7, 9}
};
int[][] sudoku2 = {
{8, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 3, 6, 0, 0, 0, 0, 0},
{0, 7, 0, 0, 9, 0, 2, 0, 0},
{0, 5, 0, 0, 0, 7, 0, 0, 0},
{0, 0, 0, 0, 4, 5, 7, 0, 0},
{0, 0, 0, 1, 0, 0, 0, 3, 0},
{0, 0, 1, 0, 0, 0, 0, 6, 8},
{0, 0, 8, 5, 0, 0, 0, 1, 0},
{0, 9, 0, 0, 0, 0, 4, 0, 0}
};
int[][] sudoku3 = {
{0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0},};
//we parse the input sudoku into string which is later turned into the giant complete coverage matrix
fillTheMatrix(parseSudoku(sudoku));
//here we just transform the giant matrix into the HashMap since it helps us with solving
rowsMatrix = matrixToRowValuesDict(coverageMatrix);
//we include the already given numbers of the sudoku into the solution
solution = addGivenNumbersToSolution(rowsMatrix);
//we generate the matrix of all possible entries for cells for algorithm X to run on
suggestValidMissingLines(parseSudoku(sudoku));
//since we re-generate both matrices because it was slightly altered above
rowsMatrix = matrixToRowValuesDict(coverageMatrix);
columnsMatrix = matrixToValueRowsDict(rowsMatrix);
//here we run the algoritm
solution = solve(rowsMatrix, columnsMatrix, solution);
parseSolution(solution, rowsMatrix);
drawSudoku(parseSolution(solution, rowsMatrix));
}
// <editor-fold desc="Sudoku parsing methods, working.">
public static ArrayList<String> addGivenNumbersToSolution(HashMap<String, ArrayList<Integer>> rowsMatrix) {
ArrayList<String> solutions = new ArrayList<String>();
for (String key : rowsMatrix.keySet()) {
solutions.add(key);
}
return solutions;
}
public static String parseSudoku(int[][] sudoku) {
String sudokuString = "";
for (int i = 0; i < sudoku.length; i++) {
for (int j = 0; j < sudoku[i].length; j++) {
if (sudoku[i][j] == 0) {
sudokuString += ".";
} else {
sudokuString += sudoku[i][j];
}
}
}
return sudokuString;
}
public static void fillTheMatrix(String sudoku) {
for (int i = 0; i < sudoku.length(); i++) {
placeInCell(i, sudoku.substring(i, i + 1));
}
}
public static void placeInCell(int index, String value) {
int[] cells = new int[81];
int[] rows = new int[81];
int[] columns = new int[81];
int[] boxes = new int[81];
int[] matrixRow = new int[324];
if (value.matches("[1-9]")) {
//this helps us place the number in the array
int arrayIndex = Integer.parseInt(value);
//we fill the first quarter of the matrix row - cells
cells[index] = 1;
//we fill the second quarter of the matrix row - rows
int multiplier = index / 9;
int rowsPosition = 9 * multiplier + arrayIndex - 1;
rows[rowsPosition] = 1;
//we fill the third quarter of the matrix row - columns
int colsPosition = index % 9 * 9 + (arrayIndex - 1);
columns[colsPosition] = 1;
//we fill the final quarter of the matrix row - boxes
int column = index % 9;
int row = index / 9;
int reducedColumn = column / 3;
int reducedRow = row / 3;
int boxNumber = reducedColumn + (reducedRow * 3);
int boxIndex = boxNumber * 9 + (arrayIndex - 1);
boxes[boxIndex] = 1;
matrixRow = mergeTheArrays(cells, rows, columns, boxes);
coverageMatrix.add(matrixRow);
}
}
//helper method for creating the single line of the giant matrix
public static int[] mergeTheArrays(int[] cells, int[] rows, int[] columns, int[] boxes) {
int[] merged = new int[324];
int mergedIndex = 0;
for (int i = 0; i < cells.length; i++) {
merged[mergedIndex] = cells[i];
mergedIndex++;
}
for (int i = 0; i < rows.length; i++) {
merged[mergedIndex] = rows[i];
mergedIndex++;
}
for (int i = 0; i < columns.length; i++) {
merged[mergedIndex] = columns[i];
mergedIndex++;
}
for (int i = 0; i < boxes.length; i++) {
merged[mergedIndex] = boxes[i];
mergedIndex++;
}
return merged;
}
public static String parseSolution(ArrayList<String> solution, HashMap<String, ArrayList<Integer>> rowsMatrix) {
//our final output
int[] numbers = new int[81];
String sudoku = "";
for (String key : solution) {
int index = (rowsMatrix.get(key).get(0));
int value = (rowsMatrix.get(key).get(2)) % 9 + 1;
numbers[index] = value;
}
for (int i = 0; i < numbers.length; i++) {
sudoku += numbers[i];
}
return sudoku;
}
public static void drawSudoku(String sudoku) {
System.out.print("+-------+-------+-------+\n");
for (int i = 0; i < 81; i += 9) {
System.out.print("| ");
for (int j = 0; j < 9; j++) {
System.out.print(sudoku.charAt(i + j) + " ");
if (j == 2 || j == 5 || j == 8) {
System.out.print("| ");
}
if ((i == 18 || i == 45 || i == 72) && j == 8) {
System.out.print("\n+-------+-------+-------+");
}
}
System.out.println("");
}
}
public static void suggestValidMissingLines(String sudoku) {
for (int i = 0; i < sudoku.length(); i++) {
//we find empty slots
if (sudoku.substring(i, i + 1).equals(".")) {
Set unitValues = new HashSet();
unitValues.addAll(checkAllColumnPreerValues(i, sudoku));
unitValues.addAll(checkAllRowPeerValues(i, sudoku));
//unitValues.addAll(checkAllBoxPeerValues4x4(i, sudoku));
//we go through values 1-9 and if that value ISN'T in any peer cell, we add the suggestion for that value
for (int j = 1; j < 10; j++) {
if (!unitValues.contains(j + "")) {
placeInCell(i, j + "");
}
}
}
}
}
public static Set checkAllColumnPreerValues(int index, String sudoku) {
Set<String> values = new HashSet<>();
//here we get the column index, which in turn later enables us easier search
int columnIndex = index % 9;
for (int i = 0; i < sudoku.length(); i += 9) {
if (sudoku.substring(columnIndex + i, columnIndex + i + 1).matches("[1-9]")) {
values.add(sudoku.substring(columnIndex + i, columnIndex + i + 1));
}
}
return values;
}
public static Set checkAllRowPeerValues(int index, String sudoku) {
int rowIndex = index / 9;
Set<String> values = new HashSet<>();
for (int i = 0; i < 9; i++) {
int look = (rowIndex * 9) + i;
if (sudoku.substring(look, look + 1).matches("[1-9]")) {
values.add(sudoku.substring(look, look + 1));
}
}
return values;
}
public static Set checkAllBoxPeerValues(int index, String sudoku) {
int column = index % 9;
int row = index / 9;
int reducedCol = column / 3;
int reducedRow = row / 3;
int boxNumber = reducedCol + 3 * reducedRow;
Set<String> values = new HashSet<>();
return null;
}
//this method allows us to lookup which columns are covered by the row
public static HashMap<String, ArrayList<Integer>> matrixToRowValuesDict(ArrayList<int[]> matrix) {
String rowName = "row ";
HashMap<String, ArrayList<Integer>> row = new HashMap<String, ArrayList<Integer>>();
ArrayList<Integer> values = new ArrayList<Integer>();
for (int i = 0; i < matrix.size(); i++) {
for (int j = 0; j < matrix.get(i).length; j++) {
if (matrix.get(i)[j] == 1) {
values.add(j);
}
}
row.put(rowName + i, values);
values = new ArrayList<Integer>();
}
return row;
}
//this method allows us to lookup which rows fill the column
public static HashMap<Integer, ArrayList<String>> matrixToValueRowsDict(HashMap<String, ArrayList<Integer>> rowValuesMatrix) {
HashMap<Integer, ArrayList<String>> valueRowsDict = new HashMap<Integer, ArrayList<String>>();
for (String key : rowValuesMatrix.keySet()) {
//iterate over every element that is stored under "key"
for (int i = 0; i < rowValuesMatrix.get(key).size(); i++) {
//if a value is already a key in the transformed dictionary
int currentValue = rowValuesMatrix.get(key).get(i);
if (valueRowsDict.keySet().contains(currentValue)) {
//add it if it isnt added already
if (!valueRowsDict.get(currentValue).contains(key)) {
valueRowsDict.get(currentValue).add(key);
}
} else {
ArrayList<String> values = new ArrayList<String>();
values.add(key);
valueRowsDict.put(currentValue, values);
values = new ArrayList<String>();
}
}
}
return valueRowsDict;
}
// </editor-fold>
//algorithm X
public static ArrayList<String> solve(HashMap<String, ArrayList<Integer>> rowColumnMatrix, HashMap<Integer, ArrayList<String>> columnRowMatrix, ArrayList<String> solution) {
if (columnRowMatrix.isEmpty()) {
//finished
return solution;
} else {
//normal run
//select column with least ones.
int column = selectColumnWithLeastOnes(columnRowMatrix);
//select a row from that column
ArrayList<String> rowsOfSelectedColumn = columnRowMatrix.get(column);
String row = selectRandomRow(rowsOfSelectedColumn);
//include row in the solution
solution.add(row);
//reduce matrix size and check if it is still valid
select(rowColumnMatrix, columnRowMatrix, row);
//repeat recursively
return solve(rowColumnMatrix, columnRowMatrix, solution);
}
}
public static void select(HashMap<String, ArrayList<Integer>> rowColumnMatrix, HashMap<Integer, ArrayList<String>> columnRowMatrix, String selectedRow) {
//here we collect all the rows that we will remove
Set<String> encounters = new HashSet<>();
//deep copy
HashMap<Integer, ArrayList<String>> testCopy = new HashMap(columnRowMatrix);
for (Integer key : columnRowMatrix.keySet()) {
ArrayList<String> tmp = new ArrayList<String>();
for (String value : columnRowMatrix.get(key)) {
tmp.add(value);
}
testCopy.put(key, tmp);
}
backupMonster.put(selectedRow, testCopy);
//this will tell us which columns to remove
ArrayList<Integer> columnsToRemove = rowColumnMatrix.get(selectedRow);
//we fill the set with all the rows that we will have to remove from columns
for (Integer column : rowColumnMatrix.get(selectedRow)) {
for (String subRow : columnRowMatrix.get(column)) {
encounters.add(subRow);
}
}
//we then remove the row letters from column matrix
for (String encouter : encounters) {
ArrayList<Integer> columns = rowColumnMatrix.get(encouter);
//createbackup
for (Integer columnd : columns) {
columnRowMatrix.get(columnd).remove(encouter);
}
}
//finally, we remove the rows from the matrix as well.
for (Integer rowToRemove : columnsToRemove) {
columnRowMatrix.remove(rowToRemove);
}
//after the matrix has been reduced, we check if it's still valid, and if not we rebuild and rerun the solve() on repaired one.
if (!areColsValid(columnRowMatrix)) {
invalidRows.add(selectedRow);
solution.remove(selectedRow);
HashMap<Integer, ArrayList<String>> testRestore = backupMonster.get(selectedRow);
for (Integer key : testRestore.keySet()) {
ArrayList<String> tmp = new ArrayList<String>();
for (String value : testRestore.get(key)) {
tmp.add(value);
}
columnRowMatrix.put(key, tmp);
}
}
}
//this method checks if the columnsMatrix is still valid state (there should be NO keys with empty values - 1 key has to have AT LEAST 1 value)
public static boolean areColsValid(HashMap<Integer, ArrayList<String>> columnsMatrix) {
for (Integer key : columnsMatrix.keySet()) {
if (columnsMatrix.get(key).isEmpty()) {
return false;
}
}
return true;
}
public static Integer selectColumnWithLeastOnes(HashMap<Integer, ArrayList<String>> columnRowMatrix) {
int leastElements = Integer.MAX_VALUE;
ArrayList<Integer> possibleCandidates = new ArrayList<Integer>();
//first we figure out lowest 1 amount
for (Integer column : columnRowMatrix.keySet()) {
if (columnRowMatrix.get(column).size() < leastElements) {
leastElements = columnRowMatrix.get(column).size();
}
}
//we add the columns with that amount of ones
for (Integer column : columnRowMatrix.keySet()) {
int counter = 0;
if (columnRowMatrix.get(column).size() == leastElements) {
//we check if all the rows yield wrong result, then we dont include this column
for(String candidateRow : columnRowMatrix.get(column))
{
if (invalidRows.contains(candidateRow)) {
counter+=1;
}
if (counter==columnRowMatrix.get(column).size()) {
invalidColumns.add(column);
}
}
if (!invalidColumns.contains(column)) {
possibleCandidates.add(column);
}
}
}
//we randomly pick a column BUT take care not to pick the one that gives unsolveable case
//int random = new Random().nextInt(possibleCandidates.size());
int random = myFakeRandomGenerator.nextInt(possibleCandidates.size());
//we generate new random until we have a valid candidate
while (invalidColumns.contains(possibleCandidates.get(random))) {
//random = new Random().nextInt(possibleCandidates.size());
random = myFakeRandomGenerator.nextInt(possibleCandidates.size());
}
return possibleCandidates.get(random);
}
public static String selectRandomRow(ArrayList<String> rowsFromSelectedCol) {
//int random = new Random().nextInt(rowsFromSelectedCol.size());
int random = myFakeRandomGenerator.nextInt(rowsFromSelectedCol.size());
while (invalidRows.contains(rowsFromSelectedCol.get(random))) {
//random = new Random().nextInt(rowsFromSelectedCol.size());
random = myFakeRandomGenerator.nextInt(rowsFromSelectedCol.size());
}
return rowsFromSelectedCol.get(random);
}
//we identify column as invalid if all of it's rows are in the invalidRows arraylist
public static boolean isColumnValid(ArrayList<String> invalidRows, int column){
ArrayList<String> rowsToCheck = columnsMatrix.get(column);
int counter = rowsToCheck.size();
for(String entry : rowsToCheck){
if (invalidRows.contains(entry)) {
counter+=1;
}
}
if (counter == rowsToCheck.size()) {
return false;
}
return true;
}
}