我正在使用高级学生设置在Scheme中编写一个函数。该函数遍历图G并查看顶点X和Y之间是否存在路径。它"有点"有效,但并非在所有情况下都有效。我知道问题在哪里,但我不知道如何解决它。首先,我看看我正在查看的顶点X是否已被访问,如果不是我继续并将其标记为已访问。然后有功能find-adju。该函数返回给定顶点的所有邻居的列表。例如,如果我有这样的图表:
(define-struct graph (vertices edges))
(define-struct vertice (name visited))
(define-struct edge (start-vertice end-vertice length))
(define vertices-list
(list (make-vertice 0 0)
(make-vertice 1 0)
(make-vertice 2 0)
(make-vertice 3 0)
(make-vertice 4 0)
)
)
(define edges-list
(list (make-edge 0 1 0)
(make-edge 0 2 0)
(make-edge 1 3 0)
(make-edge 2 0 0)
(make-edge 2 4 0)
(make-edge 4 2 0)
)
)
(define G (make-graf vertices-list edge-list))
(traverse-graph? 0 4 G)
然后对于给定的顶点0,它将返回列表(1 2)。接下来,我查看列表并询问,如果我想要的顶点Y在其中。如果没有,请再次在列表中的第一个项目处查找。但在这样做的过程中,我失去了所有其他邻居的信息。在相同的情况下,它将查看顶点1,找到他的所有邻居,然后该过程将退出,因为它没有找到方法。但是顶点2仍未被访问。如何让过程看到lsit中的每个项目,而不仅仅是第一个项目?
(define (traverse-graph X Y G)
(cond
[(not(eq? (vertex-visited (find-vertex X (graph-vertices G))) VISITED))
(begin
(set-vertex-visited! (find-vertex X (graph-vertices G)) VISITED)
(cond
[(member Y (find-adju X (graph-edges G))) #t]
[(not (empty? (find-adju X (graph-edges G)))) (traverse-graph (car (find-adju X (graph-edges G))) Y G) ]
[else #f]
)
)
]
[else #f]
)
)
我想到可能用cdr而不是car将整个列表返回到遍历函数,但我不知道如何实现它。我将如何处理X是数字而不是列表的第一步。
编辑:
我尝试添加for-each似乎工作正常,但结果并没有给我任何东西。没有真或假。如果我一步一步地调试它,我看到它可能正确遍历但是当它到达[(成员条件时,它会停止而不返回任何内容,即使条件为真。
(define (traverse-graph X Y G)
(cond
[(not(eq? (vertex-visited (find-vertex X (graph-vertices G))) VISITED))
(begin
(set-vertex-visited! (find-vertex X (graph-vertices G)) VISITED)
(cond
[(member Y (find-adju X (graph-edges G))) #t]
[(not (empty? (find-adju X (graph-edges G))))
(for-each (lambda (h)
(traverse-graph h Y G)
) (find-adju X (graph-edges G))
)
]
[else #f]
)
)
]
[else #f]
)
)
答案 0 :(得分:2)
你使用for-each
走在了正确的道路上,但这个功能是一个必要的构造。对于列表中的每个边缘,它表示“执行此操作,执行此操作”,但不保留任何值。你可以通过map
获得更多运气,它会在列表中进行迭代,并在列表中聚合结果。
使用map
,您的遍历图的结果将是一个具有深度优先搜索遍历形状的树:
'(result0 (result1 (result3))
(result2 (result4)))
您可以使用(apply append (map (lambda …) (find-adju …))
在每个步骤附加列表列表,并在cons
前面添加当前节点的结果。不要忘记返回包含您的遍历的叶节点的单个元素的列表,即使用'(#t)
和'(#f)
。然而,在最坏的情况下,时间复杂度O(N²)
有一个很大的缺点。想象一个图表,其中每个节点都有两个子节点:一个叶子作为它的右子节点,另一个节点作为它的左子节点:
(→ 0 2) (→ 0 1)
(→ 2 4) (→ 2 3)
(→ 4 6) (→ 4 5)
(→ 6 8) (→ 6 7)
(→ 8 10) (→ 8 9)
…
(→ 96 98) (→ 96 97)
(→ 98 100) (→ 98 99)
使用该图表,您的遍历将开始向左钻取,直到它到达最左边的节点(100
),
98
,向下转到99
,然后返回到98
,它会将'(result100)
追加到'(result99)
并且前置result98
,96
,检查97
,返回96
,然后将'(result98 result100 result99)
追加到'(result97) and prepend
result96`,0
,检查1
,返回0
,然后将'(… many results here …)
追加到'(result1)
,并在result0
前加append
1}}。由于n
必须复制前缀中的所有元素,因此将长度m
的列表附加到长度为n
的列表中将花费O(n)
次操作(第二次) list只是指向,并且不需要复制,因为它形成的尾部与原始的整个第二个列表相同),即它需要append 1 element to 1 element
append 3 elements to 1 element
append 5 elements to 1 element
append 7 elements to 1 element
append 9 elements to 1 element
…
append 99 elements to 1 element
次操作。对append
的调用顺序为:
O(n²)
所以总成本是1 + 3 + 5 + ... + N,其中N是节点总数,大致相当于N²乘常数因子,因此O(N²)
,这意味着节点数量很多,会很慢。有关此at wikipedia的更多信息。
为了避免(define (process node accumulator)
(foldl ; the function applied to each neighbour of the list,
; it is passed the neighbour and the accumulator returned
; by the previous iteration
(lambda (neighbour latest-accumulator)
(if (visited? neighbour)
(process neighbour latest-accumulator)
latest-accumulator)) ; return unchanged
; initial accumulator for the first iteration,
; we already prepend the result for the current node,
; but that could be done afterwards, by prepending
; to the final result of `foldl`.
(cons (compute-result-for node) accumulator)
; the list of neighbours:
(neighbours-of node)))
成本,您可以使用累加器:每个步骤都会将一个项目添加到累加器,然后传递给它。这意味着在处理节点时,必须将当前累加器赋予其第一个邻居,返回整个子树的修改累加器,将其传递给第二个邻居,返回一个新的修改累加器,将其传递给第三邻居等
为此,您可以在列表上编写自己的递归函数,将最新的累加器和列表作为参数,并使用修改后的累加器(通过处理整个子树获得)进行递归调用,以及列表的尾部。
您还可以使用对此模式进行抽象的foldl
函数。然后,节点处理函数将遵循以下结构:
O(N)
由于这永远不会附加列表,而只是在每个步骤前面添加一个结果,因此它具有neighbours-of
的复杂度(不计算#t
函数的成本,请参阅关于哈希表的说明并在下面设置)。数据流有点复杂,遗憾的是,不可变数据结构没有更好的选择。
在您的特定情况下,由于您返回一个布尔值,您也可以简单地使用ormap
,如果lambda
返回#t
,它将遍历列表并返回#f
对于任何元素。
这是否具有map
之类的O(N²)复杂度?想想它为什么没有。
作为旁注,您应该使用#t
和0
代替VISITED
和#f
,或'visited
和'
(引用visited
符号if
,在#f
语句中被视为真,与{{1}}以外的所有值一样。
为了获得更好的性能,请使用哈希表和集合来存储边缘,因为如果边缘很多,查找列表中的边缘会花费很多。
最后,我建议将一个代码块的右括号放在最后一行的末尾,而不是将它们单独放在它们的行上。这是Scheme,Racket和大多数其他Lisp变体的常见做法,并使代码更具可读性。
答案 1 :(得分:1)
“但当它到达[(成员条件时,它会停止而不返回任何内容,即使条件为真。”
首先检查列表是否由
生成 (find-adju X (graph-edges G)))
是空的。然后检查列表是否为非空,以及Y是否在该列表中。然后有一个else情况,其中列表非空,但Y不在直接邻居列表中。