在协调无向图上计算 A*(A-star) 算法中的 f 成本

Posted

技术标签:

【中文标题】在协调无向图上计算 A*(A-star) 算法中的 f 成本【英文标题】:Calculate the f cost in A*(A-star) algorithm on coordinated undirected graph 【发布时间】:2021-12-19 03:03:16 【问题描述】:

我正在尝试在 react.js 中实现 A* 算法,但在实现 fScore 函数时我很困惑。我知道 f=g+h 其中 g 是从起始节点到当前节点的 gScore,h 是从 currentNode 到结束节点的启发式距离。我使用欧几里德距离计算了启发式,我在其中发送了当前节点和结束节点的坐标,但我不知道如何计算 gScore。 我图中的每个节点都有: ID, 姓名, X, 是的, connectedToIds:[] //neihbours 或 connectedNodes 列表。 更新:我为每个节点添加了变量 parentId、fscore、gscore、hscore。所以现在每个节点都有变量:id, 姓名, X, 是的, connectedToIds:[], fscore: 0, gscore: 0, hscore: 0, 父标识:空。 Update2: originLocationId 是起始节点的id。 destinationLocationId 是结束节点的 id。位置是所有节点的列表。 我的代码:

export default class TurnByTurnComponent extends React.PureComponent 
    constructor(props) 
        super(props);
    

    render() 
        const 
            destinationLocationId,
            locations,
            originLocationId
         = this.props;
        console.log(locations)
        console.log(originLocationId)
        console.log(destinationLocationId)


        var openedList = [];
        var closedList = [];

        if (destinationLocationId != null && originLocationId != null) 
            openedList.push(originLocationId);
            while (openedList.length != 0) 
                var currentLoc = openedList[0]; //minFvalue
                const currIndex = openedList.indexOf(currentLoc);
                openedList.splice(currIndex, 1); //deleting currentNode from openedList
                closedList.push(currentLoc) //adding currentNode to closedList

                if (currentLoc == destinationLocationId) 
                    //return path
                

                

            

        

        function heuristic(currentNode, endNode)  //euclidean distance
            var x = Math.pow(endNode.x - currentNode.x, 2);
            var y = Math.pow(endNode.y - currentNode.y, 2);
            var dist = Math.sqrt(x + y);
            return dist;
        

        function gScore(startNode, currentNode) 

        




        return (
            <div className="turn-by-turn-component">
                locations.map(loc => (
                    <li key=loc.id>
                        loc.name
                    </li>
                ))

                <TodoList
                    title="Mandatory work"
                    list=[
                      
                    ]
                />
                <TodoList
                    title="Optional work"
                    list=[
                      
                    ]
                />
            </div>
        );
    


TurnByTurnComponent.propTypes = 
    destinationLocationId: PropTypes.number,
    locations: PropTypes.arrayOf(PropTypes.shape(
        id: PropTypes.number.isRequired,
        name: PropTypes.string.isRequired,
        x: PropTypes.number.isRequired,
        y: PropTypes.number.isRequired,
        connectedToIds: PropTypes.arrayOf(PropTypes.number.isRequired).isRequired
    )),
    originLocationId: PropTypes.number
;

Update3:我的代码的新版本

