算法-TSP启发式算法-04

旅行商问题 (Traveling Salesperson Problem, TSP),是计算机科学和运筹学领域一个非常经典且著名的组合优化问题。简单来说,它的核心任务是:

给定一系列城市和每对城市之间的距离,找到一条最短的哈密顿回路,使得旅行商从一个城市出发,经过所有其他城市恰好一次,最后返回到起点。

问题拆解成几个关键点:

  • 城市(Cities): 问题的节点,你可以把它们看作图中的顶点。

  • 路径/距离(Paths/Distances): 连接城市之间的边,每条边都有一个权重,通常代表距离、成本或时间。

  • 哈密顿回路(Hamiltonian Cycle): 一条路径,它从一个城市出发,经过所有其他城市恰好一次,然后回到起点。

  • 目标(Objective): 在所有可能的哈密顿回路中,找到总距离最短的那一条。

TSP 的目标就是比较路径的总距离,然后选择较短的那一条。

但当城市的数量增加时,问题的难度会呈指数级增长。

假设有 n 个城市。从一个城市出发,你可以在剩下的 n−1 个城市中选择一个,然后从剩下的 n−2 个城市中选择一个,以此类推。因此,总的可能路径数量是 (n−1)!,这是一个NP-hard 问题,这意味着目前没有已知的算法可以在多项式时间内(即时间复杂度为 n 的多项式,如 n2,n3)找到最优解。

常见的解法

小规模问题

  • 暴力穷举法(Brute-force): 这是最直观的方法,通过生成所有可能的路径,然后计算每条路径的总距离,最后找出最短的那条。这种方法能保证找到最优解,但只适用于城市数量很少的情况(通常小于10)。

  • 动态规划(Dynamic Programming): 动态规划是一种更高效的精确解法,其中最著名的是Held-Karp算法。它的时间复杂度大约为 O(n2⋅2n),空间复杂度为 O(n⋅2^n)。它利用子问题的最优解来构建整个问题的最优解,可以求解城市数量在20个左右的问题

当城市数量超过20个时,精确解法变得不可行。这时,我们会转向近似算法(Approximation Algorithms)或启发式算法(Heuristic Algorithms)。这些算法不能保证找到最优解,但可以在合理的时间内找到一个足够好的近似解。

最近邻算法

最近邻算法(Nearest Neighbor Algorithm): 从一个随机城市出发,每次都前往离当前城市最近的未访问城市,直到所有城市都被访问。这种方法简单快速,但结果可能不是最优的。这种方法可以用于快速的生成初始路径。

遗传算法

遗传算法(Genetic Algorithm): 一种基于生物进化原理的搜索算法。它通过模拟“基因突变”和“自然选择”来不断优化解,适用于大规模复杂问题。

遗传算法灵感来自生物进化。把“路径(tour)”看成个体(染色体),把路径长度看作“适应度”(fitness,通常适应度和代价成反比)。通过“选择、交叉、变异、替代(世代更新)”这些算子,不断产生新一代,期望得到更好的个体(更短的路径)。

核心步骤:

tour的表示:用一个整数列表表示顺序(例如 [0,3,1,2,4] 表示访问城市顺序)。这种表示天然适合 TSP。

初始种群:随机生成若干条合法路径(或用启发式如最近邻生成一些良好起点)。

选择(Selection):按“优胜劣汰”选择父代,常见方法:锦标赛选择(Tournament)、轮盘赌(Roulette)。锦标赛简单且稳定:随机抽 k 个个体,选最好的作为父本。

交叉(Crossover):把两个父本合并产生后代。TSP 常用的有效交叉有 Order Crossover (OX)Partially Mapped Crossover (PMX)。它们保证后代仍为合法排列(无重复城市)。

变异(Mutation):引入随机扰动(swap、inversion、scramble 等),避免早熟收敛。

替代 / 精英策略(Replacement / Elitism):保留最优个体直接进入下一代(防止好的解丢失)。

终止条件:达到固定代数、长时间无改进、或运行时间到达上限。

易实现、对复杂目标灵活、并行友好。参数较多(种群、交叉率、变异率等)、收敛速度与质量依赖细致调参。

常把 GA 与局部搜索(如 2-opt/3-opt)结合成“混合算法”,能显著提升解的质量。

代码实现:

  1. 距离矩阵计算与路径代价函数

  2. 初始种群(随机或用最近邻生成部分)

  3. 锦标赛选择、OX(Order Crossover)、PMX(可选)

  4. 变异:swap / inversion / scramble

  5. 可选 2-opt 局部改进(混合策略)

  6. 精英保留、历史记录与示例运行与可视化

"""
ga_tsp.py  — 遗传算法 (GA) 求解 TSP 的示例实现

主要入口:genetic_algorithm_tsp(dist, ...)
返回 best_tour, best_cost, history(每代的最优成本)

作者:ChatGPT(示例代码,供学习与改造)
"""

import random
import math
import time
from typing import List, Tuple, Optional
import copy

# ---------- 基础函数 ----------
def compute_distance_matrix(points: List[Tuple[float, float]]) -> List[List[float]]:
    n = len(points)
    dist = [[0.0] * n for _ in range(n)]
    for i in range(n):
        xi, yi = points[i]
        for j in range(i + 1, n):
            xj, yj = points[j]
            d = math.hypot(xi - xj, yi - yj)
            dist[i][j] = d
            dist[j][i] = d
    return dist

def tour_length(tour: List[int], dist: List[List[float]]) -> float:
    n = len(tour)
    if n == 0:
        return 0.0
    total = 0.0
    for i in range(n):
        total += dist[tour[i]][tour[(i + 1) % n]]
    return total

# --------- 选择(锦标赛) ----------
def tournament_selection(population: List[List[int]], costs: List[float], k: int = 3) -> List[int]:
    # 随机抽 k 个(不重复)进行锦标赛,返回最好者(复制)
    n = len(population)
    if k <= 1:
        # just pick best randomly among two
        idx = random.randrange(n)
        return population[idx][:]
    contestants = random.sample(range(n), min(k, n))
    best = min(contestants, key=lambda i: costs[i])
    return population[best][:]

# --------- 交叉算子 ----------
def order_crossover(p1: List[int], p2: List[int]) -> List[int]:
    # OX (Order Crossover)
    n = len(p1)
    child = [None] * n
    a = random.randint(0, n - 2)
    b = random.randint(a + 1, n - 1)
    # copy segment from p1
    child[a:b + 1] = p1[a:b + 1]
    # fill remaining from p2 in order
    pos = (b + 1) % n
    for gene in p2:
        if gene not in child:
            child[pos] = gene
            pos = (pos + 1) % n
    return child

