A* Path Finder (Java) is inefficient using a 1024 multidimentional array - java
I have the below code for a A* pathfinder, however it is taking upwards of 10 minutes to find a solution using a simple 1024 x 1024 array.
I had to comment out //Collections.sort(this.openList); as it was throwing a comparison method violates its general contract! error when running.
Is the algorithm correct and any idea why the bottleneck? Some people using C++ are getting a response time of 40ms, not 10+ mins!
When using the maze array it does it in the blink of an eye, but thats using something like a 14x10 array, rather than 1024 from the collisionMap.
Is the method flawed in some way? Or using the wrong data structures?
import java.util.List;
import javax.imageio.ImageIO;
import java.awt.Canvas;
import java.awt.Color;
import java.awt.GraphicsConfiguration;
import java.awt.Paint;
import java.awt.image.BufferedImage;
import java.io.Console;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
class AStarTwo {
// Closed list, open list and calculatedPath lists
private final List<Node> openList;
private final List<Node> closedList;
private final List<Node> calcPath;
// Collision Map to store tha map in
private final int[][] collisionMap;
// Current node the program is executing
private Node currentNode;
// Define the start and end coords
private final int xstart;
private final int ystart;
private int xEnd, yEnd;
// Node class
static class Node implements Comparable {
public Node parent;
public int x, y;
public double g;
public double h;
Node(Node parent, int xpos, int ypos, double g, double h) {
this.parent = parent;
this.x = xpos;
this.y = ypos;
this.g = g;
this.h = h;
}
// Compare f value (g + h)
#Override
public int compareTo(Object o) {
Node that = (Node) o;
return (int)((this.g + this.h) - (that.g + that.h));
}
}
// construct and initialise
public AStarTwo(int[][] collisionMap, int xstart, int ystart) {
this.openList = new ArrayList<>();
this.closedList = new ArrayList<>();
this.calcPath = new ArrayList<>();
this.collisionMap = collisionMap;
this.currentNode = new Node(null, xstart, ystart, 0, 0);
this.xstart = xstart;
this.ystart = ystart;
}
// returns a List<> of nodes to target
public List<Node> findPathTo(int xTo, int yTo) {
this.xEnd = xTo;
this.yEnd = yTo;
// Add this to the closed list
this.closedList.add(this.currentNode);
// Add neighbours to openList for iteration
addNeigborsToOpenList();
// Whilst not at our target
while (this.currentNode.x != this.xEnd || this.currentNode.y != this.yEnd) {
// If nothing in the open list then return with null - handled in error message in main calling func
if (this.openList.isEmpty()) {
return null;
}
// get the lowest f value and add it to the closed list, f calculated when neighbours are sorted
this.currentNode = this.openList.get(0);
this.openList.remove(0);
this.closedList.add(this.currentNode);
addNeigborsToOpenList();
}
// add this node to the calculated path
this.calcPath.add(0, this.currentNode);
while (this.currentNode.x != this.xstart || this.currentNode.y != this.ystart) {
this.currentNode = this.currentNode.parent;
this.calcPath.add(0, this.currentNode);
}
return this.calcPath;
}
// Searches the current list for neighbouring nodes returns bool
private static boolean checkNeighbourHasBeenSearched(List<Node> array, Node node) {
return array.stream().anyMatch((n) -> (n.x == node.x && n.y == node.y));
}
// Calculate distance from current node to the target
private double distance(int dx, int dy) {
return Math.hypot(this.currentNode.x + dx - this.xEnd, this.currentNode.y + dy - this.yEnd); // return hypothenuse
}
// Add neighbouring nodes to the open list to iterate through next
#SuppressWarnings("unchecked")
private void addNeigborsToOpenList() {
Node node;
for (int x = -1; x <= 1; x++) {
for (int y = -1; y <= 1; y++) {
node = new Node(this.currentNode, this.currentNode.x + x, this.currentNode.y + y, this.currentNode.g, this.distance(x, y));
// if we are not on the current node
if ((x != 0 || y != 0)
&& this.currentNode.x + x >= 0 && this.currentNode.x + x < this.collisionMap[0].length // check collision map boundaries
&& this.currentNode.y + y >= 0 && this.currentNode.y + y < this.collisionMap.length
&& this.collisionMap[this.currentNode.y + y][this.currentNode.x + x] != -1) { // check if tile is walkable (-1)
// and finally check we haven't already searched the nodes
if(!checkNeighbourHasBeenSearched(this.openList, node) && !checkNeighbourHasBeenSearched(this.closedList, node)){
node.g = node.parent.g + 1.; // Horizontal/vertical cost = 1.0
node.g += collisionMap[this.currentNode.y + y][this.currentNode.x + x]; // add movement cost for this square
// Add diagonal movement cost sqrt(hor_cost² + vert_cost²) + 0.4
if (x != 0 && y != 0) {
node.g += .4;
}
// Add the node to the List<>
this.openList.add(node);
}
}
}
}
// sort in ascending order
//Collections.sort(this.openList);
}
public static void main(String[] args) {
int [][] maze =
{ {1,1,1,1,1,1,1,1,1,1,1,1,1},
{1,0,-1,0,-1,0,1,0,0,0,0,0,1},
{1,0,-1,0,0,0,1,0,1,1,1,0,1},
{1,0,0,0,-1,-1,-1,0,0,0,0,0,1},
{1,0,1,0,0,0,0,0,1,1,1,0,1},
{1,0,1,0,-1,-1,-1,0,1,0,0,0,-1},
{1,0,-1,0,-1,0,0,0,-1,-1,-1,0,-1},
{1,0,1,0,-1,-1,-1,0,1,0,-1,0,-1},
{1,0,0,0,0,0,0,0,0,0,1,0,1},
{1,1,1,1,1,1,1,1,1,1,1,1,1}
};
// Define the size of the grid
final int sizeOf = 20;
int[][] collisionMap = new int[sizeOf][];
for(int i=0;i < sizeOf; i++) {
// -1 = blocked
// 0+ = cost
collisionMap[i] = new int[sizeOf];
}
// set the value of the nodes
for (int k = 0; k < sizeOf; k++) {
for (int j = 0; j < sizeOf; j++) {
if(j == 30 && k < 100) {
collisionMap[k][j] = -1;
} else if (j == 50 && k > 230) {
collisionMap[k][j] = -1;
}else {
collisionMap[k][j] = 0;
}
}
}
AStarTwo as = new AStarTwo(maze, 9, 9);
List<Node> path = as.findPathTo(0,0);
if(path == null) {
System.out.println("Unable to reach target");
}
// create image buffer to write output to
BufferedImage img = new BufferedImage(sizeOf, sizeOf, BufferedImage.TYPE_INT_RGB);
// Set colours
int r = 255;
int g = 0;
int b = 0;
int colRed = (r << 16) | (g << 8) | b;
r = 0;
g = 255;
b = 0;
int colGreen = (r << 16) | (g << 8) | b;
r = 0;
g = 0;
b = 255;
int colBlue = (r << 16) | (g << 8) | b;
r = 255;
g = 255;
b = 255;
int colWhite = (r << 16) | (g << 8) | b;
int i = 0;
int j = 0;
if (path != null) {
path.forEach((n) -> {
System.out.print("[" + n.x + ", " + n.y + "] ");
maze[n.y][n.x] = 2;
});
for (int[] maze_row : maze) {
for (int maze_entry : maze_row) {
switch (maze_entry) {
// normal tile
case 0:
img.setRGB(j, i, colWhite);
break;
// final path
case 2:
img.setRGB(j, i, colBlue);
break;
// Object to avoid
case -1:
img.setRGB(j, i, colRed);
break;
// Any other value
default:
img.setRGB(j, i, colWhite);
}
j++;
}
// count j - reset as if it were a for loop
if(i != 12) {
j=0;
}
i++;
System.out.println();
}
}
// output file
File f = new File("aStarPath.png");
try {
ImageIO.write(img, "PNG", f);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("i: " + i + ", j: " + j);
}
}
I suspect your problem is this line:
return array.stream().anyMatch((n) -> (n.x == node.x && n.y == node.y));
which is called around O(n^2) times and will take time proportional to the size of the array (which will also be O(n^2) in the worst case for a n by n maze).
You want a faster method of performing this test.
For example:
Use a set to hold the open and closed lists instead of list
Or use an extra field in the node structure to indicate if it is in the open or closed list
Related
Creating objects with a loop and still can access them outside the loop?
I am a beginner in Java as well as OOP. I am creating a simulator of "a box with particles". Here's what required in the program: A box with fixed WIDTH & HEIGHT with the pattern of "-" and " | " Particles with x, y position (0 <= x <= WIDTH; 0 <= y <= HEIGHT). Symbol: * Enum Direction of 8 directions move() all particles in random direction and create a new particle if any of them collide (same position) What I've been strugging to find the answer is that how can I create random number of particles with a loop and can still work on them outside the loop? Because I want each created particle remain after an iteration for further move() execution. Is there any syntax for this kind of access? Here's what I've tried as this sometimes output 2, 3 particles, sometimes none: public Box() { particle = new Particle(); for (int i = 0; i <= HEIGHT + 1; i++) { for (int j = 0; j <= WIDTH + 1; j++) { if (i == 0 || i == HEIGHT + 1 && i != particle.getY()){ System.out.print("-"); } else { if (j == particle.getX() && i == particle.getY()) { System.out.print("*"); } else if (j == 0 || j == WIDTH + 1 && j != particle.getX()) { System.out.print("|"); } else if (i != 0 && i != HEIGHT + 1) { System.out.print(" "); } } } System.out.println(""); } }
Coding is easier if you break the large task into smaller tasks. I separated the creation of the particles from the display of the particles. That way, I could focus on one part of the task at a time. Here's the output I generated. |------------------| | * | | | | * * * * | | * * | | * * | | * * * * | | * | | * * | |------------------| All I did was generate the initial condition. I'm leaving it up to you to move the particles and check for collisions. I'm assuming that once a particular particle moves south, as an example, it continues south until it hits another particle or the wall. Hitting the wall would change the direction to north. My Eclipse generated the hashCode and equals methods of the Particle class. These methods are essential for the Set contains method to work. I used a Set so there would be no duplicate particles. Because the particles are generated randomly, there might not be all maximum particles in the simulation. In the generateParticles method, I get a random w between 1 and WIDTH - 1, inclusive, and a random h between 1 and HEIGHT -1, inclusive. This ensures that all particles created are inside the "box". Here's the complete runnable example code. import java.util.HashSet; import java.util.Random; import java.util.Set; public class ParticleSimulator { public static void main(String[] args) { ParticleSimulator ps = new ParticleSimulator(); ps.displaySimulation(); } private static int WIDTH = 20; private static int HEIGHT = 10; private Random random; private Set<Particle> particles; public ParticleSimulator() { this.random = new Random(); this.particles = generateParticles(20); } private Set<Particle> generateParticles(int maximum) { Set<Particle> particles = new HashSet<>(); for (int i = 0; i < maximum; i++) { int x = random.nextInt(WIDTH - 1) + 1; int y = random.nextInt(HEIGHT - 1) + 1; Particle particle = createParticle(x, y); particles.add(particle); } return particles; } public void displaySimulation() { for (int h = 0; h < HEIGHT; h++) { for (int w = 0; w < WIDTH; w++) { if (w == 0 || w == (WIDTH - 1)) { System.out.print('|'); continue; } if (h == 0 || h == (HEIGHT - 1)) { System.out.print('-'); continue; } Particle particle = createParticle(w, h); if (particles.contains(particle)) { System.out.print('*'); } else { System.out.print(' '); } } System.out.println(); } } private Particle createParticle(int x, int y) { Particle particle = new Particle(); particle.setX(x); particle.setY(y); return particle; } public class Particle { private int x; private int y; public int getX() { return x; } public void setX(int x) { this.x = x; } public int getY() { return y; } public void setY(int y) { this.y = y; } #Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + getEnclosingInstance().hashCode(); result = prime * result + x; result = prime * result + y; return result; } #Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; Particle other = (Particle) obj; if (!getEnclosingInstance().equals( other.getEnclosingInstance())) return false; if (x != other.x) return false; if (y != other.y) return false; return true; } private ParticleSimulator getEnclosingInstance() { return ParticleSimulator.this; } } }
User drawn line disappear but the functionality behind it does not
I am creating a puzzle game hashi, basically the purpose of the game is to solve a puzzle using connection (Bridges) and Nodes(Islands). To solve the puzzle, the user has to connect the same amount of bridges to a corresponding nodes weight. The user is allowed to join the islands using 2 bridges from one island, but after s/he tries to connect the 3 bridges from the same node the connection should disappear and the user should be allowed to create a new one from that node. Problem: The user can draw 2 bridges from the same node but once he tries to do a third one the connection disappear JUST VISUALLY meaning the nodes are still connected and then the user cannot draw a bridge again. And in here the bridges are just deleted visually. I tried to debug it, but unfortunately I cannot seem to figure out where the problem is. Here is the code to my problem. public class BoardCreation { // This class member is used for random initialization purposes. static private final Random random = new Random(); // The difficulty levels. private static final int EASY = 0; static public final int MEDIUM = 1; static public final int HARD = 2; static public final int EMPTY = 0; private static int ConnectionFingerprint(BoardElement start, BoardElement end) { int x = start.row * 100 + start.col; int y = end.row * 100 + end.col; // Swap to get always the same fingerprint independent whether we are called // start-end or end-start if (x > y ) { int temp = x; x = y; y = temp; } Log.d("", String.format("%d %d" , x ,y)); return x ^ y; } public class State { // The elements of the board are stored in this array. // A value defined by "EMPTY" means that its not set yet. public BoardElement [][] board_elements = null; public int [][] cell_occupied = null; // The width of the board. We only assume squared boards. public int board_width=0; public State(int width) { board_width = width; board_elements = new BoardElement[width][width]; cell_occupied = new int[width][width]; } public State CloneWithoutConnections() { State newstate = new State(board_width); if (board_elements != null) { newstate.board_elements = new BoardElement[board_elements.length][board_elements.length]; for (int i = 0; i < board_elements.length; ++i) { for (int j = 0; j < board_elements.length; ++j) { if (board_elements[i][j] == null) continue; newstate.board_elements[i][j] = board_elements[i][j].clone(); } } } if (cell_occupied != null) { assert board_elements != null; newstate.cell_occupied = new int[board_elements.length][board_elements.length]; for (int i = 0; i < board_elements.length; ++i) { System.arraycopy(cell_occupied[i], 0, newstate.cell_occupied[i], 0, board_elements.length); } } return newstate; } public void AddToBridgeCache(BoardElement first, BoardElement second) { if (first == null || second == null) { return; } final int fingerprint = ConnectionFingerprint(first, second); Log.d(getClass().getName(), String.format("Fingerprint of this bridge %d", fingerprint)); // mark the end points as occupied. cell_occupied[first.row][first.col] = fingerprint; cell_occupied[second.row][second.col] = fingerprint; int dcol = second.col - first.col; int drow = second.row - first.row; if (first.row == second.row) { for (int i = (int) (first.col + Math.signum(dcol)); i != second.col; i += Math.signum(dcol)) { cell_occupied[first.row][i] = fingerprint; String.format("deleting bridge"); } } else { assert first.col == second.col; for (int i = (int) (first.row + Math.signum(drow)); i != second.row; i+= Math.signum(drow)) { cell_occupied[i][first.col] = fingerprint; String.format("deleting bridge", fingerprint); } } } } // end of state }
A* path finding algorithm. Movement Cost and Heuristic is Inaccurate
My problem is that the movement cost(G cost) of my node and heuristic is inaccurate it does not match with the picture. Here is the image of what I'm following.There are three labels here and the movement cost is labelled at the bottom left and the heuristic is at bottom right. Label at top-left is the F = H + G Here is my output. As you can see the movement cost is not the same as the desired output. The red circle is the goal node. Also the same with my Heuristic cost. public class AStarPathFinder implements PathFinder { private List<Node> open = new ArrayList<Node>(); private List<Node> close = new ArrayList<Node>(); private Node[][] nodes; private TileMap map; private Heuristic heuristic; public AStarPathFinder(TiledMapStage mapStage, Heuristic heuristic) { this.heuristic = heuristic; nodes = mapStage.getNodes(); map = mapStage.getMap(); } #Override public Path findPath(int startX, int startY, int goalX, int goalY) { clearNodes(); Node goal = nodes[goalX][goalY]; Node current = nodes[startX][startY]; open.add(current); while (!open.isEmpty()) { current = getLowestFcost(open); open.remove(current); close.add(current); if (current == goal) { Path path = new Path(); while (current != null) { path.add(current); current = current.parent; } return path; } // neighbors of current for (int x = -1; x < 2; x++) { for (int y = -1; y < 2; y++) { int dx = current.x + x; int dy = current.y + y; if (map.isValidLocation(dx, dy)) { if (!map.isWalkable(nodes[dx][dy], x, y) || close.contains(nodes[dx][dy])) continue; float newScore = movementCost(current.g, isDiagonal(x, y)); if (!open.contains(nodes[dx][dy])) { open.add(nodes[dx][dy]); } else if (newScore >= nodes[dx][dy].g) continue; nodes[dx][dy].g = newScore; nodes[dx][dy].h = heuristic.estimate(nodes[dx][dy], goal); nodes[dx][dy].f = nodes[dx][dy].g + nodes[dx][dy].h; nodes[dx][dy].parent = current; nodes[dx][dy].label.setText((int) nodes[dx][dy].g + ""); } } } } return null; } private Node getLowestFcost(List<Node> open) { Node lowestNode = open.get(0); for (int i = 0; i < open.size(); i++) { if (open.get(i).f <= lowestNode.f && open.get(i).h < lowestNode.h) { lowestNode = open.get(i); } } return lowestNode; } private boolean isDiagonal(int x, int y) { return (x == -1 && y == 1 || x == 1 && y == 1 || x == 1 && y == -1 || x == -1 && y == -1); } private float movementCost(float cost, boolean diagonal) { return diagonal ? cost + 14 : cost + 10; } #Override public void clearNodes() { for (int i = 0; i < map.getTileWidth(); i++) { for (int j = 0; j < map.getTileHeight(); j++) { if (nodes[i][j].cell != null) { nodes[i][j].label.setText(""); nodes[i][j].f = 0; nodes[i][j].h = 0; nodes[i][j].g = 0; nodes[i][j].arrow.setDrawable("cursor"); nodes[i][j].arrow.setVisible(false); nodes[i][j].parent = null; } } } close.clear(); open.clear(); } } Here is the pseudocode that I'm following. Also my heuristic is a diagonal distance
It looks like your problem is in the isWalkable method of your TileMap map variable. The image you're following doesn't allow to pass diagonally alongside a wall, where your algorithm does. You can see this because the score gets added with 14 as follows: 14 + 14 = 28. While you expected it to go as follows: 14 + 10 (going down first) + 10 (going right) = 34. I hope I explained your problem clearly. I don't know your implementation of isWalkable, so I can't provide a full solution but I hope I have pointed you in the right direction.
Changing value in one list changes it in a seperate one
I'm trying to change a value in array normMap to another, but doing so also changes a value in array fireMap. public int boardX = Board.boardX; public int boardY = Board.boardY; public int tileSizeX = Board.tileSizeX; public int tileSizeY = Board.tileSizeY; public int curr_dimension = Board.current_dimension; public BufferedImage[] grassImage = {Board.norm.grassImage, Board.fire.grassImage}; public BufferedImage[] waterImage = {Board.norm.waterImage, Board.fire.waterImage}; public BufferedImage[] pathVImage = {Board.norm.pathVImage, Board.fire.pathVImage}; public BufferedImage[] pathHImage = {Board.norm.pathHImage, Board.fire.pathHImage}; public BufferedImage[] treeImage = {Board.norm.treeImage, Board.fire.treeImage}; // y/x/tile type public List<List<Integer>> normMap = new ArrayList<List<Integer>>(); public List<List<Integer>> fireMap = new ArrayList<List<Integer>>(); #Override public void mouseClicked(MouseEvent arg0) { int y = (int) Math.ceil((arg0.getY() + boardY * tileSizeY) / 32); int x = (int) Math.ceil((arg0.getX() + boardX * tileSizeX) / 32); switch(curr_dimension) { case(0): normMap.get(y).set(x, 0); break; case(1): fireMap.get(y).set(x, 0); break; } } #Override public void keyPressed(KeyEvent arg0) { switch(arg0.getKeyCode()) { case(KeyEvent.VK_ENTER): writeFile(); break; case(KeyEvent.VK_ESCAPE): GameStateManager.setState(GameStateManager.MENU_STATE); break; case(KeyEvent.VK_UP): boardY--; break; case(KeyEvent.VK_DOWN): boardY++; break; case(KeyEvent.VK_LEFT): boardX--; break; case(KeyEvent.VK_RIGHT): boardX++; break; case(KeyEvent.VK_1): curr_dimension = 0; break; case(KeyEvent.VK_2): curr_dimension = 1; break; } } public String getPrefix(int dimension) { switch(dimension) { case(0): return "norm"; case(1): return "fire"; default: System.out.println("Incorrect dimension, " + dimension + ". Find the map in maps/custom/unidentified."); return "unidentified"; } } public void writeFile() { List<List<Integer>> map; switch(curr_dimension) { case(0): map = normMap; break; case(1): map = fireMap; break; default: map = normMap; break; } PrintWriter writer; try { for(int i = 0; i < 2; i++) { writer = new PrintWriter("/maps/custom/" + getPrefix(i) + "map1.txt", "UTF-8"); for(List<Integer> list : map) { for(int integer : list) { writer.print(integer + " "); } writer.println(""); } writer.close(); } } catch (Exception e) { e.printStackTrace(); } } #Override public void init() { Main.panel.addKeyListener(this); Main.panel.addMouseListener(this); for(int i = 0; i < 50; i++) { List<Integer> map1 = new ArrayList<Integer>(); for(int j = 0; j < 50; j++) { map1.add(9); } normMap.add(map1); fireMap.add(map1); } } public void draw(Graphics g) { switch(curr_dimension) { case(0): drawTiles(g, normMap); break; case(1): drawTiles(g, fireMap); break; } } public void drawTiles(Graphics g, List<List<Integer>> map) { for(int y = 0; y < map.size(); y++) { List<Integer> mapY = map.get(y); for(int x = 0; x < mapY.size(); x++) { if(mapY.get(x) == 0) g.drawImage(grassImage[curr_dimension], (x - boardX) * tileSizeX, (y - boardY) * tileSizeY, null); else if(mapY.get(x) == 1) g.drawImage(waterImage[curr_dimension], (x - boardX) * tileSizeX, (y - boardY) * tileSizeY, null); else if(mapY.get(x) == 2) g.drawImage(pathVImage[curr_dimension], (x - boardX) * tileSizeX, (y - boardY) * tileSizeY, null); else if(mapY.get(x) == 4) g.drawImage(pathHImage[curr_dimension], (x - boardX) * tileSizeX, (y - boardY) * tileSizeY, null); else if(mapY.get(x) == 3) g.drawImage(treeImage[curr_dimension], (x - boardX) * tileSizeX, (y - boardY) * tileSizeY - tileSizeY, null); else if(mapY.get(x) == 9) g.drawRect((x - boardX) * tileSizeX, (y - boardY) * tileSizeY, tileSizeX, tileSizeY); else if(mapY.get(x) == Board.PLAYER) GameState.player.draw(g, x, y, boardX, boardY, tileSizeX, tileSizeY); } } }} Expected output is two different lists. However, printing out fireMap and normMap reveals the same result. Unimportant pieces of code removed. Please tell me if I need to clarify on anything.
You add the same object to NormMap and FireMap right here: normMap.add(map1); fireMap.add(map1); even though the two maps are different the object inside the maps is the same. Because you have the same Objects in both maps normMap.get(y) and fireMap.get(y) are the same as well and if you do normMap.get(y).set(x, 0); you change the fireMap Object as well. So your problem is, that you have two different Maps but all the Objects inside the maps are the same. In my Opinion the easiest way to solve your problem would be to add two different Objects to your Maps: for(int i = 0; i < 50; i++) { List<Integer> map1 = new ArrayList<Integer>(); List<Integer> map2 = new ArrayList<Integer>(); for(int j = 0; j < 50; j++) { map1.add(9); map2.add(9); } normMap.add(map1); fireMap.add(map2); } Edit: If your Lists have fit sizes (50 x 50) and you never change that Size, I recommend you to use a two dimensional Array instead int fireMap[][] = new int[50][50]; (Also: In games the tile maps are normally 2-D arrays)
How to find the maximum number of points lying on the same straight line
Suppose you have an array of Points on a 2D plane. Point being defined as such: class Point { public int x; public int y; Point(int _x, int _y) { x = _x; y = _y; } } How could I find the maximum number of points lying on the same straight line in java?
For each point in the array, calculate the angle between this point and other points. Counting the number of those with the same angle using a hashMap. Expected time O(n^2) Pseudo code int result = 0; for(int i = 0; i < data.length; i++){ HashMap<Double, Integer> map = new HashMap(); for(int j = 0; j < data.length; j++){ if(i == j) continue; double angle = calculateAngle(data[i], data[j]); if(map.containsKey(slope)){ map.put(angle, map.get(slope) + 1); }else{ map.put(angle, 1); } result = max(result, map.get(slope)); } } Note: As mention in NiklasB 's comment, using double will cause some problems with precision, especially when we need to compare those floating values. We can avoid that by using the Rational class suggested by NiklasB. (Or less precise, using this)
Here is a solution in Java using precise arithmetic: import java.util.List; import java.util.Map; import java.util.HashMap; public class Solver { public int maxPointsOnLine(List<Point> points) { int ans = 0; Map<Line, Integer> lines = new HashMap<Line, Integer>(); for (Point a : points) { int max = 0; int same = 0; lines.clear(); for (Point b : points) { if (a.x == b.x && a.y == b.y) { ++same; } else { Line line = new Line(b.x - a.x, b.y - a.y); Integer count = lines.get(line); if (count == null) { count = 0; } ++count; lines.put(line, count); max = Math.max(max, count); } } ans = Math.max(ans, same + max); } return ans; } static class Line { final int dx; final int dy; Line(int dx, int dy) { if (dy == 0) { dx = Math.abs(dx); } else if (dy < 0) { dx = -dx; dy = -dy; } int gcd = gcd(Math.abs(dx), dy); dx /= gcd; dy /= gcd; this.dx = dx; this.dy = dy; } #Override public boolean equals(Object other) { if (!(other instanceof Line)) { return false; } Line another = (Line)other; return dx == another.dy && dy == another.dy; } #Override public int hashCode() { return 31 * dx + dy; } } static int gcd(int a, int b) { return b == 0 ? a : gcd(b, a % b); } }
/** * Definition for a point. * struct Point { * int x; * int y; * Point() : x(0), y(0) {} * Point(int a, int b) : x(a), y(b) {} * }; */ class Solution { public: int maxPoints(vector<Point> &points) { int n = points.size(); //number of the points if (n<=2){return n;} vector<double> k; //vector to store the slops for one point with all the other points int res = 0; for (int i=0;i<n;i++){ // for each point in the 2d plane k.clear(); int dup = 1; // number of the duplicates with currrent point for (int j=0;j<n;j++){ if (i!=j){ // for every other point if (points[i].x-points[j].x==0){ // if the slope is a vertical line if (points[i].y-points[j].y==0){ // if the point j is the same as point i dup++; }else{ k.push_back(99999); //store the vertical line to a specific slope } }else{ // if it is the regular slop between point i and j k.push_back(10000*(points[i].y-points[j].y)/(points[i].x-points[j].x)); // store the slope } } } sort(k.begin(),k.end()); //sort the slopes for counting int pp = 1; //number of points in the same line of the point i if (k.size()==0){pp=0;} for (int jj=1;jj<k.size();jj++){ //count pp if (k[jj]==k[jj-1]){ pp++; }else{ if (pp+dup>res){res=pp+dup;} // res = pp + the number of duplicates pp = 1; } } if (pp+dup>res){res = pp+dup;} } return res; } };