找到包围2D网格上的区域的最短栅栏

时间:2017-04-24 07:57:13

标签: algorithm optimization graph-algorithm path-finding topology

我有一个50 x 50的2D网格。网格单元可以具有以下三种状态之一:

1: "inside"
2: "empty"
3: "wall"

我的初始配置是一个网格,其中包含一些标记为“内部”的单元格(可能是其中的10%,大部分是连续的)。随机也有一些“墙”细胞。其他单元格是“空的”。

我正在尝试找到最短的栅栏我可以在“内部”单元格周围构建,以便所有“内部”单元格都被围起来(围栏是通过将“空”单元格更改为“墙“细胞”。如果我们对解决方案进行评分,最佳解决方案将最小化需要更改为“墙”单元格的“空”单元格数。

更严格地说,在最终配置中,存在这样的约束:对于每个“内部”单元,没有到达不通过“墙”单元的网格边缘的路径。就我而言,允许对角线移动。

我猜我可以做一些聪明的事情涉及距离变换,或计算网格的拓扑骨架,但这对我来说并不是很明显。如果这是一个经典的优化问题,我不知道google的条款。

是否有寻找最短栅栏的O(n ^ 2)算法?

编辑:这并不像找到“内部”单元的凸包那么容易,因为预先存在的“墙”单元是自由的。想象一个“C”形的预先存在的墙块,在C的内部有所有“内部”单元格。大多数时候,用“墙”单元格完成C将比在所有墙上绘制凸包更便宜“内部”细胞。这就是解决这个问题的原因。

6 个答案:

答案 0 :(得分:9)

真是个好问题!

正如@qwertyman所注意到的,问题 在"内部"之间找到最小的顶点切口。单元格和网格边界。虽然可以想象网格上的这个特殊问题可以有一个更容易的解决方案,但我不认为任何其他解决方案在所有情况下都能解决问题。

方格网格上的单元格可以看作最多有四个或最多八个邻居(@tobias_k和@hidefromkgb)。如果我们忽视现有的(免费)墙体细胞,那么在4网格和8网格上的典型围栏是什么样的:

4- vs. 8 neighbourhoods, fences, and minimal fences

在这两种情况下,并没有更多的最小围栏,但这些矩形和八边形的边界框都很小,很容易找到。此外,它们是最小的围栏,内部区域最大。

有两个并发症:免费预先存在的墙壁细胞,以及多个小栅栏组件比一个大边界框便宜的可能性。

在最小的顶点切割问题中很好地解决了这两个并发症;可以从图中删除预先存在的壁单元(并且内部单元可以与其他连接的内部单元合并,对于内部单元的每个连接组件仅留下一个内部单元)。不幸的是,最小切割通常被认为是去除边缘而不是顶点!

我怀疑任何算法都比实现干净的顶点切割算法更容易。这就是我们面临的问题:

在这个例子中,四个小栅栏比一个大栅栏便宜,但这取决于确切的比例。我们可以从一个大栅栏开始,尝试通过分区来改进它,比如@LazyMammal,或者分别对每个连接的组件进行屏蔽,但在任何一种情况下,这些情况都不是微不足道的。

这个也有问题:

我们应该使用大型免费围栏吗?我们要分别围住每个小点吗?我们应该使用三个中等栅栏吗?无论我们是像@LazyMammal那样开始大而且分裂,还是从单个边界框开始加入它们,这些情况似乎都会出现一般最小削减所解决的问题。

如果您对最佳解决方案的近似值没有问题,或者可能将自己限制在50×50网格的情况下并且可以快速排除这些并发症,那么可能有一些简单快速的方法吗?我不知道。

对于连接的设置G内部单元格,找到最便宜的围栏将如下工作:

  • 找到最短的"流程"从该组到边界的空单元格的路径FL。

  • 找到最便宜的"围栏"使用不在G或FL中的所有单元格在组周围的路径FE。尝试FL中的每个单元格作为起点,或者同时在FL和G栅栏中的任何单元格。空单元的成本为1,墙单元的成本为0,而不在G中的内单元的成本为0.您必须暂时沿FL切割网格以确保FE绕过G. / p>

(但我不知道如何填补群体中的空白以便连接所有细胞 - 存在壁细胞。)

所以也许真正的问题是哪个内部细胞围在一起?连接的内部细胞必须保持连接,没关系。此外,如果任何最小的栅栏触摸,加入他们的组。除此之外,通过尝试不同的组合来近似?

我不知道;我认为大网格上的问题与一般的最小切割问题具有相同的复杂性和复杂性 - 所以如果你真的需要最优性,那就解决它们。

相关:https://cstheory.stackexchange.com/questions/2877/(感谢qwertyman),https://en.wikipedia.org/wiki/Vertex_separator有一个不同的概念" minimal"分隔符。

答案 1 :(得分:1)

您最想要的是2-dimensional convex hull

我推荐所谓的gift-wrapping algorithm。在最坏的情况下,它的时间复杂度为O(n²)。其要点如下。

不失一般性,我们假设云中没有共线的3个点。

  1. 我们从最左边的点开始,因为有限点云上的每个船体必须包括至少一个最左边的点(并且考虑到上述限制,其中不超过两个是可能的,并且它们都属于船体)。
  2. 然后我们开始蛮力这样的观点,即所有其余部分都在两个半平面的相同位置,初始平面被从初始点绘制的线划分为所搜索的平面。
  3. 现在我们将找到的那个作为新的首字母,然后重复[2-3]直到船体关闭。
  4. [NB]请记住,找到一个作为当前初始值的前身的点会导致你无处可去(例如:•⇌•),所以应该跳过它,除非除了这两个点之外没有其他点。

答案 2 :(得分:1)

如果用最短的栅栏表示一个占用最少数量“墙”单元的栅栏,那么带有最小封闭矩形的栅栏就可以工作。

答案 3 :(得分:1)

对我而言,它仍然是一种凸壳体变体。你必须在你的凸包中包括所有与细胞内部相邻并且不是内部细胞的细胞+包括位于墙的[开始]和[结束]的细胞(连续的墙)。 然后重要的是如果那些细胞在墙内,则排除相邻细胞。要检查单元格是否位于C形墙内,例如(I),您可以从p1-p2计算ax + b线,然后使用顺时针/逆时针方向检查一种点分区,然后使用您的邻居点进一步排除壁点以排除它来自搜索。在下面的示例中,我将排除与我相邻的点。因此算法将直接找到p1-> p2连接p1-p2。

...[.p1]
. I 
. I 
...[.p2]

在下面的示例中,T点是邻居点

               TTT
...[.p1]       TIT
. I            TTT
. I 
...[.p2]
在凸壳算法之后你会得到:

               [T3]
...[.p1]        I[T2]
. I            [T1] 
. I 
...[.p2]

表示路径p2 [T1] [T2] [T3] p1,这些点之间的线条为您提供最小的换行。正如你所看到的,每个墙都必须存储一个值,如果任何I邻居点都在墙内(如C),那些墙必须包含在凸包中,但没有内部邻居的墙只能使用如果使用零成本现有墙壁到下一点的距离较小..那么它有点复杂,快速解释但我想你可以从中得到一些东西..

编辑:在计算出的凸面上,你还必须运行min flow来调整像:

这样的情况
...........[.]
.
.
. 
.
.         
.
.         I  I
.
.
.
.
.
. 
...........[.]

其中一个我在里面,但是最小围栏在我周围,我没有p1和p2参与。算法中将选择p1和p2,然后对于所有具有内部I的墙,你必须用dist(internalI,eternalI)计算dist(p1,externalI)+ dist(p2,externalI)并选择较小的一个.externalI is连接到p1和p2的那个(可能是一个或两个外部点......)

答案 4 :(得分:1)

这可以解决为图简化问题:

  1. 创建网格图(无对角线)
  2. 删除(删除)内部节点
  3. 折叠(合并)墙节点
  4. 描述图表边缘的路径
  5. 沿路径迭代
    • 检查附近的路径节点是否共享空图邻居(附近=路径节点[n .. n + k])
    • 检查新路径距离< =现有路径距离(计数边缘)
    • 检查Inside节点是否会被快捷方式搁置(不允许)
    • 如果符合条件,则调整路径
    • 删除(删除)不再需要的图形节点
  6. 当路径不能缩小时停止
  7. 最糟糕的情况是,您必须为每个边缘遍历图形中的所有顶点节点几次。所以它看起来像O(V * E)或换句话说O(N ^ 2)。

答案 5 :(得分:0)

好的我解决了这个问题,但我没有计算渐近复杂度。

我通过Dynamic Progamming做到了这一点,我在那里递归地定义了问题,并为加速提供了剔除标准。

这是递归问题陈述:

  

找出最小的防护设置,使startAreaborder之间没有路径。
  递归问题由以下提供:

     
      
  • 包含天然预先存在的墙壁的grid
  •   
  • a startArea
  •   
  • Border定义
  •   
  • 已放置且无法移除的预先存在的fences
  •   
  • 最大栅栏数N
  •   
     

返回以下内容:

     
      
  • 如果找到击剑,则返回List围栏
  •   
  • 如果不需要围栏,请返回empty list
  •   
  • 如果围栏大小必须超过N墙,请返回<invalid>(例如null
  •   

使用此观察结果完成了本声明的解决方案:

  

任何围栏应该在从起始区域到边界的最佳路径上至少包含一个墙(否则,该路径将有效并用于逃生)

因此,解决递归问题陈述的方法如下:

通过提供:

初始化递归算法

  • 最初的世界
  • startArea,
  • 边界定义(例如,单元格必须位于网格的边界上)
  • 初始围栏的空列表
  • 设置为Positive_Infinity(无约束)的最大栅栏数

然后递归,如下所示:

  • path生成最短的startAreaborder,而不通过现有的fences
  • 如果没有路径通向出口,请返回预定义的fences
    • 原理:路径被阻止,现有的防护就足够了,不需要添加任何
  • 如果预定义的fences至少包含N个元素,请返回<invalid>
    • 基本原理:预定义的围栏不足,因此至少需要N+1个围栏。由于已知存在N围栏的解决方案,因此无需进一步调查
  • bestLength初始化为输入的最大围栏数N
  • bestPath初始化为ìnvalid
  • 遍历location上的每个path(不包括startArea中的单元格,我认为这些单元格不能被挡住)
    • 生成递归问题的解决方案,如下所示:
      • 输入grid包含自然的,预先存在的墙
      • 输入startArea
      • 输入Border定义
      • 预先存在的fences 加上当前location
      • 最大栅栏数bestLength
    • 如果解决方案为<invalid>,请继续迭代
    • 如果解决方案存在
      • 将其存储为bestPath
      • 更新bestLength其长度
    • 如果解决方案长度为N+1,则将其返回
  • 返回bestPath

基本上,我们正在找到最好的路径。当我们继续前进时,路径会发生变化,我们会对此进行调整。

最佳位IMO是剔除机制,类似于beta-pruning

肮脏的复杂性分析(网格是NxN):

  • 路径的长度为N,我们正在迭代它的长度。
    • 每次递归迭代都会创建一个长度为N的路径(通常它会更长,但我会忽略它)。由于剔除我只会使log(N)复杂化
      • 我们在每一步都执行BFS N.log(N)

总复杂程度:N².log(N)²`(也许?我不知道我在做什么)

以下是它的Java版本:

MinimalFenceSolver.java(主要课程)

问题是在中构建的,并且解决了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 
    }
}
世界定义的

World.java接口

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);

}

Location.java接口

public interface Location {
     CellType getType();
     List<Location> neighbours();
}

CellType.java枚举

public enum CellType {
    WALL, EMPTY
}

GridWorld.java一个生活在网格上的世界的实现

包含自己的Location实现。能够使用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(接口)

如果您想尝试其他解算器,这是一个过度流畅的界面,如果您设法为边框制作好的启发式, a-Star 可以在这里使用

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(典型的BFS求解器)

随意跳过阅读,它是vanilla dijkstra,但它确实实现了我复杂的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();
        }
    }

}

对于这个令人惊讶的长篇文章和过度设计的解决方案感到抱歉!我非常喜欢这个问题,所以我被带走了。