GPX:TSP中的两条好路线,怎样拼成一条更好的路线?

GPX:TSP中的两条好路线,怎样拼成一条更好的路线?

假设两条旅行路线的总长度都是 44。它们访问同样的城市,也都回到出发点,但走法不同:第一条在某些地方走得短,第二条在另一些地方走得短。

能不能把它们各自走得好的部分接起来,得到一条长度为 36 的路线?

可以。接下来我们会亲手走完这样一个例子。过程中不需要增加任何一条父代没有的边,也不需要重新求解每个区域的最短路线。我们只做一件事:找出能够分别选择的部分,在每一部分中,采用两个父代里较短的那个版本。

这个想法,就是广义分区交叉------Generalized Partition Crossover,简称 GPX------的核心。真正需要解释清楚的是:哪些部分可以这样选择,为什么拼完以后仍是一条完整的旅行路线?

1 有了 PX,为什么还要提出 GPX

在 TSP 中,一条合法路线必须访问每座城市一次,最后回到起点。遗传算法把已有的路线叫作"父代",把交叉生成的新路线叫作"子代"。GPX 就负责其中的交叉环节:输入两条父代路线,利用它们已有的连接生成子代。

它的前身是 Whitley、Hains 和 Howe 提出的分区交叉 PX。PX 先把两个父代的边放在同一张图上。如果切断两条合适的公共连接,能把这张图分成两部分,就可以让一部分采用父代 A 的走法,另一部分采用父代 B 的走法,再接回原来的公共连接。1

这里的"合适"有明确含义:切开的两侧,在两个父代中都能通过相同的连接接回去。 后面会用具体城市说明怎样检查。

作者最初的 PX 实现找到一个这样的切分,就完成一次交叉。但他们发现,同一对父代往往存在多个可用切分。只用一个,就可能留下尚未利用的互补部分。因此,2010 年的 GPX 将这些可用切分联合起来,让多个分区在同一次交叉中分别选择父代版本。2

例如,四块的较好走法依次来自 A、B、A、B。GPX 希望一次就取出这四个版本,而不用先交换一部分,再把得到的子代拿去进行下一次交换。

理解这项推广,先不用记"广义"的定义。跟着下面的两条路线走一遍就够了。

2 先找出:两个父代究竟在哪些地方走得不一样

下面是一对自行构造的 16 城市教学路线,边权也为讲解而设。数字代表城市,相邻数字之间有一条边;最后一座城市还要连回第一座。这里讨论无向、对称 TSP,所以 2---3 和 3---2 是同一条边。

父代 A: 1→2→3→4→5→6→7→8→9→10→11→12→13→14→15→16→1。

父代 B: 1→3→2→4→5→7→6→8→9→11→10→12→13→15→14→16→1。

先看城市 1 到城市 4 附近:A 走 1→2→3→4,B 走 1→3→2→4。内部顺序不同,但都从 1 走到 4,再通过 4---5 去往后面的城市。

我们已经用眼睛看出一个可能交换的部分。算法怎样把它找出来?

第一步:叠加两条路线,标记公共边

把两个父代画在同一张图上。A 用过或 B 用过的边都放进去,两个父代都使用的边只画一条,并标记为公共边。

本例有八条公共边:

1---16、2---3、4---5、6---7、8---9、10---11、12---13、14---15。

其他边只有一个父代使用,称为独有边。"独有"只是说明来源,不表示它更短或更好。

第二步:暂时拿掉公共边,看剩下哪些城市还连在一起

这是寻找分区的分析步骤。公共边已经记下,最终组装子代时仍会保留。

现在从城市 1 出发,只沿下图中还存在的边走:

  1. 城市 1 与 2、3 相连,因此先找到 1、2、3。
  2. 从 2 或 3 又能走到 4,把 4 也加入。
  3. 继续检查这四座城市,已经没有剩余边通向外面,搜索停止。

于是得到第一组城市 {1,2,3,4}。从尚未分组的城市 5 再做同样的搜索,得到 {5,6,7,8},随后还会得到另外两组。

候选块 包含的城市
S₁ 1、2、3、4
S₂ 5、6、7、8
S₃ 9、10、11、12
S₄ 13、14、15、16

