以最少的转弯次数发现地图上所有区域的算法

6

假设我有这样的地图:

#####
..###
W.###

.是已发现的单元格。

#是未发现的单元格。

W是一个工作者。可以有很多工作者。每个工作者每回合只能移动一次。在一个回合中,他可以朝四个方向(上、右、下或左)移动一个单元格。他会探索他周围的所有8个单元格-将#变成.。在同一单元格上最多只能有一个工作者进行移动。

地图并不总是矩形的。起初除了W的邻居之外,所有单元格都未被发现。

目标是尽可能少的回合数使所有单元格都被发现。

第一种方法

找到最近的#并向其前进。重复此操作。

要找到最近的#,我从W开始BFS,并在找到第一个#时结束它。

在示例地图上,它可以给出这样的解决方案:

##### ##### ##### ##### ##... #.... ..... 
..### ...## ....# ..... ...W. ..W.. .W... 
W.### .W.## ..W.# ...W. ..... ..... ..... 

6个回合,距离最优状态还有一段距离:

##### ..### ...## ....# .....
..### W.### .W.## ..W.# ...W.
W.### ..### ...## ....# .....

4个回合。

问题

有哪些算法可以发现所有转弯次数最少的单元格?


1
不必寻找最近的 #,你可以尝试寻找相邻 # 最多的最近的 . - tobias_k
@tobias_k 我稍微修改了一下这个例子。在第一步中,最近的具有最多相邻 #(共 5 个)的 . 是 (1, 1)。然而,我们不知道该选择哪个方块 - 向右还是向上?它们都有 2 个相邻的 #。如果我们选择向右,它将与第一种方法相同。 - Adam Stelmaszczyk
1
地图总是长这样吗(即矩形,所有单元格均未被发现,除了W的邻居)?在这种情况下,总有一种遵循相同规则的最佳解决方案(例如,沿着波浪线前进)。 - Nico Schertler
1
即使有多个工作人员,网格也可以被细分为更小的矩形,以便根据其位置将网格的一部分分配给每个工作人员。 - Abhishek Bansal
@tobias_k 这个问题是,这种启发式方法是否能在每个只有一个工人的地图上得出最优结果? - Adam Stelmaszczyk
显示剩余10条评论
2个回答

2
这里有一个使用A*算法的基本思路。虽然可能会消耗大量时间和内存,但它保证返回最优解,并且绝对比暴力搜索更好。
A*的节点将是各种状态,即工人的位置和所有单元格的发现状态。每个唯一的状态代表一个不同的节点。
边将是所有可能的转换。一个工人有四个可能的转换。对于更多的工人,您需要每个可能的组合(大约4^n条边)。这是您可以限制工人留在网格内并且不重叠的部分。
成本将是回合数。用于近似目标(所有单元格都被发现)距离的启发式方法可以如下开发:
单个工人每次最多可以发现三个单元格。因此,n个工人最多可以发现3*n个单元格。因此,剩余回合数的最小值为“未发现的单元格数量/(3 * 工人数)”。这是要使用的启发式方法。甚至可以通过确定每个工人在下一轮中可以发现的最大单元格数量(每个工人最多为3个)来改进这一点。因此,总体启发式将是“(未发现的单元格-可发现的单元格) /(3 * 工人)+1”。
在每个步骤中,您都要检查具有最小总成本(迄今为止的回合数+启发式)的节点。对于检查的节点,您要计算每个周围节点的成本(所有工人的可能移动),然后继续进行。

这甚至可以改进...为什么这是算法的改进呢?它已经返回了最优解,不是吗?+1是必要的吗? - Adam Stelmaszczyk
1
啊,我明白了,这是更准确的启发式算法,所以它会检查更少的节点,但仍然给出最优解,对吧?我的第二个问题是关于 (未发现的单元格 - 可发现的单元格) / (3 * 工人) + 1 中的那个+1,这是必要的吗?我认为它可以被删除而不会产生影响。 - Adam Stelmaszczyk
@AdamStelmaszczyk:没有启发式的A*算法只是一种暴力搜索。你会得到相同的结果,但速度会慢得多。请使用启发式算法。 - Mooing Duck
@MooingDuck:不,没有启发式的A*算法就是Dijkstra算法。你也不会把Dijkstra算法称为暴力算法,对吧? - Nico Schertler
啊,那个+1。如果你减去所有的启发式中的+1,那么不会对功能产生影响。然而,与最小剩余转弯的关联就不那么明显了。 - Nico Schertler
显示剩余2条评论

1
严格来说,这个回答的主要部分可能被认为是“不是答案”。所以首先回答实际问题:
“发现所有单元格中最少转弯次数的算法是什么?”
答案:在每一步中,可以计算当前状态的所有可能后继状态。然后是这些后继状态的后继状态。可以递归重复此过程,直到其中一个后继状态不再包含任何#字段。通过此后继状态到达的状态序列对于达到此状态所需的移动次数来说是最优的。
到目前为止,这是微不足道的。但当然,对于一个“大”地图和/或“大”数量的工人来说,这是不可行的。
如评论中所述:我认为找到最优解可能是一个NP完全问题。无论如何,它最有可能至少是一个极其复杂的优化问题,在这里,你可以采用一些相当复杂的技术以在最短时间内找到最优解。
因此,在我看来,唯一可行的应对方法是启发式方法。
这里可以想象出几种方法。但是,我想尝试一个非常简单的方法。以下MCVE接受将地图定义为矩形字符串(空格代表“无效”区域,因此可以用它来表示非矩形地图)。工人们只被编号,从09(目前限制在这个数字上)。字符串被转换成一个MapState,包括实际的地图以及工人到目前为止走过的路径。
实际搜索方法是我在第一段中描述的“贪心”版本的穷举搜索:给定一个初始状态,它计算所有后继状态。这些状态是每个工人朝着任意方向移动的状态(例如3个工人有64种状态 - 当然这些状态被“过滤”,以确保工人不离开地图或移动到相同的区域)。
这些后继状态存储在列表中。然后它搜索“最佳”状态的列表,并再次计算此“最佳”状态的所有后继状态并将它们存储在列表中。迟早,列表包含没有遗漏字段的状态。
“最佳”状态的定义是启发式方法的应用:当缺少的字段较少时,状态比另一个状态“更好”。当两个状态具有相等数量的缺失字段时,则工人到下一个未访问字段的平均距离作为决定哪个状态“更好”的标准。
这很快找到了下面代码中所包含的示例的解决方案,并将其打印为每个工人在每个回合中必须访问的位置列表。
当然,这也不适用于“非常大”的地图或“许多”工人,因为州的列表将会迅速增长(可以考虑放弃“最差”的解决方案来加快速度,但这可能会有一些陷阱,比如被困在局部最优解中)。此外,很容易想到一些情况,“贪心”策略并不能给出最优结果。但是,在有人发布一个总能在多项式时间内计算出最优解的MVCE之前,也许有人会发现这很有趣或有帮助。
import java.awt.Point;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

public class MapExplorerTest
{
    public static void main(String[] args)
    {
        String mapString = 
            "   ###   ######"+"\n"+
            "   ###   ###1##"+"\n"+
            "###############"+"\n"+
            "#0#############"+"\n"+
            "###############"+"\n"+
            "###############"+"\n"+
            "###############"+"\n"+
            "###############"+"\n"+
            "###############"+"\n"+
            "###############"+"\n"+
            "#####   #######"+"\n"+
            "#####   #######"+"\n"+
            "#####   #######"+"\n"+
            "###############"+"\n"+
            "###############"+"\n"+
            "###############"+"\n"+
            "###   ######2##"+"\n"+
            "###   #########"+"\n";

        MapExplorer m = new MapExplorer(mapString);

        MapState solution = m.computeSolutionGreedy();
        System.out.println(solution.createString());
    }
}


class MapState
{
    private int rows;
    private int cols;
    private char map[][];
    List<List<Point>> workerPaths;
    private int missingFields = -1;

    MapState(String mapString)
    {
        workerPaths = new ArrayList<List<Point>>();
        rows = countLines(mapString);
        cols = mapString.indexOf("\n");
        map = new char[rows][cols];
        String s = mapString.replaceAll("\\n", "");
        for (int r=0; r<rows; r++)
        {
            for (int c=0; c<cols; c++)
            {
                int i = c+r*cols;
                char ch = s.charAt(i);
                map[r][c] = ch;
                if (Character.isDigit(ch))
                {
                    int workerIndex = ch - '0';
                    while (workerPaths.size() <= workerIndex)
                    {
                        workerPaths.add(new ArrayList<Point>());
                    }
                    Point p = new Point(r, c);
                    workerPaths.get(workerIndex).add(p);
                }
            }
        }
    }

    MapState(MapState other)
    {
        this.rows = other.rows;
        this.cols = other.cols;
        this.map = new char[other.map.length][];
        for (int i=0; i<other.map.length; i++)
        {
            this.map[i] = other.map[i].clone();
        }
        this.workerPaths = new ArrayList<List<Point>>();
        for (List<Point> otherWorkerPath : other.workerPaths)
        {
            this.workerPaths.add(MapExplorer.copy(otherWorkerPath));
        }
    }

    int distanceToMissing(Point p0)
    {
        if (getMissingFields() == 0)
        {
            return -1;
        }
        List<Point> points = new ArrayList<Point>();
        Map<Point, Integer> distances = new HashMap<Point, Integer>();
        distances.put(p0, 0);
        points.add(p0);
        while (!points.isEmpty())
        {
            Point p = points.remove(0);
            List<Point> successors = MapExplorer.computeSuccessors(p);
            for (Point s : successors)
            {
                if (!isValid(p))
                {
                    continue;
                }
                if (map[p.x][p.y] == '#')
                {
                    return distances.get(p)+1;
                }
                if (!distances.containsKey(s))
                {
                    distances.put(s, distances.get(p)+1);
                    points.add(s);
                }
            }
        }
        return -1;
    }

    double averageDistanceToMissing()
    {
        double d = 0;
        for (List<Point> workerPath : workerPaths)
        {
            Point p = workerPath.get(workerPath.size()-1);
            d += distanceToMissing(p);
        }
        return d / workerPaths.size();
    }

    int getMissingFields()
    {
        if (missingFields == -1)
        {
            missingFields = countMissingFields();
        }
        return missingFields;
    }

    private int countMissingFields()
    {
        int count = 0;
        for (int r=0; r<rows; r++)
        {
            for (int c=0; c<cols; c++)
            {
                if (map[r][c] == '#')
                {
                    count++;
                }
            }
        }
        return count;
    }

    void update()
    {
        for (List<Point> workerPath : workerPaths)
        {
            Point p = workerPath.get(workerPath.size()-1);
            for (int dr=-1; dr<=1; dr++)
            {
                for (int dc=-1; dc<=1; dc++)
                {
                    if (dr == 0 && dc == 0)
                    {
                        continue;
                    }
                    int nr = p.x + dr;
                    int nc = p.y + dc;
                    if (!isValid(nr, nc))
                    {
                        continue;
                    }
                    if (map[nr][nc] != '#')
                    {
                        continue;
                    }
                    map[nr][nc] = '.';
                }
            }
        }
    }

    public void updateWorkerPosition(int w, Point p)
    {
        List<Point> workerPath = workerPaths.get(w);
        Point old = workerPath.get(workerPath.size()-1);
        char oc = map[old.x][old.y];
        char nc = map[p.x][p.y];
        map[old.x][old.y] = nc;
        map[p.x][p.y] = oc;
    }


    boolean isValid(int r, int c)
    {
        if (r < 0) return false;
        if (r >= rows) return false;
        if (c < 0) return false;
        if (c >= cols) return false;
        if (map[r][c] == ' ')
        {
            return false;
        }
        return true;
    }

    boolean isValid(Point p)
    {
        return isValid(p.x, p.y);
    }


    private static int countLines(String s)
    {
        int count = 0;
        while (s.contains("\n"))
        {
            s = s.replaceFirst("\\\n", "");
            count++;
        }
        return count;
    }

    public String createMapString()
    {
        StringBuilder sb = new StringBuilder();
        for (int r=0; r<rows; r++)
        {
            for (int c=0; c<cols; c++)
            {
                sb.append(map[r][c]);
            }
            sb.append("\n");
        }
        return sb.toString();
    }

    public String createString()
    {
        StringBuilder sb = new StringBuilder();
        for (List<Point> workerPath : workerPaths)
        {
            Point p = workerPath.get(workerPath.size()-1);
            int d = distanceToMissing(p);
            sb.append(workerPath).append(", distance: "+d+"\n");
        }
        sb.append(createMapString());
        sb.append("Missing "+getMissingFields());
        return sb.toString();
    }



}