def pmx_crossover(p1: List[int], p2: List[int]) -> List[int]:
    # PMX (Partially Mapped Crossover)
    n = len(p1)
    a = random.randint(0, n - 2)
    b = random.randint(a + 1, n - 1)
    child = [-1] * n
    # copy slice
    for i in range(a, b + 1):
        child[i] = p1[i]
    # mapping function
    for i in range(a, b + 1):
        if p2[i] not in child:
            pos = i
            val = p2[i]
            while True:
                # find position of val in p1
                pos = p1.index(val)
                if child[pos] == -1:
                    child[pos] = p2[i]
                    break
                else:
                    val = p2[pos]
    # fill remaining
    for i in range(n):
        if child[i] == -1:
            child[i] = p2[i]
    return child

# --------- 变异 ----------
def swap_mutation(ind: List[int], mutation_rate: float):
    n = len(ind)
    if random.random() < mutation_rate:
        i, j = random.sample(range(n), 2)
        ind[i], ind[j] = ind[j], ind[i]

def inversion_mutation(ind: List[int], mutation_rate: float):
    n = len(ind)
    if random.random() < mutation_rate:
        a = random.randint(0, n - 2)
        b = random.randint(a + 1, n - 1)
        ind[a:b + 1] = ind[a:b + 1][::-1]

def scramble_mutation(ind: List[int], mutation_rate: float):
    n = len(ind)
    if random.random() < mutation_rate:
        a = random.randint(0, n - 2)
        b = random.randint(a + 1, n - 1)
        segment = ind[a:b + 1][:]
        random.shuffle(segment)
        ind[a:b + 1] = segment

# --------- 可选:2-opt 局部改进(混合策略) ----------
def two_opt_delta(tour: List[int], i: int, j: int, dist: List[List[float]]) -> float:
    n = len(tour)
    a = tour[i - 1]
    b = tour[i]
    c = tour[j]
    d = tour[(j + 1) % n]
    return dist[a][c] + dist[b][d] - dist[a][b] - dist[c][d]

def two_opt_local_search(tour: List[int], dist: List[List[float]], max_iter: int = 50) -> List[int]:
    # 简单的 2-opt 改进(贪心寻找首个改进)
    n = len(tour)
    if n < 4:
        return tour
    improved = True
    it = 0
    while improved and it < max_iter:
        improved = False
        it += 1
        for i in range(1, n - 1):
            for j in range(i + 1, n):
                delta = two_opt_delta(tour, i, j, dist)
                if delta < -1e-12:
                    # apply 2-opt swap
                    tour[i:j + 1] = reversed(tour[i:j + 1])
                    improved = True
                    break
            if improved:
                break
    return tour

