简体   繁体   中英

Shortest path in a grid between two points. With a catch

I have this problem where I have to find the shortest path in an NxM grid from point A (always top left) to point B (always bottom right) by only moving right or down. Sounds easy, eh? Well here's the catch: I can only move the number shown on the tile I'm sitting on at the moment. Let me illustrate:

2 5 1 2
9 2 5 3
3 3 1 1
4 8 2 7

In this 4x4 grid the shortest path would take 3 steps, walking from top left 2 nodes down to 3, and from there 3 nodes right to 1, and then 1 node down to the goal.

[2] 5  1  2
 9  2  5  3
[3] 3  1 [1]
 4  8  2 [7]

If not for the shortest path, I could also be taking this route:

[2] 5 [1][2]
 9  2  5  3
 3  3  1 [1]
 4  8  2 [7]

That would unfortunately take a whopping 4 steps , and thus, is not in my interest. That should clear things out a bit. Now about the input.


The user inputs the grid as follows:

5 4      // height and width
2 5 2 2  //
2 2 7 3  // the
3 1 2 2  // grid
4 8 2 7  //
1 1 1 1  //

Homework

I have thought this through, but cannot come to a better solution than to simplify the inputted grid into an unweighed (or negative-weight) graph and run something like dijkstra or A* (or something along those lines) on it. Well... this is the part where I get lost. I implemented something to begin with (or something to throw to thrash right away). It's got nothing to do with dijkstra or A* or anything; just straight-forward breadth-first search.


The Code

#include <iostream>
#include <vector>

struct Point;

typedef std::vector<int> vector_1D;
typedef std::vector< std::vector<int> > vector_2D;
typedef std::vector<Point> vector_point;

struct Point {
    int y, x;
    vector_point Parents;
    Point(int yPos = 0, int xPos = 0) : y(yPos), x(xPos) { }

    void operator << (const Point& point) { this->Parents.push_back(point); }
};

struct grid_t {
    int height, width;
    vector_2D tiles;

    grid_t() // construct the grid
    { 
        std::cin >> height >> width; // input grid height & width

        tiles.resize(height, vector_1D(width, 0)); // initialize grid tiles

        for(int i = 0; i < height; i++)     //
            for(int j = 0; j < width; j++)  // input each tile one at a time
                std::cin >> tiles[i][j];    // by looping through the grid
    }
};

void go_find_it(grid_t &grid)
{
    vector_point openList, closedList;
    Point previous_node; // the point is initialized as (y = 0, x = 0) if not told otherwise
    openList.push_back(previous_node); // (0, 0) is the first point we want to consult, of course

    do
    {
        closedList.push_back(openList.back()); // the tile we are at is good and checked. mark it so.
        openList.pop_back(); // we don't need this guy no more

        int y = closedList.back().y; // now we'll actually
        int x = closedList.back().x; // move to the new point

        int jump = grid.tiles[y][x]; // 'jump' is the number shown on the tile we're standing on.

        if(y + jump < grid.height) // if we're not going out of bounds
        { 
            openList.push_back(Point(y+jump, x)); // 
            openList.back() << Point(y, x); // push in the point we're at right now, since it's the parent node
        }
        if(x + jump < grid.width) // if we're not going out of bounds
        { 
            openList.push_back(Point(y, x+jump)); // push in the new promising point
            openList.back() << Point(y, x); // push in the point we're at right now, since it's the parent node
        }
    }
    while(openList.size() > 0); // when there are no new tiles to check, break out and return
}

int main()
{
    grid_t grid; // initialize grid

    go_find_it(grid); // basically a brute-force get-it-all-algorithm

    return 0;
}

I should probably also point out that the running time cannot exceed 1 second, and the maximum grid height and width is 1000. All of the tiles are also numbers from 1 to 1000.

Thanks.


Edited Code

#include <iostream>
#include <vector>

struct Point;

typedef std::vector<int> vector_1D;
typedef std::vector< std::vector<int> > vector_2D;
typedef std::vector<Point> vector_point;

struct Point {
    int y, x, depth;
    vector_point Parents;
    Point(int yPos = 0, int xPos = 0, int dDepth = 0) : y(yPos), x(xPos), depth(dDepth) { }

    void operator << (const Point& point) { this->Parents.push_back(point); }
};

struct grid_t {
    int height, width;
    vector_2D tiles;

    grid_t() // construct the grid
    { 
        std::cin >> height >> width; // input grid height & width

        tiles.resize(height, vector_1D(width, 0)); // initialize grid tiles

        for(int i = 0; i < height; i++)     //
            for(int j = 0; j < width; j++)  // input each tile one at a time
                std::cin >> tiles[i][j];    // by looping through the grid
    }
};

