This commit is contained in:
2025-06-06 14:30:41 +02:00
parent 4b73c1b5e3
commit 9e5e9eb91a
6 changed files with 632 additions and 0 deletions

View File

@@ -0,0 +1,123 @@
package sudoku;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
import static java.util.Collections.emptySet;
/**
* A field of a Sudoku board.
*/
public class Field {
private static final int DEP_LENGTH = 2 * Sudoku.SIZE + 2;
public final Sudoku sudoku;
public final int x;
public final int y;
private Value value;
private List<Field> dependents;
private final Set<Value> domain = EnumSet.allOf(Value.class);
/**
* Creates a field of the specified Sudoku board at the specified coordinates.
*
* @param sudoku the Sudoku board
* @param x x coordinate (0 <= x < 9)
* @param y y coordinate (0 <= y < 9)
*/
public Field(Sudoku sudoku, int x, int y) {
if (x < 0 || x >= Sudoku.SIZE || y < 0 || y >= Sudoku.SIZE)
throw new IllegalArgumentException("Illegal coordinates " + x + ", " + y);
this.sudoku = sudoku;
this.x = x;
this.y = y;
}
/**
* Returns the value of this field, or null if it is empty.
*
* @return the value of this field, or null if it is empty.
*/
public Value getValue() {
return value;
}
/**
* Sets the value of this field
*
* @param v the value, or null if this field shall be empty.
*/
public void setValue(Value v) {
value = v;
}
/**
* Removes the value of this field so that it is empty.
*/
public void clearValue() {
setValue(null);
}
/**
* Checks whether this field is empty.
*
* @return true iff this field is empty.
*/
public boolean isEmpty() {
return value == null;
}
/**
* Returns a string representation of this field. As a matter of fact,
* its value is returned, or "." if it is empty.
*
* @return a string representation of this field.
*/
public String toString() {
return isEmpty() ? "." : value.toString();
}
/**
* Returns the set of all values possible for this field that do not violate the Sudoku constraints,
* if the field is empty. Otherwise, it returns the empty set. The returned set may be modified if
* the field is empty.
*
* @return the domain of this field.
*/
public Set<Value> getDomain() {
return isEmpty() ? domain : emptySet();
}
/**
* Returns the list of all other fields of this Sudoku field whose values are constrained by the
* value of this field.
*
* @return the list of all dependent fields.
*/
List<Field> getDependents() {
if (dependents == null) {
dependents = new ArrayList<>(DEP_LENGTH);
// fill the list with all fields in the given row and column
for (int i = 0; i < Sudoku.SIZE; i++) {
if (i != x)
dependents.add(sudoku.field(i, y));
if (i != y)
dependents.add(sudoku.field(x, i));
}
// determine the coordinates of the upper left corner of this field's block
final int x0 = 3 * (x / 3);
final int y0 = 3 * (y / 3);
// fill the list with the whole block
for (int x1 = x0; x1 < x0 + 3; x1++)
if (x1 != x)
for (int y1 = y0; y1 < y0 + 3; y1++)
if (y1 != y)
dependents.add(sudoku.field(x1, y1));
}
return dependents;
}
}

View File