这样的每一组,就叫作剩余图的一个"连通分量":组内城市能沿剩余边互相到达,组外城市则到不了。 搜索时不区分边来自 A 还是 B,也不要求两种边交替;这里只问"有没有路能走到"。程序中可用广度优先搜索 BFS 或深度优先搜索 DFS 完成。

因此,S₁、S₂ 是城市集合,并不是两个父代;算法要找的是所有连通分量,也不是只保留城市数量最多的那一组。

还有一个容易忽略的地方:虽然公共边 2---3 暂时被拿掉了,2 和 3 仍可以通过 2→1→3 到达彼此,所以它们仍在同一组。

3 找到一组城市以后,为什么可以换它的走法

刚才只回答了"剩余图分成几组"。要知道某一组能不能单独交换,还必须把记录下来的公共连接拿回来检查。

继续只看 S₁={1,2,3,4}。检查一条公共边时,分别看它的两个端点属于哪一组:

  • 2---3: 两端都在 S₁,属于块内连接。
  • 1---16: 1 在 S₁,16 在 S₄,连接两块。
  • 4---5: 4 在 S₁,5 在 S₂,也连接两块。

所以,S₁ 通过 1---16 和 4---5 与外部相连。在块内端点 1、4 处,旅行路线从外部进入或离开这一块。下面把它们称为"端口"。

把各自父代中的块内边取出来,A 给出 1→2→3→4,B 给出 1→3→2→4。这两种走法都满足:从 1 到 4,恰好经过这一块的四座城市,没有遗漏,也没有重复。

于是,原来的局部路线

16→1→2→3→4→5

可以替换为

16→1→3→2→4→5。

改变的只有 1 和 4 之间的访问顺序。从城市 16 仍然到达 1,走完这一块后仍然从 4 到达 5,后面的路线可以照常走。

另外三块也做同样的检查:

分区 A 的块内走法 B 的块内走法 接向外部的公共边
S₁ 1→2→3→4 1→3→2→4 16---1、4---5
S₂ 5→6→7→8 5→7→6→8 4---5、8---9
S₃ 9→10→11→12 9→11→10→12 8---9、12---13
S₄ 13→14→15→16 13→15→14→16 12---13、16---1

现在试着只把 S₂ 换成 B,其余三块仍用 A。沿路线读一遍:

1→2→3→4→5→7→6→8→9→10→11→12→13→14→15→16→1。

16 座城市仍然各访问一次,最后也回到了起点。如果再把 S₄ 换成 B,走到 13 时改走 13→15→14→16,随后仍通过 16---1 回家。

这就解释了本例为什么能够独立选择四块:每块都完整访问自己的城市,并接回原来的两个端口;四条块间公共边则始终把四块接成同一个大圈。 选择 S₂ 的走法,不会改变 S₄ 需要接到哪里。

4 亲手算一次:44 怎样变成 36

前面已经确认,四块可以分别选择。现在才轮到比较距离。

下面把每块放大,直接在边旁标出权重。灰色公共边的权重都是 2;蓝色为 A 独有边,橙色为 B 独有边。坐标只用于画清结构,边旁标出的数字才是距离。

以 S₂ 为例:A 走 5→6→7→8,成本为 5+2+4=11;B 走 5→7→6→8,成本为 2+2+3=7。因此,这一块选 B 可以节省 4。

其余三块也直接相加:

分区 A 的内部路径成本 B 的内部路径成本 采用谁
S₁ 7 11 A
S₂ 11 7 B
S₃ 7 11 A
S₄ 11 7 B

四条块间连接边合计 4×2=8。两个父代的总成本分别是:

A:7+11+7+11+8=44。

B:11+7+11+7+8=44。

让四块依次采用 A、B、A、B,就得到:

GPX:7+7+7+7+8=36。

这个来源组合简记为 ABAB,对应完整路线:

1→2→3→4→5→7→6→8→9→10→11→12→13→15→14→16→1。

同一对父代,做一次 PX 能得到什么

例如,切开公共边 4---5 和 8---9,就能把 S₂ 与另外三块分开。让 S₂ 采用 B,其余采用 A,得到 ABAA,总成本为 40。

原论文把这种只需切断两条连接的切分称为 partition of cost 2。这里的"2"是切断的边数,与边旁的距离权重无关。12

为什么不能换个切法,一次 PX 也得到 ABAB?沿四块围成的圈看:一次两边切分只会分出两段连续区域,一段用 A,另一段用 B。ABAB 却要求在每相邻两块之间都切换来源,共有四处切换,因此一个两边切分做不到。