int go_find_it(grid_t &grid)
{
    vector_point openList, closedList;
    Point previous_node(0, 0, 0); // the point is initialized as (y = 0, x = 0, depth = 0) if not told otherwise
    openList.push_back(previous_node); // (0, 0) is the first point we want to consult, of course

    int min_path = 1000000;

    do
    {
        closedList.push_back(openList[0]); // the tile we are at is good and checked. mark it so.
        openList.erase(openList.begin()); // we don't need this guy no more

        int y = closedList.back().y; // now we'll actually move to the new point
        int x = closedList.back().x; //
        int depth = closedList.back().depth; // the new depth

        if(y == grid.height-1 && x == grid.width-1) return depth; // the first path is the shortest one. return it

        int jump = grid.tiles[y][x]; // 'jump' is the number shown on the tile we're standing on.

        if(y + jump < grid.height) // if we're not going out of bounds
        { 
            openList.push_back(Point(y+jump, x, depth+1)); // 
            openList.back() << Point(y, x); // push in the point we're at right now, since it's the parent node
        }
        if(x + jump < grid.width) // if we're not going out of bounds
        { 
            openList.push_back(Point(y, x+jump, depth+1)); // push in the new promising point
            openList.back() << Point(y, x); // push in the point we're at right now, since it's the parent node
        }
    }
    while(openList.size() > 0); // when there are no new tiles to check, break out and return false

    return 0;
}

int main()
{
    grid_t grid; // initialize grid

    int min_path = go_find_it(grid); // basically a brute-force get-it-all-algorithm

    std::cout << min_path << std::endl;
    //system("pause");
    return 0;
}

The program now prints the correct answer. Now I have to optimize (run time is way too big). Any hints on this one? Optimizing is the one thing I suck at.


The Answer

In the end the solution appeared to consist of little code. The less the better, as I like it. Thanks to Dejan Jovanović for the beautiful solution

#include <iostream>
#include <vector>
#include <algorithm>

struct grid_t {
    int height, width;
    std::vector< std::vector<int> > tiles;
    std::vector< std::vector<int> > distance;

    grid_t() // construct the grid
    { 
        std::cin >> height >> width; // input grid height & width

        tiles.resize(height, std::vector<int>(width, 0)); // initialize grid tiles
        distance.resize(height, std::vector<int>(width, 1000000)); // initialize grid tiles

        for(int i = 0; i < height; i++)     //
            for(int j = 0; j < width; j++)  // input each tile one at a time
                std::cin >> tiles[i][j];    // by looping through the grid
    }
};

int main()
{
    grid_t grid; // initialize grid

    grid.distance[0][0] = 0;
    for(int i = 0; i < grid.height; i++) {
        for(int j = 0; j < grid.width; j++) {
            if(grid.distance[i][j] < 1000000) {
                int d = grid.tiles[i][j];
                if (i + d < grid.height) {
                    grid.distance[i+d][j] = std::min(grid.distance[i][j] + 1, grid.distance[i+d][j]);
                }
                if (j + d < grid.width) {
                    grid.distance[i][j+d] = std::min(grid.distance[i][j] + 1, grid.distance[i][j+d]);
                }
            }
        }
    }
    if(grid.distance[grid.height-1][grid.width-1] == 1000000) grid.distance[grid.height-1][grid.width-1] = 0;
    std::cout << grid.distance[grid.height-1][grid.width-1] << std::endl;
    //system("pause");
    return 0;
}

You're overthinking it. :) Run a Breadth-First Search. The solution space is a binary tree, where each node branches into "right" or "down". From current point, generate the down point and right point, stuff their coordinates into a queue, repeat until at finish.

Without checking, something like this:

queue = [{ x: 0, y: 0, path: [] }] # seed queue with starting point
p = nil
do
  raise NoSolutionException if p.empty? # solution space exhausted
  p = queue.pop # get next state from the back of the queue
  break if p.x == MAX_X - 1 && p.y == MAX_Y - 1 # we found final state
  l = grid[p.x][p.y] # leap length

  # add right state to the front of the queue
  queue.unshift({x: p.x + l, y: p.y, path: p.path + [p] }) if p.x + l <= MAX_X

  # add down state to the front of the queue
  queue.unshift({x: p.x, y: p.y + l, path: p.path + [p] }) if p.y + l <= MAX_Y
end
puts p.path

Uglifying into C++ left as exercise for the reader :p

There is need to construct the graph, this can easily be solved with dynamic programming using one scan over the matrix.

You can set the distance matrix D[i,j] to +inf at the start, with D[0,0] = 0. While traversing the matrix you just do

if (D[i,j] < +inf) {
  int d = a[i, j];
  if (i + d < M) {
    D[i + d, j] = min(D[i,j] + 1, D[i + d, j]);
  }
  if (j + d < N) {
    D[i, j + d] = min(D[i,j] + 1, D[i, j + d]);
  }
}

The final minimal distance is in D[M -1, N-1]. If you wish to reconstruct the path you can keep a separate matrix that marks where the shortest path came from.

Build an unweighted directed graph:

  1. There are N x M vertices. In what follows, vertex v corresponds to grid square v .
  2. There is an arc from vertex u to v iff you can jump from grid square u to square v in a single move.

Now apply a shortest path algorithm from the top-right vertex to the bottom-left.

Finally, observe that you don't actually need to build the graph. You can simply implement the shortest path algoritm in terms of the original grid.

Start off with a brute force approach to get it to work, then optimize from there. The brute force is straight-forward: run it recursively. Take your two moves, recurse on those, and so on. Collect all the valid answers and retain the minimum. If the run time is too long, then you can optimize by a variety of means. For instance, some of the moves may be invalid (because they exceed a dimension of the grid) and can be eliminated, and so on. Keep optimizing until a worst case input runs at the desired speed.

Having said that, the performance requirements only make sense if you are using the same system and inputs, and even then there are some caveats. Big O notation is a much better way of analyzing the performance, plus it can point you to an algorithm and eliminate the need for profiling.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM