在这个最近邻算法中,“from distinct vertex chains”的意思是什么?

47
以下伪代码摘自The Algorithm Design Manual在线预览版的第一章(来自此PDF的第7页)。
该示例是一个有缺陷的算法,但我仍然想要理解它:
[...] 另一个想法可能是重复连接最接近的一对端点,其连接不会创建问题,例如循环过早终止。每个顶点都作为自己的单个顶点链开始。在合并所有内容之后,我们将得到一个包含其中所有点的单个链。连接最终的两个端点给我们一个循环。在执行这个最近对启发式算法的任何步骤中,我们将拥有一组可用于合并的单个顶点和顶点不相交的链。伪代码如下:
ClosestPair(P)
    Let n be the number of points in set P.
    For i = 1  to n − 1 do
        d = ∞
        For each pair of endpoints (s, t) from distinct vertex chains
            if dist(s, t) ≤ d then sm = s, tm = t, and d = dist(s, t)
        Connect (sm, tm) by an edge
    Connect the two endpoints by an edge

请注意,smtm应该是smtm
首先,我不理解“来自不同顶点链”的含义。其次,在外循环中,i被用作计数器,但实际上在任何地方都没有使用i本身!能否有比我更聪明的人解释一下这里到底发生了什么?

2
有趣,我正想提出同样的问题! - TigrouMeow
1
完全相同的问题!一字不差。我其实很沮丧,觉得自己不够聪明,无法独立阅读这本书 :-P 谢谢你的发布! - Aditya M P
这本书总体来说好吗? - user716881
4个回答

42

这是我对Ernest Friedman-Hill(被接受的答案)解释后的理解:

所以书中的例子(图1.4)如下。 我已经添加了顶点名称以使其清晰 Figure 1.4

在第一步中,所有顶点都是单个顶点链,因此我们连接A-D、B-E和C-F对,因为它们之间的距离最小。

在第二步中,我们有三条链,A-D和B-E之间的距离与B-E和C-F之间的距离相同,因此我们将A-D与B-E连接起来,然后剩下两条链 - A-D-E-B和C-F

在第三步中,连接它们的唯一方法是通过B和C,因为B-C比B-F、A-F和A-C更短(请记住我们仅考虑链的端点)。因此,我们现在只有一个链:A-D-E-B-C-F。

在最后一步中,我们连接两个端点(A和F)以形成一个循环。


1
我之前并没有理解 B-E 是如何防止 A-B 和 E-F 的,因为只有选择不同链中的顶点。现在我完全明白了。 - ricab
一张图片胜过千言万语,谢谢!但是这部分我不理解:“因为它们之间的距离最小”。要知道这一点,似乎需要遍历所有其他点,以确定哪些相应的距离最小。 - stifin
@stifin A-D 的距离为1-e,D-E 的距离为1+e,据我所知这已经是已知的了。 - Eugene Platonov
1
书中已经说明了,但是书中并没有解释这是如何知道的。 - stifin

24

1) 描述指出每个顶点始终属于一个“单一的顶点链”(即它是独立的)或属于另一条链; 一个顶点只能属于一条链。该算法在每一步中选择每对两个顶点,这两个顶点分别是它们所属的链的端点,并且它们尚未属于同一条链。有时它们将是单身人士;有时一个或两个顶点已经属于非平凡链,因此您将加入两条链。

2) 您重复循环n次,以便最终选择每个顶点;但是,实际迭代计数并没有用于任何事情。重要的是您运行足够多次的循环。


