492 lines
16 KiB
Java
492 lines
16 KiB
Java
import java.awt.BorderLayout;
|
|
import java.awt.Color;
|
|
import java.awt.Dimension;
|
|
import java.awt.Graphics;
|
|
import java.awt.GridLayout;
|
|
|
|
import java.io.FileInputStream;
|
|
import java.io.FileOutputStream;
|
|
import java.io.ObjectInputStream;
|
|
import java.io.ObjectOutputStream;
|
|
|
|
import java.util.ArrayDeque;
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.HashSet;
|
|
|
|
import javax.swing.JFrame;
|
|
import javax.swing.JPanel;
|
|
import javax.swing.JScrollPane;
|
|
|
|
import java.util.concurrent.*;
|
|
import java.util.concurrent.atomic.AtomicBoolean;
|
|
import java.util.concurrent.atomic.AtomicInteger;
|
|
|
|
final public class Solver extends JPanel {
|
|
|
|
private static final long serialVersionUID = 1L;
|
|
|
|
// The default size of the labyrinth (i.e. unless program is invoked with size arguments):
|
|
private static final int DEFAULT_WIDTH_IN_CELLS = 1000;
|
|
private static final int DEFAULT_HEIGHT_IN_CELLS = 1000;
|
|
|
|
private static final int N_RUNS_HALF = 10; // #runs will be 2*N_RUNS_HALF + 1
|
|
|
|
// The grid defining the structure of the labyrinth
|
|
private final Labyrinth labyrinth;
|
|
|
|
// For each cell in the labyrinth: Has solve() visited it yet?
|
|
private boolean[][] visited; // initialized in solve()
|
|
private AtomicBoolean[][] visitedAtomic;
|
|
private AtomicInteger numTasks = new AtomicInteger(0);
|
|
|
|
private Semaphore[] sectors;
|
|
private static final int SECTOR_SIZE = (int) Math.sqrt(DEFAULT_WIDTH_IN_CELLS * DEFAULT_HEIGHT_IN_CELLS) / 50;
|
|
|
|
// determined by trial and error
|
|
private static final int DISTANCE_PER_TASK = DEFAULT_WIDTH_IN_CELLS * DEFAULT_HEIGHT_IN_CELLS / 128;
|
|
|
|
private Point[] solution = null; // set to solution path once that has been computed
|
|
|
|
public Solver(Labyrinth labyrinth) {
|
|
this.labyrinth = labyrinth;
|
|
}
|
|
|
|
public Solver(int width, int height) {
|
|
this(new Labyrinth(width, height));
|
|
}
|
|
|
|
private boolean visitedBefore(Point p) {
|
|
return visited[p.getX()][p.getY()];
|
|
}
|
|
|
|
private void visit(Point p) {
|
|
visited[p.getX()][p.getY()] = true;
|
|
}
|
|
|
|
/**
|
|
* @return Returns a path through the labyrinth from start to end as an array, or null if no solution exists
|
|
*/
|
|
public Point[] solve() {
|
|
|
|
// Initialize the search state: This must be done here to be part of the timing measurement
|
|
|
|
Point current = labyrinth.getStart();
|
|
ArrayDeque<Point> pathSoFar = new ArrayDeque<Point>(); // Path from start to just before current
|
|
visited = new boolean[labyrinth.getWidth()][labyrinth.getHeight()]; // initially all false
|
|
ArrayDeque<PointAndDirection> backtrackStack = new ArrayDeque<PointAndDirection>();
|
|
// Used as a stack: Branches not yet taken; solver will backtrack to these branching points later
|
|
// TODO: Is it faster to allocate backtrackStack with width*height elements right away?
|
|
|
|
// Search:
|
|
|
|
while (!labyrinth.isDestination(current)) {
|
|
Point next = null;
|
|
visit(current);
|
|
|
|
// Use first random unvisited neighbor as next cell, push others on the backtrack stack:
|
|
Direction[] dirs = Direction.values();
|
|
for (Direction directionToNeighbor: dirs) {
|
|
Point neighbor = current.getNeighbor(directionToNeighbor);
|
|
if ( labyrinth.hasPassage(current, directionToNeighbor)
|
|
&& !visitedBefore(neighbor)
|
|
&& ( !labyrinth.isBlindAlley(neighbor, directionToNeighbor.opposite)
|
|
|| labyrinth.isDestination(neighbor))) {
|
|
if (next == null) // 1st unvisited neighbor
|
|
next = neighbor;
|
|
else {
|
|
// 2nd or higher unvisited neighbor: Save neighbor as starting cell for a later backtracking
|
|
backtrackStack.push(new PointAndDirection(neighbor, directionToNeighbor.opposite));
|
|
// System.out.println("Pushing " + neighbor + " to the backtracking stack.");
|
|
}
|
|
}
|
|
}
|
|
// Advance to next cell, if any:
|
|
if (next != null) {
|
|
// System.out.println("Advancing from " + current + " to " + next);
|
|
pathSoFar.addLast(current);
|
|
current = next;
|
|
} else {
|
|
// current has no unvisited neighbor: Backtrack, if possible
|
|
if (backtrackStack.isEmpty())
|
|
return null; // No more backtracking avaible: No solution exists
|
|
|
|
// Backtrack: Continue with cell saved at latest branching point:
|
|
PointAndDirection pd = backtrackStack.pop();
|
|
current = pd.getPoint();
|
|
Point branchingPoint = current.getNeighbor(pd.getDirectionToBranchingPoint());
|
|
// System.out.println("Backtracking to " + branchingPoint);
|
|
// Remove the dead end from the top of pathSoFar, i.e. all cells after branchingPoint:
|
|
while (!pathSoFar.peekLast().equals(branchingPoint)) {
|
|
// System.out.println(" Going back before " + pathSoFar.peekLast());
|
|
pathSoFar.removeLast();
|
|
}
|
|
}
|
|
}
|
|
pathSoFar.addLast(current);
|
|
// Point[0] is only for making the return value have type Point[] (and not Object[]):
|
|
return pathSoFar.toArray(new Point[0]);
|
|
}
|
|
|
|
private int getSectorsX() {
|
|
return (int)Math.ceil((double)labyrinth.getWidth() / SECTOR_SIZE);
|
|
}
|
|
private int getSectorsY() {
|
|
return (int)Math.ceil((double)labyrinth.getHeight() / SECTOR_SIZE);
|
|
}
|
|
private int getSectorId(Point p) {
|
|
return (p.getX() / SECTOR_SIZE) + (p.getY() / SECTOR_SIZE) * getSectorsX();
|
|
}
|
|
|
|
public Point[] solveConcurrently() {
|
|
// dummy origin direction for start
|
|
PointAndDirection start = new PointAndDirection(labyrinth.getStart(), null);
|
|
|
|
// visitedAtomic = new AtomicBoolean[labyrinth.getWidth()][labyrinth.getHeight()];
|
|
// for (int i = 0; i < visitedAtomic.length; i++) {
|
|
// for (int j = 0; j < visitedAtomic[i].length; j++) {
|
|
// visitedAtomic[i][j] = new AtomicBoolean(false);
|
|
// }
|
|
// }
|
|
|
|
visited = new boolean[labyrinth.getWidth()][labyrinth.getHeight()];
|
|
|
|
sectors = new Semaphore[getSectorsX() * getSectorsY()];
|
|
for (int i = 0; i < sectors.length; i++) {
|
|
sectors[i] = new Semaphore(1);
|
|
}
|
|
|
|
ForkJoinPool pool = ForkJoinPool.commonPool();
|
|
|
|
ArrayDeque<Point> pathSoFar = new ArrayDeque<>();
|
|
ConcurrentSolverTask t = new ConcurrentSolverTask(start, pathSoFar, pool);
|
|
Point[] result = pool.invoke(t);
|
|
|
|
System.out.println("Used tasks: " + numTasks.get());
|
|
System.out.println("Task to Solution Length Ratio: " + (double)result.length / numTasks.get());
|
|
|
|
return result;
|
|
}
|
|
|
|
private class ConcurrentSolverTask extends RecursiveTask<Point[]> {
|
|
private ArrayDeque<PointAndDirection> backtrackStack;
|
|
private PointAndDirection start;
|
|
private ArrayDeque<Point> pathSoFar;
|
|
private ForkJoinPool pool;
|
|
private ArrayList<ConcurrentSolverTask> subtasks;
|
|
private int currentSector;
|
|
public ConcurrentSolverTask(PointAndDirection start, ArrayDeque<Point> pathSoFar, ForkJoinPool pool) {
|
|
this.start = start;
|
|
this.pathSoFar = pathSoFar;
|
|
this.pool = pool;
|
|
|
|
this.backtrackStack = new ArrayDeque<>();
|
|
this.subtasks = new ArrayList<>();
|
|
this.currentSector = -1;
|
|
}
|
|
|
|
private void acquireSector(Point p) {
|
|
if (getSectorId(p) == this.currentSector) return;
|
|
if (this.currentSector >= 0) {
|
|
// release previous sector
|
|
sectors[this.currentSector].release();
|
|
}
|
|
this.currentSector = getSectorId(p);
|
|
try {
|
|
// acquire new sector
|
|
sectors[this.currentSector].acquire();
|
|
} catch (InterruptedException e) {
|
|
System.out.println(e);
|
|
return;
|
|
}
|
|
}
|
|
|
|
private void releaseSector() {
|
|
if (this.currentSector >= 0) {
|
|
sectors[this.currentSector].release();
|
|
}
|
|
}
|
|
|
|
public Point[] compute() {
|
|
numTasks.incrementAndGet();
|
|
int currentLength = 0;
|
|
Point current = this.start.getPoint();
|
|
|
|
Point next;
|
|
Direction[] dirs = Direction.values();
|
|
|
|
while (!labyrinth.isDestination(current) && !Thread.interrupted()) {
|
|
// System.out.println("Visiting " + current);
|
|
|
|
next = null;
|
|
|
|
for (Direction dir : dirs) {
|
|
|
|
// don't check direction of origin
|
|
if (
|
|
current.equals(this.start.getPoint())
|
|
&& dir.equals(start.getDirectionToBranchingPoint())
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
// check if labyrinth has passage in this direction
|
|
if (!labyrinth.hasPassage(current, dir)) {
|
|
continue;
|
|
}
|
|
|
|
Point neighbour = current.getNeighbor(dir);
|
|
|
|
// avoid blind alleys
|
|
if (
|
|
labyrinth.isBlindAlley(neighbour, dir.opposite)
|
|
&& !labyrinth.isDestination(neighbour)
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
// check if that cell has been visited
|
|
// set visitedAtomic to true if it has not been visited
|
|
// if (!visitedAtomic[neighbour.x][neighbour.y].compareAndSet(false, true)) {
|
|
// continue;
|
|
// }
|
|
|
|
acquireSector(neighbour);
|
|
if (visited[neighbour.x][neighbour.y]) continue;
|
|
visited[neighbour.x][neighbour.y] = true;
|
|
releaseSector();
|
|
|
|
|
|
// System.out.println("Found unvisited neighbour: " + neighbour);
|
|
if (currentLength >= DISTANCE_PER_TASK) {
|
|
releaseSector();
|
|
// if we have reached the distance limit, create a subtask
|
|
ArrayDeque<Point> subPath = this.pathSoFar.clone();
|
|
subPath.addLast(current);
|
|
ConcurrentSolverTask subtask = new ConcurrentSolverTask(
|
|
new PointAndDirection(neighbour, dir.opposite),
|
|
subPath,
|
|
this.pool
|
|
);
|
|
subtasks.add(subtask);
|
|
|
|
subtask.fork();
|
|
} else {
|
|
if (next == null) {
|
|
// visit first unvisited neighbour next
|
|
next = neighbour;
|
|
// System.out.println("Visiting next.");
|
|
} else {
|
|
// push all other unvisited neighbours on backtrack stack to be checked later
|
|
this.backtrackStack.push(new PointAndDirection(neighbour, dir.opposite));
|
|
// System.out.println("Pushing to stack.");
|
|
}
|
|
}
|
|
}
|
|
// at least one unvisited neighbour found
|
|
if (next != null) {
|
|
pathSoFar.addLast(current);
|
|
current = next;
|
|
currentLength++;
|
|
}
|
|
if (next == null) {
|
|
releaseSector();
|
|
// no unvisited neighbour found
|
|
// backtrack if possible
|
|
if (this.backtrackStack.isEmpty()) {
|
|
// no solution found from this point
|
|
|
|
// check for solutions in subtasks
|
|
// System.out.println("End of task reached - waiting for " + subtasks.size() + " subtasks");
|
|
for (ConcurrentSolverTask subtask : subtasks) {
|
|
Point[] subtaskResult = subtask.join();
|
|
if (subtaskResult != null) {
|
|
// found a solution in a subtask
|
|
return subtaskResult;
|
|
}
|
|
}
|
|
// no solution found
|
|
return null;
|
|
}
|
|
|
|
PointAndDirection pd = this.backtrackStack.pop();
|
|
current = pd.getPoint();
|
|
|
|
// Remove the dead end from the top of pathSoFar, i.e. all cells after branchingPoint:
|
|
Point branchingPoint = current.getNeighbor(pd.getDirectionToBranchingPoint());
|
|
while (!pathSoFar.isEmpty() && !pathSoFar.peekLast().equals(branchingPoint)) {
|
|
pathSoFar.removeLast();
|
|
currentLength--;
|
|
}
|
|
|
|
}
|
|
}
|
|
// parent thread has found solution or other interrupt
|
|
if (Thread.interrupted()) {
|
|
return null;
|
|
}
|
|
pathSoFar.addLast(current);
|
|
|
|
// solution found, interrupt subtasks
|
|
for (ConcurrentSolverTask subtask : subtasks) {
|
|
subtask.cancel(true);
|
|
}
|
|
|
|
return pathSoFar.toArray(new Point[0]);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void paintComponent(Graphics graphics) {
|
|
super.paintComponent(graphics);
|
|
// draw white background
|
|
graphics.setColor(Color.WHITE);
|
|
graphics.fillRect(0, 0, labyrinth.getWidth()*labyrinth.cell_size_pixels(), labyrinth.getHeight()*labyrinth.cell_size_pixels());
|
|
|
|
// draw solution path, if available
|
|
if (solution != null) {
|
|
graphics.setColor(Color.YELLOW);
|
|
for (Point p: solution)
|
|
/* // fill only white area between the walls instead of whole cell:
|
|
graphics.fillRect(p.getX()*CELL_PX+HALF_WALL_PX, p.getY()*CELL_PX+HALF_WALL_PX,
|
|
CELL_PX-2*HALF_WALL_PX, CELL_PX-2*HALF_WALL_PX);
|
|
*/
|
|
graphics.fillRect(p.getX()*labyrinth.cell_size_pixels(), p.getY()*labyrinth.cell_size_pixels(),
|
|
labyrinth.cell_size_pixels(), labyrinth.cell_size_pixels());
|
|
}
|
|
// draw walls
|
|
labyrinth.display(graphics);
|
|
}
|
|
|
|
public void printSolution() {
|
|
System.out.print("Solution: ");
|
|
for (Point p: solution)
|
|
System.out.print(p);
|
|
System.out.println();
|
|
}
|
|
|
|
public void displaySolution() {
|
|
repaint();
|
|
}
|
|
|
|
private static Solver makeAndSaveSolver(String[] args) {
|
|
|
|
// Construct solver: Either read it from a file, or create a new one
|
|
if (args.length >= 1 && args[0].endsWith(".ser")) {
|
|
|
|
// 1st argument is name of file with serialized labyrinth: Ignore other arguments
|
|
// and create a solver for the labyrinth from that file:
|
|
ObjectInputStream ois;
|
|
try {
|
|
ois = new ObjectInputStream(new FileInputStream(args[0]));
|
|
Labyrinth labyrinth = (Labyrinth)ois.readObject();
|
|
ois.close();
|
|
return new Solver(labyrinth);
|
|
} catch (Exception e) {
|
|
System.out.println(e);
|
|
return null;
|
|
}
|
|
} else {
|
|
// Create solver for new, random labyrinth:
|
|
|
|
int width = args.length >= 1 ? (Integer.parseInt(args[0])) : DEFAULT_WIDTH_IN_CELLS;
|
|
int height = args.length >= 2 ? (Integer.parseInt(args[1])) : DEFAULT_HEIGHT_IN_CELLS;
|
|
|
|
Solver solver = new Solver(width, height);
|
|
|
|
// Save labyrinth to file (may be reused in future program executions):
|
|
try {
|
|
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("labyrinth.ser"));
|
|
oos.writeObject(solver.labyrinth);
|
|
oos.close();
|
|
} catch (Exception e) {
|
|
System.out.println(e);
|
|
}
|
|
|
|
return solver;
|
|
}
|
|
}
|
|
|
|
|
|
private static void displayLabyrinth(Solver solver) {
|
|
JFrame frame = new JFrame("Sequential labyrinth solver");
|
|
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
|
|
// TODO: Window is initially displayed somewhat smaller than
|
|
// the indicated frame size, therefore use width+5 and height+5:
|
|
frame.setSize((solver.labyrinth.getWidth()+5) * solver.labyrinth.cell_size_pixels(),
|
|
(solver.labyrinth.getHeight()+5) * solver.labyrinth.cell_size_pixels());
|
|
|
|
// Put a scroll pane around the labyrinth frame if the latter is too large
|
|
// (by Joern Lenselink)
|
|
Dimension displayDimens = java.awt.GraphicsEnvironment.getLocalGraphicsEnvironment().getMaximumWindowBounds().getSize();
|
|
Dimension labyrinthDimens = frame.getSize();
|
|
if(labyrinthDimens.height > displayDimens.height) {
|
|
JScrollPane scroll = new JScrollPane();
|
|
solver.setBackground(Color.LIGHT_GRAY);
|
|
frame.getContentPane().add(scroll);
|
|
JPanel borderlayoutpanel = new JPanel();
|
|
borderlayoutpanel.setBackground(Color.darkGray);
|
|
scroll.setViewportView(borderlayoutpanel);
|
|
borderlayoutpanel.setLayout(new BorderLayout(0, 0));
|
|
|
|
JPanel columnpanel = new JPanel();
|
|
borderlayoutpanel.add(columnpanel, BorderLayout.NORTH);
|
|
columnpanel.setLayout(new GridLayout(0, 1, 0, 1));
|
|
columnpanel.setOpaque(false);
|
|
columnpanel.setBackground(Color.darkGray);
|
|
|
|
columnpanel.setSize(labyrinthDimens.getSize());
|
|
columnpanel.setPreferredSize(labyrinthDimens.getSize());
|
|
columnpanel.add(solver);
|
|
} else {
|
|
// No scroll pane needed:
|
|
frame.getContentPane().add(solver);
|
|
}
|
|
|
|
frame.setVisible(true); // will draw the labyrinth (without solution)
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param args If the first argument is a file name ending in .ser, the serialized labyrinth in that file
|
|
* is used; else the first two arguments are optional numbers giving the width and height of a new
|
|
* labyrinth to be constructed. Then the labyrinth is solved and displayed (unless too large).
|
|
* This is run a certain number of times and then the median run time is printed.
|
|
*/
|
|
public static void main(String[] args) {
|
|
long[] runTimes = new long[2*N_RUNS_HALF + 1];
|
|
|
|
for (int run = 0; run < 2*N_RUNS_HALF + 1; ++run) {
|
|
|
|
Solver solver = makeAndSaveSolver(args);
|
|
if (solver.labyrinth.smallEnoughToDisplay()) {
|
|
displayLabyrinth(solver);
|
|
}
|
|
|
|
long startTime = System.currentTimeMillis();
|
|
solver.solution = solver.solveConcurrently();
|
|
long endTime = System.currentTimeMillis();
|
|
|
|
if (solver.solution == null)
|
|
System.out.println("No solution exists.");
|
|
else {
|
|
System.out.println("Computed sequential solution of length " + solver.solution.length + " to labyrinth of size " +
|
|
solver.labyrinth.getWidth() + "x" + solver.labyrinth.getHeight() + " in " + (endTime - startTime) + "ms.");
|
|
|
|
runTimes[run] = endTime - startTime;
|
|
|
|
if (solver.labyrinth.smallEnoughToDisplay()) {
|
|
solver.displaySolution();
|
|
solver.printSolution();
|
|
}
|
|
|
|
if (solver.labyrinth.checkSolution(solver.solution))
|
|
System.out.println("Solution correct :-)");
|
|
else
|
|
System.out.println("Solution incorrect :-(");
|
|
}
|
|
}
|
|
Arrays.sort(runTimes);
|
|
System.out.println("Median run time was " + runTimes[N_RUNS_HALF] + " ms.");
|
|
}
|
|
} |