本例中,检查原父代的全部有效两边切分,单次 PX 能达到的最低成本是 40;GPX 的最佳组合是 36。文末程序会逐项验证。PX 如果继续用子代进行后续交叉,也可能得到 36;GPX 的进步,是把这些已经能够独立选择的部分,在一次重组中一起利用。

5 组合很多,为什么计算很少

每块都有 A、B 两种选择,四块就有 2 4 = 16 2^4=16 24=16 种组合,其中包括两个原父代。

但为了找到最短的组合,并不需要把 16 条路线全部生成出来。因为四块已经能独立选择,每块直接选较短者即可。本例就是 8+7+7+7+7=36。

一般地,若确认了 q q q 个可独立选择的分区,最短组合的成本为

L b e s t = L f i x e d + ∑ i = 1 q min ⁡ ( L i A , L i B ) . L_{\mathrm{best}}=L_{\mathrm{fixed}}+\sum_{i=1}^{q}\min(L_i^A,L_i^B). Lbest=Lfixed+i=1∑qmin(LiA,LiB).

其中, L f i x e d L_{\mathrm{fixed}} Lfixed 是各块之外固定保留的连接成本; L i A L_i^A LiA、 L i B L_i^B LiB 是第 i i i 块两个完整版本的成本。本例中,固定成本是 8,两种内部走法的成本就是上一节表中的 7 或 11。

20 个分区会有超过一百万种来源组合,但选择时仍只需要逐块比较这 20 对候选版本。原论文给出的 GPX 时间复杂度为 O ( n ) O(n) O(n),这里的 n n n 是城市数;它利用已有的可分解结构,并不逐条枚举这些组合。2

这个"最佳"有明确范围:它是所识别分区的父代版本组合中最短的路线。 如果某块还有比两个父代版本都短的第三种走法,这次选择并不会自动发现它。最佳组合也可能恰好等于某个父代,不一定每次都改善。

6 回到作者的原图:GPX 分区不一定只有两个端口

前面的四块各只有两个端口,容易跟着路线检查。原始 GPX 还允许多个有效切分共同确定一个更复杂的分区。下面直接看 2010 年论文的原图。2

先看右图,可以找到左、中、右三组城市。再回到左图,看两条粗黑线切断了什么:

  • 切分 A: 切断 d---e、a---n,把左块 {a,b,c,d} 分出来。
  • 切分 B: 切断 f---g、l---k,把右块 {g,h,i,j,k} 分出来。

一次原始 PX 可以使用其中一条切分;GPX 将两条切分一起利用,得到左、中、右三个可分别选择的部分。

现在重点看中间的 {e,f,l,m,n}。它有 e、f、l、n 四个对外端点,两个父代在其中的走法如下:

父代版本 连接 e 与 f 的一段 连接 l 与 n 的一段
实线版本 e→f l→m→n
虚线版本 e→m→f l→n

两种版本都连接 e 与 f、l 与 n,区别是城市 m 被安排在哪一段。采用任一完整版本,m 都恰好访问一次,对外连接关系也没有改变。

可以实际沿完整路线核对。左块、右块先固定用实线版本,中间也用实线时是:

a→b→c→d→e→f→g→h→j→i→k→l→m→n→a。

只把中间改成虚线版本,就变成:

a→b→c→d→e→m→f→g→h→j→i→k→l→n→a。

路线仍然连成一个大圈,所有城市仍然各经过一次。

这里尤其要注意,中间的两段必须作为同一个父代版本一起选择。如果分别挑出 e→f 和 l→n 两条直连,就漏掉了 m;如果分别选择 e→m→f 和 l→m→n,又会重复使用 m。因此,GPX 比较的是这一整块的两个版本,不能把块内每条边都单独拿出来挑短的。

算法如何认定中间块也能交换

共同作者 Hains 的学位论文把这一步写得更具体:3

先认定左块和右块的两个有效两边切分,记录四条切边 d---e、a---n、f---g、l---k。中间块所有对外的公共连接,都已出现在这些有效切分中,因此它也被识别为可用分区。

这和"看见四个端点,就允许交换"不同。可用性来自已确认的切分结构;在这个原图中,我们又通过逐段走法直接看到了交换为何成立。

