3
\$\begingroup\$

This program primary effort is the pathfinding of an 8x8 grid without the use of Java Collection Framework.

The starting cell is at the top left corner and the end is the bottom left corner. All valid moving directions are Up, Down, Left and Right.

A path is only valid when all cells has been visited and only visited once, so a path has 63 moves.

This program accepts user input to act as a constraint and finds all possible paths based on the input.

The inputs are:

  • U for Up
  • D for Down
  • L for Left
  • R for Right
  • "*" for all of the above

And example input would be:

Input: "*****DR******R******R********************R*D************L******" Total paths: 5739 

My approach to the algorithm is a naive one with DFS, backtracking and pruning for early invalid moves. I've also implemented Multithreading for faster runtime.

However, for the case where every direction is permitted in the 63 moves, the runtime is really long.

My hope is for help on the optimization of the algorithm or perhaps a different approach that ensures a fast runtime when pathfinding all possible paths.

import java.util.Scanner; public class Main { private static final int GRID_SIZE = 8; private static final int[][] DIRECTIONS = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}}; // U, D, L, R private static final char[] DIRECTION_CHARS = {'U', 'D', 'L', 'R'}; private static final int TOTAL_CELLS = GRID_SIZE * GRID_SIZE; private static final int THREAD_COUNT = Runtime.getRuntime().availableProcessors(); private static volatile long totalPaths = 0; private static String pathConstraints = ""; private static volatile boolean isRunning = true; private static long startTime = 0; private static Thread[] workers; private static long[] visitedRows = new long[GRID_SIZE]; private static final long[] POSITION_MASKS = new long[GRID_SIZE]; private static final byte[][][] VALID_MOVES = new byte[GRID_SIZE][GRID_SIZE][]; private static final byte[][][] PRIORITIZED_MOVES = new byte[GRID_SIZE][GRID_SIZE][]; static { for (int i = 0; i < GRID_SIZE; i++) { POSITION_MASKS[i] = 1L << i; } initializeValidMoves(); initializePrioritizedMoves(); } private static void initializeValidMoves() { for (int row = 0; row < GRID_SIZE; row++) { for (int col = 0; col < GRID_SIZE; col++) { int validCount = 0; byte[] temp = new byte[4]; for (byte dir = 0; dir < DIRECTIONS.length; dir++) { int newRow = row + DIRECTIONS[dir][0]; int newCol = col + DIRECTIONS[dir][1]; if (isValidCell(newRow, newCol)) { temp[validCount++] = dir; } } VALID_MOVES[row][col] = new byte[validCount]; System.arraycopy(temp, 0, VALID_MOVES[row][col], 0, validCount); } } } private static void initializePrioritizedMoves() { for (int row = 0; row < GRID_SIZE; row++) { for (int col = 0; col < GRID_SIZE; col++) { byte[] validMoves = VALID_MOVES[row][col]; byte[] prioritized = new byte[validMoves.length]; System.arraycopy(validMoves, 0, prioritized, 0, validMoves.length); // Sort moves by priority for (int i = 0; i < prioritized.length - 1; i++) { for (int j = 0; j < prioritized.length - i - 1; j++) { if (getMovePriority(row, col, prioritized[j]) > getMovePriority(row, col, prioritized[j + 1])) { // Swap byte temp = prioritized[j]; prioritized[j] = prioritized[j + 1]; prioritized[j + 1] = temp; } } } PRIORITIZED_MOVES[row][col] = prioritized; } } } private static int getMovePriority(int currentRow, int currentCol, byte direction) { int newRow = currentRow + DIRECTIONS[direction][0]; int newCol = currentCol + DIRECTIONS[direction][1]; // Calculate the Manhattan distance to the end (7,0) int distanceToEnd = Math.abs(newRow - (GRID_SIZE - 1)) + Math.abs(newCol - 0); // Prioritise Down and Left moves int directionBonus = 0; if (DIRECTION_CHARS[direction] == 'D') directionBonus -= 2; if (DIRECTION_CHARS[direction] == 'L') directionBonus -= 1; return distanceToEnd + directionBonus; } public static class PathFinderTask extends Thread { private final int threadId; private final int startRow; private final int startCol; private final int secondRow; // Adding the position of the 2nd move private final int secondCol; private final int totalThreads; private long pathCount = 0; private final long[] localVisited; public PathFinderTask(int id, int totalThreads, int startRow, int startCol, int secondRow, int secondCol) { this.threadId = id; this.totalThreads = totalThreads; this.startRow = startRow; this.startCol = startCol; this.secondRow = secondRow; this.secondCol = secondCol; this.localVisited = new long[GRID_SIZE]; } @Override public void run() { System.out.println("Thread " + threadId + " starting at positions (" + startRow + "," + startCol + ") -> (" + secondRow + "," + secondCol + ")"); long threadStartTime = System.currentTimeMillis(); // Initialize the initial state with 3 cells that has been passed setLocalVisited(0, 0, true); setLocalVisited(startRow, startCol, true); setLocalVisited(secondRow, secondCol, true); findPathsParallel(3, secondRow, secondCol); // Starting from the 3rd move synchronized(PathFinderTask.class) { totalPaths += pathCount; } long threadEndTime = System.currentTimeMillis(); System.out.println("Thread " + threadId + " completed in " + (threadEndTime - threadStartTime) / 1000.0 + "s. Found " + pathCount + " paths"); } private void setLocalVisited(int row, int col, boolean value) { if (value) { localVisited[row] |= POSITION_MASKS[col]; } else { localVisited[row] &= ~POSITION_MASKS[col]; } } private boolean isLocalCellVisited(int row, int col) { return (localVisited[row] & POSITION_MASKS[col]) != 0; } private void findPathsParallel(int steps, int row, int col) { if (!isRunning) return; if (steps == TOTAL_CELLS) { if (row == GRID_SIZE - 1 && col == 0) { pathCount++; if (pathCount % 1000 == 0) { System.out.println("Thread " + threadId + " found " + pathCount + " paths"); checkAndPrintTime(); } } return; } if (!isPathViableParallel(steps, row, col)) { return; } for (byte dirIndex : PRIORITIZED_MOVES[row][col]) { int newRow = row + DIRECTIONS[dirIndex][0]; int newCol = col + DIRECTIONS[dirIndex][1]; if (!isLocalCellVisited(newRow, newCol)) { char dirChar = DIRECTION_CHARS[dirIndex]; char allowedMove = getConstraint(steps - 1); if (allowedMove == '*' || allowedMove == dirChar) { setLocalVisited(newRow, newCol, true); findPathsParallel(steps + 1, newRow, newCol); setLocalVisited(newRow, newCol, false); } } } } private boolean isPathViableParallel(int steps, int row, int col) { if (row == GRID_SIZE - 1 && col == 0 && steps < TOTAL_CELLS) { return false; } int remainingMoves = TOTAL_CELLS - steps; int distanceToEnd = Math.abs(row - (GRID_SIZE - 1)) + Math.abs(col); if (distanceToEnd > remainingMoves) { return false; } return !hasIsolatedCellsParallel(); } private boolean hasIsolatedCellsParallel() { int unvisitedCount = countUnvisitedCellsParallel(); if (unvisitedCount == 0) return false; long[] tempVisited = new long[GRID_SIZE]; int connectedCells = optimizedFloodFillParallel(tempVisited, findUnvisitedRowParallel(), findUnvisitedColParallel(), unvisitedCount); return connectedCells != unvisitedCount; } private int countUnvisitedCellsParallel() { int count = 0; for (int row = 0; row < GRID_SIZE; row++) { for (int col = 0; col < GRID_SIZE; col++) { if (!isLocalCellVisited(row, col)) { count++; } } } return count; } private int findUnvisitedRowParallel() { for (int row = 0; row < GRID_SIZE; row++) { for (int col = 0; col < GRID_SIZE; col++) { if (!isLocalCellVisited(row, col)) { return row; } } } return -1; } private int findUnvisitedColParallel() { for (int row = 0; row < GRID_SIZE; row++) { for (int col = 0; col < GRID_SIZE; col++) { if (!isLocalCellVisited(row, col)) { return col; } } } return -1; } private int optimizedFloodFillParallel(long[] tempVisited, int row, int col, int unvisitedCount) { if (row < 0 || row >= GRID_SIZE || col < 0 || col >= GRID_SIZE || isLocalCellVisited(row, col) || (tempVisited[row] & POSITION_MASKS[col]) != 0) { return 0; } tempVisited[row] |= POSITION_MASKS[col]; int count = 1; for (int[] dir : DIRECTIONS) { count += optimizedFloodFillParallel(tempVisited, row + dir[0], col + dir[1], unvisitedCount); if (count == unvisitedCount) break; } return count; } } private static boolean isValidCell(int row, int col) { return row >= 0 && row < GRID_SIZE && col >= 0 && col < GRID_SIZE; } private static char getConstraint(int index) { return index < pathConstraints.length() ? pathConstraints.charAt(index) : '*'; } private static void checkAndPrintTime() { long currentTime = System.currentTimeMillis(); long elapsedSeconds = (currentTime - startTime) / 1000; // Print progress every 10 seconds if (elapsedSeconds > 0 && elapsedSeconds % 10 == 0) { synchronized(PathFinderTask.class) { System.out.println("Time elapsed: " + elapsedSeconds + "s, Paths found: " + totalPaths); } } // Limit the maximum runtime to 5 minutes if (currentTime - startTime > 300000) { // 5 minutes System.out.println("Search time exceeded 5 minutes. Stopping..."); isRunning = false; } } private static int calculateTotalMoves() { int count = 0; for (byte dir1 = 0; dir1 < DIRECTIONS.length; dir1++) { int firstRow = DIRECTIONS[dir1][0]; int firstCol = DIRECTIONS[dir1][1]; if (isValidCell(firstRow, firstCol)) { for (byte dir2 = 0; dir2 < DIRECTIONS.length; dir2++) { int secondRow = firstRow + DIRECTIONS[dir2][0]; int secondCol = firstCol + DIRECTIONS[dir2][1]; if (isValidCell(secondRow, secondCol) && (secondRow != 0 || secondCol != 0)) { count++; } } } } return count; } private static void populateMoves(int[][] moves) { int moveIndex = 0; for (byte dir1 = 0; dir1 < DIRECTIONS.length; dir1++) { int firstRow = DIRECTIONS[dir1][0]; int firstCol = DIRECTIONS[dir1][1]; if (isValidCell(firstRow, firstCol)) { for (byte dir2 = 0; dir2 < DIRECTIONS.length; dir2++) { int secondRow = firstRow + DIRECTIONS[dir2][0]; int secondCol = firstCol + DIRECTIONS[dir2][1]; if (isValidCell(secondRow, secondCol) && (secondRow != 0 || secondCol != 0)) { moves[moveIndex][0] = firstRow; moves[moveIndex][1] = firstCol; moves[moveIndex][2] = secondRow; moves[moveIndex][3] = secondCol; moveIndex++; } } } } } private static void userInterface() { Scanner scanner = new Scanner(System.in); System.out.println("Enter path constraints (63 characters of U/D/L/R/* or press Enter for no constraints):"); pathConstraints = scanner.nextLine().trim(); final int maxThreads = Runtime.getRuntime().availableProcessors() * 2; final int totalMoves = calculateTotalMoves(); final int[][] moves = new int[totalMoves][4]; populateMoves(moves); final int numThreads = Math.min(totalMoves, maxThreads); workers = new Thread[numThreads]; startTime = System.currentTimeMillis(); isRunning = true; totalPaths = 0; for (int i = 0; i < numThreads; i++) { final int threadIndex = i; workers[i] = new Thread(() -> { for (int j = threadIndex; j < totalMoves; j += numThreads) { PathFinderTask task = new PathFinderTask( threadIndex, numThreads, moves[j][0], moves[j][1], moves[j][2], moves[j][3] ); task.start(); try { task.join(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return; } } }); workers[i].start(); } try { for (Thread worker : workers) { worker.join(); } } catch (InterruptedException e) { System.out.println("Search interrupted!"); return; } long endTime = System.currentTimeMillis(); System.out.println("Total paths found: " + totalPaths); System.out.println("Time taken: " + (endTime - startTime) / 1000.0 + " seconds"); } public static void main(String[] args) { userInterface(); } } 
\$\endgroup\$
0

1 Answer 1

5
\$\begingroup\$

More aggressive pruning

If you ever encounter the situation where the only possible moves are in opposite directions (L and R, or U and D), you’ve created a “hole” which cannot be filled in or cannot be gotten out of:

> > > > > > > v . . . * . . . v . . . ^ . . . v . . . ^ . . . v . . . ^ < < < < . . . . . . . . 

In the above situation, at the *, if the path exploration turns “right”, it can fill in every spot in the hole, but cannot escape to fill the entire grid. On the other hand, if it turns “left”, it can touch the rest of the grid, but can never work its way back to fill in the hole.

This also applies when encountering edges of the grid:

> > v . . * . . . . v . . ^ . . . . > > > ^ . . . . . . . . . . 

With only a left/right choice available, the grid has been divided into two areas — only one of which can be filled in, and since every cell must be visited, this means further exploration from this point forward is pointless.

\$\endgroup\$

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.