1
对于1)的一点澄清:它们只枚举链的端点。这不是某种Kruskal算法,而是TSP启发式算法,链仅在端点处增长。 - unkulunkulu
我已经理解了整体思路,但我不明白的是第一次迭代。在第一次迭代中没有端点,那么单个顶点如何枚举?例如:-5、-1、0、2。 - ChrisOdney
单例模式是一条只有一个端点的链。在开始时,每个点都是一个单例。 - Ernest Friedman-Hill
谢谢,你不知道你的评论有多有用,尤其是在我感到沮丧的时候。我的意思是,这些点是否有特定的处理顺序。以下是我的理解: - ChrisOdney
或许我在这里提供的回答(https://dev59.com/6mgu5IYBdhLWcg3wQE4D#27093895)能够帮到你。 - ricab

4
尽管问题已经有了答案,这里提供了一个最近点启发式的Python实现。它从每个点作为链开始,然后逐步延伸链以构建包含所有点的长链。
该算法确实构建了一条路径,但它不是机械臂运动的序列,因为起点未知。
import matplotlib.pyplot as plot
import math
import random


def draw_arrow(axis, p1, p2, rad):
    """draw an arrow connecting point 1 to point 2"""
    axis.annotate("",
              xy=p2,
              xytext=p1,
              arrowprops=dict(arrowstyle="-", linewidth=0.8, connectionstyle="arc3,rad=" + str(rad)),)


def closest_pair(points):
    distance = lambda c1p, c2p:  math.hypot(c1p[0] - c2p[0], c1p[1] - c2p[1])
    chains = [[points[i]] for i in range(len(points))]
    edges = []
    for i in range(len(points)-1):
        dmin = float("inf")  # infinitely big distance
        # test each chain against each other chain
        for chain1 in chains:
            for chain2 in [item for item in chains if item is not chain1]:
                # test each chain1 endpoint against each of chain2 endpoints
                for c1ind in [0, len(chain1) - 1]:
                    for c2ind in [0, len(chain2) - 1]:
                        dist = distance(chain1[c1ind], chain2[c2ind])
                        if dist < dmin:
                            dmin = dist
                            # remember endpoints as closest pair
                            chain2link1, chain2link2 = chain1, chain2
                            point1, point2 = chain1[c1ind], chain2[c2ind]
        # connect two closest points
        edges.append((point1, point2))
        chains.remove(chain2link1)
        chains.remove(chain2link2)
        if len(chain2link1) > 1:
            chain2link1.remove(point1)
        if len(chain2link2) > 1:
            chain2link2.remove(point2)
        linkedchain = chain2link1
        linkedchain.extend(chain2link2)
        chains.append(linkedchain)
    # connect first endpoint to the last one
    edges.append((chains[0][0], chains[0][len(chains[0])-1]))
    return edges


data = [(0.3, 0.2), (0.3, 0.4), (0.501, 0.4), (0.501, 0.2), (0.702, 0.4), (0.702, 0.2)]
# random.seed()
# data = [(random.uniform(0.01, 0.99), 0.2) for i in range(60)]
edges = closest_pair(data)
# draw path
figure = plot.figure()
axis = figure.add_subplot(111)
plot.scatter([i[0] for i in data], [i[1] for i in data])
nedges = len(edges)
for i in range(nedges - 1):
    draw_arrow(axis, edges[i][0], edges[i][1], 0)
# draw last - curved - edge
draw_arrow(axis, edges[nedges-1][0], edges[nedges-1][1], 0.3)
plot.show()

谢谢您的提问。我可以问一下为什么最长的线是弯曲的而不是直的吗?这个曲线使得线条更长。 - DBedrenko
1
@DBedrenko 因为如果你直接画它,那么它会穿过之前画的线。我认为曲线使它看起来更好一些。在倒数第二行中,如果你想让它变直,将draw_arrow的最后一个参数改为0。 - 047

2

TLDR: 如果您已经熟悉本主题的问题及到目前为止提供的答案,请跳至下面的“ClosestPair启发式方法的明确说明”部分。

备注: 我最近开始阅读《算法设计手册》,但是其中的 ClosestPair 启发式例子让我感到不太清晰。看起来其他人也有类似的感觉。不幸的是,这个主题上提供的答案对我来说都有点含糊和笼统。但这些答案确实帮助我朝着我认为是Skiena的正确解释方向推进。

问题陈述和背景: 对于没有此书的人(第三版),从书的第5页开始:

enter image description here

Skiena首先详细介绍了NearestNeighbor启发式算法的不正确性,使用以下图片来帮助说明他的论点:

enter image description here

上面的图表描绘了NearestNeighbor启发式方法所采用的问题,而下面的图表则是最优解。显然,需要采用不同的方法来找到这个最优解。这就引出了ClosestPair启发式方法以及这个问题的原因。

书籍描述:关于ClosestPair启发式方法的以下描述在书中被概述:

也许我们需要一种不同的方法来处理最近邻启发式算法中被证明是坏实例的情况。总是走向最近的点太过严格,因为这似乎会让我们陷入我们不想要的移动中。
另一个想法可能是重复连接最接近的一对端点,其连接不会创建问题,例如循环的过早终止。每个顶点都作为自己的单个顶点链开始。将所有东西合并在一起后,我们最终会得到一个包含其中所有点的单个链。连接最终的两个端点会给我们一个循环。在执行这个最近对启发式算法的任何步骤中,我们将有一组可用于合并的单个顶点和顶点不相交链的末尾。实现此描述的伪代码如下所示。 enter image description here

ClosestPair 启发式算法的澄清描述

首先,我们可以“放大”一点,回答一个基本问题,即在图论术语中我们要找到什么:

什么是最短的闭合路径?

也就是说,我们想要找到一系列边 (e_1, e_2, ..., e_{n-1}),其中有一个顶点序列 (v_1, v_2, ..., v_n),满足 v_1 = v_n 且所有边都不同。这些边都有权重,每条边的权重都是组成该边的顶点之间的距离 - 我们希望尽可能减小任何闭合路径的总权重。

实际上,ClosestPair启发式算法为我们提供了每次外部for循环迭代(代码行3-10)中的一个这些不同的边缘,其中内部for循环(代码行5-9)确保在每个步骤选择不同的边缘(s_m, t_m),由来自不同顶点链的端点组成;也就是说,s_m来自一个顶点链的端点,而t_m来自另一个不同的顶点链的端点。内部for循环只是确保我们考虑所有这样的对,在此过程中最小化潜在顶点之间的距离。
注意(顶点之间距离相等的情况):一个潜在的混淆源是,在任何一个for循环中都没有指定任何“处理顺序”。我们如何确定比较端点和这些端点的顶点的顺序?这并不重要。内部for循环的性质表明,在距离相等的情况下,选择最近遇到的具有最小距离的顶点配对。

ClosestPair启发式算法的良好实例

回想一下在应用NearestNeighbor启发式算法时发生的“坏实例”(请注意新添加的顶点标签):

enter image description here

总距离很荒谬,因为我们一直在0上下跳动。

现在考虑使用ClosestPair启发式算法时会发生什么。我们有n = 7个顶点;因此,伪代码表明外部的for循环将被执行6次。正如书中所述,每个顶点最初都是自己的单个顶点链(即每个点都是一个单点,其中单点是具有一个端点的链)。在我们的情况下,鉴于上面的图,内部的for循环将执行多少次?嗯,从n个元素集合中选择一个2元素子集的方法有多少种(即2元素子集表示潜在的顶点配对)?有n choose 2这样的子集:

由于在我们的情况下 n = 7,因此有总共21种可能的顶点配对需要研究。上图的性质清楚地表明,由于开始时顶点之间的最小距离为1且 dist(C, D) = dist(D, E) = 1,因此 (C, D)(D, E) 是第一次迭代的唯一可能结果。哪些顶点实际上连接起来形成了第一条边 (C, D) 或者 (D, E) 是不清楚的,因为没有处理顺序。假设我们最后遇到顶点 DE,因此导致 (D, E) 成为我们的第一条边。

现在还有5次迭代和6个顶点链需要考虑:A, B, C, (D, E), F, G

注意(每次迭代都会消除一个顶点链):ClosestPair启发式的外部for循环的每次迭代中,都会消除一个顶点链。外部for循环迭代将继续进行,直到我们只剩下一个由所有顶点组成的单个顶点链,最后一步是通过边连接这个单个顶点链的两个端点。更准确地说,对于由n个顶点组成的图G,我们从n个顶点链开始(即,每个顶点都作为自己的单个顶点链)。外部for循环的每次迭代都会连接G的两个顶点,使得这些顶点来自不同的顶点链; 也就是说,连接这些顶点会将两个不同的顶点链合并成一个,从而将要考虑的顶点链总数减少1。对于具有n个顶点的图进行n-1次重复此过程将导致只剩下一个包含G所有点的单一链,即n-(n-1)=1个顶点链。连接最终的两个端点会给我们一个循环。

每个迭代的可能表现如下:

ClosestPair outer for loop iterations
  1: connect D to E # -> dist: 1, chains left (6): A, B, C, (D, E), F, G
  2: connect D to C # -> dist: 1, chains left (5): A, B, (C, D, E), F, G
  3: connect E to F # -> dist: 3, chains left (4): A, B, (C, D, E, F), G
  4: connect C to B # -> dist: 4, chains left (3): A, (B, C, D, E, F), G
  5: connect F to G # -> dist: 8, chains left (2): A, (B, C, D, E, F, G)
  6: connect B to A # -> dist: 16, single chain: (A, B, C, D, E, F, G)

Final step: connect A and G

因此,在这个示例中,ClosestPair启发式方法做出了正确的决策,而之前的NearestNeighbor启发式方法则做出了错误的决策。

enter image description here

ClosestPair启发式算法的错误实例

考虑下面图中点集上ClosestPair算法的执行情况(首先尝试想象没有任何边连接顶点的点集可能会有所帮助):

enter image description here

我们如何使用ClosestPair连接顶点?我们有n = 6个顶点;因此,外部的for循环将执行6-1=5次,我们首先要做的是研究顶点之间的距离。

总可能的配对数。上面的图表帮助我们看到,在第一次迭代中,dist(A, D) = dist(B, E) = dist(C, F) = 1 - ɛ 是唯一可能的选项,因为1 - ɛ是任意两个顶点之间最短的距离。我们随意选择(A, D)作为第一个配对。

现在还有4次迭代和5个顶点链需要考虑:(A, D), B, C, E, F。每次迭代的可能描述如下:

ClosestPair outer for loop iterations
  1: connect A to D # --> dist: 1-ɛ, chains left (5): (A, D), B, C, E, F
  2: connect B to E # --> dist: 1-ɛ, chains left (4): (A, D), (B, E), C, F
  3: connect C to F # --> dist: 1-ɛ, chains left (3): (A, D), (B, E), (C, F)
  4: connect D to E # --> dist: 1+ɛ, chains left (2): (A, D, E, B), (C, F)
  5: connect B to C # --> dist: 1+ɛ, single chain: (A, D, E, B, C, F)

Final step: connect A and F

注意(正确考虑连接不同顶点链的端点):上面所示的迭代1-3在某种意义上是相当平淡无奇的,因为我们没有其他有意义的选择。即使我们已经有了不同的顶点链(A, D)(B, E)(C, F),下一个选择也同样平淡无奇且任意。由于第四次迭代中顶点之间的最小距离是1 + ɛ,因此有四种可能性:(A, B), (D, E), (B, C), (E, F)。以上所有点之间的距离都是1 + ɛ。选择(D, E)是任意的。其他三个顶点配对中的任何一个都可以同样有效地工作。但请注意,在第5次迭代期间会发生什么——我们的顶点配对选择范围已经被严格缩小。具体来说,顶点链(A, D, E, B)(C, F),它们的端点分别为(A, B)(C, F),只允许四种可能的顶点配对:(A, C), (A, F), (B, C), (B, F)。即使这似乎是显而易见的,值得明确指出的是,DE都不是上述可行的顶点候选——它们都没有包含在它们是顶点的顶点链的端点(A, D, E, B)中的端点(A, B)中。此时没有任意选择。以上配对中顶点之间的距离没有平局。(B, C)配对结果产生了顶点之间最小的距离:1 + ɛ。一旦顶点BC被边连接,所有迭代都已完成,我们只剩下一个顶点链:(A, D, E, B, C, F)。连接AF会给我们一个循环并结束整个过程。

(A, D, E, B, C, F) 路线上的总行程如下:

上述距离计算结果为5 - ɛ + √(5ɛ^2 + 6ɛ + 5),而不是仅绕边界行驶的总路程(上图中右侧的所有边缘均为红色):6 + 2ɛ。当ɛ -> 0时,我们可以看到5 + √5 ≈ 7.24 > 6,其中6是必要的行驶距离。因此,我们最终行驶的距离约为

在这种情况下使用ClosestPair启发式方法会比必要的更进一步。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接