export default class TurnByTurnComponent extends React.PureComponent 
    constructor(props) 
        super(props);
        this.state =  shortestPath: [] 
    


    render() 
        const 
            destinationLocationId,
            locations,
            originLocationId
         = this.props;


        if (destinationLocationId != null && originLocationId != null) 

            if (originLocationId == destinationLocationId)  //check if the startNode node is the end node
                return originLocationId;
            

            var openList = [];
            let startNode = getNodeById(originLocationId);
            let endNode = getNodeById(destinationLocationId)

            startNode.gcost = 0
            startNode.heuristic = manhattanDistance(startNode, endNode)
            startNode.fcost = startNode.gcost + startNode.heuristic;


            //start A*
            openList.push(startNode); //starting with the startNode 
            while (openList.length) 
                console.log("inside while")

                var currentNode = getNodeOfMinFscore(openList);

                if (currentIsEqualDistanation(currentNode)) 
                    var path = getPath(currentNode)
                    this.setState(
                        shortestPath: path,
                    );
                    return path //todo
                
                deleteCurrentFromOpenList(currentNode, openList);

                for (let neighbourId of currentNode.connectedToIds) 

                    var neighbourNode = getNodeById(neighbourId);
                    var currentNodeGcost = currentNode.gcost + manhattanDistance(currentNode,         neighbourNode);
                    console.log(currentNodeGcost)
                    console.log(neighbourNode.gcost)
                    if (currentNodeGcost < neighbourNode.gcost) 
                        console.log("Helloooo")
                        neighbourNode.parentId = currentNode.id;
                        // keep track of the path
                        // total cost saved in neighbour.g
                        neighbourNode.gcost = currentNodeGcost;
                        neighbourNode.heuristic = manhattanDistance(neighbourNode, endNode);
                        neighbourNode.fcost = neighbourNode.gcost + neighbourNode.heuristic;
                        if (!openList.includes(neighbourId)) 
                            openList.push(neighbourNode);
                        
                    
                
            
            return null;
        


        function deleteCurrentFromOpenList(currentNode, openList) 
            const currIndex = openList.indexOf(currentNode);
            openList.splice(currIndex, 1); //deleting currentNode from openList
        

        function currentIsEqualDistanation(currentNode) 
            //check if we reached out the distanation node
            return (currentNode.id == destinationLocationId)
        

        function getNodeById(id) 
            var node;
            for (let i = 0; i < locations.length; i++) 
                if (locations[i].id == id) 
                    node = locations[i]
                
            
            return node
        

        function getPath(endNode) 
            var path = []
            while (endNode.parentId) 
                path.push(endNode.name)
                endNode = endNode.parentId;
            
            return path;
        

        function getNodeOfMinFscore(openList) 
            var minFscore = openList[0].fcost; //initValue
            var nodeOfminFscore;
            for (let i = 0; i < openList.length; i++) 

                if (openList[i].fcost <= minFscore) 

                    minFscore = openList[i].fcost //minFvalue
                    nodeOfminFscore = openList[i]
                
            

            return nodeOfminFscore
        

        //manhattan distance is for heuristic and gScore. Here I use Manhattan instead of Euclidean 
        //because in this example we dont have diagnosal path.
        function manhattanDistance(startNode, endNode) 
            var x = Math.abs(endNode.x - startNode.x);
            var y = Math.abs(endNode.y - startNode.y);
            var dist = x + y;
            return dist;
        


        return (
            <div className="turn-by-turn-component">
                locations.map(loc => (
                    <li key=loc.id>
                        JSON.stringify(loc.name),
                    </li>
                ))
                <TodoList
                    title="Mandatory work"
                    list=
                        this.state.shortestPath
                    
                />
                <TodoList
                    title="Optional work"
                    list=[

                    ]
                />
            </div>
        );
    


TurnByTurnComponent.propTypes = 
    destinationLocationId: PropTypes.number,
    locations: PropTypes.arrayOf(PropTypes.shape(
        id: PropTypes.number.isRequired,
        name: PropTypes.string.isRequired,
        x: PropTypes.number.isRequired,
        y: PropTypes.number.isRequired,
        connectedToIds: PropTypes.arrayOf(PropTypes.number.isRequired).isRequired
    )),
    originLocationId: PropTypes.number
;

【问题讨论】:

【参考方案1】:

h 是启发式方法,是对到达最终节点所需的可能成本的合理猜测,g 是到达当前节点所花费的实际成本。在您的情况下,它甚至可能与您用于 h 的欧几里得距离相同。

在真实案例场景中,使用欧几里得距离,连接不同城市的图,h 是两个城市的空中距离,g 是它们的道路距离。

此外,如果您使用单调启发式(或低估启发式,例如欧几里德距离),则不需要关闭列表,因为已证明第一个找到的路径也是最短的:更长或已经访问过的路径将在被探索之前被丢弃。

可能令人困惑的是,您需要在探索图形的过程中跟踪g,而h 只是测量当前节点和结束节点之间的直线,g 测量之间的所有线您探索的节点到达当前。

// h
function heuristic(n1, n2) 
    return Math.sqrt(
        Math.pow(n1.x - n2.x, 2) +
        Math.pow(n1.y - n2.y, 2)
    );

