diff --git a/uebung06/src/sudoku/Field.java b/uebung06/src/sudoku/Field.java new file mode 100644 index 0000000..bb27e97 --- /dev/null +++ b/uebung06/src/sudoku/Field.java @@ -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 dependents; + private final Set 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 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 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; + } +} diff --git a/uebung06/src/sudoku/Sudoku.java b/uebung06/src/sudoku/Sudoku.java new file mode 100644 index 0000000..f61e64a --- /dev/null +++ b/uebung06/src/sudoku/Sudoku.java @@ -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. + *

+ * 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 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 solveAll() { + List solutions = new ArrayList<>(); + solveAll(solutions); + return solutions; + } + + private void solveAll(List 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 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; + } +} diff --git a/uebung06/src/sudoku/SudokuApp.java b/uebung06/src/sudoku/SudokuApp.java new file mode 100644 index 0000000..6e91986 --- /dev/null +++ b/uebung06/src/sudoku/SudokuApp.java @@ -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 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!"); + } +} diff --git a/uebung06/src/sudoku/Value.java b/uebung06/src/sudoku/Value.java new file mode 100644 index 0000000..d2707ec --- /dev/null +++ b/uebung06/src/sudoku/Value.java @@ -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;} +} diff --git a/uebung06/test/sudoku/FieldTest.java b/uebung06/test/sudoku/FieldTest.java new file mode 100644 index 0000000..d368d31 --- /dev/null +++ b/uebung06/test/sudoku/FieldTest.java @@ -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()); + } +} diff --git a/uebung06/test/sudoku/SudokuAppTest.java b/uebung06/test/sudoku/SudokuAppTest.java new file mode 100644 index 0000000..fdbec38 --- /dev/null +++ b/uebung06/test/sudoku/SudokuAppTest.java @@ -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 rows = new HashSet<>(); + HashSet cols = new HashSet<>(); + + HashMap> 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; + } +}