Given a 2d matrix, I have a start cell, end cell, cells that must be visited and cells that cannot be visited.
What would be the optimal way to find a path that:
- starts at the start cell
- ends at the end cell
- passes through all must visit cell
- does not pass through any cell that cannot be visited
- does not pass any cell twice
- we can only move left, right, up and down
The matrix size can be at most 10x10.
For example:
S - start
E - end
0 - can visit
X - can not visit
M - must visit
S 0 0 M 0
0 0 0 X 0
0 M 0 0 0
0 X 0 0 0
0 0 0 0 E
One solution would be:
* 0 * * *
* 0 * X *
* * * 0 *
0 X 0 0 *
0 0 0 0 *
The path doesn't necessarily should be the shortest one, but it would be nice if it can be, and it can be calculated quiet fast.
CodePudding user response:
Essentially, you need to calculate the minimum spanning tree ( MST, google it ).
Start with the cells that must be visited. These will be the nodes in the graph you apply the MST algorithm to. Find the lengths of the shortest paths between each pair of nodes ( in such a small problem you can easily do that manually ) These will be the links, and link costs you input to the MST.
Input the constructed graph to the MST method in your favorite graph-theory library.
For a more complicated version of this problem, which is solved using the same technique, see How to find a shortest path in a graph that while travelling it, you can "see" all the nodes within a radius
CodePudding user response:
You can model the grid with a class Grid
that would have a width
and a height
, and a set of must
locations a set of mustNot
locations:
public class Grid {
private final int width;
private final int height;
private final Set<Location> must = new HashSet<>();
private final Set<Location> mustNot = new HashSet<>();
public Grid(int width, int height,
Collection<Location> must, Collection<Location> mustNot) {
this.width = width;
this.height = height;
this.must.addAll(must);
this.mustNot.addAll(mustNot);
}
In that class, you would have a method solve
that would take a start
location and an end
location, and would return the best path:
public PathNode solve(Location start, Location end) {
...
}
class Location
looks like this:
public class Location {
public final int row;
public final int col;
public Location(int row, int col) {
this.row = row;
this.col = col;
}
@Override
public int hashCode() {
int hash = 7;
hash = 41 * hash this.row;
hash = 41 * hash this.col;
return hash;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final Location other = (Location) obj;
if (this.row != other.row) {
return false;
}
return this.col == other.col;
}
@Override
public String toString() {
return "(" row ", " col ')';
}
}
Basically, it's a row
and a col
.
A path is a linked list of PathNode
s:
public class PathNode {
public final Location loc;
public final PathNode link;
public PathNode(Location loc, PathNode link) {
this.loc = loc;
this.link = link;
}
public boolean contains(Location loc) {
PathNode n = this;
while (n != null) {
if (n.loc.equals(loc)) {
return true;
}
n = n.link;
}
return false;
}
}
I've added a contains
method because of the condition that a path must not go through the same location twice.
Now, the algorithm is a breadth-first search:
public PathNode solve(Location start, Location end) {
List<PathNode> paths = new ArrayList<>();
if (validLoc(start.row, start.col) == null) {
return null;
}
paths.add(new PathNode(start, null));
for (int i = 0; i < paths.size(); i) {
PathNode path = paths.get(i);
Location loc = path.loc;
if (loc.equals(end)) {
// at the end point, we must still check the path goes through
// all the 'must' cells
if (allMustInPath(path)) {
// found a path
return path;
}
} else {
addToPaths(path, left(loc), paths);
addToPaths(path, right(loc), paths);
addToPaths(path, up(loc), paths);
addToPaths(path, down(loc), paths);
}
}
return null;
}
We still need a few more methods:
private Location left(Location loc) {
return validLoc(loc.row, loc.col-1);
}
private Location right(Location loc) {
return validLoc(loc.row, loc.col 1);
}
private Location up(Location loc) {
return validLoc(loc.row-1, loc.col);
}
private Location down(Location loc) {
return validLoc(loc.row 1, loc.col);
}
private Location validLoc(int row, int col) {
if (row >= 0 && row < height && col >= 0 && col < width) {
Location loc = new Location(row, col);
return mustNot.contains(loc) ? null : loc;
}
return null;
}
private boolean allMustInPath(PathNode path) {
for (Location loc: must) {
if (!path.contains(loc)) {
return false;
}
}
return true;
}
private void addToPaths(PathNode path, Location loc, List<PathNode> paths) {
if (loc == null) {
return;
}
loc = validLoc(loc.row, loc.col);
if (loc != null && !path.contains(loc)) {
paths.add(new PathNode(loc, path));
}
}
The path returned by the solve
method, is actually in backward order. For your example above, it finds the path that you mentionned:
Grid grid = new Grid(5,5,
Arrays.asList(
new Location(0,3),
new Location(2,1)),
Arrays.asList(
new Location(1,3),
new Location(3,1)));
PathNode path = grid.solve(
new Location(0,0),
new Location(4,4));
if (path == null) {
System.out.println("No solution found");
}
while (path != null) {
System.out.println(path.loc);
path = path.link;
}
(4, 4)
(3, 4)
(2, 4)
(1, 4)
(0, 4)
(0, 3)
(0, 2)
(1, 2)
(2, 2)
(2, 1)
(1, 1)
(0, 1)
(0, 0)