Kirjautuminen

Haku

Tehtävät

Keskustelu: Koodit: A*-hakualgoritmi C++:lla Python-moduuliksi

Sivun loppuun

osw [18.11.2008 10:30:21]

#

A*-algoritmia käytetään ratkaisun hakemiseen hakupuusta (kopypastaa wikipediasta :-). Eli suomeksi sanottuna, tällä voidaan toteuttaa esim. reitinhakua jos tiedetään lähtö- ja maalipisteet.

En ajatellut tässä sen kummemmin kertoa algoritmista, kun siihen on parempiakin selityksiä netti pullollaan (http://www.policyalmanac.org/games/aStarTutorial.htm). Suosittelenkin lukaisemaan tuon annetun artikkelin ainakin pintapuolisesti, niin pysyy kärryillä tässäkin koodissa.

Koodivinkin tarkoituksena on lähinnä näyttää, kuinka Boost-kirjaston avulla voidaan kohtuullisen kivuttomasti liittää C++ ja Python yhteen (olisi tässä kyllä melkeinpä kahteen vinkkiin ollut ainesta).

C++ osuuden kääntäminen onnistuu (YMMV) komennolla:

g++ -o astarcpp.so -Wall -I/usr/include/python2.6/ -lboost_python -lpython2.6 -fPIC -shared astar.cpp

Riippuvuuksia:
- Boost (http://www.boost.org/)
- Python (http://www.python.org/)

(eikä sitten tartte kertoa, että mie en osaa Pyyttonia ja/tai C++:aa ... ;-)

astar.cpp

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

#include <boost/foreach.hpp>
#include <boost/python.hpp>

#define foreach BOOST_FOREACH
#define reverse_foreach BOOST_REVERSE_FOREACH

// spice things up
using namespace std;
using namespace boost;

/**
 * PathNode is used for in the A* algorithm to represent
 * different locations.
 */
struct PathNode
{
    PathNode()
        : f(0), g(0), h(0), x(0), y(0), parent(NULL)
    {}

    PathNode(int x, int y)
        : f(0), g(0), h(0), x(x), y(y), parent(NULL)
    {}

    bool operator<(const PathNode& node)
    {
        return f < node.f;
    }

    bool operator>(const PathNode& node)
    {
        return f > node.f;
    }

    bool operator==(const PathNode& node)
    {
        return (x == node.x && y == node.y);
    }

    bool operator!=(const PathNode& node)
    {
        return !operator==(node);
    }

    void update() { f = g + h; }

    // f = g + h
    int f;

    // the cost of movement from starting node to this node along the
    // generated path
    int g;

    // the cost of movement from this node to the goal node ("heuristic")
    int h;

    // this node's location
    int x, y;

    // used for traversing the generated path
    PathNode *parent;
};

struct PathNodePtrCmp: public binary_function<PathNode*, PathNode*, bool>
{
    bool operator()(PathNode* a, PathNode* b)
    {
        return (*a > *b);
    }
};

// a little helper struct to manage our nodes
struct PathNodeHeap
{
    void push(PathNode* v)
    {
        heap.push_back(v);
        push_heap(heap.begin(), heap.end(), PathNodePtrCmp());
    }

    PathNode* pop()
    {
        pop_heap(heap.begin(), heap.end(), PathNodePtrCmp());
        PathNode* res = heap.back();
        heap.pop_back();
        return res;
    }

    PathNode* top()
    {
        return heap.front();
    }

    PathNode* find(int x, int y)
    {
        PathNode* node;

        foreach(node, heap)
        {
            if (node->x == x && node->y == y)
            {
                return node;
            }
        }

        return NULL;
    }

    void sort()
    {
        sort_heap(heap.begin(), heap.end(), PathNodePtrCmp());
    }

    bool empty()
    {
        return heap.empty();
    }

    vector<PathNode*> heap;
};

// here is the algorithm
python::object search_path(python::object map, python::tuple start,
                           python::tuple goal)
{
    PathNodeHeap open, closed;

    PathNode* current = NULL;

    PathNode* first = new PathNode(python::extract<int>(start[0]),
                                   python::extract<int>(start[1]));

    PathNode dest = PathNode(python::extract<int>(goal[0]),
                             python::extract<int>(goal[1]));

    open.push(first);

    // stop, when the open list is empty (path was not found)
    while (!open.empty())
    {
        // take the node that has the lowest f-score from the open list
        // and switch it to the closed list
        current = open.pop();
        closed.push(current);

        // when the goal node is switched to the closed list,
        // we have found our path
        if (*current == dest)
        {
            break;
        }

        // we'll want to check every 8 directions
        int directions[][2] = { {-1, 0}, {1, 0},
                                {0, -1}, {0, 1},
                                {-1, -1}, {1, 1},
                                {1, -1}, {-1, 1} };

        for (int i = 0; i < 8; ++i)
        {
            int dx = directions[i][0];
            int dy = directions[i][1];

            int x = current->x + dx;
            int y = current->y + dy;

            bool walkable = python::extract<bool>(
                map.attr("walkable")(x, y));

            // ignore if on closed list and if not walkable
            if (!closed.find(x, y) && walkable)
            {
                PathNode* node = open.find(x, y);

                if (node == NULL)
                {
                    // node wasn't on the open list,
                    // so add it to there

                    node = new PathNode(x, y);

                    node->parent = current;

                    // the cost of movement is bigger when movement is diagonal
                    if (i > 3)
                    {
                        node->g = current->g + 14;
                    }
                    else
                    {
                        node->g = current->g + 10;
                    }

                    node->h = abs(dest.x - x) + abs(dest.y - y);
                    node->update();

                    open.push(node);
                }
                else
                {
                    // node was found from the open list,
                    // check if current path is better ...

                    int newg = current->g;

                    if (i > 3)
                    {
                        newg += 14;
                    }
                    else
                    {
                        newg += 10;
                    }

                    if (newg < node->g)
                    {
                        node->g = newg;
                        node->parent = current;
                        node->update();
                        open.sort();
                    }
                }
            }
        }

        // if current is NULL after this loop, there was no path found
        current = NULL;
    }

    // collect the results in a nice python list

    python::list result;

    while (current)
    {
        result.append(python::make_tuple(current->x, current->y));
        current = current->parent;
    }

    foreach (PathNode *p, open.heap)
    {
        delete p;
    }

    foreach (PathNode *p, closed.heap)
    {
        delete p;
    }

    return result;
}

BOOST_PYTHON_MODULE(astarcpp)
{
    python::def("search", search_path);
}

astar-test.py

#!/usr/bin/env python

from __future__ import print_function
import astarcpp

class GameMap:
    def __init__(self, filename):
        self.data = []
        self.load(filename)

    def load(self, filename):
        f = open(filename)
        lines = f.readlines()
        f.close()

        for line in lines:
            line = line.strip()
            tmp = map(ord, line)
            self.data.append(tmp)

        self.w = len(self.data[0])
        self.h = len(self.data)

    def validCoord(self, x, y):
        return (x >= 0 and x < self.w and y >= 0 and y < self.h)

    # this must be implemented, because it's called from C++
    def walkable(self, x, y):
        return (self.validCoord(x, y) and self.data[y][x] != ord("#"))

    def __str__(self):
        tmprepr = []
        repr = ""

        for line in data:
            tmprepr.append("".join(map(chr, line)))

        repr = "\n".join(tmprepr)

        return repr

    def findpath(self, start, goal):
        return astarkikkare.search(self, start, goal)

    def print_with_path(self, start, goal):
        path = self.findpath(start, goal)

        for y in range(len(self.data)):
            for x in range(len(self.data[0])):
                if (x, y) in path:
                    print("x", sep="", end="")
                else:
                    print(chr(self.data[y][x]), sep="", end="")

            print()


a = GameMap("astar-test.map")
a.print_with_path((19,1), (1, 10))

astar-test.map

#####################
####      #         #
#    ####    ### ####
# #####   ## ### ## #
# ##     ### # #  # #
# #       ## # # ## #
# #  ##    # # # ## #
# #  ###   # #      #
# #   ###  # # # ####
# #   ##   # #   ####
#     #        # ####
#####################

Sisuaski [21.11.2008 02:48:58]

#

bool walkable = python::extract<bool>(
    map.attr("walkable")(x, y));

Tämä vaikuttaa sangen epäoptimaaliselta operaatiolta algoritmin sisäsilmukassa. Olisi tehokkaampaa lukea koko kartta kerralla C++-koodin puolelle.

if (i > 3)
{
    node->g = current->g + 14;
}
else
{
    node->g = current->g + 10;
}

node->h = abs(dest.x - x) + abs(dest.y - y);

Tämä on täysin ristiriitaista: lasket nykyisen kuljetun matkan 10-kertaisena, mutta sijainnin hyvyysfunktion arvon määrität suoraan manhattan-distancena loppupisteeseen.
Tässä on siis kaksi virheoletusta: Ensiksikin koodissa oletetaan hyvyysfunktion laskussa, että sivuttain liikkuminen on mahdotonta. Toiseksi et suorita siinä samaa kymmenellä kertomista, kuin kuljetun matkan laskussa. Nämä virheet yhdistettynä kyllä sattuvat toimimaan oikein useimmissa tapauksissa, mutta nyt koodi ei ole suorituskyvyltään juuri Dijkstran algoritmia parempi.

Lisähidasteina koodissa ovat vielä sisäsilmukassa suoritettavat open.find() ja closed.find() -funktiokutsut, jotka vievät pahimmillaan lineaarisen ajan, ja jotka voitaisiin korvata esim. kartan kokoisella taulukolla tai hajautustaululla.

Koodivinkissä ainoaa järkevää sisältöä on lähinnä boostin avulla tapahtuva C++:n ja pythonin yhdistäminen, joten olisi ollut parempi, jos se olisi nostettu enemmän pääaiheeksi huonon A*-toteutuksen sijaan.

osw [21.11.2008 11:25:08]

#

Tjoo, ton 10-kertaisuuden vetäisin suoraan tuolta artikkelista johon viittasin, mutta en tietysti tuota hyvyysarvoa tajunnut ottaa mukaan. Mitä tarkoitat tuolla, että "sivuttain liikkuminen on mahdotonta"?

Mielessä kävi tuo taulukkototeutus listalta etsimisen sijaan kyllä, mutten sitä tähän jostain syystä kirjoitellut. Täytynee tutkailla. :-)

Sisuaski [21.11.2008 11:34:17]

#

Ääh, piti siis kirjoittaa vinottain eikä sivuttain liikkuminen.
Jos etäisyys lasketaan abs(x-ex)+abs(y-ey), niin silloin etäisyysarvio maaliin on liian pessimistinen, sillä esim. tapauksessa: x=1,y=1,ex=2,ey=2 etäisyys olisi vain sqrt(2)=1,4142..., mutta tuo kaava antaa tuloksen 2. (Tässä siis (x,y) on nykyinen sijainti ja (ex,ey) maalipiste.)

A*-algoritmissa maalin etäisyyden arvioinnissa on tärkeää, että etäisyysarvio on optimistinen, eli se ei saa olla koskaan pidempi kuin todellinen etäisyys. Jos tämä ei toteudu, ei tuloksena välttämättä saada lyhintä reittiä.

osw [21.11.2008 12:19:23]

#

Niinjuu. Kyllähän miekin sen ymmärsin tuota tehdessä, ettei tuo kaava anna ihan täydellistä arviota siitä matkasta. Olettaisin kuitenkin, että peruspikkupeleihin tuo soveltuu paremman puutteessa ihan riittävästi. :-)

ville-v [30.11.2008 10:01:27]

#

Miksi teit vertailua varten luokan etkä funktiota? Eikö

inline bool PathNodePtrCmp(PathNode* a, PathNode* b)
{
        return (*a > *b);
}

olisi ollut järkevämpi, kun kuitenkin kutsut vain yhtä luokan sisäfunktiota.

osw [30.11.2008 11:47:30]

#

Tjaa-a. Aika hyvä kysymys. Ei varmaan käynyt mielessä.. :-P

Metabolix [06.08.2015 16:05:40]

#

Koska em. virheitä ei vinkistä ikinä korjattu ja koska vinkki muutenkin oli hieman sekava, kirjoitin itse uuden version.


Sivun alkuun

Vastaus

Aihe on jo aika vanha, joten et voi enää vastata siihen.

Tietoa sivustosta