class MapExplorer
{
    MapState mapState;

    public MapExplorer(String mapString)
    {
        mapState = new MapState(mapString);
        mapState.update();

        computeSuccessors(mapState);
    }

    static List<Point> copy(List<Point> list)
    {
        List<Point> result = new ArrayList<Point>();
        for (Point p : list)
        {
            result.add(new Point(p));
        }
        return result;
    }

    public MapState computeSolutionGreedy()
    {
        Comparator<MapState> comparator = new Comparator<MapState>()
        {
            @Override
            public int compare(MapState ms0, MapState ms1)
            {
                int m0 = ms0.getMissingFields();
                int m1 = ms1.getMissingFields();
                if (m0 != m1)
                {
                    return m0-m1;
                }
                double d0 = ms0.averageDistanceToMissing();
                double d1 = ms1.averageDistanceToMissing();
                return Double.compare(d0, d1);
            }
        };
        Set<MapState> handled = new HashSet<MapState>();
        List<MapState> list = new ArrayList<MapState>();
        list.add(mapState);
        while (true)
        {
            MapState best = list.get(0);
            for (MapState mapState : list)
            {
                if (!handled.contains(mapState))
                {
                    if (comparator.compare(mapState, best) < 0)
                    {
                        best = mapState;
                    }
                }
            }
            if (best.getMissingFields() == 0)
            {
                return best;
            }
            handled.add(best);
            list.addAll(computeSuccessors(best));
            System.out.println("List size "+list.size()+", handled "+handled.size()+", best\n"+best.createString());
        }
    }

    List<MapState> computeSuccessors(MapState mapState)
    {
        int numWorkers = mapState.workerPaths.size();
        List<Point> oldWorkerPositions = new ArrayList<Point>();
        for (int i=0; i<numWorkers; i++)
        {
            List<Point> workerPath = mapState.workerPaths.get(i);
            Point p = workerPath.get(workerPath.size()-1);
            oldWorkerPositions.add(p);
        }

        List<List<Point>> successorPositionsForWorkers = new ArrayList<List<Point>>();
        for (int w=0; w<oldWorkerPositions.size(); w++)
        {
            Point p = oldWorkerPositions.get(w);
            List<Point> ps = computeSuccessors(p);
            successorPositionsForWorkers.add(ps);
        }

        List<List<Point>> newWorkerPositionsList = new ArrayList<List<Point>>();
        int numSuccessors = (int)Math.pow(4, numWorkers);
        for (int i=0; i<numSuccessors; i++)
        {
            String s = Integer.toString(i, 4);
            while (s.length() < numWorkers)
            {
                s = "0"+s;
            }
            List<Point> newWorkerPositions = copy(oldWorkerPositions);
            for (int w=0; w<numWorkers; w++)
            {
                int index = s.charAt(w) - '0';
                Point newPosition = successorPositionsForWorkers.get(w).get(index); 
                newWorkerPositions.set(w, newPosition);
            }
            newWorkerPositionsList.add(newWorkerPositions);
        }

        List<MapState> successors = new ArrayList<MapState>();
        for (int i=0; i<newWorkerPositionsList.size(); i++)
        {
            List<Point> newWorkerPositions = newWorkerPositionsList.get(i);
            if (workerPositionsValid(newWorkerPositions))
            {
                MapState successor = new MapState(mapState);
                for (int w=0; w<numWorkers; w++)
                {
                    Point p = newWorkerPositions.get(w);
                    successor.updateWorkerPosition(w, p);
                    successor.workerPaths.get(w).add(p);
                }
                successor.update();
                successors.add(successor);
            }
        }
        return successors;
    }

    private boolean workerPositionsValid(List<Point> workerPositions)
    {
        Set<Point> set = new HashSet<Point>();
        for (Point p : workerPositions)
        {
            if (!mapState.isValid(p.x,  p.y))
            {
                return false;
            }
            set.add(p);
        }
        return set.size() == workerPositions.size();
    }

    static List<Point> computeSuccessors(Point p)
    {
        List<Point> result = new ArrayList<Point>();
        result.add(new Point(p.x+0, p.y+1));
        result.add(new Point(p.x+0, p.y-1));
        result.add(new Point(p.x+1, p.y+0));
        result.add(new Point(p.x-1, p.y+0));
        return result;
    }

}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接