// g - actually is the same as h
const cost = heuristic;
function astar(start, end, graph, h, g) 
    if (start.id == end.id)  return [ start.id ] 

    // omitted CLEAN-UP of the graph

    // close is not needed with an optimistic heuristic 
    // so I've commented the "close" parts.
    // An optimistic heuristic works also if you decomment them

    // var close = [];
    var open  = [];

    start.g = 0;
    start.h = h(start, end);
    start.f = start.g + start.h;

    open.push(start);

    while (open.length) 
        // since open is sorted, by popping
        // the last element we take the cheapest node
        var curr = open.pop();

        if (curr == end)  return resolvePath(curr, graph); 
        // close.push(curr);
        for (let nid of curr.connectedToIds) 
            // if (close.some(n => n.id == nid))  continue; 
            var neighbour = graph.find(n => n.id == nid);
            
            // HERE you compute and store
            // the current g of the node 
            var current_g = curr.g + g(curr, neighbour);

            var isBetter_g = false;
            var isInOpen = open.some(n => n.id == nid);

            // Some implementations skip this check 
            // because they assume that the cost function
            // is a non-negative distance.
            // if so you could check just the two nodes g
            // and insert the node if not already in the open set
            // because the default g of a node is 0
            if (!isInOpen) 
                // unexplored node, maybe we should explore it
                // later, so we add to the open set and
                // we update it with the current path cost
                open.push(neighbour)
                isBetter_g = true;
            
            else if (current_g < neighbour.g) 
                // the current path is better than
                // those already explored, we need to 
                // update the neighbour with the current path cost
                isBetter_g = true;
            
            if (isBetter_g) 
                // === path cost update ===

                // track current path
                neighbour.parent = curr.id;
           
                // HERE you keep track of the path
                // total cost saved in neighbour.g
                neighbour.g = current_g;
           
                // neighbour.h is redundant, can be stored directly in f
                // but keep it for debugging purpose
                neighbour.h = h(neighbour, end);

                neighbour.f = neighbour.g + neighbour.h;
                
                if (!isInOpen) 
                    // sorting makes the last element of
                    // open the cheapest node. We sort after
                    // the path cost update to ensure this property
                    open.sort((n1, n2) => n2.f - n1.f)
                
            
        
    

    // failure
    return []; // or return null

// utility to retrieve an array of ids
// from the tracked path
function resolvePath(end, graph) 
    let path = [ end.id ];
    while (end && end.parent >= 0) 
        path.unshift(end.parent);
        end = graph.find(n => n.id == end.parent);
    
    return path;

// example of using the function
astar(startNode, endNode, locations, heuristic, cost);

【讨论】:

谢谢。您能解释一下您使用 isBest_g 变量的方式吗?我的意思是我们为什么需要它?我们不能只检查最低 Fcost 吗? 如果你的移动不允许对角线移动,你的启发式应该使用节点之间的曼哈顿距离,它可以在没有 sqrt 或 pow 的情况下计算。 H = Math.abs(ax - bx) + Math.abs(ay - by) @AdamA isBest_g,也许不是最好的变量名(isBetter_g 可能会“更好”),跟踪是否应该更新或忽略邻居。是的,我们应该检查f 成本,我在移植代码时弄错了,因为开放集必须按f 排序,这样每次您执行open.pop 时,您实际上得到的节点更便宜。请注意,如果您发现一个未探索的节点,您必须将其添加到打开列表中(不检查f),这就是isBest_g 的目标。我将编辑答案以澄清 @AdamA 我鼓励阅读以下链接,因为我知道很多人已经根据这些文章编写了 A* 实现。 redblobgames.com @AdamA 我认为你错过/误解了g(curr, neighbour) 部分,如果current_g 从未不同于0。current_g = curr.g + g(curr, neighbour) -=-&gt; 0 + costcurrent_node -&gt; child_nodeg(curr, neighbour) 与您的代码中的 manhattanDistance(currentNode, neighbourNode) 相同。顺便说一句,您更新的代码对我来说似乎很好,至少在逻辑层面上,只是缺少非负检查(但它不适用于曼哈顿)。

以上是关于在协调无向图上计算 A*(A-star) 算法中的 f 成本的主要内容,如果未能解决你的问题,请参考以下文章

手撸golang 基本数据结构与算法 图的最短路径 A*(A-Star)算法

为啥A-star算法需要g(n)?

日日算法:Dijkstra算法

A*算法导航网格路径点寻路对比(A-Star VS NavMesh VS WayPoint)

A*算法基本原理

『Tarjan算法 无向图的割点与割边』