如果还有一些剩余连通分量无法被确认为可独立重组,Hains 描述的实现会将它们合并为一个整体,再统一继承某个父代的版本。它不会对这些未确认的部分分别作任意选择。3

由此也能看清寻找分区的完整逻辑:删除公共边,是为了发现候选部分;检查原图中的公共连接及有效切分,才决定哪些部分能够分别选择。 两个步骤缺一不可。

7 把一次 GPX 连起来看

现在可以把前面的动作整理成一个流程。这里描述原始 GPX 的重组思路,具体切分识别需要记录公共连接关系。23

text 复制代码
输入两条父代路线

1. 叠加父代边,记录全部公共边。
2. 暂时去掉公共边,搜索剩余图的所有连通分量。
3. 回到原图,检查公共连接构成的有效两边切分:
   联合这些切分,确定可独立重组的分区;
   尚未确认可独立重组的剩余部分,合并后整体处理。
4. 对每个可选择的分区,比较父代 A、B 的完整版本成本。
5. 为最佳组合逐块选择较短版本,并保留全部公共边。
6. 将选中的边组装为完整子代路线。

输出最佳组合;也可以通过其他来源选择生成额外子代。

如果两个父代共同经过一串连续城市,这段公共路径也可以在分析时缩成一条辅助边,并记录原来的城市顺序;组装时再展开。这样,一长串已经一致的连接不必反复参与分区判断。1

在前面的 16 城市例子中,第 3 步很直接:每个候选块都只通过两条公共边连接外部,两个父代在块内都提供完整的端口间路径。原图中间块则展示了多个切分联合起作用的情形。

8 GPX 能给 TSP 求解带来多大帮助

理解 GPX 的价值,可以从"单条路线已经很难继续改进"这个情形出发。

局部搜索会反复尝试改变一条路线的部分连接;当采用的邻域里已经找不到更短路线时,就到达了相应的局部最优。不同起点的搜索可能停在不同路线:一条在这里更好,另一条在那里更好。

GPX 让这些结果有机会互相补充。如果它们的差异恰好落在可以独立重组的分区中,就能一次接入多个较好的部分,形成新的路线。原作者把局部最优父代经交叉后直接产生新的局部最优子代的现象称为"穿隧"(tunneling);这是在其研究中高频出现的现象,具体是否仍局部最优取决于所采用的邻域与输入。12

它怎样进入一个完整求解器

2010 年论文采用的混合遗传算法先生成一批路线,用 Lin--Kernighan(LK)局部搜索分别改进。之后,让当前最好路线与其他路线尝试 GPX 重组,对新一代继续调用 LK。2

算法还保留当前最好解,并在选择下一代时考虑不同路线提供的边,避免种群迅速变得一样;遇到无法有效重组的配对,则使用 double-bridge 扰动来改变路线。因此,实际求解能力来自 GPX、局部搜索、种群选择和扰动的配合。

这也解释了为什么只会拼接还不够:GPX 使用的边都来自父代,父代之外的新连接需要由其他搜索操作带入。

原论文取得了什么结果

2010 年论文使用 10 条路线的种群,与 Chained-LK 比较。两者使用相同的 LK 实现及参数。在各调用 LK 1010 次后,50 次试验中找到最优解的次数如下。2

实例 城市数 含 GPX 的混合 GA Chained-LK
rand500 500 50/50 38/50
att532 532 26/50 16/50
nrw1379 1379 1/50 1/50
rand1500 1500 12/50 2/50
u1817 1817 1/50 0/50

这组结果显示,混合算法在若干实例上提高了找到最优解的成功率,同时也有两者持平、成功率仍很低的实例。这里对齐的是 LK 调用次数,比较的是两个完整算法;它没有单独测量"只把 PX 换成 GPX"能提高多少。

后续 2012 年研究进一步将 GPX 与 LKH-2 结合,测试了 3162、10000、31623 城市的聚集分布实例。例如,在 C31k.0 上,带 IPT 交叉的多轮 LKH-2(multi-trial LKH-2)所得路线平均高出 Held--Karp 下界 0.9639%,跨运行 GPX 方案则为 0.7012%。该实验采用 10 次独立运行、每次 50 次迭代,跨运行重组的子代不反馈给原来的运行。4

