This commit is contained in:
Johannes Schmelz 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;}
}

View File

@ -0,0 +1,65 @@
package uebung06.test.sudoku;
import org.junit.Before;
import org.junit.Test;
import sudoku.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
public class FieldTest {
private static final int X = 8;
private static final int Y = 5;
private static final int NUL = -1;
private static final Value VAL = Value.EIGHT;
private static Sudoku sudoku;
private static Field field;
@Before
public void init() {
sudoku = new Sudoku();
field = new Field(sudoku, X, Y);
}
@Test(expected = IllegalArgumentException.class)
public void illegalXLowerBoundFieldTest() {
field = new Field(sudoku, NUL, Y);
}
@Test(expected = IllegalArgumentException.class)
public void illegalXUpperBoundFieldTest() {
field = new Field(sudoku, Sudoku.SIZE, Y);
}
@Test(expected = IllegalArgumentException.class)
public void illegalYLowerBoundFieldTest() {
field = new Field(sudoku, X, NUL);
}
@Test(expected = IllegalArgumentException.class)
public void illegalYUpperBoundFieldTest() {
field = new Field(sudoku, X, Sudoku.SIZE);
}
@Test
public void valueFieldTest() {
field.setValue(VAL);
assertEquals(field.getValue(), VAL);
}
@Test
public void clearValueFieldTest() {
field.setValue(VAL);
field.clearValue();
assertNull(field.getValue());
}
@Test
public void isValueEmptyFieldTest() {
field.setValue(VAL);
field.clearValue();
assertTrue(field.isEmpty());
}
}

View File

@ -0,0 +1,129 @@
package uebung06.test.sudoku;
import org.junit.Before;
import org.junit.Test;
import sudoku.*;
import java.util.HashMap;
import java.util.HashSet;
import java.util.stream.IntStream;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class SudokuAppTest {
private static final int MAX = 81;
private static final int MIN = 0;
private static final int SUDOKU_SIZE = 9;
private static Sudoku board;
private static int[] init;
private static int[] wrongSol;
@Before
public void init() {
board = new Sudoku();
init = IntStream.of(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).toArray();
wrongSol = IntStream.of(4, 5, 1, 8, 7, 3, 2, 6, 9,
6, 7, 3, 9, 2, 4, 8, 1, 5,
8, 2, 9, 5, 6, 1, 3, 4, 7,
2, 9, 8, 4, 1, 7, 6, 5, 3,
5, 1, 6, 2, 3, 8, 9, 7, 4,
7, 3, 4, 6, 5, 9, 1, 8, 2,
3, 6, 7, 1, 4, 2, 5, 9, 8,
1, 8, 2, 7, 9, 5, 4, 3, 6,
9, 5, 4, 3, 8, 6, 7, 2, 1).toArray();
}
//Testing first If-condition in Sudoku.init
@Test(expected = IllegalArgumentException.class)
public void illegalLowerBoundInitSudokuAppTest() {
board.initialize(IntStream.range(MIN, MIN+1).toArray());
}
//Testing first If-condition in Sudoku.init
@Test(expected = IllegalArgumentException.class)
public void illegalUpperBoundInitSudokuAppTest() {
board.initialize(IntStream.range(MIN, MAX+1).toArray());
}
//Testing ArrayIndex of Value
@Test(expected = ArrayIndexOutOfBoundsException.class)
public void illegalArrayIndexOutOfBoundInitSudokuAppTest() {
board.initialize(IntStream.range(MIN, MAX).toArray());
}
//Testing Sudoku.solve()
@Test
public void correctSolutionSudokuAppTest() {
board.initialize(init);
board.solve();
assertTrue(verifySudokuSol());
}
//Just testing verifySudokuSol-Method
@Test
public void wrongSolutionSudokuAppTest() {
try {
board.initialize(wrongSol);
} catch (final IllegalArgumentException e) {
//do nothing, just handling throwing exceptions from Sudoku-class
}
assertFalse(verifySudokuSol());
}
public Boolean verifySudokuSol() {
HashSet<Value> rows = new HashSet<>();
HashSet<Value> cols = new HashSet<>();
HashMap<Integer, HashSet<Value>> blocks = new HashMap<>();
IntStream.range(0, SUDOKU_SIZE)
.forEach(block -> blocks.put(block, new HashSet<>())); //init Hashsets for every block
for (int y = 0; y < SUDOKU_SIZE; y++) {
for (int x = 0; x < SUDOKU_SIZE; x++) {
//check column
Value val = board.field(x, y).getValue();
if (!cols.add(val))
return false;
if (cols.size() == SUDOKU_SIZE)
cols.clear();
//check row
val = board.field(y, x).getValue();
if (!rows.add(val))
return false;
if (rows.size() == SUDOKU_SIZE)
rows.clear();
//check 3x3 block
val = board.field(x, y).getValue();
if (!blocks.get(getBlockIndex(x, y)).add(val))
return false;
}
}
return true;
}
public int getBlockIndex(int x, int y) {
return getColIndex(x) + getRowIndex(y);
}
public int getColIndex(int x) {
return x / 3;
}
public int getRowIndex(int y) {
return (y/3)*3;
}
}