algorithm - pathfinding - Encuentra la cerca más corta que encierra un área en una grilla 2D
pathfinding traduccion (6)
¡Qué lindo problema!
Como @qwertyman ha notado, el problema es encontrar un corte de vértice mínimo entre las celdas "internas" y el límite de la cuadrícula. Si bien es concebible que este problema especial en una grilla pueda tener una solución más fácil, no creo que ninguna de las otras soluciones resuelva el problema en todos los casos.
Se puede ver que las celdas en cuadrículas cuadradas tienen hasta cuatro o hasta ocho vecinos (@tobias_k y @hidefromkgb). Si no tomamos en cuenta las celdas de pared existentes (libres), esto es lo que las cercas típicas se verán en las cuadrículas de 4 y 8:
No es que en ambos casos, haya muchas más cercas mínimas, pero estas cajas rectangulares y octagonales son mínimas y fáciles de encontrar. Además, son cercas mínimas con un área interior máxima.
Hay dos complicaciones: celdas de pared preexistentes libres, y la posibilidad de que múltiples componentes pequeños de la valla sean más baratos que una caja grande.
Ambas complicaciones están bien resueltas en un problema de corte de vértice mínimo; las celdas de pared preexistentes pueden simplemente eliminarse del gráfico (y las celdas internas pueden fusionarse con otras celdas internas conectadas, dejando solo una celda interna para cada componente conectado de las celdas internas). Desafortunadamente, los cortes mínimos generalmente se consideran remoción de bordes en lugar de vértices.
Dudo que cualquier algoritmo sea más fácil que implementar un algoritmo de corte de vertice limpio. Esto es a lo que nos enfrentamos:
En este ejemplo, cuatro vallas pequeñas son más baratas que una grande, pero eso depende de las proporciones exactas. Podríamos comenzar con una valla grande e intentar mejorarla por división, como lo hace @LazyMammal, o con esgrima cada componente conectado por separado, pero en cualquier caso, estas situaciones no son triviales.
Este también es problemático:
¿Usaremos el segmento grande de valla libre? ¿Debemos cercar cada punto por separado? ¿Usaremos tres vallas medianas? No importa si estamos comenzando a lo grande y dividiéndonos, como lo hace @LazyMammal, o comenzamos con recuadros delimitadores individuales y nos unimos a ellos, estas situaciones parecen presentar los mismos problemas que los cortes mínimos generales están resolviendo.
Si está de acuerdo con las aproximaciones a soluciones óptimas, o tal vez se limita a instancias en cuadrículas de 50 por 50 y puede descartar rápidamente estas complicaciones, ¿tal vez hay algo fácil y rápido? No lo sé.
Para un conjunto conectado G de celdas internas, encontrar una valla más barata funcionaría así:
Encuentre una ruta FL de flujo más corta de celdas vacías desde ese grupo hasta el límite.
Encuentre una ruta FE de "valla" más barata alrededor del grupo usando todas las celdas que no estén en G o FL. Pruebe cada celda en FL como punto de partida, o cualquier celda que esté tanto en FL como en la valla de G. El costo de las celdas vacías es 1, el costo de las celdas de pared es 0 y el costo de las celdas internas que no están en G es 0. Tendrás que cortar temporalmente la cuadrícula a lo largo de FL para asegurarte de que FE recorra G.
(Pero no sé cómo llenar las lagunas en un grupo para conectar todas sus células, en presencia de células de pared).
Entonces, ¿quizás el verdadero problema es qué células internas unir entre sí? Las celdas internas conectadas deben permanecer conectadas, bien. Además, si toca alguna cerca mínima, únete a sus grupos. Aparte de eso, aproxima con solo probar diferentes combinaciones?
No lo sé; Creo que el problema en las grillas grandes presenta las mismas complicaciones y complejidad que los problemas generales de corte mínimo, por lo que si realmente necesitas la optimización, resuélvelos.
Relevante: https://cstheory.stackexchange.com/questions/2877/ (gracias qwertyman), https://en.wikipedia.org/wiki/Vertex_separator con una noción diferente de separadores "mínimos".
Tengo una cuadrícula 2D de 50 x 50. Las celdas de cuadrícula pueden tener uno de tres estados:
1: "inside"
2: "empty"
3: "wall"
Mi configuración inicial es una cuadrícula con algunas celdas (tal vez el 10% de ellas, en su mayoría contiguas) marcadas como "dentro". Aleatoriamente hay algunas células de "pared" también. Las otras celdas están "vacías".
Estoy tratando de encontrar la cerca más corta que pueda construir alrededor de las celdas "internas" para que todas las celdas "internas" estén cercadas (las vallas se construyen cambiando las celdas "vacías" por celdas "de pared"). Si estamos evaluando soluciones, la mejor solución reducirá al mínimo la cantidad de células "vacías" que deben cambiarse a células "de pared".
Más rigurosamente, en la configuración final, existe la restricción de que para cada celda "interna" no hay una ruta al borde de la cuadrícula que no pase a través de una celda de "pared". En mi caso, el movimiento diagonal está permitido.
Supongo que puedo hacer algo inteligente que implique la transformación a distancia, o calcular el esqueleto topológico de la cuadrícula, pero no es inmediatamente obvio para mí. Si este es un problema de optimización clásico, no sé qué términos usar en google.
¿Hay un algoritmo O (n ^ 2) para encontrar la cerca más corta?
EDITAR: No es tan fácil como encontrar el casco convexo de las celdas "internas", porque las celdas de "pared" preexistentes están libres. Imagina un trozo en forma de "C" de una pared preexistente, con todas las celdas "internas" en el interior de la C. La mayoría de las veces, completar la C con celdas de "pared" será más barato que dibujar un casco convexo alrededor de todo las celdas "adentro". Esto es lo que hace que este problema sea difícil.
De acuerdo, resolví el problema, pero no he calculado la complejidad asintótica.
Lo hice a través de Dynamic Progamming, donde definí el problema recursivamente y también agregué un criterio de eliminación para una buena aceleración.
Aquí está la declaración del problema recursivo:
Encuentra el conjunto de cercado más pequeño de forma que no haya camino desde un
startArea
destartArea
hasta unborder
.
El problema recursivo se proporciona con:
- una
grid
contiene paredes naturales preexistentes- un
startArea
- una definición de
Border
- un conjunto preexistente de
fences
que se colocaron y no se pueden quitar- un número máximo de cercas
N
Devuelve lo siguiente:
- Si se encuentra una cerca, devuelva una
List
de las vallas- Si no se requieren vallas, devuelva una
empty list
- Si el tamaño de la cerca debe superar
N
paredes, devuelva<invalid>
(por ejemplo,null
)
La solución a esta declaración se hizo usando esta observación:
Cualquier cercado debe contener al menos una pared en la ruta óptima desde el área de inicio hasta el borde (de lo contrario, esa ruta sería válida y se usaría para escapar)
Por lo tanto, una forma de resolver el enunciado recursivo del problema es la siguiente:
Inicialice el algoritmo recursivo proporcionando:
- El mundo inicial
- The startArea,
- La definición del borde (por ejemplo, la celda debe estar en la frontera de la cuadrícula)
- Una lista vacía de vallas iniciales
- Un número máximo de vallas establecido en
Positive_Infinity
(sin restricción)
Luego recurse de la siguiente manera:
- Genere la
path
más corta desdestartArea
hasta elborder
sin pasar porfences
existentes - Si no hay camino que conduzca a la salida, devuelva las
fences
predefinidas- Justificación : el camino está bloqueado, el cercado existente es suficiente, no es necesario agregar ningún
- Si las
fences
predefinidas tienen al menosN
elementos, devuelve<invalid>
- Justificación : las vallas predefinidas son insuficientes, por lo que se necesitarían al menos
N+1
vallas. Como se sabe que existe una solución conN
vallas, no es necesario investigar más a fondo
- Justificación : las vallas predefinidas son insuficientes, por lo que se necesitarían al menos
- Inicialice
bestLength
como la entrada número máximo de vallasN
como - Inicialice
bestPath
como "ìnvalid
- Iterar sobre cada
location
largo de lapath
(excluyendo las celdas que están en el área destartArea
, que supongo que no se puedenstartArea
)- generar una solución al problema recursivo de la siguiente manera:
- la
grid
entrada contiene paredes naturales preexistentes - la entrada
startArea
- la definición de
Border
entrada - el conjunto preexistente de
fences
más lalocation
actual - el número máximo de cercas
bestLength
- la
- Si la solución es
<invalid>
, continúe la iteración - Si la solución existe
- Guárdelo como
bestPath
- Actualiza
bestLength
su longitud
- Guárdelo como
- Si la longitud de la solución es
N+1
devuélvala
- generar una solución al problema recursivo de la siguiente manera:
- Return
bestPath
Básicamente, estamos cerrando los mejores caminos a medida que los encontramos. A medida que avanzamos, el camino cambia y lo bloqueamos.
El mejor IMO es el mecanismo de eliminación selectiva, similar a una beta-pruning .
Análisis de complejidad sucia (la grilla es NxN):
- La ruta tiene una longitud
N
, estamos iterando sobre su longitud.- Cada iteración recursiva crea una ruta de longitud N (normalmente es más larga, pero lo ignoraré). Gracias a la eliminación, voy a hacer esto una complejidad de
log(N)
solamente- En cada paso estamos realizando BFS, por lo que
N.log(N)
- En cada paso estamos realizando BFS, por lo que
- Cada iteración recursiva crea una ruta de longitud N (normalmente es más larga, pero lo ignoraré). Gracias a la eliminación, voy a hacer esto una complejidad de
Complejidad total: N².log (N) ²` (¿quizás? No sé lo que estoy haciendo)
Aquí hay una versión de Java:
MinimalFenceSolver.java
(clase principal)
El problema se construye en general , y se resuelven static
métodos static
.
public class MinimalFenceSolver {
public static final Predicate<Location> borderDetector = loc -> ((GridWorld.GridLocation)loc).isBorder();
public static void main(String[] argc){
GridWorld world = new GridWorld(10, 10, 0);
// Find best fence
List<Location> startArea = new ArrayList<>();
startArea.add(world.at(5, 4));
findBestFence(world, startArea);
}
private static List<Location> findBestFence(GridWorld world, List<Location> startArea) {
List<Location> bestFence = findBestFenceRec(world, startArea, new ArrayList<>(), Integer.MAX_VALUE);
return bestFence;
}
private static List<Location> findBestFenceRec(GridWorld world, List<Location> startArea, List<Location> fencePrefix, int bestLengthYet) {
List<Location> bestFence = null;
int bestLength = bestLengthYet;
// Iterate
World fencedWorld = world.withWall(fencePrefix);
Path shortestPath = EscapePathfinder.begin(fencedWorld).setStart(startArea).setEnd(borderDetector).solve();
if(!shortestPath.exists()){
return fencePrefix;
}
if(fencePrefix.size() >= bestLength){
return null; // Culling
}
for (Location loc : shortestPath.fullPath()) {
if(!fencePrefix.contains(loc) && !startArea.contains(loc)){
List<Location> newfence = new ArrayList<>(fencePrefix);
newfence.add(loc);
List<Location> bestChild = findBestFenceRec(world, startArea, newfence, bestLength);
if(bestChild != null){
if(bestFence == null || bestChild.size() < bestLength) {
bestFence = bestChild;
bestLength = bestChild.size();
}
}
}
}
return bestFence; // null if no fence found
}
}
Interfaz World.java
para una definición mundial
public interface World {
Location at(int i, int j);
/**
* Removes walled Locations in the input, return the result
* @param neighbours (untouched)
* @return a List of Locations, none of which has any wall in it
*/
List<Location> noWalls(List<Location> neighbours);
}
Interfaz Location.java
public interface Location {
CellType getType();
List<Location> neighbours();
}
CellType.java
enum
public enum CellType {
WALL, EMPTY
}
GridWorld.java
una implementación de un mundo que vive en una red
Contiene su propia implementación de Ubicación. Tiene la capacidad de ser decorado temporalmente con vallas adicionales usando la subclase GridWorldWithWall
.
public class GridWorld implements World{
private final GridLocation[][] world;
final int nx;
final int ny;
public GridWorld(int nx, int ny, long seed){
this.nx = nx;
this.ny = ny;
Random rand = new Random(seed);
world = new GridLocation[nx][ny];
for(int i=0; i<nx; i++){
for(int j=0; j<ny; j++){
CellType locationType = rand.nextBoolean() ? CellType.EMPTY: CellType.WALL;
world[i][j] = new GridLocation(locationType, i, j);
}
}
}
public GridLocation at(int i, int j){
return world[(i+nx) % nx][(j+ny) % ny];
}
public String toString(){
StringBuilder builder = new StringBuilder();
for(int i=0; i<nx; i++){
for(int j=0; j<ny; j++){
builder.append(world[i][j].type == CellType.WALL ? "#" : ".");
}
builder.append("/n");
}
return builder.toString();
}
private static final int[][] offsets = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
public class GridLocation implements Location{
private final CellType type;
private final int i;
private final int j;
public GridLocation(CellType type, int i, int j) {
super();
this.type = type;
this.i = i;
this.j = j;
}
@Override
public CellType getType() {
return type;
}
@Override
public List<Location> neighbours() {
List<Location> result = new ArrayList<>();
for(int[] offset: offsets){
result.add(GridWorld.this.at(i + offset[0], j + offset[1]));
}
return result;
}
public boolean isBorder(){
return i==0 || j==0 || i==nx-1 || j==ny-1;
}
public String toString(){
return (type == CellType.WALL ? "#" : ".") + "(" + i + ", " + j + ")";
}
public boolean equals(Object obj){
if(!(obj instanceof GridLocation)){
return false;
} else {
GridLocation other = (GridLocation) obj;
return other.i == this.i && other.j == this.j;
}
}
}
@Override
public List<Location> noWalls(List<Location> neighbours) {
return neighbours.stream().filter(cell -> cell.getType()!=CellType.WALL).collect(Collectors.toList());
}
public World withWall(List<Location> walls) {
return new GridWorldWithWall(walls);
}
private class GridWorldWithWall implements World {
public List<Location> walls;
public GridWorldWithWall(List<Location> walls) {
this.walls = walls;
}
@Override
public Location at(int i, int j) {
return new LocationWithWalls(GridWorld.this.at(i, j));
}
@Override
public List<Location> noWalls(List<Location> neighbours) {
List<Location> noWalls = GridWorld.this.noWalls(neighbours);
noWalls.removeAll(walls);
return noWalls;
}
private class LocationWithWalls implements Location{
private final GridLocation location;
public LocationWithWalls(GridLocation location) {
this.location = location;
}
@Override
public CellType getType() {
if(GridWorldWithWall.this.walls.contains(location)) {
return CellType.WALL;
}
return location.getType();
}
@Override
public List<Location> neighbours() {
List<Location> neighbours = location.neighbours();
neighbours().removeAll(walls);
return neighbours;
}
}
}
}
Pathfinder.java
(Interfaz)
Esta es una interfaz con exceso de fluidez en caso de que quiera probar otros solucionadores, si logra hacer una buena heurística para el borde, a-Star puede usarse aquí
public interface Pathfinder {
public static interface PathStartSetter {
PathEndSetter setStart(Iterator<? extends Location> startSupplier, Predicate<Location> startTester);
default PathEndSetter setStart(final Location start){
return setStart(
new Iterator<Location>() {
boolean provided = false;
@Override
public boolean hasNext() {
return !provided;
}
@Override
public Location next() {
if(provided){
return null;
} else {
provided = true;
return start;
}
}
},
loc -> loc.equals(start)
);
}
default PathEndSetter setStart(final List<Location> start){
return setStart(start.iterator(),
loc -> start.contains(loc)
);
}
}
public static interface PathEndSetter{
public PathSolver setEnd(Predicate<Location> endTester);
default PathSolver setEnd(final Location end){
return setEnd(loc -> loc.equals(end));
}
default PathSolver setEnd(final List<Location> end){
return setEnd(loc -> end.contains(loc));
}
}
public static interface PathSolver{
public Path solve();
}
public static interface Path{
List<Location> fullPath();
Location pathAt(int step);
public boolean exists();
}
public static class NoPath implements Path {
@Override
public List<Location> fullPath() {
return null;
}
@Override
public Location pathAt(int step) {
return null;
}
@Override
public boolean exists() {
return false;
}
}
}
Dijkstra.java
(El solucionador típico de BFS)
Siéntase libre de omitir la lectura, es dijkstra vainilla, pero implementa mi intrincada interfaz Pathfinder
public class Dijkstra implements Pathfinder{
public static DijkstraStartSetter begin(World world) {
return new DijkstraStartSetter(world);
}
public static class DijkstraStartSetter implements PathStartSetter {
private final World world;
public DijkstraStartSetter(World world) {
this.world = world;
}
public DijkstraEndSetter setStart(Iterator<? extends Location> startSupplier, Predicate<Location> startTester) {
return new DijkstraEndSetter(world, startSupplier, startTester);
}
}
public static class DijkstraEndSetter implements PathEndSetter{
private final World world;
private final Iterator<? extends Location> startSupplier;
private final Predicate<Location> startTester;
public DijkstraEndSetter(World world, Iterator<? extends Location> startSupplier, Predicate<Location> startTester) {
this.world = world;
this.startSupplier = startSupplier;
this.startTester = startTester;
}
public DijkstraSolver setEnd(Location end){
return new DijkstraSolver(world, startSupplier, startTester, end);
}
public DijkstraSolver setEnd(Predicate<Location> endTester){
return new DijkstraSolver(world, startSupplier, startTester, null);
}
}
public static class DijkstraSolver implements PathSolver{
private final World world;
private final Iterator<? extends Location> startSupplier;
private final Predicate<Location> startTester;
private final Location end;
public DijkstraSolver(World world, Iterator<? extends Location> startSupplier, Predicate<Location> startTester, Location end) {
this.world = world;
this.startSupplier = startSupplier;
this.startTester = startTester;
this.end = end;
}
public Path solve(){
Queue<Location> open = new LinkedList<>();
List<Location> closed = new ArrayList<>();
Map<Location, Location> parents = new HashMap<>();
while (startSupplier.hasNext()) {
open.add(startSupplier.next());
}
while(!open.isEmpty()){
Location current = open.remove();
closed.add(current);
List<Location> neighbours = world.noWalls(current.neighbours());
for (Location n : neighbours) {
if(open.contains(n) || closed.contains(n)) {
continue;
}
open.add(n);
parents.put(n, current);
if(n == end){
return makePath(parents);
}
}
}
return new NoPath();
}
private Path makePath(Map<Location, Location> parents) {
StandardPathBuilder path = StandardPath.begin();
Location current = end;
while(!startTester.test(current)){
path.add(current);
current = parents.get(current);
}
path.add(current);
return path.buildReverse();
}
}
}
Perdón por la increíblemente larga publicación y solución sobre ingeniería. Me encantó tanto la Pregunta que me dejé llevar.
Esto se puede resolver como un problema de simplificación de gráficos:
- crea un gráfico de la grilla (sin diagonales)
- eliminar (eliminar) nodos internos
- colapsar (fusionar) Nodos de pared
- describir un camino alrededor del borde del gráfico
- iterar a lo largo de la ruta
- compruebe si los nodos de ruta cercanos comparten un vecino de gráfico vacío (cercano = nodos de ruta [n .. n + k])
- compruebe si hay una nueva distancia de trayectoria <= distancia de trayectoria existente (bordes de conteo)
- verificar si el nodo Inside se va a quedar varado por un atajo (no permitir)
- ajustar la ruta si se cumplen las condiciones
- eliminar (eliminar) nodos gráficos que ya no son necesarios
- detener cuando el camino no puede ser reducido más
En el peor de los casos, debe atravesar todos los nodos de los vértices en el gráfico varias veces para cada borde. Entonces parece que O (V * E) o en otras palabras O (N ^ 2).
Lo que probablemente quiera es un casco convexo bidimensional .
Yo recomendaría el llamado algoritmo de envoltura de regalos . Su complejidad temporal es O (n²) en el peor de los casos. Su esencia es la siguiente.
Sin pérdida de generalidad, asumamos que no hay 3 puntos en la nube que sean colineales.
- Comenzamos con un punto más a la izquierda, ya que cada casco sobre una nube de puntos finita debe incluir al menos un punto que está más a la izquierda (y, teniendo en cuenta las restricciones anteriores, no más de dos son posibles, y ambos pertenecen al casco).
- Entonces comenzamos a usar la fuerza bruta en tal punto que el resto está en el mismo plano de los dos semiplanos, en el cual el plano inicial está dividido por una línea trazada desde el punto inicial al que se busca.
- Ahora hacemos que el encontrado sea una nueva inicial, y luego repetimos [2-3] hasta que el casco se cierre.
[NB] tenga en cuenta que encontrar un punto que es un predecesor de la inicial actual no lo lleva a ninguna parte (como esto: • ⇌ •), por lo que debe omitirse, a menos que no haya más puntos que estos dos.
Para mí, sigue siendo una variante de casco convexo. debe incluir en su casco convexo todas las celdas que sean vecinas de la celda interna y que no sean una celda interna + incluir celdas que estén al [principio] y [fin] de una pared (muro contiguo). Entonces parte importante es excluir las células vecinas si esas células están dentro de una pared. Para verificar si la celda está dentro de una C con forma de pared, por ejemplo (I) puede calcular una línea ax + b desde p1 - p2, luego usando un tipo de partición puntual, en sentido horario / antihorario y luego puntos de pared adicionales con su vecino. de la búsqueda. En el siguiente ejemplo, los puntos vecinos a I serán excluidos. Por lo tanto, el algoritmo encontrará la conexión p1-> p2 p1-p2 como directa.
...[.p1]
. I
. I
...[.p2]
En el siguiente ejemplo, los puntos T son puntos vecinos
TTT
...[.p1] TIT
. I TTT
. I
...[.p2]
después del casco convexo algo obtendrás:
[T3]
...[.p1] I[T2]
. I [T1]
. I
...[.p2]
eso significa que una ruta p2 [T1] [T2] [T3] p1 y las líneas entre esos puntos le dan un ajuste mínimo. Como puede ver, cada pared tiene que almacenar también un valor si cualquier punto adyacente está DENTRO de una forma de muro (como C), esas paredes deben incluirse en un casco convexo, pero las que no tienen vecinos internos solo se pueden usar si la distancia al siguiente punto es más pequeña usando la pared existente de costo cero ... Bueno, es un poco complicado para una explicación rápida, pero supongo que puedes obtener algo de eso ...
EDITAR: en esa convexa computada deberá ejecutar también flujo mínimo para afinar casos como:
...........[.]
.
.
.
.
.
.
. I I
.
.
.
.
.
.
...........[.]
donde uno de I está dentro, pero la valla de min está alrededor de ambos I sin p1 ni p2. En algo p1 y p2 se seleccionará, luego para todas las paredes que tengan I interno, debe calcular dist (p1, externalI) + dist (p2, externalI) con dist (internalI, eternalI) y elegir la más pequeña ...externalI es el conectado a p1 y p2 (podría ser uno o dos puntos externos ...)
Si con la valla más corta se refiere a una celda que requiere un número mínimo de celdas de "pared", la valla con un rectángulo mínimo de cerramiento funcionará.