@@ -0,0 +1,206 @@
package sudoku;
import java.util.ArrayList;
import java.util.List;
/**
* A Sudoku board as a matrix of {@linkplain sudoku.Field} instances.
* <p>
* Call {@linkplain #solve()} to solve it.
*/
public class Sudoku {
/**
* Edge size of the Sudoku board.
*/
public static final int SIZE = 9;
private static final String LS = System.lineSeparator();
private final Field[] board = new Field[SIZE * SIZE];
/**
* Creates a Sudoku board with all of its fields being empty.
*/
public Sudoku() {
for (int x = 0; x < SIZE; x++)
for (int y = 0; y < SIZE; y++)
board[SIZE * y + x] = new Field(this, x, y);
}
/**
* Returns the field at the specified coordinates
*
* @param x x coordinate (0 <= x < 9)
* @param y y coordinate (0 <= x < 9)
* @return the field at the specified coordinates
*/
public Field field(int x, int y) {
return board[SIZE * y + x];
}
/**
* Bulk operation to set values of the fields.
*
* @param values a sequence of integers where 0 means empty
*/
public void initialize(int... values) {
if (values.length != board.length)
throw new IllegalArgumentException("Wrong Sudoku board with " + values.length + " values");
for (int i = 0; i < values.length; i++)
if (values[i] != 0)
setValue(board[i], Value.of(values[i]));
}
/**
* Sets the specified value of the specified empty field
*
* @param field the empty field whose value is set
* @param value the value of the field
* @throws IllegalStateException if the field is not empty
* @throws IllegalArgumentException if setting this value would violate the Sudoku constraints
* @throws NullPointerException if the value is null
*/
public void setValue(Field field, Value value) {
if (value == null)
throw new NullPointerException("value is null");
if (!field.isEmpty())
throw new IllegalStateException("Value already set: " + field);
if (!field.getDomain().contains(value))
throw new IllegalArgumentException(value + " is not permitted");
for (Field dependent : field.getDependents())
dependent.getDomain().remove(value);
field.setValue(value);
}
/**
* Returns a string representation of this board.
*
* @return a string representation of this board
*/
public String toString() {
StringBuilder sb = new StringBuilder();
for (int y = 0; y < SIZE; y++) {
if (y == 3 || y == 6) sb.append("---------+---------+---------").append(LS);
for (int x = 0; x < SIZE; x++) {
if (x == 3 || x == 6) sb.append("|");
sb.append(" ").append(field(x, y)).append(" ");
}
sb.append(LS);
}
return sb.toString();
}
/**
* Looks for an empty field with minimal domain.
*
* @return a field with minimal domain, or null no field is empty
*/
private Field findBestCandidate() {
Field best = null;
int min = Integer.MAX_VALUE;
for (Field f : board)
if (f.isEmpty() && f.getDomain().size() < min) {
best = f;
min = best.getDomain().size();
}
return best;
}
/**
* Tries to solve the current board. The board is in the solved state if a solution is found,
* and unmodified if no solution is found.
*
* @return true iff a solution has been found.
*/
public boolean solve() {
final Field field = findBestCandidate();
if (field == null)
// A solution has been found
return true;
// There are empty fields. Try all values of its domain
final List<Field> revoke = new ArrayList<>();
for (Value v : field.getDomain()) {
field.setValue(v);
boolean consistent = true;
for (Field dependent : field.getDependents())
if (dependent.getDomain().contains(v)) {
if (dependent.getDomain().size() == 1) {
// This is a dead end because field 'dependent'
// would have an empty domain
consistent = false;
break;
}
revoke.add(dependent);
dependent.getDomain().remove(v);
}
if (consistent && solve())
return true;
// Choosing v as a value lead to a dead end.
// Revoke all modifications
field.clearValue();
for (Field f : revoke)
f.getDomain().add(v);
revoke.clear();
}
return false;
}
public List<Sudoku> solveAll() {
List<Sudoku> solutions = new ArrayList<>();
solveAll(solutions);
return solutions;
}
private void solveAll(List<Sudoku> solutions) {
final Field field = findBestCandidate();
if (field == null) {
// A solution has been found
Sudoku copy = new Sudoku();
copy.initialize(this.export());
solutions.add(copy);
return;
}
// There are empty fields. Try all values of its domain
final List<Field> revoke = new ArrayList<>();
for (Value v : field.getDomain()) {
field.setValue(v);
boolean consistent = true;
for (Field dependent : field.getDependents())
if (dependent.getDomain().contains(v)) {
if (dependent.getDomain().size() == 1) {
// This is a dead end because field 'dependent'
// would have an empty domain
consistent = false;
break;
}
revoke.add(dependent);
dependent.getDomain().remove(v);
}
if (consistent) solveAll(solutions);
// Revoke all modifications
field.clearValue();
for (Field f : revoke)
f.getDomain().add(v);
revoke.clear();
}
}
private int[] export() {
int[] values = new int[SIZE * SIZE];
int idx = 0;
while (idx < values.length) {
Field f = board[idx];
if (f != null) {
Value value = f.getValue();
if (value != null) values[idx] = value.getId();
else values[idx] = 0;
}
else throw new IllegalStateException("at least one field is not set");
++idx;
}
return values;
}
}