这表明 GPX 的组合思路能够用于三万余城市的求解过程。上述百分比以一个下界为参照,并不是已经证明的真实最优差距;这些结果也对应论文当时的实例与配置。

为什么有时好边都在,还是拼不出最优路线

回想原图中间块:选实线,就必须完整采用实线版本;选虚线,也必须完整采用虚线版本。

如果真正的最好走法需要把这一块内部两种版本的边再作细致混合,当前分区粒度下的 GPX 就做不到。即使种群已经包含最优路线需要的边,这些边也可能被绑定在不同父代的同一个大分区中,无法靠逐块选择把它们全部取出。这正是原论文讨论的一项限制。2

因此,GPX 的贡献很具体:它把两条路线中已经能够兼容组合的部分利用起来,快速选出较好的整体。找不到足够的可交换部分时,还需要继续搜索,产生新的父代结构。

附录:把四分区例子运行起来

下面程序直接实现前面的 16 城市例子:自动找候选块,检查内部路径和端口,计算两个版本的成本,再逐块选择。源码完整列出,可保存为 gpx_demo.py,用 Python 3 运行,不依赖第三方库。

为使每一步容易核对,程序限定为"所有剩余连通分量都满足两端口路径条件"的情形。第 6 节原图的四端口中间块不在此教学程序的处理范围内。 程序还枚举所有来源组合及公共边对,用于验证 36 和 40 两个结果;这些核验不是 GPX 选择最佳组合所必需的步骤。

python 复制代码
"""GPX 四分区示例;仅依赖 Python 3 标准库。

GPX 演示部分要求所有残余分量都满足两端口路径条件。
single_px_children 穷举共同边对,仅用于验证单次 PX 的可达范围。
两者均为便于阅读的参考实现,不是论文的线性时间工程实现。
"""
from collections import defaultdict
from itertools import combinations, product


A = tuple(range(1, 17))
B = tuple(v for s in (1, 5, 9, 13) for v in (s, s+2, s+1, s+3))


def edge(u, v):
    return tuple(sorted((u, v)))


def edges(route):
    return {edge(u, v) for u, v in zip(route, route[1:] + route[:1])}


def graph(edge_set):
    adj = defaultdict(set)
    for u, v in edge_set:
        adj[u].add(v)
        adj[v].add(u)
    return adj


def components(vertices, edge_set):
    adj, unseen, result = graph(edge_set), set(vertices), []
    while unseen:
        start = min(unseen)
        stack, found = [start], {start}
        while stack:
            u = stack.pop()
            for v in adj[u] - found:
                found.add(v)
                stack.append(v)
        unseen -= found
        result.append(found)
    return result


def inside(edge_set, region):
    return {e for e in edge_set if set(e) <= region}


def path_check(edge_set, region, portals):
    adj = graph(edge_set)
    if any(len(adj[v]) != (1 if v in portals else 2) for v in region):
        raise ValueError("块内度数不满足端口间路径条件")
    start, end = sorted(portals)
    route, visited = [start], {start}
    while route[-1] != end:
        nxt = adj[route[-1]] - visited
        if len(nxt) != 1:
            raise ValueError("路径提前终止或形成环")
        v = nxt.pop()
        route.append(v)
        visited.add(v)
    if visited != region:
        raise ValueError("路径没有覆盖本块所有城市")
    return tuple(route)


def discover(a, b):
    """找出独有边分量,再检查共同边接口与父代完整内部路径。"""
    if (len(a) < 3 or len(set(a)) != len(a)
            or len(a) != len(b) or set(a) != set(b)):
        raise ValueError("父代必须各访问同一组城市一次")
    ea, eb = edges(a), edges(b)
    common, parts = ea & eb, []
    for region in components(set(a), ea ^ eb):
        boundary = {e for e in common if len(set(e) & region) == 1}
        portals = {v for e in boundary for v in e if v in region}
        if len(boundary) != 2 or len(portals) != 2:
            raise NotImplementedError("本示例只支持所有分量均为两端口的输入")
        versions = (inside(ea, region), inside(eb, region))
        paths = tuple(path_check(es, region, portals) for es in versions)
        parts.append({
            "cities": region, "portals": portals, "paths": paths,
            "versions": versions,
            "exclusive": tuple(es - common for es in versions),
        })
    return common, parts


