如何使用SWI Prolog找到加权有向图的唯一最短路径?

时间:2017-12-12 18:08:08

标签: graph prolog unique shortest-path

这是我在Prolog课程中所做的扩展。在课堂上,我被要求写一个谓词path(X, Y, N),如果且只有从节点X到节点Y的路径长度为N,则返回true。给出的是具有相应权重的有向边的列表,例如, edge(a, b, 3)edge(c, d, 10)

给定的问题非常简单(只有一些递归和基本情况)。但是,我想也许我可以进一步扩展它。假设简单的有向图输入可能包含循环并且仅包含非负权重,那么来自给定节点AB唯一最短路径的长度是多少。 (通过唯一,我的意思是如果从AB存在多条最短路径,则此谓词应返回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+等谓词。 varnonvarasserta和类似的谓词在某种程度上也是可以接受的(因为没有其他方法可以实现相同的功能)。

  • 我希望它尽可能一般;也就是说,谓词的任何参数都应该能够作为变量给出。 (或者至少有shortestpath/3的最后一个参数,这是最短路径的长度,一个变量)。

我已经查看了以下问题,并没有解答我的情况:

请随时指出解决我问题的任何其他问题。

1 个答案:

答案 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保持兴奋!