View File

@@ -0,0 +1,68 @@
package sudoku;
import java.util.List;
public class SudokuApp {
public static void main(String[] args) {
System.out.println("Sudoku Aufgabenblatt:");
System.out.println();
final Sudoku boardBlatt = new Sudoku();
boardBlatt.initialize(4, 5, 0, 0, 0, 0, 2, 0, 0,
6, 0, 0, 0, 2, 4, 8, 0, 0,
8, 0, 0, 0, 6, 1, 3, 0, 0,
0, 9, 0, 4, 0, 0, 0, 5, 0,
0, 1, 0, 2, 0, 8, 0, 7, 0,
0, 3, 0, 0, 0, 9, 0, 8, 0,
0, 0, 7, 1, 4, 0, 0, 0, 8,
0, 0, 2, 7, 9, 0, 0, 0, 6,
0, 0, 5, 0, 0, 0, 0, 2, 1);
printSolution(boardBlatt);
System.out.println("Sudoku Handzettel:");
System.out.println();
final Sudoku boardHandzettel = new Sudoku();
boardHandzettel.initialize(7, 0, 0, 0, 2, 5, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 9, 1,
0, 0, 0, 0, 0, 0, 0, 0, 0,
4, 0, 9, 1, 0, 0, 0, 0, 0,
0, 0, 0, 6, 0, 0, 2, 0, 0,
0, 0, 0, 0, 0, 0, 0, 3, 0,
8, 2, 0, 0, 0, 0, 5, 0, 0,
0, 0, 0, 0, 3, 0, 6, 0, 0,
0, 0, 0, 9, 0, 0, 0, 0, 0);
printSolution(boardHandzettel);
System.out.println("Sudoku mit 2 Lösungen:");
System.out.println();
final Sudoku boardWithTwoSolutions = new Sudoku();
boardWithTwoSolutions.initialize(2, 9, 5, 7, 4, 3, 8, 6, 1,
4, 3, 1, 8, 6, 5, 9, 0, 0,
8, 7, 6, 1, 9, 2, 5, 4, 3,
3, 8, 7, 4, 5, 9, 2, 1, 6,
6, 1, 2, 3, 8, 7, 4, 9, 5,
5, 4, 9, 2, 1, 6, 7, 3, 8,
7, 6, 3, 5, 2, 4, 1, 8, 9,
9, 2, 8, 6, 7, 1, 3, 5, 4,
1, 5, 4, 9, 3, 8, 6, 0, 0);
System.out.println(boardWithTwoSolutions);
System.out.println("Solutions:");
System.out.println();
List<Sudoku> solutions = boardWithTwoSolutions.solveAll();
System.out.println(String.join(System.lineSeparator(), solutions.stream().map(Sudoku::toString).toList()));
}
private static void printSolution(Sudoku sudoku) {
System.out.println(sudoku);
System.out.println();
if (sudoku.solve()) {
System.out.println("Solution:");
System.out.println();
System.out.println(sudoku);
}
else
System.out.println("No solution!");
}
}

View File

@@ -0,0 +1,41 @@
package sudoku;
/**
* The enumeration type of all possible values of a Sudoku field.
*/
public enum Value {
ONE(1), TWO(2), THREE(3), FOUR(4), FIVE(5), SIX(6), SEVEN(7), EIGHT(8), NINE(9);
private final int id;
Value(int id) {
this.id = id;
}
/**
* Returns a string representation of this value. As a matter of fact, its id is returned.
*
* @return a string representation of this value.
*/
@Override
public String toString() {
return String.valueOf(id);
}
/**
* Returns the value with the specified id.
*
* @param id an id (1 <= id <= 9)
* @return the value with the specified id.
*/
public static Value of(int id) {
return values()[id - 1];
}
/**
* Returns the id of this instance.
*
* @return the id of this instance.
*/
public int getId() {return id;}
}