Erlang 中 Dijkstra 算法使用什么数据结构?
免责声明:作者是 Erlang 新手。
想象一下,我们有一个由 1M 个节点组成的图,每个节点有 0-4 个邻居(边从每个节点发散到这些邻居,所以图是有向且连通的)。
以下是我选择的数据结构:
为了存储图表,我使用有向图,它基于 ETS 表。这允许快速(O(1))访问节点的邻居。
对于未访问节点列表,我使用gb_sets:take_smallest(节点已经排序,获取后同时删除)。
对于前驱列表,我使用 dict 结构,它允许通过以下方式存储前驱:{Node1,Node1_predecessor},{Node2,Node2_predecessor}。
对于访问节点的列表,我使用一个简单的列表。
问题:
- 当我尝试更新有向图结构和 Unvisited_nodes 结构中节点的权重时,代码变得非常难以阅读和维护。将一个“对象”与需要在两个数据结构中同时更新的“字段”保留在一起似乎不是正确的方法。这样做的正确方法是什么?
- 同样的问题是关于前辈名单的。我应该在哪里存储节点“对象”的前驱“字段”?也许在图中(有向图结构)?
- 也许我应该根据进程和消息而不是对象(节点和边)及其字段(权重)重新思考整个 Dijkstra 算法?
UPD:
以下是基于 Antonakos 的建议的代码:
dijkstra(Graph,Start_node_name) ->
io:format("dijkstra/2: start~n"),
Paths = dict:new(),
io:format("dijkstra/2: initialized empty Paths~n"),
Unvisited = gb_sets:new(),
io:format("dijkstra/2: initialized empty Unvisited nodes priority queue~n"),
Unvisited_nodes = gb_sets:insert({0,Start_node_name,root},Unvisited),
io:format("dijkstra/2: Added start node ~w with the weight 0 to the Unvisited nodes: ~w~n", [Start_node_name, Unvisited_nodes]),
Paths_updated = loop_through_nodes(Graph,Paths,Unvisited_nodes),
io:format("dijkstra/2: Finished searching for shortest paths: ~w~n", [Paths_updated]).
loop_through_nodes(Graph,Paths,Unvisited_nodes) ->
%% We need this condition to stop looping through the Unvisited nodes if it is empty
case gb_sets:is_empty(Unvisited_nodes) of
false ->
{{Current_weight,Current_name,Previous_node}, Unvisited_nodes_updated} = gb_sets:take_smallest(Unvisited_nodes),
case dict:is_key(Current_name,Paths) of
false ->
io:format("loop_through_nodes: Found a new smallest unvisited node ~w~n",[Current_name]),
Paths_updated = dict:store(Current_name,{Previous_node,Current_weight},Paths),
io:format("loop_through_nodes: Updated Paths: ~w~n",[Paths_updated]),
Out_edges = digraph:out_edges(Graph,Current_name),
io:format("loop_through_nodes: Ready to iterate through the out edges of node ~w: ~w~n",[Current_name,Out_edges]),
Unvisited_nodes_updated_2 = loop_through_edges(Graph,Out_edges,Paths_updated,Unvisited_nodes_updated,Current_weight),
io:format("loop_through_nodes: Looped through out edges of the node ~w and updated Unvisited nodes: ~w~n",[Current_name,Unvisited_nodes_updated_2]),
loop_through_nodes(Graph,Paths_updated,Unvisited_nodes_updated_2);
true ->
loop_through_nodes(Graph,Paths,Unvisited_nodes_updated)
end;
true ->
Paths
end.
loop_through_edges(Graph,[],Paths,Unvisited_nodes,Current_weight) ->
io:format("loop_through_edges: No more out edges ~n"),
Unvisited_nodes;
loop_through_edges(Graph,Edges,Paths,Unvisited_nodes,Current_weight) ->
io:format("loop_through_edges: Start ~n"),
[Current_edge|Rest_edges] = Edges,
{Current_edge,Current_node,Neighbour_node,Edge_weight} = digraph:edge(Graph,Current_edge),
case dict:is_key(Neighbour_node,Paths) of
false ->
io:format("loop_through_edges: Inserting new neighbour node ~w into Unvisited nodes~n",[Current_node]),
Unvisited_nodes_updated = gb_sets:insert({Current_weight+Edge_weight,Neighbour_node,Current_node},Unvisited_nodes),
io:format("loop_through_edges: The unvisited nodes are: ~w~n",[Unvisited_nodes_updated]),
loop_through_edges(Graph,Rest_edges,Paths,Unvisited_nodes_updated,Current_weight);
true ->
loop_through_edges(Graph,Rest_edges,Paths,Unvisited_nodes,Current_weight)
end.
Disclaimer: The author is a newbie in Erlang.
Imagine, we have a graph consisting of 1M nodes, and each node has 0-4 neighbours (the edges are emanating from each node to those neighbours, so the graph is directed and connected).
Here is my choice of data structures:
To store the graph I use digraph, which is based on ETS tables. This allows fast (O(1)) access to the neighbours of a node.
For the list of unvisited nodes, I use gb_sets:take_smallest (the node is already sorted, and it is simultaneously deleted after fetching).
For the list of predecessors I use the dict structure, which allows to store the predecessors in the following way: {Node1,Node1_predecessor},{Node2,Node2_predecessor}.
For the list of visited nodes I use a simple list.
Problems:
- The code becomes very hard to read and maintain when I try to update the weight of a node both in the digraph structure and in the Unvisited_nodes structure. It doesn't seem the right way to keep one 'object' with the 'fields' that need to be updated in two data structures simultaneously. What is the right way to do that?
- The same question is about predecessors list. Where should I store the predecessor 'field' of a node 'object'? Maybe in the Graph (digraph structure)?
- Maybe I should rethink the whole Dijkstra's algorithm in terms of processes and messages instead of objects (nodes and edges) and their fields(weights)?
UPD:
Here is the code based on the recommendations of Antonakos:
dijkstra(Graph,Start_node_name) ->
io:format("dijkstra/2: start~n"),
Paths = dict:new(),
io:format("dijkstra/2: initialized empty Paths~n"),
Unvisited = gb_sets:new(),
io:format("dijkstra/2: initialized empty Unvisited nodes priority queue~n"),
Unvisited_nodes = gb_sets:insert({0,Start_node_name,root},Unvisited),
io:format("dijkstra/2: Added start node ~w with the weight 0 to the Unvisited nodes: ~w~n", [Start_node_name, Unvisited_nodes]),
Paths_updated = loop_through_nodes(Graph,Paths,Unvisited_nodes),
io:format("dijkstra/2: Finished searching for shortest paths: ~w~n", [Paths_updated]).
loop_through_nodes(Graph,Paths,Unvisited_nodes) ->
%% We need this condition to stop looping through the Unvisited nodes if it is empty
case gb_sets:is_empty(Unvisited_nodes) of
false ->
{{Current_weight,Current_name,Previous_node}, Unvisited_nodes_updated} = gb_sets:take_smallest(Unvisited_nodes),
case dict:is_key(Current_name,Paths) of
false ->
io:format("loop_through_nodes: Found a new smallest unvisited node ~w~n",[Current_name]),
Paths_updated = dict:store(Current_name,{Previous_node,Current_weight},Paths),
io:format("loop_through_nodes: Updated Paths: ~w~n",[Paths_updated]),
Out_edges = digraph:out_edges(Graph,Current_name),
io:format("loop_through_nodes: Ready to iterate through the out edges of node ~w: ~w~n",[Current_name,Out_edges]),
Unvisited_nodes_updated_2 = loop_through_edges(Graph,Out_edges,Paths_updated,Unvisited_nodes_updated,Current_weight),
io:format("loop_through_nodes: Looped through out edges of the node ~w and updated Unvisited nodes: ~w~n",[Current_name,Unvisited_nodes_updated_2]),
loop_through_nodes(Graph,Paths_updated,Unvisited_nodes_updated_2);
true ->
loop_through_nodes(Graph,Paths,Unvisited_nodes_updated)
end;
true ->
Paths
end.
loop_through_edges(Graph,[],Paths,Unvisited_nodes,Current_weight) ->
io:format("loop_through_edges: No more out edges ~n"),
Unvisited_nodes;
loop_through_edges(Graph,Edges,Paths,Unvisited_nodes,Current_weight) ->
io:format("loop_through_edges: Start ~n"),
[Current_edge|Rest_edges] = Edges,
{Current_edge,Current_node,Neighbour_node,Edge_weight} = digraph:edge(Graph,Current_edge),
case dict:is_key(Neighbour_node,Paths) of
false ->
io:format("loop_through_edges: Inserting new neighbour node ~w into Unvisited nodes~n",[Current_node]),
Unvisited_nodes_updated = gb_sets:insert({Current_weight+Edge_weight,Neighbour_node,Current_node},Unvisited_nodes),
io:format("loop_through_edges: The unvisited nodes are: ~w~n",[Unvisited_nodes_updated]),
loop_through_edges(Graph,Rest_edges,Paths,Unvisited_nodes_updated,Current_weight);
true ->
loop_through_edges(Graph,Rest_edges,Paths,Unvisited_nodes,Current_weight)
end.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(1)
您选择的数据结构看起来不错,因此主要是在其中插入什么内容以及如何使它们保持最新的问题。我建议如下(我稍微更改了名称):
Result
:将Node
映射到的
,其中dict
{Cost, Prev}Cost
是到Node
的路径的总成本,Prev
是路径上其前身.Open
:{Cost, Node, Prev}
的gb_sets
结构。具有
{EdgeCost, NextNode}
形式的边的图。搜索结果由
Result
表示,并且图表根本不更新。没有多重处理或消息传递。算法如下:
在
Open
中插入{0, StartNode, Nil}
,其中Nil
是标记结束的内容令
{{Cost, Node, Prev}, Open1} = gb_sets:take_smallest(Open)
。如果Node
已经在Result
中,则不执行任何操作;否则将{Cost, Node, Prev}
添加到Result
,并为Node
的每条边{EdgeCost, NextNode}
> 将{Cost + EdgeCost, NextNode, Node}
添加到Open1
,但前提是NextNode
尚未在Result.继续
Open1
,直到集合为空。Dijkstra 算法要求对
Open
集执行decrease_key()
操作。由于gb_sets
不容易支持这一点,因此我们使用了为NextNode
插入元组的解决方法,即使NextNode
可能存在于中>已经打开
。这就是为什么我们检查从Open
中提取的节点是否已在Result
中。优先级队列使用的扩展讨论 将
优先级队列与 Dijkstra 算法结合使用的方法有多种。
在维基百科的标准版本中,仅插入节点
v
一次,但当v
的成本和前身发生更改时,v
的位置会更新。通常通过用
add v to Q
替换decrease-key v in Q
来简化实现。这意味着v
可以添加多次,因此算法必须检查从队列中提取的u
是否尚未添加到结果中。在我的版本中,我将上面的整个块替换为
add v to Q
。因此,队列将包含更多条目,但由于它们总是按顺序提取,因此不会影响算法的正确性。如果您不需要这些额外的条目,您可以使用字典来跟踪每个节点的最低成本。Your choice of data structures looks OK, so it is mostly a question of what to insert in them and how to keep them up to date. I'd suggest the following (I have changed the names a bit):
Result
: Adict
mappingNode
to{Cost, Prev}
, whereCost
is the total cost of the path toNode
andPrev
is its predecessor on the path.Open
: Agb_sets
structure of{Cost, Node, Prev}
.A graph with edges of the form
{EdgeCost, NextNode}
.The result of the search is represented by
Result
and the graph isn't updated at all. There is no multiprocessing or message passing.The algorithm goes as follows:
Insert
{0, StartNode, Nil}
inOpen
, whereNil
is something that marks the end of the path.Let
{{Cost, Node, Prev}, Open1} = gb_sets:take_smallest(Open)
. IfNode
is already inResult
then do nothing; otherwise add{Cost, Node, Prev}
toResult
, and for every edge{EdgeCost, NextNode}
ofNode
add{Cost + EdgeCost, NextNode, Node}
toOpen1
, but only ifNextNode
isn't already inResult
. Continue withOpen1
until the set is empty.Dijkstra's algorithm asks for a
decrease_key()
operation on theOpen
set. Since this isn't readily supported bygb_sets
we have used the workaround of inserting a tuple forNextNode
even ifNextNode
might be present inOpen
already. That's why we check if the node extracted fromOpen
is already inResult
.Extended discussion of the use of the priority queue
There are several ways of using a priority queue with Dijkstra's algorithm.
In the standard version of Wikipedia a node
v
is inserted only once but the position ofv
is updated when the cost and predecessor ofv
is changed.Implementations often simplify by replacing
decrease-key v in Q
withadd v to Q
. This means thatv
can be added more than once, and the algorithm must therefore check that anu
extracted from the queue hasn't already been added to the result.In my version I am replacing the entire block above with
add v to Q
. The queue will therefore contain even more entries, but since they are always extracted in order it doesn't affect the correctness of the algorithm. If you don't want these extra entries, you can use a dictionary to keep track of the minimum cost for each node.