def cycle(edge_set, vertices):
    adj = graph(edge_set)
    if set(adj) != set(vertices) or any(len(adj[v]) != 2 for v in vertices):
        raise ValueError("城市覆盖或度数错误")
    start = min(vertices)
    route, visited = [start], {start}
    previous, current = None, start
    while True:
        nxt = min(adj[current] - {previous})
        if nxt == start:
            break
        if nxt in visited:
            raise ValueError("出现重复城市")
        route.append(nxt)
        visited.add(nxt)
        previous, current = current, nxt
    if visited != set(vertices):
        raise ValueError("出现多个不连通子回路")
    return tuple(route)


def assemble(common, parts, choices):
    if len(parts) != len(choices) or any(c not in (0, 1) for c in choices):
        raise ValueError("每块必须选择 A=0 或 B=1")
    result = set(common)
    for part, c in zip(parts, choices):
        result.update(part["exclusive"][c])
    cycle(result, set().union(*(p["cities"] for p in parts)))
    return frozenset(result)


def make_weights():
    w = {(u, v): 100 for u in A for v in A if u < v}
    w.update({e: 2 for e in edges(A) & edges(B)})
    for i, s in enumerate((1, 5, 9, 13)):
        av, bv = ((2, 3), (5, 4)) if i % 2 == 0 else ((5, 4), (2, 3))
        w[edge(s, s+1)], w[edge(s+2, s+3)] = av
        w[edge(s, s+2)], w[edge(s+1, s+3)] = bv
    return w


def cost(edge_set, w):
    return sum(w[e] for e in edge_set)


def greedy(common, parts, w):
    """共同边固定不变,只需比较各块独有边成本;相等时选 A。"""
    choices = tuple(
        0 if cost(p["exclusive"][0], w) <= cost(p["exclusive"][1], w)
        else 1 for p in parts
    )
    return choices, assemble(common, parts, choices)


def single_px_children(a, b):
    """枚举原父代并图中的全部共同两边切分,返回其单次 PX 子代。"""
    ea, eb = edges(a), edges(b)
    union, common = ea | eb, ea & eb
    cuts, children = 0, set()
    for pair in combinations(sorted(common), 2):
        regions = components(set(a), union - set(pair))
        if len(regions) != 2:
            continue
        cuts += 1
        s, t = regions
        for left, right in ((ea, eb), (eb, ea)):
            child = set(pair) | inside(left, s) | inside(right, t)
            cycle(child, set(a))
            children.add(frozenset(child))
    children -= {frozenset(ea), frozenset(eb)}
    return cuts, children


def main():
    common, parts = discover(A, B)
    w = make_weights()
    ea, eb = edges(A), edges(B)
    choices, best = greedy(common, parts, w)

    # 以下枚举只为核验本例;GPX 逐块取优本身不需要枚举。
    all_children = {}
    for source_choices in product((0, 1), repeat=len(parts)):
        child = assemble(common, parts, source_choices)
        assert common <= child <= ea | eb
        all_children[child] = cost(child, w)
    cuts, px_children = single_px_children(A, B)
    px_best = min(cost(child, w) for child in px_children)
    assert [p["cities"] for p in parts] == [
        set(range(s, s+4)) for s in (1, 5, 9, 13)
    ]
    assert len(all_children) == 16 and cuts == 6 and len(px_children) == 12
    assert px_best == 40
    assert cost(best, w) == min(all_children.values()) == 36
    assert best not in px_children

    # 输出完整块内路径成本,便于与图中的 7、11 直接对应。
    internal_common = set().union(*(inside(common, p["cities"]) for p in parts))
    external_cost = cost(common - internal_common, w)
    chosen_costs = []
    print(f"父代成本:A={cost(ea, w)},B={cost(eb, w)}")
    for i, (part, choice) in enumerate(zip(parts, choices), 1):
        costs = tuple(cost(es, w) for es in part["versions"])
        chosen_costs.append(costs[choice])
        print(f"分区 S{i}:A 路径={costs[0]},B 路径={costs[1]},选择 {'AB'[choice]}")
    print(f"块间共同边成本:{external_cost}")
    print("GPX 来源组合:", "".join("AB"[c] for c in choices), sep="")
    formula = "+".join(map(str, chosen_costs + [external_cost]))
    print(f"GPX 子代成本:{formula}={cost(best, w)}")
    print(f"单次 PX 最低成本:{px_best}")
    print(f"核验通过:{len(all_children)} 种 GPX 组合均合法;"
          f"{cuts} 种有效两边切分产生 {len(px_children)} 条不同 PX 新子代。")
    route = cycle(best, set(A))
    print("GPX 回路:", "-".join(map(str, route + route[:1])), sep="")


