6

Let's say I have such map:

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

. is a discovered cell.

# is an undiscovered cell.

W is a worker. There can be many workers. Each of them can move once per turn. In one turn he can move by one cell in 4 directions (up, right, down or left). He discovers all 8 cells around him - turns # into .. In one turn, there can be maximum one worker on the same cell.

Maps are not always rectangular. In the beginning all cells are undiscovered, except the neighbours of W.

The goal is to make all the cells discovered, in as least turns as possible.

First approach

Find the nearest # and go towards it. Repeat.

To find the nearest # I start BFS from W and finish it when first # is found.

On exemplary map it can give such solution:

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

6 turns. Pretty far from optimal:

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

4 turns.

Question

What is the algorithm that discovers all the cells with as least turns as possible?

Adam Stelmaszczyk
  • 19,665
  • 4
  • 70
  • 110
  • 1
    Instead of finding the nearest `#`, you could instead try to find the nearest `.` with the most adjacent `#`... – tobias_k Aug 14 '14 at 09:38
  • @tobias_k I slightly edited the example. In the first turn, the nearest `.` with the most adjacent `#` (5 of them) is (1, 1). However, to go there, we don't know which square to choose - right or up? They both have 2 adjacent `#`. If we choose right, it will work the same as first approach. – Adam Stelmaszczyk Aug 14 '14 at 09:51
  • 1
    Do the maps always look like this one (i.e. rectangular, all cells undiscovered, except the neighbours of W)? In this case, there is always an optimal solution that follows the same rules (e.g. advance in wavy lines). – Nico Schertler Aug 14 '14 at 09:56
  • @NicoSchertler They are not always rectangular. In the beginning always all cells are undiscovered, except the neighbours of `W`. But there can be many workers. – Adam Stelmaszczyk Aug 14 '14 at 10:00
  • Can workers be at the same cell at the same time? – Nico Schertler Aug 14 '14 at 10:03
  • @NicoSchertler No, they can't. Thank you for very good questions, I'm editing my post. – Adam Stelmaszczyk Aug 14 '14 at 10:05
  • It remembers me with [transitive closure](http://books.google.com.eg/books?id=7XUSn0IKQEgC&pg=PA212&lpg=PA212&dq=blackmail+graph+from+Skiena.-.TheAlgorithmDesignManual&source=bl&ots=z85KKOPS-f&sig=p-u7t5nYxXeojVfqD9Ou7wrXJL4&hl=en&sa=X&ei=lYXsU8qdJ6rE0QWr7YHYAQ&ved=0CCAQ6AEwAQ#v=onepage&q=blackmail%20graph%20from%20Skiena.-.TheAlgorithmDesignManual&f=false) and blackmail graph. –  Aug 14 '14 at 10:08
  • 1
    IMO Even if there are multiple workers, the grid can be sub-divided into smaller rectangles so that a part of the grid is allocated to one worker each, depending on its location. – Abhishek Bansal Aug 14 '14 at 10:17
  • Regarding the comment @tobias_k : I think this heuristic *will* yield the optimal result in the example case: In the first turn, it goes either up or right. If it goes right, then in the *next* turn it will move up. (Then 2*right, giving the optimal result here). Of course, this can not be generalized. Particularly: That there are *many* workers makes this very difficult. One has to coordinate them, and prevent one worker from "eating away" the fields that another worker *should* uncover. Very interesting problem, +1 – Marco13 Aug 14 '14 at 10:46
  • @Marco13 You and tobias_k are right, I was wrong in my comment, sorry. – Adam Stelmaszczyk Aug 14 '14 at 12:51
  • @tobias_k The question is - will that heuristic yield the optimal result on every map with one worker? – Adam Stelmaszczyk Aug 14 '14 at 12:53
  • My gut feeling is that finding **THE** optimal solution could be NP complete. There are certainly several heuristics (e.g. the one that tobias_k mentioned, but also the subdivision among the workers that user1990169 mentioned (where each worker could basically work in his "Voronoi region"), but in order to guarantee the optimal result, one would have to check all 4^numWorkers possible steps for each turn. (Also, this may be optimized, maybe with dynamic programming, but still may take exponential time) – Marco13 Aug 14 '14 at 13:49
  • @Marco13 I changed my mind - I was right in my comment. If he goes right in the first turn, in the second turn he not necessarily goes up. Because the nearest `.` with the most `#` will be (2, 1), 5 adjacent `#`. So, the path will be to that point. But whether he will go up or right on that path is not specified by this heuristic, he can go right. – Adam Stelmaszczyk Aug 14 '14 at 14:28
  • Maybe I misunderstood this. But when you go 1. right and 2. up, then the result is the third state of the optimal solution, and from there it proceeds with the two final steps. – Marco13 Aug 14 '14 at 14:34
  • @Marco13 Yes, but it is not guaranteed. So, I answered my question "Will that heuristic yield the optimal result on every map with one worker?". Unfortunately, no. – Adam Stelmaszczyk Aug 14 '14 at 14:42

2 Answers2

2

Here is a basic idea that uses A*. It is probably quite time- and memory-consuming, but it is guaranteed to return an optimal solution and is definitely better than brute force.

The nodes for A* will be the various states, i.e. where the workers are positioned and the discovery state of all cells. Each unique state represents a different node.

Edges will be all possible transitions. One worker has four possible transitions. For more workers, you will need every possible combination (about 4^n edges). This is the part where you can constrain the workers to remain within the grid and not to overlap.

The cost will be the number of turns. The heuristic to approximate the distance to the goal (all cells discovered) can be developed as follows:

A single worker can discover at most three cells per turn. Thus, n workers can discover at most 3*n cells. The minimum number of remaining turns is therefore "number of undiscovered cells / (3 * worker count)". This is the heuristic to use. This could even be improved by determining the maximum number of cells that each worker can discover in the next turn (will be max. 3 per worker). So overall heuristic would be "(undiscorvered cells - discoverable cells) / (3 * workers) + 1".

In each step you examine the node with the least overall cost (turns so far + heuristic). For the examined node, you calculate the costs for each surrounding node (possible movements of all workers) and go on.

Nico Schertler
  • 32,049
  • 4
  • 39
  • 70
  • *This could even be improved...* Why is it an improvement to the algorithm? It already returns an optimal solution, isn't it? Is +1 needed? – Adam Stelmaszczyk Aug 14 '14 at 13:56
  • Improved in terms of time complexity. – Nico Schertler Aug 14 '14 at 17:08
  • 1
    Ah, I see, it's more accurate heuristic, so it will examine less nodes, but still give optimal solution, right? My second question is about that +1 in *(undiscorvered cells - discoverable cells) / (3 * workers) + 1*, is it necessary? I think it can be removed without impact. – Adam Stelmaszczyk Aug 14 '14 at 17:38
  • @AdamStelmaszczyk: An A* without a heuristic is just a brute-force search. You'll get the same result, but _significantly_ slower. Use the heuristic. – Mooing Duck Aug 14 '14 at 18:04
  • @MooingDuck: No, A* without heuristic is Dijkstra's algorithm. And you wouldn't call Dijkstra's algorithm brute force, would you? – Nico Schertler Aug 14 '14 at 18:54
  • Ah, that +1. If you subtract the +1 for all heuristics, there will be no functional impact. However, the connection to minimum turns left is not so obvious any more. – Nico Schertler Aug 14 '14 at 18:56
  • @NicoSchertler I see, thank you. (*Is +1 needed?* was not reffering to the up-vote :D I already up-voted your answer) – Adam Stelmaszczyk Aug 14 '14 at 19:39
1

Strictly speaking, the main part of this answer may be considered as "Not An Answer". So to first cover the actual question:

What is the algorithm that discovers all the cells with as least turns as possible?

Answer: In each step, you can compute all possible successors of the current state. Then the successors of these successors. This can be repeated recursively, until one of the successors contains no more #-fields. The sequence of states through which this successor was reached is optimal regarding the number of moves that have been necessary to reach this state.


So far, this is trivial. But of course, this is not feasible for a "large" map and/or a "large" number of workers.

As mentioned in the comments: I think that finding the optimal solution may be an NP-complete problem. In any case, it's most likely at least a tremendously complicated optimization problem where you may employ some rather sophisticated techniques to find the optimal solution in optimal time.

So, IMHO, the only feasible approach for tackling this are heuristics.

Several approaches can be imagined here. However, I wanted to give it a try, with a very simple approach. The following MCVE accepts the definition of the map as a rectangular string (empty spaces represent "invalid" regions, so it's possible to represent non-rectangular maps with that). The workers are simply enumerated, from 0 to 9 (limited to this number, at the moment). The string is converted into a MapState that consists of the actual map, as well as the paths that the workers have gone through until then.

The actual search here is a "greedy" version of the exhaustive search that I described in the first paragraph: Given an initial state, it computes all successor states. These are the states where each worker has moved in either direction (e.g. 64 states for 3 workers - of course these are "filtered" to make sure that workers don't leave the map or move to the same field).

These successor states are stored in a list. Then it searches the list for the "best" state, and again computes all successors of this "best" state and stores them in the list. Sooner or later, the list contains a state where no fields are missing.

The definition of the "best" state is where the heuristics come into play: A state is "better" than another when there are fewer fields missing (unvisited). When two states have an equal number of missing fields, then the average distance of the workers to the next unvisited fields serves as the criterion to decide which one is "better".

This finds and a solution for the example that is contained in the code below rather quickly, and prints it as the lists of positions that each worker has to visit in each turn.

Of course, this will also not be applicable to "really large" maps or "many" workers, because the list of states will grow rather quickly (one could consider dropping the "worst" solutions to speed this up a little, but this may have caveats, like being stuck in local optima). Additionally, one can easily think of cases where the "greedy" strategy does not give optimal results. But until someone posts an MVCE that always computes the optimal solution in polynomial time, maybe someone finds this interesting or helpful.

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

}
Community
  • 1
  • 1
Marco13
  • 53,703
  • 9
  • 80
  • 159