这是我在Prolog课程中所做的扩展。在课堂上,我被要求写一个谓词path(X, Y, N)
,如果且只有从节点X
到节点Y
的路径长度为N
,则返回true。给出的是具有相应权重的有向边的列表,例如, edge(a, b, 3)
和edge(c, d, 10)
。
给定的问题非常简单(只有一些递归和基本情况)。但是,我想也许我可以进一步扩展它。假设简单的有向图输入可能包含循环并且仅包含非负权重,那么来自给定节点A
和B
的唯一最短路径的长度是多少。 (通过唯一,我的意思是如果从A
到B
存在多条最短路径,则此谓词应返回false。
以下是包含循环(a,b,c,e,a)的数据库示例。
edge(a, b, 1).
edge(b, c, 2).
edge(c, d, 1).
edge(b, d, 4).
edge(c, e, 1).
edge(e, a, 10).
edge(e, d, 6).
我认为为了满足唯一条件,我认为我应该扩充原始path/3
谓词以包含路径信息作为列表(以便我可以比较路径独特性)。这种新的扩充反映在新的path/4
谓词中。
path(X, X, 0, []).
path(X, Y, N, [Y]) :- edge(X, Y, N).
path(X, Y, N, [Z|T]) :- edge(X, Z, N1), N2 is N - N1, N2 > 0, path(Z, Y, N2, T).
path(X, Y, N) :- path(X, Y, N, _).
在此代码中,我已经发现了一个问题:如果我尝试将谓词与path(a, b, N, Z)
统一起来,则无效,因为N
将无法与N2 is N - N1
统一。但是,如果我将此部分更改为N is N1 + N2
,则仍然无效,因为N2
仍未统一。如果我将整个谓词行更改为:
path(X, Y, N, [Z|T]) :- edge(X, Z, N1), path(Z, Y, N2, T), N is N1 + N2.
然后这将无休止地运行,因为路径的数量可能是无限的,因为图形可能包含循环(我想尝试将其保持为挑战)。
至于shortestpath/3
谓词,我找不到所有路径并检查所有路径是否都更长,因为路径的数量可能是无限的,因为有一个循环。相反,我试图找到长度介于0和给定N
之间的任何路径;如果没有路径,那么这绝对是最短路径。
countdown(N, N).
countdown(N, X) :- N1 is N - 1, N1 >= 0, countdown(N1, X).
shortestpath(A, B, N) :- path(A, B, N), \+((countdown(N, N1), N > N1, path(A, B, N1))).
然而,这并没有解决作为变量给出的N
(因为倒计时功能不起作用),更不用说唯一的约束。
所以我的问题是,有没有办法让这个问题有效或者实际上不可能这样做?如果有这样的解决方案,请在这里提供(或者如果您认为这是一个"家庭作业"问题,请至少指导我正确的方向)。
约束:
我不想使用任何内置谓词。只有'简单'或者'核心'例如,\+
,is
,+
等谓词。 var
,nonvar
,asserta
和类似的谓词在某种程度上也是可以接受的(因为没有其他方法可以实现相同的功能)。
我希望它尽可能一般;也就是说,谓词的任何参数都应该能够作为变量给出。 (或者至少有shortestpath/3
的最后一个参数,这是最短路径的长度,一个变量)。
我已经查看了以下问题,并没有解答我的情况:
Find the shortest path between two nodes in a graph in Prolog(不解决加权边缘问题,也使用复杂谓词(例如path/4
)。
search all paths and the shortest path for a graph - Prolog(不会对包含周期的图表进行处理)。
Find the shortest path between two nodes in a graph in (Prolog) and display the result as array(不解决加权边缘问题)。
请随时指出解决我问题的任何其他问题。
答案 0 :(得分:3)
很高兴得到一个受家庭作业启发的问题,而不仅仅是实际的家庭作业!让我们从您的谓词开始,看看我们是否可以将其击败,然后我们可以讨论一些替代方法。
首先,我从你的简化谓词开始:
path(X, Y, N, [X-Y]) :- edge(X, Y, N).
path(X, Z, N, [X-Y|T]) :-
edge(X, Y, N0),
path(Y, Z, N1, T),
N is N0 + N1.
这里的主要区别是我只是生成路径然后计算长度。我这里没有做任何减法。在Prolog中常见的是从最简单的生成和测试方法开始,然后改进生成器或测试或两者,直到你开心,所以这只是一个非常简单的生成器。我现在将源节点和目标节点都保留在路径序列中,以帮助我直观地了解正在发生的事情,并且使用它可以立即看到周期问题:
?- path(a, e, N, T).
N = 4,
T = [a-b, b-c, c-e] ;
N = 18,
T = [a-b, b-c, c-e, e-a, a-b, b-c, c-e] ;
N = 32,
T = [a-b, b-c, c-e, e-a, a-b, b-c, c-e, e-a, ... - ...|...] .
我认为我们没有使用您的示例图表,但我们可能会在Prolog的深度优先搜索中受到一些影响:只要没有失败,Prolog就没有有理由备份并尝试另一条路径。你看到那里的周期。如果它使用广度优先搜索,你就可以确定第一个解决方案是最短的,因为通过一步推进所有步骤,你就不会在生成第一个解决方案之前陷入兔子洞。 Dijkstra的算法(感谢提醒@JakobLovern)通过对访问过的节点着色并且不多计算它们来避开问题。
可以通过创建一个元解释器来控制搜索行为,这不像听起来那么糟糕,但比调整搜索以解决周期更有效,我想大多数人在这方面做了什么有图表的情况,所以让我们先尝试一下:
path(X, Y, N, Path) :- path(X, Y, N, [], Path).
path(X, Y, N, Seen, [X]) :-
\+ memberchk(X, Seen),
edge(X, Y, N).
path(X, Z, N, Seen, [X|T]) :-
\+ memberchk(X, Seen),
edge(X, Y, N0),
path(Y, Z, N1, [X|Seen], T),
\+ memberchk(X, T),
N is N0 + N1.
添加Seen
参数并使用\+ memberchk/2
以避免向路径中已有的路径添加内容并不是一件非常常见的事情。 memberchk/2
不是ISO,但它是一个非常常见的谓词。你可以像这样自己实现它(请不要!):
memberchk(X, L) :- once(member(X, L)).
member(X, [X|_]).
member(X, [_|Xs]) :- member(X, Xs).
我认为值得注意的是memberchk/2
+列表等于Dijkstra算法中使用的基本设置。这就像Python中的in
;如果没有至少member/2
的话,尝试在Prolog中做任何真实的事情会是一种疯狂。
这些更改使path/4
避免了周期,因此您现在可以找到所有解决方案而不会产生任何虚假解决方案。 注意:我没有让你的图形非循环。我只是让path/4
知道周期。
请注意,我们可以获得多种解决方案:
?- path(a, d, X, Y).
X = 5,
Y = [a, b] ;
X = 4,
Y = [a, b, c] ;
X = 10,
Y = [a, b, c, e] ;
false.
有一个很好的库,aggregate对这种情况有帮助。但是,你没有要求虚假的库。 :)
让我们走最短路径:
uniq_shortest_path(X, Y, MinCost, Path) :-
path(X, Y, MinCost, Path),
\+ (path(X, Y, LowerCost, OtherPath),
OtherPath \= Path,
LowerCost =< MinCost).
这字面上说的是Path
是X和Y之间唯一的最短路径(发生成本MinCost
)当且仅当没有其他路径的成本小于或等于我们的成本。尝试一下:
?- uniq_shortest_path(a, d, MinCost, Path).
MinCost = 4,
Path = [a, b, c] ;
这个伎俩并不便宜;通过比较所有解决方案,它可能起作用。但它确实有效,没有任何额外的恶作剧。
在报告第一个解决方案的成本和路径之前,可能只需获取所有解决方案,按成本排序,然后确保前两个成本不同,就可以获得显着改进。
通过直接实现Dijkstra算法,或者可能通过尝试制作广度优先的元解释器,可以找到更大的改进。做一个迭代深化的方法可能工作,但我怀疑它会表现得更好,因为它经常需要做并重新做所有的工作导致结果被修剪过昂贵。
无论如何,我希望这会有所帮助!对Prolog保持兴奋!