# --------- GA 主过程 ----------
def genetic_algorithm_tsp(
    dist: List[List[float]],
    pop_size: int = 100,
    generations: int = 500,
    crossover_rate: float = 0.9,
    mutation_rate: float = 0.2,
    elite_size: int = 1,
    selection_k: int = 3,
    crossover_type: str = "ox",  # "ox" or "pmx"
    mutation_type: str = "swap",  # "swap", "inversion", "scramble"
    initial_method: str = "random",  # "random" or "nearest"
    use_local_search: bool = False,
    ls_max_iter: int = 20,
    seed: Optional[int] = None,
    verbose: bool = False
):
    """
    返回 best_tour, best_cost, history(每代最优cost)
    """
    if seed is not None:
        random.seed(seed)

    n = len(dist)
    if n == 0:
        return [], 0.0, []

    # 初始化种群
    population: List[List[int]] = []
    if initial_method == "nearest":
        # 产生若干最近邻起点的解(不同起点)
        for start in range(min(pop_size, n)):
            tour = nearest_neighbor_solution(dist, start=start)
            population.append(tour)
        # 若不够,再随机补齐
    while len(population) < pop_size:
        tour = list(range(n))
        random.shuffle(tour)
        population.append(tour)

    # 计算初代代价
    costs = [tour_length(ind, dist) for ind in population]
    best_idx = min(range(pop_size), key=lambda i: costs[i])
    best_tour = population[best_idx][:]
    best_cost = costs[best_idx]
    history = [best_cost]

    # 迭代进化
    start_time = time.time()
    for gen in range(1, generations + 1):
        # 精英保留:排序并保留 elite_size 个
        pop_with_cost = list(zip(population, costs))
        pop_with_cost.sort(key=lambda x: x[1])
        new_population = [copy.deepcopy(ind) for ind, _c in pop_with_cost[:elite_size]]

        # 生成其余个体
        while len(new_population) < pop_size:
            parent1 = tournament_selection(population, costs, k=selection_k)
            parent2 = tournament_selection(population, costs, k=selection_k)
            # crossover
            if random.random() < crossover_rate:
                if crossover_type == "pmx":
                    child1 = pmx_crossover(parent1, parent2)
                    child2 = pmx_crossover(parent2, parent1)
                else:
                    child1 = order_crossover(parent1, parent2)
                    child2 = order_crossover(parent2, parent1)
            else:
                child1 = parent1[:]
                child2 = parent2[:]
            # mutation
            if mutation_type == "swap":
                swap_mutation(child1, mutation_rate)
                swap_mutation(child2, mutation_rate)
            elif mutation_type == "inversion":
                inversion_mutation(child1, mutation_rate)
                inversion_mutation(child2, mutation_rate)
            else:
                scramble_mutation(child1, mutation_rate)
                scramble_mutation(child2, mutation_rate)
            # optional local search
            if use_local_search:
                child1 = two_opt_local_search(child1, dist, max_iter=ls_max_iter)
                child2 = two_opt_local_search(child2, dist, max_iter=ls_max_iter)
            new_population.append(child1)
            if len(new_population) < pop_size:
                new_population.append(child2)

        # 更新种群与代价
        population = new_population
        costs = [tour_length(ind, dist) for ind in population]
        gen_best_idx = min(range(pop_size), key=lambda i: costs[i])
        gen_best_cost = costs[gen_best_idx]
        gen_best_tour = population[gen_best_idx][:]

        if gen_best_cost < best_cost:
            best_cost = gen_best_cost
            best_tour = gen_best_tour[:]

        history.append(best_cost)

        if verbose and (gen % max(1, generations // 10) == 0 or gen <= 5):
            elapsed = time.time() - start_time
            print(f"Gen {gen}/{generations}  best_cost={best_cost:.4f}  elapsed={elapsed:.1f}s")

    return best_tour, best_cost, history

# --------- 辅助:最近邻初解 ----------
def nearest_neighbor_solution(dist: List[List[float]], start: Optional[int] = None) -> List[int]:
    n = len(dist)
    if n == 0:
        return []
    if start is None:
        start = random.randrange(n)
    unvisited = set(range(n))
    tour = [start]
    unvisited.remove(start)
    current = start
    while unvisited:
        next_node = min(unvisited, key=lambda x: dist[current][x])
        tour.append(next_node)
        unvisited.remove(next_node)
        current = next_node
    return tour

# ---------------- Demo / example ----------------
if __name__ == "__main__":
    # 生成随机点并运行 GA
    random.seed(1)
    N = 50  # 城市数量,可修改尝试(例如 20, 50, 100)
    points = [(random.random() * 100, random.random() * 100) for _ in range(N)]
    dist = compute_distance_matrix(points)

    best_tour, best_cost, history = genetic_algorithm_tsp(
        dist,
        pop_size=120,
        generations=400,
        crossover_rate=0.9,
        mutation_rate=0.2,
        elite_size=2,
        selection_k=3,
        crossover_type="ox",
        mutation_type="inversion",
        initial_method="nearest",
        use_local_search=True,  # 启用 2-opt 局部改进会显著提高解质量但更慢
        ls_max_iter=10,
        seed=1,
        verbose=True
    )

    print("Best cost:", best_cost)
    # 可视化(如果安装 matplotlib)
    try:
        import matplotlib.pyplot as plt
        xs = [points[i][0] for i in best_tour] + [points[best_tour[0]][0]]
        ys = [points[i][1] for i in best_tour] + [points[best_tour[0]][1]]
        plt.figure(figsize=(6, 6))
        plt.plot(xs, ys, '-o')
        plt.title(f"GA TSP best (cost={best_cost:.2f})")
        plt.show()

        plt.figure()
        plt.plot(history)
        plt.xlabel("Generation")
        plt.ylabel("Best cost so far")
        plt.title("Convergence")
        plt.show()
    except Exception:
        pass

其他经验:

种群大小(pop_size):通常 50–300;种群大收敛更稳定但更慢。

代数(generations):视问题规模,几十到几千代不等。

交叉率(crossover_rate):常设 0.7–0.95。

变异率(mutation_rate):一般较小 0.01–0.3(对排列编码,30%也常见),如果多代无改进可稍微调高。

选择方式与 k(selection_k):锦标赛 k=2~5,k 越大选择压力越强(更快收敛但易陷入局部)。

精英数(elite_size):1~5,保证好解不会丢失。

是否加局部搜索:把 GA 与 2-opt/3-opt 混合通常大幅提升解质量(代价是慢)。在后代上做一次轻量 2-opt 是常用实践。

多次运行取最好:GA 有随机性,重复多次取最好结果简单且有效。

为什么GA+局部搜索 混合方式较好?

GA 的全局搜索,与局部搜索的快速局部改进,优势互补,

GA 的全局搜索可以交叉产生多样性,局部搜索可以进行快速局部改进。前者探索结构化的解空间,后者快速消除局部不优。

tips:实际强算法通常是“元启发式 + 局部搜索”的组合(memetic algorithm)。

模拟退火

模拟退火算法(Simulated Annealing): 借鉴了物理学中固体退火的过程,通过接受一些次优的解来跳出局部最优,从而寻找全局最优解。

目标:把 TSP 中的一条路径(tour)看成“系统状态”,把路径长度看成“能量(cost)”,我们希望使能量尽可能低。

邻域(如何从一个解跳到另一个解):常用的邻域操作有 swap(交换两个城市)和 2-opt(选取 i<j,然后把 i..j 段翻转)。2-opt 在 TSP 中通常很有效。

温度(Temperature):一个控制随机接受劣解概率的参数。温度高时更容易接受变差的解(允许跳出局部最优);温度低时趋向只接受更好的解。

接受准则:若新解更好(cost 减少),直接接受;若更差,按概率 exp(-Δ / T) 接受(Δ = 新cost - 旧cost)。

降温(Cooling schedule):常用指数冷却:T = alpha * T,alpha 在 [0.90, 0.9999] 中,越接近 1 冷却越慢。

停止条件:温度降到某个阈值或达到最大迭代次数或长时间没有改进。

2-opt(选取 i<j,然后把 i..j 段翻转)翻转一段能消除两条交叉边,常能显著改善路径,而且用解析式可以快速计算成本变化(不用重算整条路径),因此速度与效果都不错。

import math
import random
import time
from typing import List, Tuple, Optional


def compute_distance_matrix(points: List[Tuple[float,float]]) -> List[List[float]]:
    """Compute symmetric Euclidean distance matrix."""
    n = len(points)
    dist = [[0.0]*n for _ in range(n)]
    for i in range(n):
        xi, yi = points[i]
        for j in range(i+1, n):
            xj, yj = points[j]
            d = math.hypot(xi-xj, yi-yj)
            dist[i][j] = d
            dist[j][i] = d
    return dist


def tour_length(tour: List[int], dist: List[List[float]]) -> float:
    n = len(tour)
    total = 0.0
    for i in range(n):
        total += dist[tour[i]][tour[(i+1)%n]]
    return total


def nearest_neighbor_solution(dist: List[List[float]], start: Optional[int]=None) -> List[int]:
    """Greedy nearest-neighbor initial solution (fast, usually reasonable)."""
    n = len(dist)
    if n == 0:
        return []
    if start is None:
        start = random.randrange(n)
    unvisited = set(range(n))
    tour = [start]
    unvisited.remove(start)
    current = start
    while unvisited:
        next_node = min(unvisited, key=lambda x: dist[current][x])
        tour.append(next_node)
        unvisited.remove(next_node)
        current = next_node
    return tour


def two_opt_delta(tour: List[int], i: int, j: int, dist: List[List[float]]) -> float:
    """
    Compute cost difference if we reverse tour[i:j+1] (2-opt).
    delta = new_cost - old_cost. Positive -> worse.
    Uses only 4 edge lookups (constant time).
    """
    n = len(tour)
    a = tour[i-1]
    b = tour[i]
    c = tour[j]
    d = tour[(j+1) % n]
    # Remove edges (a-b) and (c-d), add (a-c) and (b-d)
    return dist[a][c] + dist[b][d] - dist[a][b] - dist[c][d]


def simulated_annealing_tsp(
    dist: List[List[float]],
    initial_solution: str = "nearest",  # "nearest" or "random"
    initial_temp: Optional[float] = None,
    stopping_temp: float = 1e-3,
    alpha: float = 0.995,
    iter_per_temp: Optional[int] = None,
    max_iterations: int = 1_000_000,
    seed: Optional[int] = None,
    verbose: bool = False
):
    """
    Simulated annealing for symmetric TSP using 2-opt neighbor.
    Returns best_tour, best_cost, history (list of best_costs over time).
    """
    if seed is not None:
        random.seed(seed)
    n = len(dist)
    if n == 0:
        return [], 0.0, []

    # initial solution
    if initial_solution == "nearest":
        current_tour = nearest_neighbor_solution(dist, start=0)
    else:
        current_tour = list(range(n))
        random.shuffle(current_tour)

    current_cost = tour_length(current_tour, dist)
    best_tour = current_tour[:]
    best_cost = current_cost

    # automatic temperature initialization based on average edge length
    if initial_temp is None:
        sum_edges = 0.0
        for i in range(n):
            for j in range(i+1, n):
                sum_edges += dist[i][j]
        avg_edge = (2 * sum_edges) / (n * (n - 1)) if n > 1 else 0.0
        initial_temp = avg_edge * n if avg_edge > 0 else 1.0

    if iter_per_temp is None:
        iter_per_temp = 100 * n  # heuristic: try ~100*n attempts per temperature

    T = initial_temp
    iteration = 0
    history = [best_cost]

    start_time = time.time()
    while T > stopping_temp and iteration < max_iterations:
        for _ in range(iter_per_temp):
            # choose i < j for 2-opt
            i = random.randint(0, n-2)
            j = random.randint(i+1, n-1)
            # skip reversing the entire tour (gives same tour)
            if i == 0 and j == n-1:
                continue
            delta = two_opt_delta(current_tour, i, j, dist)
            # accept if better or by probability
            if delta < 0 or random.random() < math.exp(-delta / T):
                # apply 2-opt: reverse the segment i..j
                current_tour = current_tour[:i] + current_tour[i:j+1][::-1] + current_tour[j+1:]
                current_cost += delta
                # update best
                if current_cost < best_cost:
                    best_cost = current_cost
                    best_tour = current_tour[:]
                    history.append(best_cost)
            iteration += 1
            if iteration >= max_iterations:
                break
        T *= alpha
        if verbose and iteration % (10 * iter_per_temp) == 0:
            elapsed = time.time() - start_time
            print(f"iter={iteration}, T={T:.5f}, best_cost={best_cost:.4f}, elapsed={elapsed:.1f}s")
    return best_tour, best_cost, history


# ---------------- Example usage ----------------
if __name__ == "__main__":
    import random
    random.seed(0)
    N = 50
    # generate random points in 0..100 square
    pts = [(random.random()*100, random.random()*100) for _ in range(N)]
    d = compute_distance_matrix(pts)
    # run SA
    best, cost, hist = simulated_annealing_tsp(
        d,
        initial_solution="nearest",
        alpha=0.995,
        iter_per_temp=200*len(d),  # more attempts per T -> better but slower
        seed=0,
        verbose=True
    )
    print("best cost:", cost)
    # optional plotting (requires matplotlib)
    try:
        import matplotlib.pyplot as plt
        def plot_tour(points, tour, title=None):
            xs = [points[i][0] for i in tour] + [points[tour[0]][0]]
            ys = [points[i][1] for i in tour] + [points[tour[0]][1]]
            plt.figure(figsize=(6,6))
            plt.plot(xs, ys, '-o')
            if title:
                plt.title(title)
            plt.show()
        plot_tour(pts, best, f"SA best (cost={cost:.2f})")
    except Exception:
        pass

蚁群优化算法

蚁群优化算法(Ant Colony Optimization): 灵感来源于蚂蚁寻找食物的路径行为。蚂蚁在寻找食物时会在路径上留下信息素(pheromone),其他蚂蚁更倾向于沿着信息素浓度高的路径走。好的路径(短)会被更多蚂蚁强化,从而逐渐成为主流解。

一个“蚂蚁”构造一条 TSP 路径(tour),路径长度为目标(越短越好)。

两种信息

  • 信息素(pheromone)τ(i,j):基于历史经验,某条边的吸引力(学习到的好/坏)。

  • 启发式信息(visibility)η(i,j):瞬时启发式,通常取 1 / distance(i,j),也就是更倾向走短边。

转移概率:蚂蚁从城市 i 选择下一个城市 j 的概率与 (τ(i,j)^α) * (η(i,j)^β) 成正比,α 控制信息素重要性,β 控制启发式重要性。

信息素更新

  • 挥发(evaporation):所有边的 τ 乘上 (1 - ρ),避免过早收敛。

  • 沉积(deposit):每只(或最优)蚂蚁在其经过的边上增加 Δτ = Q / L(L 为该蚂蚁路径长度),优良路径贡献更大。

优缺点:

适合组合优化、容易并行化、能自然平衡探索/利用。缺点:参数较敏感、对大规模问题单纯 ACO 收敛慢。

把 ACO 和局部搜索(如 2-opt/3-opt)结合,或采用 Max-Min ACO/Ant Colony System 的变种以改进收敛。

"""
aco_tsp.py — 教学版 ACO 求解 TSP

返回值:
    best_tour, best_cost, history

主要参数说明见函数注释(ant_colony_tsp)。
"""
import random
import math
import time
from typing import List, Tuple, Optional

# ---------- 基础工具 ----------
def compute_distance_matrix(points: List[Tuple[float, float]]) -> List[List[float]]:
    n = len(points)
    dist = [[0.0]*n for _ in range(n)]
    for i in range(n):
        xi, yi = points[i]
        for j in range(i+1, n):
            xj, yj = points[j]
            d = math.hypot(xi - xj, yi - yj)
            dist[i][j] = d
            dist[j][i] = d
    return dist

def tour_length(tour: List[int], dist: List[List[float]]) -> float:
    n = len(tour)
    if n == 0:
        return 0.0
    total = 0.0
    for i in range(n):
        total += dist[tour[i]][tour[(i+1) % n]]
    return total

def nearest_neighbor_solution(dist: List[List[float]], start: int = 0) -> List[int]:
    n = len(dist)
    unvisited = set(range(n))
    tour = [start]
    unvisited.remove(start)
    current = start
    while unvisited:
        next_node = min(unvisited, key=lambda x: dist[current][x])
        tour.append(next_node)
        unvisited.remove(next_node)
        current = next_node
    return tour

# ---------- 可选的 2-opt 局部改进(提升解质量) ----------
def two_opt_delta(tour: List[int], i: int, j: int, dist: List[List[float]]) -> float:
    n = len(tour)
    a = tour[i-1]
    b = tour[i]
    c = tour[j]
    d = tour[(j+1) % n]
    return dist[a][c] + dist[b][d] - dist[a][b] - dist[c][d]

def two_opt_local_search(tour: List[int], dist: List[List[float]], max_iter: int = 50) -> List[int]:
    n = len(tour)
    if n < 4:
        return tour
    improved = True
    it = 0
    while improved and it < max_iter:
        improved = False
        it += 1
        for i in range(1, n-1):
            for j in range(i+1, n):
                delta = two_opt_delta(tour, i, j, dist)
                if delta < -1e-12:
                    tour[i:j+1] = reversed(tour[i:j+1])
                    improved = True
                    break
            if improved:
                break
    return tour

# ---------- ACO 主算法 ----------
def ant_colony_tsp(
    dist: List[List[float]],
    n_ants: Optional[int] = None,
    n_iterations: int = 200,
    alpha: float = 1.0,          # 信息素重要度
    beta: float = 2.0,           # 启发函数(visibility)重要度
    rho: float = 0.1,            # 挥发率
    q: float = 1.0,              # 信息素强度因子(沉积时 Q / L)
    deposit: str = "iteration_best",  # "all" / "iteration_best" / "global_best"
    initial_pheromone: Optional[float] = None,
    use_2opt: bool = False,
    ls_max_iter: int = 10,
    seed: Optional[int] = None,
    verbose: bool = False
):
    """
    dist: 对称距离矩阵
    n_ants: 蚂蚁数,默认等于城市数
    n_iterations: 迭代次数
    alpha, beta: 控制转移概率中信息素和启发信息的权重
    rho: 信息素挥发率 (0 < rho < 1)
    q: 信息素沉积常数(通常与问题规模和距离尺度相关)
    deposit: 沉积策略,"all" 所有蚂蚁沉积, "iteration_best" 本代最佳沉积, "global_best" 全局最佳沉积
    initial_pheromone: 如果 None,会根据最近邻解自动估计一个合理值
    use_2opt: 是否对每个蚂蚁(或最佳)做 2-opt 局部改进
    ls_max_iter: 2-opt 的最大迭代次数(每次局部改进的限制)
    seed: 随机种子(便于复现)
    """
    if seed is not None:
        random.seed(seed)

    n = len(dist)
    if n == 0:
        return [], 0.0, []

    if n_ants is None:
        n_ants = n

    # 计算启发信息(visibility = 1 / distance)
    eps = 1e-12
    visibility = [[0.0]*n for _ in range(n)]
    for i in range(n):
        for j in range(n):
            if i != j:
                visibility[i][j] = 1.0 / (dist[i][j] + eps)

    # 初始信息素估计(用最近邻长度做一个启发)
    if initial_pheromone is None:
        try:
            nn = nearest_neighbor_solution(dist, start=0)
            L_nn = tour_length(nn, dist)
            initial_pheromone = 1.0 / (n * L_nn) if L_nn > 0 else 1.0
        except Exception:
            initial_pheromone = 1.0

    # 信息素矩阵(对称)
    tau = [[initial_pheromone]*n for _ in range(n)]
    for i in range(n):
        tau[i][i] = 0.0

    best_tour = None
    best_cost = float('inf')
    history = []

    start_time = time.time()
    for it in range(1, n_iterations+1):
        all_tours = []
        all_costs = []

        # 每只蚂蚁构造解
        for a in range(n_ants):
            start_city = random.randrange(n)
            tour = [start_city]
            visited = set(tour)
            current = start_city

            while len(tour) < n:
                # 计算可选城市的选择概率(未访问城市)
                probs = []
                denom = 0.0
                for j in range(n):
                    if j in visited:
                        probs.append(0.0)
                    else:
                        val = (tau[current][j] ** alpha) * (visibility[current][j] ** beta)
                        probs.append(val)
                        denom += val

                if denom == 0.0:
                    # 若所有值都为 0(极端情况),随机选择一个未访问城市
                    choices = [c for c in range(n) if c not in visited]
                    next_city = random.choice(choices)
                else:
                    # 轮盘赌选择
                    r = random.random()
                    cumulative = 0.0
                    next_city = None
                    for j, p in enumerate(probs):
                        if j in visited:
                            continue
                        cumulative += p / denom
                        if r <= cumulative:
                            next_city = j
                            break
                    if next_city is None:
                        # 数值误差导致未选中,退化为随机选择
                        choices = [c for c in range(n) if c not in visited]
                        next_city = random.choice(choices)

                tour.append(next_city)
                visited.add(next_city)
                current = next_city

            # 可选局部改进
            if use_2opt:
                tour = two_opt_local_search(tour, dist, max_iter=ls_max_iter)

            cost = tour_length(tour, dist)
            all_tours.append(tour)
            all_costs.append(cost)

        # 找出本代最优解
        iter_best_idx = min(range(len(all_costs)), key=lambda i: all_costs[i])
        iter_best_cost = all_costs[iter_best_idx]
        iter_best_tour = all_tours[iter_best_idx]

        if iter_best_cost < best_cost:
            best_cost = iter_best_cost
            best_tour = iter_best_tour[:]

        history.append(best_cost)

        # 信息素挥发
        for i in range(n):
            for j in range(n):
                tau[i][j] *= (1.0 - rho)
                if tau[i][j] < 1e-16:
                    tau[i][j] = 1e-16  # 避免数值归零

        # 信息素沉积
        if deposit == "all":
            # 所有蚂蚁沉积
            for k, tour in enumerate(all_tours):
                Lk = all_costs[k]
                delta = q / (Lk + 1e-12)
                for idx in range(n):
                    i = tour[idx]
                    j = tour[(idx+1) % n]
                    tau[i][j] += delta
                    tau[j][i] += delta
        elif deposit == "iteration_best":
            # 本代最好蚂蚁沉积
            delta = q / (iter_best_cost + 1e-12)
            for idx in range(n):
                i = iter_best_tour[idx]
                j = iter_best_tour[(idx+1) % n]
                tau[i][j] += delta
                tau[j][i] += delta
        elif deposit == "global_best":
            # 全局最好蚂蚁沉积
            delta = q / (best_cost + 1e-12)
            for idx in range(n):
                i = best_tour[idx]
                j = best_tour[(idx+1) % n]
                tau[i][j] += delta
                tau[j][i] += delta
        else:
            # 默认与 iteration_best 相同
            delta = q / (iter_best_cost + 1e-12)
            for idx in range(n):
                i = iter_best_tour[idx]
                j = iter_best_tour[(idx+1) % n]
                tau[i][j] += delta
                tau[j][i] += delta

        if verbose and (it % max(1, n_iterations//10) == 0 or it <= 5):
            elapsed = time.time() - start_time
            print(f"Iter {it}/{n_iterations}  best_cost_so_far={best_cost:.4f}  iter_best={iter_best_cost:.4f}  elapsed={elapsed:.1f}s")

    return best_tour, best_cost, history

# ------------------ 示例与可视化 ------------------
if __name__ == "__main__":
    # 生成随机点并运行示例
    random.seed(0)
    N = 40
    points = [(random.random()*100, random.random()*100) for _ in range(N)]
    dist = compute_distance_matrix(points)

    best_tour, best_cost, history = ant_colony_tsp(
        dist,
        n_ants=30,
        n_iterations=200,
        alpha=1.0,
        beta=3.0,
        rho=0.1,
        q=1.0,
        deposit="iteration_best",
        use_2opt=True,
        ls_max_iter=8,
        seed=0,
        verbose=True
    )

    print("Best cost:", best_cost)

    # 可视化(需要 matplotlib)
    try:
        import matplotlib.pyplot as plt
        xs = [points[i][0] for i in best_tour] + [points[best_tour[0]][0]]
        ys = [points[i][1] for i in best_tour] + [points[best_tour[0]][1]]
        plt.figure(figsize=(6,6))
        plt.plot(xs, ys, '-o')
        plt.title(f"ACO best (cost={best_cost:.2f})")
        plt.show()

        plt.figure()
        plt.plot(history)
        plt.xlabel("Iteration")
        plt.ylabel("Best cost so far")
        plt.title("Convergence")
        plt.show()
    except Exception:
        pass

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>TSP 蚁群算法动画演示</title>
    <style>
        body {
            margin: 0;
            padding: 20px;
            font-family: 'Arial', sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            min-height: 100vh;
            color: white;
        }
        
        .container {
            max-width: 1200px;
            margin: 0 auto;
            background: rgba(255, 255, 255, 0.1);
            backdrop-filter: blur(10px);
            border-radius: 20px;
            padding: 30px;
            box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
        }
        
        h1 {
            text-align: center;
            margin-bottom: 30px;
            font-size: 2.5em;
            text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
        }
        
        .controls {
            display: flex;
            justify-content: center;
            gap: 15px;
            margin-bottom: 30px;
            flex-wrap: wrap;
        }
        
        .control-group {
            display: flex;
            flex-direction: column;
            align-items: center;
            gap: 5px;
        }
        
        button {
            padding: 12px 24px;
            border: none;
            border-radius: 25px;
            background: linear-gradient(45deg, #4CAF50, #45a049);
            color: white;
            font-size: 16px;
            cursor: pointer;
            transition: all 0.3s ease;
            box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
        }
        
        button:hover {
            transform: translateY(-2px);
            box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3);
        }
        
        button:disabled {
            background: linear-gradient(45deg, #666, #555);
            cursor: not-allowed;
            transform: none;
        }
        
        .reset-btn {
            background: linear-gradient(45deg, #f44336, #d32f2f);
        }
        
        input[type="range"] {
            width: 150px;
            margin: 0 10px;
        }
        
        label {
            font-size: 14px;
            color: rgba(255, 255, 255, 0.9);
        }
        
        #canvas {
            border: 2px solid rgba(255, 255, 255, 0.3);
            border-radius: 15px;
            background: rgba(0, 0, 0, 0.1);
            display: block;
            margin: 0 auto;
            box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
        }
        
        .stats {
            display: flex;
            justify-content: space-around;
            margin-top: 20px;
            gap: 20px;
            flex-wrap: wrap;
        }
        
        .stat-card {
            background: rgba(255, 255, 255, 0.1);
            padding: 15px 25px;
            border-radius: 15px;
            text-align: center;
            min-width: 120px;
            backdrop-filter: blur(5px);
        }
        
        .stat-title {
            font-size: 14px;
            opacity: 0.8;
            margin-bottom: 5px;
        }
        
        .stat-value {
            font-size: 24px;
            font-weight: bold;
            color: #4CAF50;
        }
        
        .info {
            margin-top: 20px;
            padding: 15px;
            background: rgba(255, 255, 255, 0.1);
            border-radius: 10px;
            font-size: 14px;
            line-height: 1.6;
        }
        
        .legend {
            display: flex;
            justify-content: center;
            gap: 30px;
            margin-top: 15px;
            flex-wrap: wrap;
        }
        
        .legend-item {
            display: flex;
            align-items: center;
            gap: 8px;
        }
        
        .legend-color {
            width: 20px;
            height: 20px;
            border-radius: 50%;
            border: 2px solid white;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>🐜 TSP 蚁群算法动画演示</h1>
        
        <div class="controls">
            <div class="control-group">
                <label>城市数量</label>
                <input type="range" id="cityCount" min="5" max="20" value="10">
                <span id="cityCountValue">10</span>
            </div>
            <div class="control-group">
                <label>蚂蚁数量</label>
                <input type="range" id="antCount" min="10" max="50" value="20">
                <span id="antCountValue">20</span>
            </div>
            <div class="control-group">
                <label>动画速度</label>
                <input type="range" id="speed" min="1" max="10" value="5">
                <span id="speedValue">5</span>
            </div>
        </div>
        
        <div class="controls">
            <button id="generateBtn">🏙️ 生成新城市</button>
            <button id="startBtn">🚀 开始算法</button>
            <button id="pauseBtn" disabled>⏸️ 暂停</button>
            <button id="resetBtn" class="reset-btn">🔄 重置</button>
        </div>
        
        <canvas id="canvas" width="800" height="600"></canvas>
        
        <div class="stats">
            <div class="stat-card">
                <div class="stat-title">当前代数</div>
                <div class="stat-value" id="generation">0</div>
            </div>
            <div class="stat-card">
                <div class="stat-title">最佳距离</div>
                <div class="stat-value" id="bestDistance">∞</div>
            </div>
            <div class="stat-card">
                <div class="stat-title">活跃蚂蚁</div>
                <div class="stat-value" id="activeAnts">0</div>
            </div>
            <div class="stat-card">
                <div class="stat-title">信息素强度</div>
                <div class="stat-value" id="pheromoneLevel">1.0</div>
            </div>
        </div>
        
        <div class="legend">
            <div class="legend-item">
                <div class="legend-color" style="background: #FFD700;"></div>
                <span>城市</span>
            </div>
            <div class="legend-item">
                <div class="legend-color" style="background: #FF6B6B;"></div>
                <span>蚂蚁</span>
            </div>
            <div class="legend-item">
                <div class="legend-color" style="background: #4ECDC4;"></div>
                <span>最佳路径</span>
            </div>
            <div class="legend-item">
                <div class="legend-color" style="background: rgba(100, 200, 100, 0.3);"></div>
                <span>信息素</span>
            </div>
        </div>
        
        <div class="info">
            <strong>🧠 算法原理:</strong>
            蚁群算法模拟真实蚂蚁寻找食物的行为。蚂蚁在路径上留下信息素,越短的路径信息素浓度越高,
            后续蚂蚁更倾向于选择信息素浓度高的路径。通过多代迭代,最终找到近似最优解。
            <br><br>
            <strong>📊 参数说明:</strong>
            信息素重要性(α)=1,启发式重要性(β)=2,信息素挥发率(ρ)=0.1,信息素增强系数(Q)=100
        </div>
    </div>

    <script>
        class TSPAntColony {
            constructor() {
                this.canvas = document.getElementById('canvas');
                this.ctx = this.canvas.getContext('2d');
                this.width = this.canvas.width;
                this.height = this.canvas.height;
                
                // 算法参数
                this.alpha = 1; // 信息素重要性
                this.beta = 2;  // 启发式重要性
                this.rho = 0.1; // 信息素挥发率
                this.Q = 100;   // 信息素增强系数
                
                // 状态变量
                this.cities = [];
                this.ants = [];
                this.pheromones = [];
                this.distances = [];
                this.bestPath = [];
                this.bestDistance = Infinity;
                this.generation = 0;
                this.isRunning = false;
                this.animationId = null;
                
                this.initializeEventListeners();
                this.generateRandomCities();
            }
            
            initializeEventListeners() {
                // 控制按钮
                document.getElementById('generateBtn').onclick = () => this.generateRandomCities();
                document.getElementById('startBtn').onclick = () => this.startAlgorithm();
                document.getElementById('pauseBtn').onclick = () => this.pauseAlgorithm();
                document.getElementById('resetBtn').onclick = () => this.resetAlgorithm();
                
                // 滑动条更新
                ['cityCount', 'antCount', 'speed'].forEach(id => {
                    const slider = document.getElementById(id);
                    const valueSpan = document.getElementById(id + 'Value');
                    slider.oninput = () => {
                        valueSpan.textContent = slider.value;
                        if (id === 'cityCount' && !this.isRunning) {
                            this.generateRandomCities();
                        }
                    };
                });
            }
            
            generateRandomCities() {
                const count = parseInt(document.getElementById('cityCount').value);
                this.cities = [];
                
                // 生成随机城市位置
                for (let i = 0; i < count; i++) {
                    this.cities.push({
                        x: Math.random() * (this.width - 100) + 50,
                        y: Math.random() * (this.height - 100) + 50,
                        id: i
                    });
                }
                
                this.calculateDistances();
                this.initializePheromones();
                this.resetStats();
                this.draw();
            }
            
            calculateDistances() {
                const n = this.cities.length;
                this.distances = Array(n).fill().map(() => Array(n).fill(0));
                
                for (let i = 0; i < n; i++) {
                    for (let j = 0; j < n; j++) {
                        if (i !== j) {
                            const dx = this.cities[i].x - this.cities[j].x;
                            const dy = this.cities[i].y - this.cities[j].y;
                            this.distances[i][j] = Math.sqrt(dx * dx + dy * dy);
                        }
                    }
                }
            }
            
            initializePheromones() {
                const n = this.cities.length;
                this.pheromones = Array(n).fill().map(() => Array(n).fill(1));
            }
            
            startAlgorithm() {
                if (this.cities.length < 3) return;
                
                this.isRunning = true;
                document.getElementById('startBtn').disabled = true;
                document.getElementById('pauseBtn').disabled = false;
                document.getElementById('generateBtn').disabled = true;
                
                this.runGeneration();
            }
            
            pauseAlgorithm() {
                this.isRunning = false;
                if (this.animationId) {
                    cancelAnimationFrame(this.animationId);
                }
                document.getElementById('startBtn').disabled = false;
                document.getElementById('pauseBtn').disabled = true;
                document.getElementById('generateBtn').disabled = false;
            }
            
            resetAlgorithm() {
                this.pauseAlgorithm();
                this.resetStats();
                this.initializePheromones();
                this.draw();
            }
            
            resetStats() {
                this.bestPath = [];
                this.bestDistance = Infinity;
                this.generation = 0;
                this.ants = [];
                this.updateStats();
            }
            
            runGeneration() {
                if (!this.isRunning) return;
                
                // 创建蚂蚁
                const antCount = parseInt(document.getElementById('antCount').value);
                this.ants = [];
                
                for (let i = 0; i < antCount; i++) {
                    this.ants.push({
                        currentCity: Math.floor(Math.random() * this.cities.length),
                        visitedCities: [],
                        path: [],
                        totalDistance: 0,
                        isComplete: false
                    });
                }
                
                this.generation++;
                this.animateAnts();
            }
            
            animateAnts() {
                const speed = parseInt(document.getElementById('speed').value);
                let antIndex = 0;
                let stepCount = 0;
                
                const animate = () => {
                    if (!this.isRunning) return;
                    
                    // 移动蚂蚁
                    let allComplete = true;
                    for (let ant of this.ants) {
                        if (!ant.isComplete) {
                            allComplete = false;
                            this.moveAnt(ant);
                        }
                    }
                    
                    this.draw();
                    this.updateStats();
                    
                    if (allComplete) {
                        // 更新信息素
                        this.updatePheromones();
                        
                        // 继续下一代
                        setTimeout(() => {
                            if (this.isRunning) {
                                this.runGeneration();
                            }
                        }, 1000 / speed);
                    } else {
                        this.animationId = requestAnimationFrame(() => {
                            setTimeout(animate, 100 / speed);
                        });
                    }
                };
                
                animate();
            }
            
            moveAnt(ant) {
                if (ant.path.length === 0) {
                    ant.path.push(ant.currentCity);
                    ant.visitedCities.push(ant.currentCity);
                }
                
                if (ant.visitedCities.length === this.cities.length) {
                    // 回到起始城市
                    const startCity = ant.path[0];
                    ant.path.push(startCity);
                    ant.totalDistance += this.distances[ant.currentCity][startCity];
                    ant.isComplete = true;
                    
                    // 更新最佳路径
                    if (ant.totalDistance < this.bestDistance) {
                        this.bestDistance = ant.totalDistance;
                        this.bestPath = [...ant.path];
                    }
                    return;
                }
                
                // 选择下一个城市
                const nextCity = this.selectNextCity(ant);
                if (nextCity !== -1) {
                    ant.totalDistance += this.distances[ant.currentCity][nextCity];
                    ant.currentCity = nextCity;
                    ant.path.push(nextCity);
                    ant.visitedCities.push(nextCity);
                }
            }
            
            selectNextCity(ant) {
                const unvisited = [];
                for (let i = 0; i < this.cities.length; i++) {
                    if (!ant.visitedCities.includes(i)) {
                        unvisited.push(i);
                    }
                }
                
                if (unvisited.length === 0) return -1;
                
                // 计算概率
                const probabilities = [];
                let totalProbability = 0;
                
                for (let city of unvisited) {
                    const pheromone = Math.pow(this.pheromones[ant.currentCity][city], this.alpha);
                    const heuristic = Math.pow(1.0 / this.distances[ant.currentCity][city], this.beta);
                    const probability = pheromone * heuristic;
                    
                    probabilities.push(probability);
                    totalProbability += probability;
                }
                
                // 轮盘赌选择
                let random = Math.random() * totalProbability;
                for (let i = 0; i < unvisited.length; i++) {
                    random -= probabilities[i];
                    if (random <= 0) {
                        return unvisited[i];
                    }
                }
                
                return unvisited[unvisited.length - 1];
            }
            
            updatePheromones() {
                const n = this.cities.length;
                
                // 信息素挥发
                for (let i = 0; i < n; i++) {
                    for (let j = 0; j < n; j++) {
                        this.pheromones[i][j] *= (1 - this.rho);
                    }
                }
                
                // 蚂蚁留下信息素
                for (let ant of this.ants) {
                    if (ant.isComplete) {
                        const delta = this.Q / ant.totalDistance;
                        for (let i = 0; i < ant.path.length - 1; i++) {
                            const from = ant.path[i];
                            const to = ant.path[i + 1];
                            this.pheromones[from][to] += delta;
                            this.pheromones[to][from] += delta;
                        }
                    }
                }
            }
            
            draw() {
                this.ctx.clearRect(0, 0, this.width, this.height);
                
                // 绘制信息素
                this.drawPheromones();
                
                // 绘制最佳路径
                if (this.bestPath.length > 0) {
                    this.drawPath(this.bestPath, '#4ECDC4', 4);
                }
                
                // 绘制城市
                this.drawCities();
                
                // 绘制蚂蚁
                this.drawAnts();
            }
            
            drawPheromones() {
                const maxPheromone = Math.max(...this.pheromones.flat());
                
                for (let i = 0; i < this.cities.length; i++) {
                    for (let j = i + 1; j < this.cities.length; j++) {
                        const intensity = this.pheromones[i][j] / maxPheromone;
                        if (intensity > 0.1) {
                            this.ctx.strokeStyle = `rgba(100, 200, 100, ${intensity * 0.5})`;
                            this.ctx.lineWidth = intensity * 3;
                            this.ctx.beginPath();
                            this.ctx.moveTo(this.cities[i].x, this.cities[i].y);
                            this.ctx.lineTo(this.cities[j].x, this.cities[j].y);
                            this.ctx.stroke();
                        }
                    }
                }
            }
            
            drawCities() {
                this.ctx.fillStyle = '#FFD700';
                this.ctx.strokeStyle = '#FFA500';
                this.ctx.lineWidth = 2;
                
                for (let i = 0; i < this.cities.length; i++) {
                    const city = this.cities[i];
                    this.ctx.beginPath();
                    this.ctx.arc(city.x, city.y, 8, 0, 2 * Math.PI);
                    this.ctx.fill();
                    this.ctx.stroke();
                    
                    // 城市标签
                    this.ctx.fillStyle = 'white';
                    this.ctx.font = '12px Arial';
                    this.ctx.textAlign = 'center';
                    this.ctx.fillText(i.toString(), city.x, city.y + 4);
                    this.ctx.fillStyle = '#FFD700';
                }
            }
            
            drawAnts() {
                for (let ant of this.ants) {
                    if (!ant.isComplete && ant.path.length > 0) {
                        const city = this.cities[ant.currentCity];
                        
                        // 蚂蚁身体
                        this.ctx.fillStyle = '#FF6B6B';
                        this.ctx.beginPath();
                        this.ctx.arc(city.x, city.y, 4, 0, 2 * Math.PI);
                        this.ctx.fill();
                        
                        // 蚂蚁路径
                        if (ant.path.length > 1) {
                            this.ctx.strokeStyle = 'rgba(255, 107, 107, 0.3)';
                            this.ctx.lineWidth = 1;
                            this.ctx.beginPath();
                            for (let i = 0; i < ant.path.length - 1; i++) {
                                const from = this.cities[ant.path[i]];
                                const to = this.cities[ant.path[i + 1]];
                                if (i === 0) {
                                    this.ctx.moveTo(from.x, from.y);
                                }
                                this.ctx.lineTo(to.x, to.y);
                            }
                            this.ctx.stroke();
                        }
                    }
                }
            }
            
            drawPath(path, color, width) {
                if (path.length < 2) return;
                
                this.ctx.strokeStyle = color;
                this.ctx.lineWidth = width;
                this.ctx.setLineDash([]);
                this.ctx.beginPath();
                
                const startCity = this.cities[path[0]];
                this.ctx.moveTo(startCity.x, startCity.y);
                
                for (let i = 1; i < path.length; i++) {
                    const city = this.cities[path[i]];
                    this.ctx.lineTo(city.x, city.y);
                }
                
                this.ctx.stroke();
            }
            
            updateStats() {
                document.getElementById('generation').textContent = this.generation;
                document.getElementById('bestDistance').textContent = 
                    this.bestDistance === Infinity ? '∞' : Math.round(this.bestDistance);
                document.getElementById('activeAnts').textContent = 
                    this.ants.filter(ant => !ant.isComplete).length;
                
                if (this.pheromones.length > 0) {
                    const avgPheromone = this.pheromones.flat().reduce((a, b) => a + b, 0) / 
                                        (this.pheromones.length * this.pheromones.length);
                    document.getElementById('pheromoneLevel').textContent = avgPheromone.toFixed(2);
                }
            }
        }
        
        // 初始化应用
        window.addEventListener('load', () => {
            new TSPAntColony();
        });
    </script>
</body>
</html>

https://www.ceas3.uc.edu/ret/archive/2016/ret/docs/abstract/reading33.pdf

posted @ 2025-09-16 17:50  jack-chen666  阅读(8)  评论(0)    收藏  举报