if __name__ == "__main__":
    main()

运行结果:

text 复制代码
父代成本:A=44,B=44
分区 S1:A 路径=7,B 路径=11,选择 A
分区 S2:A 路径=11,B 路径=7,选择 B
分区 S3:A 路径=7,B 路径=11,选择 A
分区 S4:A 路径=11,B 路径=7,选择 B
块间共同边成本:8
GPX 来源组合:ABAB
GPX 子代成本:7+7+7+7+8=36
单次 PX 最低成本:40
核验通过:16 种 GPX 组合均合法;6 种有效两边切分产生 12 条不同 PX 新子代。
GPX 回路:1-2-3-4-5-7-6-8-9-10-11-12-13-15-14-16-1

本例的路线和权重是教学构造,未画出的城市对距离统一设为 100,构成一个完整的对称 TSP;它不要求满足欧氏距离或三角不等式。程序优先展示过程,对每块重复扫描边集,发现与检查部分为 O ( q n ) O(qn) O(qn),另有枚举核验开销,不是原论文的线性时间工程实现。

参考文献与阅读说明

本文以 2010 年 GPX 原论文为主体,以 Hains 的学位论文补充分区识别细节。图 6 直接节选原论文图 1,其余为教学示意图。历史上,2009 年 PX 论文已讨论多分区组合与逐块选择的可能性;2010 年 GPX 将联合利用多个切分系统化,并放入混合遗传算法开展实验。本文所说的"一次 PX",指原作者早期实现所使用的一次两边切分操作。12

1 Whitley, D., Hains, D., & Howe, A. (2009). Tunneling between Optima: Partition Crossover for the Traveling Salesman Problem. Proceedings of GECCO , 915--922. DOI: 10.1145/1569901.1570026. 作者公开全文.

2 Whitley, D., Hains, D., & Howe, A. (2010). A Hybrid Genetic Algorithm for the Traveling Salesman Problem Using Generalized Partition Crossover. Parallel Problem Solving from Nature---PPSN XI , LNCS 6238, 566--575. DOI: 10.1007/978-3-642-15844-5_57. 作者公开全文. 算法与原图见第 2 节,实验表见 Table 3。

3 Hains, D. R. (2011). Generalized Partition Crossover for the Traveling Salesman Problem . Master's thesis, Colorado State University. 学校存档全文. 第 2.2 节,印刷页码 10--12,补充分区识别、切边标记和剩余部分的处理。

4 Hains, D., Whitley, D., & Howe, A. (2012). Improving Lin-Kernighan-Helsgaun with Crossover on Clustered Instances of the TSP. Parallel Problem Solving from Nature---PPSN XII , LNCS 7492, 388--397. DOI: 10.1007/978-3-642-32964-7_39. 作者公开全文.

相关推荐
树獭哥1 小时前
Deep Agents 学习笔记(一):从 Agent Harness 到第一个 Deep Agent
人工智能·python
霸道流氓气质1 小时前
在线 AI 绘图平台:Flux 模型集成、Serverless 弹性部署与 ComfyUI 工作流的 React + Spring AI 全栈实战
人工智能·react.js·serverless
CallFay云起未来1 小时前
2026年AI客服能不能减少人工回复?从知识检索到人机协同的技术实现
java·大数据·前端·人工智能·架构·文心一言
xieyan08111 小时前
HyperFrames:给一段文字让 AI 自动做视频,实测效果怎么样?
人工智能·音视频
IT_陈寒1 小时前
一文搞懂SpringBoot的核心概念
前端·人工智能·后端
LX567771 小时前
用AI减少网点重复劳动:从识别问题到设计辅助工作流
人工智能
十铭忘1 小时前
四大坐标系的转换10——协向量
人工智能
一招合理1 小时前
人工智能:增强智能,而非替代人类
人工智能
浅安的邂逅1 小时前
261014-Google 发布 Gemini 4 Argon:面向复杂工程与科研任务的前沿模型
人工智能·ai模型·行业动态·产品发布/更新·模型发布/更新